Skip to content
Open
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
4 changes: 4 additions & 0 deletions integ/aws/storage/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ rds.groups: ## Test RDS SubnetGroup, ParameterGroup (both binds), OptionGroup, D
go test -v -count 1 -timeout 20m ./... -run ^TestRdsGroups$
.PHONY: rds.groups

rds.instance: ## Test DatabaseInstance L2 (live Postgres db.t3.micro + attached secret)
go test -v -count 1 -timeout 45m ./... -run ^TestRdsInstance$
.PHONY: rds.instance

bucket-notifications: ## Test S3 Bucket with EventBridge Notifications
go test -v -count 1 -timeout 15m ./... -run ^TestBucketNotifications$
.PHONY: bucket-notifications
80 changes: 80 additions & 0 deletions integ/aws/storage/apps/rds.instance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// Live test for the storage.rds DatabaseInstance L2 (RDS PR 2c): a real
// Postgres db.t3.micro deployed through the ported construct in an isolated
// VPC, with credentials auto-generated into a DatabaseSecret and attached via
// the TerraConstructs attach() protocol -- the deployed secret must end up
// carrying engine/host/port/dbname/dbInstanceIdentifier merged in by
// `DatabaseInstanceBase.asSecretAttachmentTarget()` (the shipped reference
// implementation replacing the TEST-ONLY adapters in integ/aws/encryption).
//
// NOTE: the auto-generated DatabaseSecret has a deterministic name and no
// recovery-window override -- a re-run within 30 days of destroy needs
// `aws secretsmanager delete-secret --force-delete-without-recovery` first.
import { App, LocalBackend, TerraformOutput } from "cdktn";
import { aws, Duration } from "../../../../src";

const environmentName = process.env.ENVIRONMENT_NAME ?? "test";
const region = process.env.AWS_REGION ?? "us-east-1";
const outdir = process.env.OUT_DIR ?? "cdktf.out";
const stackName = process.env.STACK_NAME ?? "rds.instance";

const app = new App({
outdir,
});

const stack = new aws.AwsStack(app, stackName, {
gridUUID: "g44444444-4444",
environmentName,
providerConfig: {
region,
},
});
new LocalBackend(stack, {
path: `${stackName}.tfstate`,
});

const vpc = new aws.compute.Vpc(stack, "Vpc", {
maxAzs: 2,
natGateways: 0,
subnetConfiguration: [
{
name: "isolated",
subnetType: aws.compute.SubnetType.PRIVATE_ISOLATED,
cidrMask: 24,
},
],
});

const instance = new aws.storage.rds.DatabaseInstance(stack, "Database", {
engine: aws.storage.rds.DatabaseInstanceEngine.postgres({
version: aws.storage.rds.PostgresEngineVersion.VER_16,
}),
instanceType: aws.compute.InstanceType.of(
aws.compute.InstanceClass.BURSTABLE3,
aws.compute.InstanceSize.MICRO,
),
vpc,
vpcSubnets: { subnetType: aws.compute.SubnetType.PRIVATE_ISOLATED },
credentials: aws.storage.rds.Credentials.fromGeneratedSecret("dbadmin"),
databaseName: "appdb",
allocatedStorage: 20,
backupRetention: Duration.days(0),
multiAz: false,
// Terraform-native replacement for upstream removalPolicy (see the
// TERRACONSTRUCTS DEVIATION on the props): allow clean destroy.
skipFinalSnapshot: true,
});

new TerraformOutput(stack, "instance_identifier", {
value: instance.instanceIdentifier,
staticId: true,
});
new TerraformOutput(stack, "instance_endpoint_address", {
value: instance.instanceEndpoint.hostname,
staticId: true,
});
new TerraformOutput(stack, "secret_arn", {
value: instance.secret!.secretArn,
staticId: true,
});

app.synth();
68 changes: 68 additions & 0 deletions integ/aws/storage/rds_instance_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package test

import (
"encoding/json"
"testing"

"github.com/gruntwork-io/terratest/modules/aws"
"github.com/gruntwork-io/terratest/modules/terraform"
test_structure "github.com/gruntwork-io/terratest/modules/test-structure"
"github.com/stretchr/testify/require"
)

// Run the apps/rds.instance.ts integration test: a real Postgres db.t3.micro
// deployed through the storage.rds DatabaseInstance L2 with auto-generated
// credentials. Validates the instance read-back, the attach() protocol's
// merged secret value (engine/host/port/dbname/dbInstanceIdentifier), and the
// post-apply drift oracle.
func TestRdsInstance(t *testing.T) {
runStorageIntegrationTest(t, "rds.instance", "us-east-1", validateRdsInstance)
}

func validateRdsInstance(t *testing.T, tfWorkingDir string, awsRegion string) {
terraformOptions := test_structure.LoadTerraformOptions(t, tfWorkingDir)
outputs := terraform.OutputAll(t, terraformOptions)

instanceID := outputs["instance_identifier"].(string)
endpointAddress := outputs["instance_endpoint_address"].(string)
secretArn := outputs["secret_arn"].(string)

// --- 1. Instance read-back. ---
details, err := aws.GetRdsInstanceDetailsE(t, instanceID, awsRegion)
require.NoError(t, err)
require.Equal(t, "available", *details.DBInstanceStatus)
require.Equal(t, "postgres", *details.Engine)
require.Equal(t, "db.t3.micro", *details.DBInstanceClass)
require.False(t, *details.MultiAZ)
require.NotNil(t, details.Endpoint)
require.Equal(t, endpointAddress, *details.Endpoint.Address)
t.Logf("rds-instance: %s available (%s %s at %s:%d)", instanceID,
*details.Engine, *details.DBInstanceClass, *details.Endpoint.Address, *details.Endpoint.Port)

// --- 2. The attached DatabaseSecret carries the merged connection fields
// from DatabaseInstanceBase.asSecretAttachmentTarget() (the shipped
// reference ISecretAttachmentTarget implementation). ---
// NOTE: `port` is a JSON NUMBER (not a string) -- CloudFormation's
// SecretTargetAttachment writes it as a number too, and the construct
// preserves that parity, so the map must be mixed-type.
secretValue := aws.GetSecretValue(t, awsRegion, secretArn)
var connection map[string]interface{}
require.NoError(t, json.Unmarshal([]byte(secretValue), &connection))
require.Equal(t, "dbadmin", connection["username"])
require.NotEmpty(t, connection["password"])
require.Equal(t, "postgres", connection["engine"])
require.Equal(t, endpointAddress, connection["host"])
require.Equal(t, float64(*details.Endpoint.Port), connection["port"],
"port must be a JSON number (CFN SecretTargetAttachment parity)")
require.Equal(t, "appdb", connection["dbname"])
require.Equal(t, instanceID, connection["dbInstanceIdentifier"],
"attach() must merge dbInstanceIdentifier (CFN SecretTargetAttachment parity)")
t.Logf("rds-instance: attached secret carries full connection details incl. dbInstanceIdentifier=%s", instanceID)

// --- Drift oracle: re-planning the already-applied stack must show zero
// changes. Catches sentinel-default and read-back mismatches invisible at
// synth time (password ignore_changes, storage/iops normalization, etc.). ---
planExitCode := terraform.PlanExitCode(t, terraformOptions)
require.Equal(t, terraform.DefaultSuccessExitCode, planExitCode,
"expected `tofu plan -detailed-exitcode` to report no drift after apply (got exit code %d)", planExitCode)
}
48 changes: 35 additions & 13 deletions src/aws/encryption/secret.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1009,6 +1009,27 @@ export class Secret extends SecretBase {
this.rotationAttached = true;
}

/**
* The value generated by `generateSecretString` (the underlying
* `aws_secretsmanager_random_password` data source's token), if this secret
* was constructed with one. `undefined` for secrets seeded via
* `secretStringValue`/`secretObjectValue`, or for imported secrets.
*
* TERRACONSTRUCTS DEVIATION: added for callers (e.g. `rds.DatabaseInstance`,
* via `rds.DatabaseSecret`) that must feed the SAME generated password into
* another resource's plaintext-password argument (e.g. `aws_db_instance.password`)
* so the stored secret and the live resource never drift apart. Upstream CDK
* solves this with `secret.secretValueFromJson('password').unsafeUnwrap()` (a
* CloudFormation dynamic reference), which has no Terraform-native equivalent
* and is not ported here (see the deviation note on `ISecret` above) -- this
* getter is the Terraform-native substitute: the raw generated-password token,
* surfaced directly instead of re-derived from the secret's JSON value.
* @internal
*/
public get _generatedPassword(): string | undefined {
return this.randomPassword?.randomPassword;
}

/**
* Registers connection details contributed by an attached target so they are
* merged into this secret's sole version. Called by `SecretTargetAttachment`.
Expand All @@ -1034,14 +1055,14 @@ export class Secret extends SecretBase {
/**
* A secret attachment target.
*
* NOTE: TerraConstructs does not yet ship any concrete implementations of
* this interface. In upstream aws-cdk, `ISecretAttachmentTarget` is
* implemented by `DatabaseInstance`, `DatabaseCluster`, and `DatabaseProxy`
* (aws-rds), and by the DocDB/Redshift equivalents -- none of which have
* been ported to TerraConstructs yet. Until they are, consumers who want to
* attach a secret to a database (or other supported target) must implement
* this interface themselves. See `integ/aws/encryption/apps/*.ts` for a
* worked (test-only) example.
* NOTE: `aws/storage/rds` `DatabaseInstanceBase` (`instance.ts`) is now the shipped reference
* implementation of this interface -- see its `asSecretAttachmentTarget()` for the AGREED DESIGN
* worked out here in practice. In upstream aws-cdk, `ISecretAttachmentTarget` is also implemented by
* `DatabaseCluster` and `DatabaseProxy` (aws-rds), and by the DocDB/Redshift equivalents -- none of
* which have been ported to TerraConstructs yet (tracked for PR 2d/2e and the DocDB/Redshift slices).
* Until they are, consumers who want to attach a secret to one of those not-yet-ported targets must
* implement this interface themselves. See `integ/aws/encryption/apps/*.ts` for a worked (test-only)
* example of that pattern.
*/
export interface ISecretAttachmentTarget {
/**
Expand Down Expand Up @@ -1183,11 +1204,12 @@ export interface ISecretTargetAttachment extends ISecret {
* The result is exactly one `aws_secretsmanager_secret_version` containing the
* base credentials plus the target's connection details.
*
* Concrete `ISecretAttachmentTarget` implementers (`DatabaseInstance`,
* `DatabaseCluster`, ... in aws-rds/docdb/redshift) are not yet ported to
* TerraConstructs; until they are, consumers implement the interface
* themselves. See `integ/aws/encryption/apps/*.ts` for a worked (test-only)
* example of a target that supplies real connection details.
* `aws/storage/rds` `DatabaseInstance`/`DatabaseInstanceFromSnapshot` (via
* `DatabaseInstanceBase.asSecretAttachmentTarget()` in `instance.ts`) are now the shipped concrete
* `ISecretAttachmentTarget` implementers. `DatabaseCluster`/`DatabaseProxy` (aws-rds) and the
* docdb/redshift equivalents are not yet ported (tracked for PR 2d/2e and the DocDB/Redshift
* slices); until they are, consumers implement the interface themselves. See
* `integ/aws/encryption/apps/*.ts` for a worked (test-only) example of that pattern.
*
* `addToResourcePolicy` calls are forwarded to the original secret so that
* only a single resource policy is ever created for the secret (AWS allows
Expand Down
8 changes: 4 additions & 4 deletions src/aws/storage/rds/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ export * from "./parameter-group";
export * from "./database-secret";
export * from "./endpoint";
export * from "./option-group";
// TODO: omitted — upstream also exports `./instance`, `./proxy`, `./proxy-endpoint`, and
// `./serverless-cluster` here (DatabaseInstance, DatabaseProxy/-Endpoint, ServerlessCluster v1).
// Those land in later PRs (RDS PR 2c/2e) —
// https://github.com/aws/aws-cdk/blob/v2.263.0/packages/aws-cdk-lib/aws-rds/lib/index.ts#L14-L17
export * from "./instance";
// TODO: omitted — upstream also exports `./proxy`, `./proxy-endpoint`, and `./serverless-cluster`
// here (DatabaseProxy/-Endpoint, ServerlessCluster v1). Those land in a later PR (RDS PR 2e) —
// https://github.com/aws/aws-cdk/blob/v2.263.0/packages/aws-cdk-lib/aws-rds/lib/index.ts#L15-L17
export * from "./subnet-group";
// TODO: omitted — upstream also exports `./aurora-cluster-instance` here (Aurora Serverless v2
// cluster-instance helper). Lands in a later PR (RDS PR 2d) —
Expand Down
Loading
Loading