Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dynamic secret resource implementation #1097

Merged
merged 15 commits into from
Sep 24, 2024
73 changes: 73 additions & 0 deletions docs/resources/vault_secrets_dynamic_secret.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
---
maxcoulombe marked this conversation as resolved.
Show resolved Hide resolved
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "hcp_vault_secrets_dynamic_secret Resource - terraform-provider-hcp"
subcategory: ""
description: |-
The Vault Secrets dynamic secret resource manages a dynamic secret configuration.
---

# hcp_vault_secrets_dynamic_secret (Resource)

The Vault Secrets dynamic secret resource manages a dynamic secret configuration.

## Example Usage

```terraform
resource "hcp_vault_secrets_dynamic_secret" "example_aws" {
app_name = "my-app-1"
secret_provider = "aws"
maxcoulombe marked this conversation as resolved.
Show resolved Hide resolved
name = "my_aws_1"
integration_name = "my-integration-1"
default_ttl = "900s"
aws_assume_role = {
iam_role_arn = "arn:aws:iam::<account_id>>:role/<role_name>"
}
}

resource "hcp_vault_secrets_dynamic_secret" "example_gcp" {
app_name = "my-app-1"
secret_provider = "gcp"
name = "my_gcp_1"
integration_name = "my-integration-1"
default_ttl = "900s"
gcp_impersonate_service_account = {
service_account_email = "<name>@<project>.iam.gserviceaccount.com"
}
}
```

<!-- schema generated by tfplugindocs -->
## Schema

### Required

- `app_name` (String) Vault Secrets application name that owns the secret.
- `integration_name` (String) The Vault Secrets integration name with the capability to rotate the secret.
- `name` (String) The Vault Secrets secret name.
- `secret_provider` (String) The third party platform the dynamic credentials give access to. One of `aws` or `gcp`.

### Optional

- `aws_assume_role` (Attributes) AWS configuration to generate dynamic credentials by assuming an IAM role. Required if `provider` is `aws`. (see [below for nested schema](#nestedatt--aws_assume_role))
- `default_ttl` (String) TTL the generated credentials will be valid for.
- `gcp_impersonate_service_account` (Attributes) GCP configuration to generate dynamic credentials by impersonating a service account. Required if `secret_provider` is `gcp`. (see [below for nested schema](#nestedatt--gcp_impersonate_service_account))
- `project_id` (String) HCP project ID that owns the HCP Vault Secrets integration. Inferred from the provider configuration if omitted.

### Read-Only

- `organization_id` (String) HCP organization ID that owns the HCP Vault Secrets integration.

<a id="nestedatt--aws_assume_role"></a>
### Nested Schema for `aws_assume_role`

Required:

- `iam_role_arn` (String) AWS IAM role ARN to assume when generating credentials.


<a id="nestedatt--gcp_impersonate_service_account"></a>
### Nested Schema for `gcp_impersonate_service_account`

Required:

- `service_account_email` (String) GCP service account email to impersonate.
21 changes: 21 additions & 0 deletions examples/resources/hcp_vault_secrets_dynamic_secret/resource.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
resource "hcp_vault_secrets_dynamic_secret" "example_aws" {
app_name = "my-app-1"
secret_provider = "aws"
name = "my_aws_1"
integration_name = "my-integration-1"
default_ttl = "900s"
aws_assume_role = {
iam_role_arn = "arn:aws:iam::<account_id>>:role/<role_name>"
maxcoulombe marked this conversation as resolved.
Show resolved Hide resolved
}
}

resource "hcp_vault_secrets_dynamic_secret" "example_gcp" {
app_name = "my-app-1"
secret_provider = "gcp"
name = "my_gcp_1"
integration_name = "my-integration-1"
default_ttl = "900s"
gcp_impersonate_service_account = {
service_account_email = "<name>@<project>.iam.gserviceaccount.com"
}
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ require (
github.com/hashicorp/go-cty v1.4.1-0.20200723130312-85980079f637
github.com/hashicorp/go-uuid v1.0.3
github.com/hashicorp/go-version v1.7.0
github.com/hashicorp/hcp-sdk-go v0.110.0
github.com/hashicorp/hcp-sdk-go v0.113.0
github.com/hashicorp/terraform-plugin-docs v0.19.4
github.com/hashicorp/terraform-plugin-framework v1.5.0
github.com/hashicorp/terraform-plugin-framework-validators v0.12.0
Expand Down
6 changes: 6 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,12 @@ github.com/hashicorp/hcl/v2 v2.19.1 h1://i05Jqznmb2EXqa39Nsvyan2o5XyMowW5fnCKW5R
github.com/hashicorp/hcl/v2 v2.19.1/go.mod h1:ThLC89FV4p9MPW804KVbe/cEXoQ8NZEh+JtMeeGErHE=
github.com/hashicorp/hcp-sdk-go v0.110.0 h1:eaDO6XoEb0H+g00Ka3C+ZbRibhwWyA2YNmv48xFCL2w=
github.com/hashicorp/hcp-sdk-go v0.110.0/go.mod h1:vQ4fzdL1AmhIAbCw+4zmFe5Hbpajj3NvRWkJoVuxmAk=
github.com/hashicorp/hcp-sdk-go v0.111.0 h1:tPQs4N3HdwF8NF3gwZQ8b00CJDeuEzmrQh/OsJlhSSs=
github.com/hashicorp/hcp-sdk-go v0.111.0/go.mod h1:vQ4fzdL1AmhIAbCw+4zmFe5Hbpajj3NvRWkJoVuxmAk=
github.com/hashicorp/hcp-sdk-go v0.112.0 h1:gKzxaPhzJj4NobFw7Sc1rGf3nMSqUKBgTtsbZ6bzd14=
github.com/hashicorp/hcp-sdk-go v0.112.0/go.mod h1:vQ4fzdL1AmhIAbCw+4zmFe5Hbpajj3NvRWkJoVuxmAk=
github.com/hashicorp/hcp-sdk-go v0.113.0 h1:JuA6mZosEezE550lNMEq43VLUCzVc+/jPmPC1Nd39Gk=
github.com/hashicorp/hcp-sdk-go v0.113.0/go.mod h1:vQ4fzdL1AmhIAbCw+4zmFe5Hbpajj3NvRWkJoVuxmAk=
github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y=
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
github.com/hashicorp/terraform-exec v0.21.0 h1:uNkLAe95ey5Uux6KJdua6+cv8asgILFVWkd/RG0D2XQ=
Expand Down
1 change: 1 addition & 0 deletions internal/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ func (p *ProviderFramework) Resources(ctx context.Context) []func() resource.Res
vaultsecrets.NewVaultSecretsIntegrationGCPResource,
vaultsecrets.NewVaultSecretsIntegrationMongoDBAtlasResource,
vaultsecrets.NewVaultSecretsIntegrationTwilioResource,
vaultsecrets.NewVaultSecretsDynamicSecretResource,
// IAM
iam.NewServicePrincipalResource,
iam.NewServicePrincipalKeyResource,
Expand Down
99 changes: 99 additions & 0 deletions internal/provider/vaultsecrets/dynamic_secret_aws.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
maxcoulombe marked this conversation as resolved.
Show resolved Hide resolved

package vaultsecrets

import (
"context"
"fmt"

"github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/preview/2023-11-28/client/secret_service"
secretmodels "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/preview/2023-11-28/models"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-provider-hcp/internal/clients"
)

var _ dynamicSecret = &awsDynamicSecret{}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, like the interface pattern for adding new dynamic secret providers.


type awsDynamicSecret struct{}

func (s *awsDynamicSecret) readFunc(ctx context.Context, client secret_service.ClientService, secret *DynamicSecret) (any, error) {
response, err := client.GetAwsDynamicSecret(
secret_service.NewGetAwsDynamicSecretParamsWithContext(ctx).
WithOrganizationID(secret.OrganizationID.ValueString()).
WithProjectID(secret.ProjectID.ValueString()).
WithAppName(secret.AppName.ValueString()).
WithName(secret.Name.ValueString()), nil)
if err != nil && !clients.IsResponseCodeNotFound(err) {
return nil, err
}
if response == nil || response.Payload == nil {
return nil, nil
}
return response.Payload.Secret, nil
}

func (s *awsDynamicSecret) createFunc(ctx context.Context, client secret_service.ClientService, secret *DynamicSecret) (any, error) {
response, err := client.CreateAwsDynamicSecret(
secret_service.NewCreateAwsDynamicSecretParamsWithContext(ctx).
WithOrganizationID(secret.OrganizationID.ValueString()).
WithProjectID(secret.ProjectID.ValueString()).
WithAppName(secret.AppName.ValueString()).
WithBody(&secretmodels.SecretServiceCreateAwsDynamicSecretBody{
DefaultTTL: secret.DefaultTtl.ValueString(),
IntegrationName: secret.IntegrationName.ValueString(),
Name: secret.Name.ValueString(),
AssumeRole: &secretmodels.Secrets20231128AssumeRoleRequest{
RoleArn: secret.AWSAssumeRole.IAMRoleARN.ValueString(),
},
}),
nil)
if err != nil {
return nil, err
}
if response == nil || response.Payload == nil {
return nil, nil
}
return response.Payload.Secret, nil
}

func (s *awsDynamicSecret) updateFunc(ctx context.Context, client secret_service.ClientService, secret *DynamicSecret) (any, error) {
response, err := client.UpdateAwsDynamicSecret(
secret_service.NewUpdateAwsDynamicSecretParamsWithContext(ctx).
WithOrganizationID(secret.OrganizationID.ValueString()).
WithProjectID(secret.ProjectID.ValueString()).
WithAppName(secret.AppName.ValueString()).
WithName(secret.Name.ValueString()).
WithBody(&secretmodels.SecretServiceUpdateAwsDynamicSecretBody{
DefaultTTL: secret.DefaultTtl.ValueString(),
AssumeRole: &secretmodels.Secrets20231128AssumeRoleRequest{
RoleArn: secret.AWSAssumeRole.IAMRoleARN.ValueString(),
},
}),
nil)
if err != nil {
return nil, err
}
if response == nil || response.Payload == nil {
return nil, nil
}
return response.Payload.Secret, nil
}

func (s *awsDynamicSecret) fromModel(secret *DynamicSecret, model any) diag.Diagnostics {
diags := diag.Diagnostics{}

secretModel, ok := model.(*secretmodels.Secrets20231128AwsDynamicSecret)
if !ok {
diags.AddError("Invalid model type, this is a bug on the provider.", fmt.Sprintf("Expected *secretmodels.Secrets20231128AwsDynamicSecret, got: %T", model))
return diags
}

secret.DefaultTtl = types.StringValue(secretModel.DefaultTTL)
secret.AWSAssumeRole = &awsAssumeRole{
IAMRoleARN: types.StringValue(secretModel.AssumeRole.RoleArn),
}

return diags
}
99 changes: 99 additions & 0 deletions internal/provider/vaultsecrets/dynamic_secret_gcp.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package vaultsecrets

import (
"context"
"fmt"

"github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/preview/2023-11-28/client/secret_service"
secretmodels "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/preview/2023-11-28/models"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-provider-hcp/internal/clients"
)

var _ dynamicSecret = &gcpDynamicSecret{}

type gcpDynamicSecret struct{}

func (s *gcpDynamicSecret) readFunc(ctx context.Context, client secret_service.ClientService, secret *DynamicSecret) (any, error) {
response, err := client.GetGcpDynamicSecret(
secret_service.NewGetGcpDynamicSecretParamsWithContext(ctx).
WithOrganizationID(secret.OrganizationID.ValueString()).
WithProjectID(secret.ProjectID.ValueString()).
WithAppName(secret.AppName.ValueString()).
WithName(secret.Name.ValueString()), nil)
if err != nil && !clients.IsResponseCodeNotFound(err) {
return nil, err
}
if response == nil || response.Payload == nil {
return nil, nil
}
return response.Payload.Secret, nil
}

func (s *gcpDynamicSecret) createFunc(ctx context.Context, client secret_service.ClientService, secret *DynamicSecret) (any, error) {
response, err := client.CreateGcpDynamicSecret(
secret_service.NewCreateGcpDynamicSecretParamsWithContext(ctx).
WithOrganizationID(secret.OrganizationID.ValueString()).
WithProjectID(secret.ProjectID.ValueString()).
WithAppName(secret.AppName.ValueString()).
WithBody(&secretmodels.SecretServiceCreateGcpDynamicSecretBody{
DefaultTTL: secret.DefaultTtl.ValueString(),
IntegrationName: secret.IntegrationName.ValueString(),
Name: secret.Name.ValueString(),
ServiceAccountImpersonation: &secretmodels.Secrets20231128ServiceAccountImpersonationRequest{
ServiceAccountEmail: secret.GCPImpersonateServiceAccount.ServiceAccountEmail.ValueString(),
},
}),
nil)
if err != nil {
return nil, err
}
if response == nil || response.Payload == nil {
return nil, nil
}
return response.Payload.Secret, nil
}

func (s *gcpDynamicSecret) updateFunc(ctx context.Context, client secret_service.ClientService, secret *DynamicSecret) (any, error) {
response, err := client.UpdateGcpDynamicSecret(
secret_service.NewUpdateGcpDynamicSecretParamsWithContext(ctx).
WithOrganizationID(secret.OrganizationID.ValueString()).
WithProjectID(secret.ProjectID.ValueString()).
WithAppName(secret.AppName.ValueString()).
WithName(secret.Name.ValueString()).
WithBody(&secretmodels.SecretServiceUpdateGcpDynamicSecretBody{
DefaultTTL: secret.DefaultTtl.ValueString(),
ServiceAccountImpersonation: &secretmodels.Secrets20231128ServiceAccountImpersonationRequest{
ServiceAccountEmail: secret.GCPImpersonateServiceAccount.ServiceAccountEmail.ValueString(),
},
}),
nil)
if err != nil {
return nil, err
}
if response == nil || response.Payload == nil {
return nil, nil
}
return response.Payload.Secret, nil
}

func (s *gcpDynamicSecret) fromModel(secret *DynamicSecret, model any) diag.Diagnostics {
diags := diag.Diagnostics{}

secretModel, ok := model.(*secretmodels.Secrets20231128GcpDynamicSecret)
if !ok {
diags.AddError("Invalid model type, this is a bug on the provider.", fmt.Sprintf("Expected *secretmodels.Secrets20231128GcpDynamicSecret, got: %T", model))
return diags
}

secret.DefaultTtl = types.StringValue(secretModel.DefaultTTL)
secret.GCPImpersonateServiceAccount = &gcpImpersonateServiceAccount{
ServiceAccountEmail: types.StringValue(secretModel.ServiceAccountImpersonation.ServiceAccountEmail),
}

return diags
}
Loading
Loading