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

vault secrets | rotated secrets data source #854

Merged
merged 24 commits into from
Jun 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .changelog/854.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:feature
add vault_secrets_rotating_secret data source
```
72 changes: 72 additions & 0 deletions internal/clients/vault_secrets_preview.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,75 @@ func OpenVaultSecretsAppSecrets(ctx context.Context, client *Client, loc *shared

return secrets.GetPayload().Secrets, nil
}

func GetRotatingSecretState(ctx context.Context, client *Client, loc *sharedmodels.HashicorpCloudLocationLocation, appName, secretName string) (*secretmodels.Secrets20231128RotatingSecretState, error) {
params := secret_service.NewGetRotatingSecretStateParamsWithContext(ctx).
WithOrganizationID(loc.OrganizationID).
WithProjectID(loc.ProjectID).
WithAppName(appName).
WithSecretName(secretName)

resp, err := client.VaultSecretsPreview.GetRotatingSecretState(params, nil)
if err != nil {
return nil, err
}

return resp.GetPayload().State, nil
}

// CreateMongoDBAtlasRotationIntegration NOTE: currently just needed for tests
func CreateMongoDBAtlasRotationIntegration(ctx context.Context, client *Client, loc *sharedmodels.HashicorpCloudLocationLocation, integrationName, mongodbAtlasPublicKey, mongodbAtlasPrivateKey string) (*secretmodels.Secrets20231128MongoDBAtlasIntegration, error) {
body := secret_service.CreateMongoDBAtlasIntegrationBody{
IntegrationName: integrationName,
MongodbAPIPublicKey: mongodbAtlasPublicKey,
MongodbAPIPrivateKey: mongodbAtlasPrivateKey,
}
params := secret_service.NewCreateMongoDBAtlasIntegrationParamsWithContext(ctx).
WithOrganizationID(loc.OrganizationID).
WithProjectID(loc.ProjectID).
WithBody(body)

resp, err := client.VaultSecretsPreview.CreateMongoDBAtlasIntegration(params, nil)
if err != nil {
return nil, err
}

return resp.GetPayload().Integration, nil
}

// DeleteMongoDBAtlasRotationIntegration NOTE: currently just needed for tests
func DeleteMongoDBAtlasRotationIntegration(ctx context.Context, client *Client, loc *sharedmodels.HashicorpCloudLocationLocation, integrationName string) error {
params := secret_service.NewDeleteMongoDBAtlasIntegrationParamsWithContext(ctx).
WithOrganizationID(loc.OrganizationID).
WithProjectID(loc.ProjectID).
WithIntegrationName(integrationName)

_, err := client.VaultSecretsPreview.DeleteMongoDBAtlasIntegration(params, nil)
if err != nil {
return err
}

return nil
}

// CreateMongoDBAtlasRotatingSecret NOTE: currently just needed for tests
func CreateMongoDBAtlasRotatingSecret(
ctx context.Context,
client *Client,
loc *sharedmodels.HashicorpCloudLocationLocation,
appName string,
requestBody secret_service.CreateMongoDBAtlasRotatingSecretBody,
) (*secretmodels.Secrets20231128CreateMongoDBAtlasRotatingSecretResponse, error) {
params := secret_service.NewCreateMongoDBAtlasRotatingSecretParamsWithContext(ctx).
WithOrganizationID(loc.OrganizationID).
WithProjectID(loc.ProjectID).
WithAppName(appName).
WithBody(requestBody)

resp, err := client.VaultSecretsPreview.CreateMongoDBAtlasRotatingSecret(params, nil)
if err != nil {
return nil, err
}

return resp.GetPayload(), nil
}
1 change: 1 addition & 0 deletions internal/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ func (p *ProviderFramework) DataSources(ctx context.Context) []func() datasource
// Vault Secrets
vaultsecrets.NewVaultSecretsAppDataSource,
vaultsecrets.NewVaultSecretsSecretDataSource,
vaultsecrets.NewVaultSecretsRotatingSecretDataSource,
// IAM
iam.NewServicePrincipalDataSource,
iam.NewGroupDataSource,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package vaultsecrets

import (
"context"
"fmt"

sharedmodels "github.com/hashicorp/hcp-sdk-go/clients/cloud-shared/v1/models"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-provider-hcp/internal/clients"
)

type DataSourceVaultSecretsRotatingSecret struct {
client *clients.Client
}

type DataSourceVaultSecretsRotatingSecretModel struct {
ID types.String `tfsdk:"id"`
AppName types.String `tfsdk:"app_name"`
ProjectID types.String `tfsdk:"project_id"`
OrgID types.String `tfsdk:"organization_id"`
SecretName types.String `tfsdk:"secret_name"`
SecretValues types.Map `tfsdk:"secret_values"`
SecretVersion types.Int64 `tfsdk:"secret_version"`
SecretProvider types.String `tfsdk:"secret_provider"`
}

func NewVaultSecretsRotatingSecretDataSource() datasource.DataSource {
return &DataSourceVaultSecretsRotatingSecret{}
}

func (d *DataSourceVaultSecretsRotatingSecret) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_vault_secrets_rotating_secret"
}

func (d *DataSourceVaultSecretsRotatingSecret) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "The Vault Secrets secret data source retrieves a rotating secret with its latest version.",
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Computed: true,
Description: "The ID of this resource.",
},
"app_name": schema.StringAttribute{
Description: "The name of the Vault Secrets application.",
Required: true,
},
"secret_name": schema.StringAttribute{
Description: "The name of the Vault Secrets secret.",
Required: true,
},
"secret_values": schema.MapAttribute{
Description: "The secret values corresponding to the secret name input.",
Computed: true,
Sensitive: true,
ElementType: types.StringType,
},
"secret_version": schema.Int64Attribute{
Description: "The version of the Vault Secrets secret.",
Computed: true,
},
"secret_provider": schema.StringAttribute{
Description: "The name of the provider this rotating secret is for",
Computed: true,
},
"organization_id": schema.StringAttribute{
Description: "The ID of the HCP organization where the Vault Secrets app is located.",
Computed: true,
},
"project_id": schema.StringAttribute{
Description: "The ID of the HCP project where the Vault Secrets app is located.",
Computed: true,
},
},
}
}

func (d *DataSourceVaultSecretsRotatingSecret) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
if req.ProviderData == nil {
return
}

client, ok := req.ProviderData.(*clients.Client)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Data Source Configure Type",
fmt.Sprintf("Expected *clients.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
)
return
}
d.client = client
}

func (d *DataSourceVaultSecretsRotatingSecret) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var data DataSourceVaultSecretsRotatingSecretModel
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)

client := d.client
if client == nil {
resp.Diagnostics.AddError(
"Unconfigured HCP Client",
"Expected configured HCP client. Please report this issue to the provider developers.",
)
return
}

loc := &sharedmodels.HashicorpCloudLocationLocation{
OrganizationID: client.Config.OrganizationID,
ProjectID: client.Config.ProjectID,
}

openSecret, err := clients.OpenVaultSecretsAppSecret(ctx, client, loc, data.AppName.ValueString(), data.SecretName.ValueString())
if err != nil {
resp.Diagnostics.AddError(err.Error(), "Unable to open secret")
return
}

var secretValues map[string]string
var secretVersion int64
switch {
case openSecret.RotatingVersion != nil:
secretValues = openSecret.RotatingVersion.Values
secretVersion = openSecret.RotatingVersion.Version
default:
resp.Diagnostics.AddError(
"Unsupported HCP Secret type",
fmt.Sprintf("HCP Secrets secret type %q is not currently supported by terraform-provider-hcp", openSecret.Type),
)
return
}

secretsOutput, diag := types.MapValueFrom(ctx, types.StringType, secretValues)
resp.Diagnostics.Append(diag...)

// TODO: what is ID supposed to be?
// data.ID = ?
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this ID actually used for unique identification of data sources or by anything TF-provider-specific? If not, maybe one option is to just remove it altogether.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

im honestly not sure, would like some feedback from @hashicorp/cloud-foundations
otherwise maybe something like
project/<project id>/app/<app name>/secret/<secret name>
wdyt?

Copy link
Contributor

@averche averche Jun 3, 2024

Choose a reason for hiding this comment

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

If our goal here is a unique resource identifier, this will almost work. The only scenario where I see it failing is if you delete the secret and re-create it with the same name. However, this is much better than the AppName that we are using elsewhere.

My preferred solution would still be to remove it if possible.

data.OrgID = types.StringValue(client.Config.OrganizationID)
data.ProjectID = types.StringValue(client.Config.ProjectID)
data.SecretValues = secretsOutput
data.SecretVersion = types.Int64Value(secretVersion)
data.SecretProvider = types.StringValue(openSecret.Provider)

resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
package vaultsecrets_test

import (
"context"
"fmt"
"os"
"testing"
"time"

sharedmodels "github.com/hashicorp/hcp-sdk-go/clients/cloud-shared/v1/models"
"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-testing/helper/resource"
"github.com/hashicorp/terraform-plugin-testing/terraform"

"github.com/hashicorp/terraform-provider-hcp/internal/clients"
"github.com/hashicorp/terraform-provider-hcp/internal/provider/acctest"
)

func checkRequiredEnvVarOrFail(t *testing.T, varName string) string {
value, exists := os.LookupEnv(varName)
if !exists {
t.Skipf("%s must be set to execute this test", varName)
}
return value
}

func TestAcc_dataSourceVaultSecretsRotatingSecret(t *testing.T) {

mongodbAtlasPublicKey := checkRequiredEnvVarOrFail(t, "MONGODB_ATLAS_API_PUBLIC_KEY")
mongodbAtlasPrivateKey := checkRequiredEnvVarOrFail(t, "MONGODB_ATLAS_API_PRIVATE_KEY")
mongodbAtlasGroupID := checkRequiredEnvVarOrFail(t, "MONGODB_ATLAS_GROUP_ID")
mongodbAtlasDBName := checkRequiredEnvVarOrFail(t, "MONGODB_ATLAS_DB_NAME")

testAppName := generateRandomSlug()
testIntegrationName := generateRandomSlug()
dataSourceAddress := "data.hcp_vault_secrets_rotating_secret.foo"

testSecretName := "secret_one"

tfconfig := fmt.Sprintf(`data "hcp_vault_secrets_rotating_secret" "foo" {
app_name = %q
secret_name = %q
}`, testAppName, testSecretName)

client := acctest.HCPClients(t)
loc := &sharedmodels.HashicorpCloudLocationLocation{
OrganizationID: client.Config.OrganizationID,
ProjectID: client.Config.ProjectID,
}

resource.Test(t, resource.TestCase{
PreCheck: func() { acctest.PreCheck(t) },
ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories,
Steps: []resource.TestStep{
{
PreConfig: func() {
createTestApp(t, testAppName)
ctx := context.Background()

_, err := clients.CreateMongoDBAtlasRotationIntegration(ctx, client, loc, testIntegrationName, mongodbAtlasPublicKey, mongodbAtlasPrivateKey)
if err != nil {
t.Fatalf("could not create mongodb rotation integration: %v", err)
}

reqBody := secret_service.CreateMongoDBAtlasRotatingSecretBody{
SecretName: testSecretName,
RotationIntegrationName: testIntegrationName,
RotationPolicyName: "built-in:30-days-2-active",
MongodbGroupID: mongodbAtlasGroupID,
MongodbRoles: []*secretmodels.Secrets20231128MongoDBRole{
{
DatabaseName: mongodbAtlasDBName,
RoleName: "read",
CollectionName: "",
},
},
}
_, err = clients.CreateMongoDBAtlasRotatingSecret(ctx, client, loc, testAppName, reqBody)
if err != nil {
t.Fatalf("could not create rotating mongodb atlas secret: %v", err)
}

// block until the secret is done
timeout := time.AfterFunc(10*time.Minute, func() {
t.Fatalf("timed out waiting for mongodb rotating secret to be created")
})

waitForSecret := func() {
for {
state, err := clients.GetRotatingSecretState(ctx, client, loc, testAppName, testSecretName)
if err != nil {
t.Fatalf("could not get rotating secret state: %v", err)
}
switch *state.Status {
case secretmodels.Secrets20231128RotatingSecretStatusERRORED:
t.Fatalf("error rotating secret: %q", state.ErrorMessage)
case secretmodels.Secrets20231128RotatingSecretStatusWAITINGFORNEXTROTATION:
timeout.Stop()
t.Log("secret successfully rotated")
return
default:
t.Log("waiting to check rotating secret state")
time.Sleep(10 * time.Second)
}
}
}

waitForSecret()

},
Config: tfconfig,
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttrSet(dataSourceAddress, "organization_id"),
resource.TestCheckResourceAttrSet(dataSourceAddress, "project_id"),
resource.TestCheckResourceAttr(dataSourceAddress, "secret_values.%", "2"), // required: check the number of elements in the map
resource.TestCheckResourceAttr(dataSourceAddress, "app_name", testAppName),
resource.TestCheckResourceAttr(dataSourceAddress, "secret_provider", "mongodb-atlas"),
),
},
},
CheckDestroy: func(_ *terraform.State) error {
ctx := context.Background()
err := clients.DeleteVaultSecretsAppSecret(ctx, client, loc, testAppName, testSecretName)
if err != nil {
return fmt.Errorf("could not delete rotating secret: %v", err)
}

err = clients.DeleteMongoDBAtlasRotationIntegration(ctx, client, loc, testIntegrationName)
if err != nil {
return fmt.Errorf("could not delete rotation integration: %v", err)
}

deleteTestApp(t, testAppName)

return nil
},
})
}