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 .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