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 @@ -40,6 +40,10 @@ rds.proxy: ## Test DatabaseProxy L2 (live RDS Proxy fronting MySQL db.t3.micro)
go test -v -count 1 -timeout 45m ./... -run ^TestRdsProxy$
.PHONY: rds.proxy

docdb.cluster: ## Test DocDB DatabaseCluster L2 (live DocumentDB cluster + instance)
go test -v -count 1 -timeout 45m ./... -run ^TestDocdbCluster$
.PHONY: docdb.cluster

bucket-notifications: ## Test S3 Bucket with EventBridge Notifications
go test -v -count 1 -timeout 15m ./... -run ^TestBucketNotifications$
.PHONY: bucket-notifications
76 changes: 76 additions & 0 deletions integ/aws/storage/apps/docdb.cluster.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// Live test for the storage.docdb DatabaseCluster L2: a real DocumentDB
// cluster + one db.t3.medium instance deployed through the ported construct,
// with a generated master password attached via the attach() protocol
// (dbClusterIdentifier/engine "mongo"/ssl "true"/host/number-port merged into
// the DatabaseSecret -- the fields the MongoDB rotation Lambda requires).
//
// 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 ?? "docdb.cluster";

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

const stack = new aws.AwsStack(app, stackName, {
gridUUID: "g11111111-1111",
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 cluster = new aws.storage.docdb.DatabaseCluster(stack, "Cluster", {
masterUser: {
username: "docadmin",
},
vpc,
vpcSubnets: { subnetType: aws.compute.SubnetType.PRIVATE_ISOLATED },
instanceType: aws.compute.InstanceType.of(
aws.compute.InstanceClass.T3,
aws.compute.InstanceSize.MEDIUM,
),
instances: 1,
backup: {
retention: Duration.days(1),
},
// Terraform-native replacement for upstream removalPolicy: allow clean destroy.
skipFinalSnapshot: true,
});

new TerraformOutput(stack, "cluster_identifier", {
value: cluster.clusterIdentifier,
staticId: true,
});
new TerraformOutput(stack, "cluster_endpoint_address", {
value: cluster.clusterEndpoint.hostname,
staticId: true,
});
new TerraformOutput(stack, "secret_arn", {
value: cluster.secret!.secretArn,
staticId: true,
});

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

import (
"context"
"encoding/json"
"testing"

"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/service/rds"
"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/docdb.cluster.ts integration test: a real DocumentDB cluster +
// db.t3.medium instance deployed through the storage.docdb DatabaseCluster L2.
// Validates cluster/instance read-back (DocDB shares the RDS API), the
// attach() protocol's merged secret (incl. the mongo/ssl fields the rotation
// Lambda requires), and the post-apply drift oracle.
func TestDocdbCluster(t *testing.T) {
runStorageIntegrationTest(t, "docdb.cluster", "us-east-1", validateDocdbCluster)
}

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

clusterID := outputs["cluster_identifier"].(string)
endpointAddress := outputs["cluster_endpoint_address"].(string)
secretArn := outputs["secret_arn"].(string)

ctx := context.Background()
cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion(awsRegion))
require.NoError(t, err)
client := rds.NewFromConfig(cfg)

// --- 1. Cluster read-back (DocDB answers on the RDS API). ---
dc, err := client.DescribeDBClusters(ctx, &rds.DescribeDBClustersInput{
DBClusterIdentifier: &clusterID,
})
require.NoError(t, err)
require.Len(t, dc.DBClusters, 1)
c := dc.DBClusters[0]
require.Equal(t, "available", *c.Status)
require.Equal(t, "docdb", *c.Engine)
require.Equal(t, endpointAddress, *c.Endpoint)
require.True(t, *c.StorageEncrypted, "storage encryption defaults to true")
t.Logf("docdb-cluster: %s available (engine docdb %s, encrypted)", clusterID, *c.EngineVersion)

// --- 2. The single instance is db.t3.medium. ---
require.Len(t, c.DBClusterMembers, 1)
instanceID := *c.DBClusterMembers[0].DBInstanceIdentifier
di, err := client.DescribeDBInstances(ctx, &rds.DescribeDBInstancesInput{
DBInstanceIdentifier: &instanceID,
})
require.NoError(t, err)
require.Len(t, di.DBInstances, 1)
require.Equal(t, "db.t3.medium", *di.DBInstances[0].DBInstanceClass)
t.Logf("docdb-cluster: instance %s is db.t3.medium", instanceID)

// --- 3. Attached secret carries the merged connection fields incl. the
// mongo/ssl fields the MongoDB rotation Lambda requires (port is a JSON
// NUMBER -- CFN SecretTargetAttachment parity). ---
secretValue := aws.GetSecretValue(t, awsRegion, secretArn)
var connection map[string]interface{}
require.NoError(t, json.Unmarshal([]byte(secretValue), &connection))
require.Equal(t, "docadmin", connection["username"])
require.NotEmpty(t, connection["password"])
require.Equal(t, "mongo", connection["engine"])
require.Equal(t, "true", connection["ssl"])
require.Equal(t, endpointAddress, connection["host"])
require.Equal(t, float64(*c.Port), connection["port"])
require.Equal(t, clusterID, connection["dbClusterIdentifier"])
t.Logf("docdb-cluster: attached secret carries mongo connection details incl. dbClusterIdentifier=%s", clusterID)

// --- Drift oracle: re-planning the already-applied stack must show zero changes. ---
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)
}
102 changes: 102 additions & 0 deletions src/aws/storage/docdb/cluster-ref.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
// https://github.com/aws/aws-cdk/blob/v2.263.0/packages/aws-cdk-lib/aws-docdb/lib/cluster-ref.ts

import type { Endpoint } from "./endpoint";
import { IAwsConstruct } from "../../aws-construct";
import * as ec2 from "../../compute";
import * as secretsmanager from "../../encryption";

/**
* Create a clustered database with a given number of instances.
*
* TODO: omitted — upstream also extends `aws_docdb.IDBClusterRef`, a CloudFormation cross-stack
* "Reference" marker interface generated from the CFN resource spec. TerraConstructs has no
* equivalent generated-reference layer (identical omission to `dbClusterRef` on `IDatabaseCluster`
* in `../rds/cluster-ref.ts` — see the TODO there), so `dbClusterRef` is dropped —
* https://github.com/aws/aws-cdk/blob/v2.263.0/packages/aws-cdk-lib/aws-docdb/lib/cluster-ref.ts#L10
*/
export interface IDatabaseCluster
extends IAwsConstruct,
ec2.IConnectable,
secretsmanager.ISecretAttachmentTarget {
/**
* Identifier of the cluster
*/
readonly clusterIdentifier: string;

/**
* Identifiers of the replicas
*/
readonly instanceIdentifiers: string[];

/**
* The endpoint to use for read/write operations
*/
readonly clusterEndpoint: Endpoint;

/**
* Endpoint to use for load-balanced read-only operations.
*/
readonly clusterReadEndpoint: Endpoint;

/**
* Endpoints which address each individual replica.
*/
readonly instanceEndpoints: Endpoint[];

/**
* The security group for this database cluster
*/
readonly securityGroupId: string;
}

/**
* Properties that describe an existing cluster instance
*/
export interface DatabaseClusterAttributes {
/**
* The database port
*
* @default - none
*/
readonly port?: number;

/**
* The security group of the database cluster
*
* @default - no security groups
*/
readonly securityGroup?: ec2.ISecurityGroup;

/**
* Identifier for the cluster
*/
readonly clusterIdentifier: string;

/**
* Identifier for the instances
*
* @default - no instance identifiers
*/
readonly instanceIdentifiers?: string[];

/**
* Cluster endpoint address
*
* @default - no cluster endpoint address
*/
readonly clusterEndpointAddress?: string;

/**
* Reader endpoint address
*
* @default - no reader endpoint address
*/
readonly readerEndpointAddress?: string;

/**
* Endpoint addresses of individual instances
*
* @default - no instance endpoint addresses
*/
readonly instanceEndpointAddresses?: string[];
}
Loading
Loading