Skip to content

Commit

Permalink
vault secrets | rotated secrets data source (#854)
Browse files Browse the repository at this point in the history
* switch some methods over

* update vault_secrets_secret

* rotated secrets to map

* add changelog

* Update internal/provider/vaultsecrets/data_source_vault_secrets_secret.go

Co-authored-by: Anton Averchenkov <[email protected]>

* add new data source

* add some fields

* not really adding a test...

* remove tests for now

* rename

* lint

* create test

* rename

* rename for lint

* update api

* added change log

* block until the secret is created

* break in wrong place

* test passes

* rename

---------

Co-authored-by: Anton Averchenkov <[email protected]>
  • Loading branch information
dhuckins and averche authored Jun 5, 2024
1 parent 68a655f commit e0b3611
Show file tree
Hide file tree
Showing 5 changed files with 363 additions and 0 deletions.
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 = ?
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
},
})
}

0 comments on commit e0b3611

Please sign in to comment.