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
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ require (
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16 // indirect
github.com/aws/aws-sdk-go-v2/service/neptune v1.48.4 // indirect
github.com/aws/aws-sdk-go-v2/service/rds v1.91.0 // indirect
github.com/aws/aws-sdk-go-v2/service/redshift v1.65.4 // indirect
github.com/aws/aws-sdk-go-v2/service/route53 v1.46.2 // indirect
github.com/aws/aws-sdk-go-v2/service/signin v1.0.4 // indirect
github.com/aws/aws-sdk-go-v2/service/ssm v1.56.0 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@ github.com/aws/aws-sdk-go-v2/service/neptune v1.48.4 h1:OKqe8wmRV8/b0PgfEm2xBKUd
github.com/aws/aws-sdk-go-v2/service/neptune v1.48.4/go.mod h1:QpgA5y9y/SywZTBw8ylR5gDS4Uc2qlVYbudXmzr/VSw=
github.com/aws/aws-sdk-go-v2/service/rds v1.91.0 h1:eqHz3Uih+gb0vLE5Cc4Xf733vOxsxDp6GFUUVQU4d7w=
github.com/aws/aws-sdk-go-v2/service/rds v1.91.0/go.mod h1:h2jc7IleH3xHY7y+h8FH7WAZcz3IVLOB6/jXotIQ/qU=
github.com/aws/aws-sdk-go-v2/service/redshift v1.65.4 h1:kpCVKoiWkkd0Ma4Z03brq3sQpGRv47FTaaf0LgjsZwo=
github.com/aws/aws-sdk-go-v2/service/redshift v1.65.4/go.mod h1:sMXbazIzJ+VjS4GmSlSTRnIpC7bpLuFpt0Lhv+qYANs=
github.com/aws/aws-sdk-go-v2/service/route53 v1.46.2 h1:wmt05tPp/CaRZpPV5B4SaJ5TwkHKom07/BzHoLdkY1o=
github.com/aws/aws-sdk-go-v2/service/route53 v1.46.2/go.mod h1:d+K9HESMpGb1EU9/UmmpInbGIUcAkwmcY6ZO/A3zZsw=
github.com/aws/aws-sdk-go-v2/service/s3 v1.93.2 h1:U3ygWUhCpiSPYSHOrRhb3gOl9T5Y3kB8k5Vjs//57bE=
Expand Down
4 changes: 4 additions & 0 deletions integ/aws/storage/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ neptune.cluster: ## Test Neptune DatabaseCluster L2 (live serverless cluster + i
go test -v -count 1 -timeout 45m ./... -run ^TestNeptuneCluster$
.PHONY: neptune.cluster

redshift.cluster: ## Test Redshift Cluster L2 (live single-node ra3.large cluster)
go test -v -count 1 -timeout 45m ./... -run ^TestRedshiftCluster$
.PHONY: redshift.cluster

bucket-notifications: ## Test S3 Bucket with EventBridge Notifications
go test -v -count 1 -timeout 15m ./... -run ^TestBucketNotifications$
.PHONY: bucket-notifications
97 changes: 97 additions & 0 deletions integ/aws/storage/apps/redshift.cluster.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// Live test for the storage.redshift Cluster L2 (scope-reduced alpha port): a real
// single-node ra3.large Redshift cluster deployed through the ported construct in an
// isolated VPC. Validates the aws_redshift_cluster mapping, the generated-secret
// master-password double-freeze (drift oracle), the ClusterParameterGroup attachment,
// IAM role association plus the native default_iam_role_arn deviation
// (upstream: AwsCustomResource), and grid-scoped lowercased naming against live AWS.
import { App, LocalBackend, TerraformOutput } from "cdktn";
import { aws } 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 ?? "redshift.cluster";

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

const stack = new aws.AwsStack(app, stackName, {
gridUUID: "gcccccccc-cccc",
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 role = new aws.iam.Role(stack, "ClusterRole", {
assumedBy: new aws.iam.ServicePrincipal("redshift.amazonaws.com"),
});

const parameterGroup = new aws.storage.redshift.ClusterParameterGroup(
stack,
"Params",
{
description: "Redshift integ cluster parameter group",
parameters: {
require_ssl: "true",
},
},
);

const cluster = new aws.storage.redshift.Cluster(stack, "Cluster", {
// No masterPassword: exercises the generated DatabaseSecret + the
// master_password ignore_changes double-freeze (validated by the drift oracle).
masterUser: {
masterUsername: "admin",
},
vpc,
vpcSubnets: { subnetType: aws.compute.SubnetType.PRIVATE_ISOLATED },
clusterType: aws.storage.redshift.ClusterType.SINGLE_NODE,
nodeType: aws.storage.redshift.NodeType.RA3_LARGE,
parameterGroup,
roles: [role],
// Exercises the native default_iam_role_arn deviation (upstream shells out to
// modifyClusterIamRoles via an AwsCustomResource).
defaultRole: role,
// Terraform-native replacement for upstream removalPolicy: allow clean destroy.
skipFinalSnapshot: true,
});

new TerraformOutput(stack, "cluster_identifier", {
value: cluster.clusterName,
staticId: true,
});
new TerraformOutput(stack, "cluster_endpoint_address", {
value: cluster.clusterEndpoint.hostname,
staticId: true,
});
new TerraformOutput(stack, "parameter_group_name", {
value: parameterGroup.clusterParameterGroupName,
staticId: true,
});
new TerraformOutput(stack, "default_role_arn", {
value: role.roleArn,
staticId: true,
});
new TerraformOutput(stack, "secret_arn", {
value: cluster.secret!.secretArn,
staticId: true,
});

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

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

"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/service/redshift"
"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/redshift.cluster.ts integration test: a real single-node
// ra3.large Redshift cluster deployed through the storage.redshift Cluster L2
// (scope-reduced alpha port). Validates cluster read-back incl. the attached
// ClusterParameterGroup, IAM role association plus the native
// default_iam_role_arn deviation, the attach() protocol's merged secret,
// grid-scoped lowercased naming, and the post-apply drift oracle (the
// generated-secret master_password double-freeze).
func TestRedshiftCluster(t *testing.T) {
runStorageIntegrationTest(t, "redshift.cluster", "us-east-1", validateRedshiftCluster)
}

func validateRedshiftCluster(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)
parameterGroupName := outputs["parameter_group_name"].(string)
defaultRoleArn := outputs["default_role_arn"].(string)
secretArn := outputs["secret_arn"].(string)

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

// --- 1. Cluster read-back: status, node shape, encryption, endpoint, naming. ---
dc, err := client.DescribeClusters(ctx, &redshift.DescribeClustersInput{
ClusterIdentifier: &clusterID,
})
require.NoError(t, err)
require.Len(t, dc.Clusters, 1)
c := dc.Clusters[0]
require.Equal(t, "available", *c.ClusterStatus)
require.Equal(t, "ra3.large", *c.NodeType)
require.Equal(t, int32(1), *c.NumberOfNodes)
require.Equal(t, "admin", *c.MasterUsername)
require.True(t, *c.Encrypted, "encryption defaults to true")
require.False(t, *c.PubliclyAccessible)
require.Equal(t, endpointAddress, *c.Endpoint.Address)
t.Logf("redshift-cluster: %s available (%s x%d, encrypted, private)",
clusterID, *c.NodeType, *c.NumberOfNodes)

// --- 2. Parameter group attached and in-sync-able. ---
foundParams := false
for _, pg := range c.ClusterParameterGroups {
if *pg.ParameterGroupName == parameterGroupName {
foundParams = true
}
}
require.True(t, foundParams,
"cluster must be associated with the ported ClusterParameterGroup %s", parameterGroupName)
t.Logf("redshift-cluster: parameter group %s attached", parameterGroupName)

// --- 3. IAM role associated AND set as the cluster default (the native
// default_iam_role_arn deviation -- upstream uses an AwsCustomResource). ---
foundRole := false
for _, r := range c.IamRoles {
if *r.IamRoleArn == defaultRoleArn {
foundRole = true
}
}
require.True(t, foundRole, "role %s must be associated with the cluster", defaultRoleArn)
require.NotNil(t, c.DefaultIamRoleArn)
require.Equal(t, defaultRoleArn, *c.DefaultIamRoleArn,
"default_iam_role_arn must reach AWS (native replacement for upstream's AwsCustomResource)")
t.Logf("redshift-cluster: default IAM role %s set natively", defaultRoleArn)

// --- 4. Attached secret carries merged connection fields (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, "admin", connection["username"])
require.NotEmpty(t, connection["password"])
require.Equal(t, "redshift", connection["engine"])
require.Equal(t, endpointAddress, connection["host"])
require.Equal(t, float64(*c.Endpoint.Port), connection["port"])
require.Equal(t, clusterID, connection["dbClusterIdentifier"],
"attach() must merge dbClusterIdentifier (CFN SecretTargetAttachment parity)")
t.Logf("redshift-cluster: attached secret carries full connection details incl. dbClusterIdentifier=%s", clusterID)

// --- Drift oracle: re-planning the already-applied stack must show zero
// changes. Proves the generated-secret master_password ignore_changes
// double-freeze reads back cleanly. ---
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)
}
3 changes: 3 additions & 0 deletions src/aws/storage/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,6 @@ export * as elasticache from "./elasticache";

// aws-neptune-alpha
export * as neptune from "./neptune";

// aws-redshift-alpha
export * as redshift from "./redshift";
Loading
Loading