Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(core): RemovalPolicies.of(scope) #32283

Open
wants to merge 39 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
70f9e43
1.0
watany-dev Nov 26, 2024
47a41f1
integ first
watany-dev Dec 19, 2024
5d76562
integ
watany-dev Dec 19, 2024
3770eb8
readme
watany-dev Dec 19, 2024
d4a2db3
types
watany-dev Dec 19, 2024
2437d3c
exit resource
watany-dev Dec 19, 2024
3ca49c8
type check
watany-dev Dec 20, 2024
e0db4c8
integ
watany-dev Dec 20, 2024
42b82fd
rename
watany-dev Dec 21, 2024
710c35e
RemovalPolicys to RemovalPolicies
watany-dev Dec 21, 2024
2d90ae7
integ
watany-dev Dec 21, 2024
f8d1734
enum like class
watany-dev Dec 21, 2024
af4e0ff
miss spell
watany-dev Dec 22, 2024
5f6197c
adding `anyL1Type).CFN_RESOURCE_TYPE_NAME`
watany-dev Dec 22, 2024
c0d4fd7
remove try-catch
watany-dev Dec 22, 2024
f2df86d
Merge remote-tracking branch 'origin/main' into removalpolicy-all
watany-dev Dec 26, 2024
dec21e2
AspectPriority
watany-dev Dec 26, 2024
da6b4c5
readme updated
watany-dev Dec 26, 2024
467ebbd
integ
watany-dev Dec 26, 2024
c0733be
adding overwrite and priority
watany-dev Dec 27, 2024
bf5eff9
integ
watany-dev Dec 27, 2024
93fa6de
Update packages/aws-cdk-lib/core/lib/removal-policies.ts
watany-dev Dec 31, 2024
c1adaef
Update packages/aws-cdk-lib/core/README.md
watany-dev Dec 31, 2024
3da50fa
Update packages/aws-cdk-lib/core/test/removal-policies.test.ts
watany-dev Dec 31, 2024
832e33e
Update packages/aws-cdk-lib/core/test/removal-policies.test.ts
watany-dev Dec 31, 2024
f1fda5a
Update packages/aws-cdk-lib/core/test/removal-policies.test.ts
watany-dev Dec 31, 2024
9612fce
Update packages/aws-cdk-lib/core/test/removal-policies.test.ts
watany-dev Dec 31, 2024
5ee5ef2
Update packages/aws-cdk-lib/core/test/removal-policies.test.ts
watany-dev Dec 31, 2024
5a4a584
Update packages/aws-cdk-lib/core/test/removal-policies.test.ts
watany-dev Dec 31, 2024
baef89d
Update packages/aws-cdk-lib/core/test/removal-policies.test.ts
watany-dev Dec 31, 2024
cfe62be
Update packages/aws-cdk-lib/core/test/removal-policies.test.ts
watany-dev Dec 31, 2024
68436bf
Update packages/aws-cdk-lib/core/test/removal-policies.test.ts
watany-dev Dec 31, 2024
112f0f6
Update packages/aws-cdk-lib/core/test/removal-policies.test.ts
watany-dev Dec 31, 2024
94a454f
Update packages/aws-cdk-lib/core/test/removal-policies.test.ts
watany-dev Dec 31, 2024
f73db08
Merge branch 'main' into removalpolicy-all
watany-dev Jan 1, 2025
40d7a5f
Update packages/@aws-cdk-testing/framework-integ/test/core/test/integ…
watany-dev Jan 6, 2025
1876ce9
Update packages/aws-cdk-lib/core/lib/removal-policies.ts
watany-dev Jan 6, 2025
7c3e579
Merge branch 'main' into removalpolicy-all
watany-dev Jan 6, 2025
9436df3
remove import
watany-dev Jan 6, 2025
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
137 changes: 137 additions & 0 deletions packages/aws-cdk-lib/core/lib/removal-policys.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { IConstruct } from 'constructs';
import { Aspects, IAspect } from './aspect';
import { CfnResource } from './cfn-resource';
import { RemovalPolicy } from './removal-policy';

/**
* Properties for applying a removal policy
*/
export interface RemovalPolicyProps {
/**
* Apply the removal policy only to specific resource types.
* You can specify either the CloudFormation resource type (e.g., 'AWS::S3::Bucket')
* or the CDK resource class (e.g., CfnBucket).
* @default - apply to all resources
*/
readonly applyToResourceTypes?: Array<string | { new (...args: any[]): CfnResource }>;

/**
* Exclude specific resource types from the removal policy.
* You can specify either the CloudFormation resource type (e.g., 'AWS::S3::Bucket')
* or the CDK resource class (e.g., CfnBucket).
* @default - no exclusions
*/
readonly excludeResourceTypes?: Array<string | { new (...args: any[]): CfnResource }>;
}

/**
* The RemovalPolicyAspect handles applying a removal policy to resources
*/
class RemovalPolicyAspect implements IAspect {
constructor(
private readonly policy: RemovalPolicy,
private readonly props: RemovalPolicyProps = {},
) {}

private getResourceTypeFromClass(resourceClass: { new (...args: any[]): CfnResource }): string {
// Create a prototype instance to get the type without instantiating
const prototype = resourceClass.prototype;
if ('cfnResourceType' in prototype) {
return prototype.cfnResourceType;
}
// Fallback to checking constructor properties
const instance = Object.create(prototype);
return instance.constructor.CFN_RESOURCE_TYPE_NAME ?? '';
}

private matchesResourceType(resourceType: string, pattern: string | { new (...args: any[]): CfnResource }): boolean {
if (typeof pattern === 'string') {
return resourceType === pattern;
}
return resourceType === this.getResourceTypeFromClass(pattern);
}

public visit(node: IConstruct): void {
if (!CfnResource.isCfnResource(node)) {
return;
}

const cfnResource = node as CfnResource;
const resourceType = cfnResource.cfnResourceType;

// Skip if resource type is excluded
if (this.props.excludeResourceTypes?.some(pattern => this.matchesResourceType(resourceType, pattern))) {
return;
}

// Skip if specific resource types are specified and this one isn't included
if (this.props.applyToResourceTypes?.length &&
!this.props.applyToResourceTypes.some(pattern => this.matchesResourceType(resourceType, pattern))) {
return;
}

// Apply the removal policy
cfnResource.applyRemovalPolicy(this.policy);
}
}

/**
* Manages removal policies for all resources within a construct scope
*/
export class RemovalPolicys {
watany-dev marked this conversation as resolved.
Show resolved Hide resolved
/**
* Returns the removal policies API for the given scope
* @param scope The scope
*/
public static of(scope: IConstruct): RemovalPolicys {
return new RemovalPolicys(scope);
}

private constructor(private readonly scope: IConstruct) {}

/**
* Apply a removal policy to all resources within this scope
*
* @param policy The removal policy to apply
* @param props Configuration options
*/
public apply(policy: RemovalPolicy, props: RemovalPolicyProps = {}) {
Aspects.of(this.scope).add(new RemovalPolicyAspect(policy, props));
}

/**
* Apply DESTROY removal policy to all resources within this scope
*
* @param props Configuration options
*/
public destroy(props: RemovalPolicyProps = {}) {
this.apply(RemovalPolicy.DESTROY, props);
}

/**
* Apply RETAIN removal policy to all resources within this scope
*
* @param props Configuration options
*/
public retain(props: RemovalPolicyProps = {}) {
this.apply(RemovalPolicy.RETAIN, props);
}

/**
* Apply SNAPSHOT removal policy to all resources within this scope
*
* @param props Configuration options
*/
public snapshot(props: RemovalPolicyProps = {}) {
this.apply(RemovalPolicy.SNAPSHOT, props);
}

/**
* Apply RETAIN_ON_UPDATE_OR_DELETE removal policy to all resources within this scope
*
* @param props Configuration options
*/
public retainOnUpdateOrDelete(props: RemovalPolicyProps = {}) {
this.apply(RemovalPolicy.RETAIN_ON_UPDATE_OR_DELETE, props);
}
}
207 changes: 207 additions & 0 deletions packages/aws-cdk-lib/core/test/removal-policys.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
import { Construct } from 'constructs';
import { CfnResource, RemovalPolicy, Stack, Aspects } from '../lib';
import { synthesize } from '../lib/private/synthesis';
import { RemovalPolicys } from '../lib/removal-policys';

class TestResource extends CfnResource {
public static readonly CFN_RESOURCE_TYPE_NAME = 'AWS::Test::Resource';

constructor(scope: Construct, id: string) {
super(scope, id, {
type: TestResource.CFN_RESOURCE_TYPE_NAME,
});
}
}

class TestBucketResource extends CfnResource {
public static readonly CFN_RESOURCE_TYPE_NAME = 'AWS::S3::Bucket';

constructor(scope: Construct, id: string) {
super(scope, id, {
type: TestBucketResource.CFN_RESOURCE_TYPE_NAME,
});
}
}

class TestTableResource extends CfnResource {
public static readonly CFN_RESOURCE_TYPE_NAME = 'AWS::DynamoDB::Table';

constructor(scope: Construct, id: string) {
super(scope, id, {
type: TestTableResource.CFN_RESOURCE_TYPE_NAME,
});
}
}

describe('removal-policys', () => {
test('applies removal policy to all resources in scope', () => {
// GIVEN
const stack = new Stack();
const parent = new Construct(stack, 'Parent');
const resource1 = new TestResource(parent, 'Resource1');
const resource2 = new TestResource(parent, 'Resource2');

// WHEN
RemovalPolicys.of(parent).destroy();

// THEN
synthesize(stack);
expect(resource1.cfnOptions.deletionPolicy).toBe('Delete');
expect(resource2.cfnOptions.deletionPolicy).toBe('Delete');
});

test('applies removal policy only to specified resource types using strings', () => {
// GIVEN
const stack = new Stack();
const parent = new Construct(stack, 'Parent');
const bucket = new TestBucketResource(parent, 'Bucket');
const table = new TestTableResource(parent, 'Table');
const resource = new TestResource(parent, 'Resource');

// WHEN
RemovalPolicys.of(parent).retain({
applyToResourceTypes: ['AWS::S3::Bucket', 'AWS::DynamoDB::Table'],
});

// THEN
synthesize(stack);
expect(bucket.cfnOptions.deletionPolicy).toBe('Retain');
expect(table.cfnOptions.deletionPolicy).toBe('Retain');
expect(resource.cfnOptions.deletionPolicy).toBeUndefined();
});

test('applies removal policy only to specified resource types using classes', () => {
// GIVEN
const stack = new Stack();
const parent = new Construct(stack, 'Parent');
const bucket = new TestBucketResource(parent, 'Bucket');
const table = new TestTableResource(parent, 'Table');
const resource = new TestResource(parent, 'Resource');

// WHEN
RemovalPolicys.of(parent).retain({
applyToResourceTypes: [TestBucketResource, TestTableResource],
});

// THEN
synthesize(stack);
expect(bucket.cfnOptions.deletionPolicy).toBe('Retain');
expect(table.cfnOptions.deletionPolicy).toBe('Retain');
expect(resource.cfnOptions.deletionPolicy).toBeUndefined();
});

test('excludes specified resource types using strings', () => {
// GIVEN
const stack = new Stack();
const parent = new Construct(stack, 'Parent');
const bucket = new TestBucketResource(parent, 'Bucket');
const table = new TestTableResource(parent, 'Table');
const resource = new TestResource(parent, 'Resource');

// WHEN
RemovalPolicys.of(parent).snapshot({
excludeResourceTypes: ['AWS::Test::Resource'],
});

// THEN
synthesize(stack);
expect(bucket.cfnOptions.deletionPolicy).toBe('Snapshot');
expect(table.cfnOptions.deletionPolicy).toBe('Snapshot');
expect(resource.cfnOptions.deletionPolicy).toBeUndefined();
});

test('excludes specified resource types using classes', () => {
// GIVEN
const stack = new Stack();
const parent = new Construct(stack, 'Parent');
const bucket = new TestBucketResource(parent, 'Bucket');
const table = new TestTableResource(parent, 'Table');
const resource = new TestResource(parent, 'Resource');

// WHEN
RemovalPolicys.of(parent).snapshot({
excludeResourceTypes: [TestResource],
});

// THEN
synthesize(stack);
expect(bucket.cfnOptions.deletionPolicy).toBe('Snapshot');
expect(table.cfnOptions.deletionPolicy).toBe('Snapshot');
expect(resource.cfnOptions.deletionPolicy).toBeUndefined();
});

test('applies different removal policies', () => {
// GIVEN
const stack = new Stack();
const destroy = new TestResource(stack, 'DestroyResource');
const retain = new TestResource(stack, 'RetainResource');
const snapshot = new TestResource(stack, 'SnapshotResource');
const retainOnUpdate = new TestResource(stack, 'RetainOnUpdateResource');

// WHEN
RemovalPolicys.of(destroy).destroy();
RemovalPolicys.of(retain).retain();
RemovalPolicys.of(snapshot).snapshot();
RemovalPolicys.of(retainOnUpdate).retainOnUpdateOrDelete();

// THEN
synthesize(stack);
expect(destroy.cfnOptions.deletionPolicy).toBe('Delete');
expect(retain.cfnOptions.deletionPolicy).toBe('Retain');
expect(snapshot.cfnOptions.deletionPolicy).toBe('Snapshot');
expect(retainOnUpdate.cfnOptions.deletionPolicy).toBe('RetainExceptOnCreate');
});

test('last applied removal policy takes precedence', () => {
// GIVEN
const stack = new Stack();
const resource = new TestResource(stack, 'Resource');

// WHEN
RemovalPolicys.of(resource).destroy();
RemovalPolicys.of(resource).retain();
RemovalPolicys.of(resource).snapshot();

// THEN
synthesize(stack);
expect(resource.cfnOptions.deletionPolicy).toBe('Snapshot');
});

test('child scope can override parent scope removal policy', () => {
// GIVEN
const stack = new Stack();
const parent = new Construct(stack, 'Parent');
const child = new Construct(parent, 'Child');
const parentResource = new TestResource(parent, 'ParentResource');
const childResource = new TestResource(child, 'ChildResource');

// WHEN
RemovalPolicys.of(parent).destroy();
RemovalPolicys.of(child).retain();

// THEN
synthesize(stack);
expect(parentResource.cfnOptions.deletionPolicy).toBe('Delete');
expect(childResource.cfnOptions.deletionPolicy).toBe('Retain');
});

test('can mix string and class resource type specifications', () => {
// GIVEN
const stack = new Stack();
const parent = new Construct(stack, 'Parent');
const bucket = new TestBucketResource(parent, 'Bucket');
const table = new TestTableResource(parent, 'Table');
const resource = new TestResource(parent, 'Resource');

// WHEN
RemovalPolicys.of(parent).retain({
applyToResourceTypes: [TestBucketResource, 'AWS::DynamoDB::Table'],
});

// THEN
synthesize(stack);
expect(bucket.cfnOptions.deletionPolicy).toBe('Retain');
expect(table.cfnOptions.deletionPolicy).toBe('Retain');
expect(resource.cfnOptions.deletionPolicy).toBeUndefined();
});
});
Loading