Skip to content
Open
Show file tree
Hide file tree
Changes from 8 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 .projenrc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1555,6 +1555,7 @@ const cliInteg = configureProject(
'@octokit/rest@^20', // newer versions are ESM only
'@aws-sdk/client-codeartifact',
'@aws-sdk/client-cloudformation',
'@aws-sdk/client-dynamodb',
'@aws-sdk/client-ecr',
'@aws-sdk/client-ecr-public',
'@aws-sdk/client-ecs',
Expand Down
4 changes: 4 additions & 0 deletions packages/@aws-cdk-testing/cli-integ/.projen/deps.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/@aws-cdk-testing/cli-integ/.projen/tasks.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

29 changes: 15 additions & 14 deletions packages/@aws-cdk-testing/cli-integ/package.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

31 changes: 31 additions & 0 deletions packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/app/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -476,6 +476,35 @@ class LambdaStack extends cdk.Stack {
}
}

class OrphanableStack extends cdk.Stack {
constructor(parent, id, props) {
super(parent, id, props);

const table = new cdk.aws_dynamodb.Table(this, 'MyTable', {
partitionKey: { name: 'PK', type: cdk.aws_dynamodb.AttributeType.STRING },
billingMode: cdk.aws_dynamodb.BillingMode.PAY_PER_REQUEST,
removalPolicy: cdk.RemovalPolicy.RETAIN,
});

// Lambda that references the table via Ref (TABLE_NAME) and GetAtt (TABLE_ARN)
const fn = new lambda.Function(this, 'Consumer', {
runtime: lambda.Runtime.NODEJS_LATEST,
handler: 'index.handler',
code: lambda.Code.fromInline('exports.handler = async () => {}'),
environment: {
TABLE_NAME: table.tableName,
TABLE_ARN: table.tableArn,
},
});

table.grantReadData(fn);

new cdk.CfnOutput(this, 'TableName', { value: table.tableName });
new cdk.CfnOutput(this, 'TableArn', { value: table.tableArn });
new cdk.CfnOutput(this, 'FunctionName', { value: fn.functionName });
}
}

class DriftableStack extends cdk.Stack {
constructor(parent, id, props) {
const synthesizer = parent.node.tryGetContext('legacySynth') === 'true' ?
Expand Down Expand Up @@ -1046,6 +1075,8 @@ switch (stackSet) {

new DriftableStack(app, `${stackPrefix}-driftable`);

new OrphanableStack(app, `${stackPrefix}-orphanable`);

new EarlyValidationStack(app, `${stackPrefix}-early-validation-stack1`);
new EarlyValidationStack(app, `${stackPrefix}-early-validation-stack2`);
break;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { DescribeStacksCommand, GetTemplateCommand } from '@aws-sdk/client-cloudformation';
import { DynamoDBClient, PutItemCommand, GetItemCommand, DeleteTableCommand } from '@aws-sdk/client-dynamodb';
import * as yaml from 'yaml';
import { integTest, withDefaultFixture } from '../../../lib';

integTest(
'cdk orphan detaches a resource from the stack without deleting it',
withDefaultFixture(async (fixture) => {
const stackName = fixture.fullStackName('orphanable');

// Deploy the stack with a DynamoDB table + Lambda consumer
await fixture.cdkDeploy('orphanable');

// Get outputs
const describeResponse = await fixture.aws.cloudFormation.send(
new DescribeStacksCommand({ StackName: stackName }),
);
const outputs = describeResponse.Stacks?.[0]?.Outputs ?? [];
const tableName = outputs.find((o) => o.OutputKey === 'TableName')?.OutputValue;
expect(tableName).toBeDefined();

const dynamodb = new DynamoDBClient({ region: fixture.aws.region });

try {
// Verify the table resource exists in the template before orphaning
const templateBefore = await fixture.aws.cloudFormation.send(
new GetTemplateCommand({ StackName: stackName }),
);
const templateBodyBefore = yaml.parse(templateBefore.TemplateBody!);
expect(templateBodyBefore.Resources).toHaveProperty('MyTable794EDED1');

// Put an item in the table before orphan
await dynamodb.send(new PutItemCommand({
TableName: tableName!,
Item: { PK: { S: 'before-orphan' } },
}));

// Orphan the table
const orphanOutput = await fixture.cdk([
'orphan',
'--path', `${stackName}/MyTable`,
'--unstable=orphan',
'--force',
]);

// Verify the output contains a resource mapping for import
expect(orphanOutput).toContain('resource-mapping-inline');
expect(orphanOutput).toContain('TableName');

// Verify the template after orphan: table gone, Lambda env vars replaced with literals
const templateAfter = await fixture.aws.cloudFormation.send(
new GetTemplateCommand({ StackName: stackName }),
);
const templateBody = yaml.parse(templateAfter.TemplateBody!);

expect(templateBody.Resources).not.toHaveProperty('MyTable794EDED1');
expect(templateBody).toMatchObject({
Resources: expect.objectContaining({
Consumer8D6BE417: expect.objectContaining({
Type: 'AWS::Lambda::Function',
Properties: expect.objectContaining({
Environment: {
Variables: {
TABLE_NAME: expect.stringContaining('MyTable'),
TABLE_ARN: expect.stringContaining('arn:aws:dynamodb'),
},
},
}),
}),
}),
});

// Verify the table still exists and data is intact (strongly consistent read)
const getItemResult = await dynamodb.send(new GetItemCommand({
TableName: tableName!,
Key: { PK: { S: 'before-orphan' } },
ConsistentRead: true,
}));
expect(getItemResult.Item?.PK?.S).toBe('before-orphan');
} finally {
// Clean up the retained table to avoid leaking resources
await dynamodb.send(new DeleteTableCommand({ TableName: tableName! })).catch(() => {
});
}
}),
);
1 change: 1 addition & 0 deletions packages/@aws-cdk/toolkit-lib/docs/message-registry.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ Please let us know by [opening an issue](https://github.com/aws/aws-cdk-cli/issu
| `CDK_TOOLKIT_I8900` | Refactor result | `result` | {@link RefactorResult} |
| `CDK_TOOLKIT_I8910` | Confirm refactor | `info` | {@link ConfirmationRequest} |
| `CDK_TOOLKIT_W8010` | Refactor execution not yet supported | `warn` | n/a |
| `CDK_TOOLKIT_I8810` | Confirm orphan resources | `info` | {@link ConfirmationRequest} |
| `CDK_TOOLKIT_I9000` | Provides bootstrap times | `info` | {@link Duration} |
| `CDK_TOOLKIT_I9100` | Bootstrap progress | `info` | {@link BootstrapEnvironmentProgress} |
| `CDK_TOOLKIT_I9210` | Confirm the deletion of a batch of assets | `info` | {@link AssetBatchDeletionRequest} |
Expand Down
1 change: 1 addition & 0 deletions packages/@aws-cdk/toolkit-lib/lib/actions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export * from './destroy';
export * from './diff';
export * from './drift';
export * from './list';
export * from './orphan';
export * from './publish-assets';
export * from './refactor';
export * from './rollback';
Expand Down
26 changes: 26 additions & 0 deletions packages/@aws-cdk/toolkit-lib/lib/actions/orphan/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
export interface OrphanOptions {
/**
* Construct path prefix(es) to orphan. Each path must be in the format
* `StackName/ConstructPath`, e.g. `MyStack/MyTable`.
*
* The stack is derived from the path — all paths must reference the same stack.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Ah yeah? 😅

Bummer. But acceptable for now I guess.

*/
readonly constructPaths: string[];

/**
* Role to assume in the target environment.
*/
readonly roleArn?: string;

/**
* Toolkit stack name for bootstrap resources.
*/
readonly toolkitStackName?: string;

/**
* Whether to execute without prompting for confirmation.
*
* @default false
*/
readonly force?: boolean;
}
Loading
Loading