diff --git a/packages/@aws-cdk-testing/framework-integ/test/core/test/integ.removal-policies.js.snapshot/RemovalPoliciesTestDefaultTestDeployAssertF0ACFC0A.assets.json b/packages/@aws-cdk-testing/framework-integ/test/core/test/integ.removal-policies.js.snapshot/RemovalPoliciesTestDefaultTestDeployAssertF0ACFC0A.assets.json new file mode 100644 index 0000000000000..dc05445c7e546 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/core/test/integ.removal-policies.js.snapshot/RemovalPoliciesTestDefaultTestDeployAssertF0ACFC0A.assets.json @@ -0,0 +1,19 @@ +{ + "version": "40.0.0", + "files": { + "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22": { + "source": { + "path": "RemovalPoliciesTestDefaultTestDeployAssertF0ACFC0A.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/core/test/integ.removal-policies.js.snapshot/RemovalPoliciesTestDefaultTestDeployAssertF0ACFC0A.template.json b/packages/@aws-cdk-testing/framework-integ/test/core/test/integ.removal-policies.js.snapshot/RemovalPoliciesTestDefaultTestDeployAssertF0ACFC0A.template.json new file mode 100644 index 0000000000000..ad9d0fb73d1dd --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/core/test/integ.removal-policies.js.snapshot/RemovalPoliciesTestDefaultTestDeployAssertF0ACFC0A.template.json @@ -0,0 +1,36 @@ +{ + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/core/test/integ.removal-policies.js.snapshot/TestStack.assets.json b/packages/@aws-cdk-testing/framework-integ/test/core/test/integ.removal-policies.js.snapshot/TestStack.assets.json new file mode 100644 index 0000000000000..50b37905cc4b7 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/core/test/integ.removal-policies.js.snapshot/TestStack.assets.json @@ -0,0 +1,19 @@ +{ + "version": "40.0.0", + "files": { + "3738a8e1895f1c778b390af43a062f64cb576f54f0d5d4971c41fae883f802bd": { + "source": { + "path": "TestStack.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "3738a8e1895f1c778b390af43a062f64cb576f54f0d5d4971c41fae883f802bd.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/core/test/integ.removal-policies.js.snapshot/TestStack.template.json b/packages/@aws-cdk-testing/framework-integ/test/core/test/integ.removal-policies.js.snapshot/TestStack.template.json new file mode 100644 index 0000000000000..e262d32e58e49 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/core/test/integ.removal-policies.js.snapshot/TestStack.template.json @@ -0,0 +1,114 @@ +{ + "Resources": { + "TestBucket560B80BC": { + "Type": "AWS::S3::Bucket", + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "TestTable5769773A": { + "Type": "AWS::DynamoDB::Table", + "Properties": { + "AttributeDefinitions": [ + { + "AttributeName": "id", + "AttributeType": "S" + } + ], + "KeySchema": [ + { + "AttributeName": "id", + "KeyType": "HASH" + } + ], + "ProvisionedThroughput": { + "ReadCapacityUnits": 5, + "WriteCapacityUnits": 5 + } + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "TestUser6A619381": { + "Type": "AWS::IAM::User", + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "DestroyBucket924C7F03": { + "Type": "AWS::S3::Bucket", + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "MissingPoliciesTestPreConfigured993B6B53": { + "Type": "AWS::S3::Bucket", + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "MissingPoliciesTestNotConfiguredECEB0D31": { + "Type": "AWS::S3::Bucket", + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "FilteredMissingPoliciesTestBucketToRetainB723E6AC": { + "Type": "AWS::S3::Bucket", + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "FilteredMissingPoliciesTestTableToSkip835B5C39": { + "Type": "AWS::DynamoDB::Table", + "Properties": { + "AttributeDefinitions": [ + { + "AttributeName": "id", + "AttributeType": "S" + } + ], + "KeySchema": [ + { + "AttributeName": "id", + "KeyType": "HASH" + } + ], + "ProvisionedThroughput": { + "ReadCapacityUnits": 5, + "WriteCapacityUnits": 5 + } + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + } + }, + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/core/test/integ.removal-policies.js.snapshot/cdk.out b/packages/@aws-cdk-testing/framework-integ/test/core/test/integ.removal-policies.js.snapshot/cdk.out new file mode 100644 index 0000000000000..1e02a2deb191b --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/core/test/integ.removal-policies.js.snapshot/cdk.out @@ -0,0 +1 @@ +{"version":"40.0.0"} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/core/test/integ.removal-policies.js.snapshot/integ.json b/packages/@aws-cdk-testing/framework-integ/test/core/test/integ.removal-policies.js.snapshot/integ.json new file mode 100644 index 0000000000000..861347d396e1f --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/core/test/integ.removal-policies.js.snapshot/integ.json @@ -0,0 +1,12 @@ +{ + "version": "40.0.0", + "testCases": { + "RemovalPoliciesTest/DefaultTest": { + "stacks": [ + "TestStack" + ], + "assertionStack": "RemovalPoliciesTest/DefaultTest/DeployAssert", + "assertionStackName": "RemovalPoliciesTestDefaultTestDeployAssertF0ACFC0A" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/core/test/integ.removal-policies.js.snapshot/manifest.json b/packages/@aws-cdk-testing/framework-integ/test/core/test/integ.removal-policies.js.snapshot/manifest.json new file mode 100644 index 0000000000000..5e9ab845253e4 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/core/test/integ.removal-policies.js.snapshot/manifest.json @@ -0,0 +1,227 @@ +{ + "version": "40.0.0", + "artifacts": { + "TestStack.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "TestStack.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "TestStack": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "TestStack.template.json", + "terminationProtection": false, + "validateOnSynth": false, + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", + "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/3738a8e1895f1c778b390af43a062f64cb576f54f0d5d4971c41fae883f802bd.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "TestStack.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "dependencies": [ + "TestStack.assets" + ], + "metadata": { + "/TestStack/TestBucket": [ + { + "type": "aws:cdk:analytics:construct", + "data": "*" + } + ], + "/TestStack/TestBucket/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "TestBucket560B80BC" + } + ], + "/TestStack/TestTable": [ + { + "type": "aws:cdk:analytics:construct", + "data": { + "partitionKey": { + "name": "*", + "type": "S" + } + } + } + ], + "/TestStack/TestTable/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "TestTable5769773A" + } + ], + "/TestStack/TestTable/ScalingRole": [ + { + "type": "aws:cdk:analytics:construct", + "data": "*" + } + ], + "/TestStack/TestUser": [ + { + "type": "aws:cdk:analytics:construct", + "data": "*" + } + ], + "/TestStack/TestUser/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "TestUser6A619381" + } + ], + "/TestStack/DestroyBucket/Default": [ + { + "type": "aws:cdk:analytics:construct", + "data": "*" + } + ], + "/TestStack/DestroyBucket/Default/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "DestroyBucket924C7F03" + } + ], + "/TestStack/MissingPoliciesTest/PreConfigured": [ + { + "type": "aws:cdk:analytics:construct", + "data": { + "removalPolicy": "destroy" + } + } + ], + "/TestStack/MissingPoliciesTest/PreConfigured/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "MissingPoliciesTestPreConfigured993B6B53" + } + ], + "/TestStack/MissingPoliciesTest/NotConfigured": [ + { + "type": "aws:cdk:analytics:construct", + "data": "*" + } + ], + "/TestStack/MissingPoliciesTest/NotConfigured/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "MissingPoliciesTestNotConfiguredECEB0D31" + } + ], + "/TestStack/FilteredMissingPoliciesTest/BucketToRetain": [ + { + "type": "aws:cdk:analytics:construct", + "data": "*" + } + ], + "/TestStack/FilteredMissingPoliciesTest/BucketToRetain/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "FilteredMissingPoliciesTestBucketToRetainB723E6AC" + } + ], + "/TestStack/FilteredMissingPoliciesTest/TableToSkip": [ + { + "type": "aws:cdk:analytics:construct", + "data": { + "partitionKey": { + "name": "*", + "type": "S" + } + } + } + ], + "/TestStack/FilteredMissingPoliciesTest/TableToSkip/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "FilteredMissingPoliciesTestTableToSkip835B5C39" + } + ], + "/TestStack/FilteredMissingPoliciesTest/TableToSkip/ScalingRole": [ + { + "type": "aws:cdk:analytics:construct", + "data": "*" + } + ], + "/TestStack/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/TestStack/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "TestStack" + }, + "RemovalPoliciesTestDefaultTestDeployAssertF0ACFC0A.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "RemovalPoliciesTestDefaultTestDeployAssertF0ACFC0A.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "RemovalPoliciesTestDefaultTestDeployAssertF0ACFC0A": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "RemovalPoliciesTestDefaultTestDeployAssertF0ACFC0A.template.json", + "terminationProtection": false, + "validateOnSynth": false, + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", + "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "RemovalPoliciesTestDefaultTestDeployAssertF0ACFC0A.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "dependencies": [ + "RemovalPoliciesTestDefaultTestDeployAssertF0ACFC0A.assets" + ], + "metadata": { + "/RemovalPoliciesTest/DefaultTest/DeployAssert/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/RemovalPoliciesTest/DefaultTest/DeployAssert/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "RemovalPoliciesTest/DefaultTest/DeployAssert" + }, + "Tree": { + "type": "cdk:tree", + "properties": { + "file": "tree.json" + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/core/test/integ.removal-policies.js.snapshot/tree.json b/packages/@aws-cdk-testing/framework-integ/test/core/test/integ.removal-policies.js.snapshot/tree.json new file mode 100644 index 0000000000000..ad5d9a661de90 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/core/test/integ.removal-policies.js.snapshot/tree.json @@ -0,0 +1,399 @@ +{ + "version": "tree-0.1", + "tree": { + "id": "App", + "path": "", + "children": { + "TestStack": { + "id": "TestStack", + "path": "TestStack", + "children": { + "TestBucket": { + "id": "TestBucket", + "path": "TestStack/TestBucket", + "children": { + "Resource": { + "id": "Resource", + "path": "TestStack/TestBucket/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::S3::Bucket", + "aws:cdk:cloudformation:props": {} + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_s3.CfnBucket", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_s3.Bucket", + "version": "0.0.0", + "metadata": [ + "*" + ] + } + }, + "TestTable": { + "id": "TestTable", + "path": "TestStack/TestTable", + "children": { + "Resource": { + "id": "Resource", + "path": "TestStack/TestTable/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::DynamoDB::Table", + "aws:cdk:cloudformation:props": { + "attributeDefinitions": [ + { + "attributeName": "id", + "attributeType": "S" + } + ], + "keySchema": [ + { + "attributeName": "id", + "keyType": "HASH" + } + ], + "provisionedThroughput": { + "readCapacityUnits": 5, + "writeCapacityUnits": 5 + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_dynamodb.CfnTable", + "version": "0.0.0" + } + }, + "ScalingRole": { + "id": "ScalingRole", + "path": "TestStack/TestTable/ScalingRole", + "constructInfo": { + "fqn": "aws-cdk-lib.Resource", + "version": "0.0.0", + "metadata": [ + "*" + ] + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_dynamodb.Table", + "version": "0.0.0", + "metadata": [ + { + "partitionKey": { + "name": "*", + "type": "S" + } + } + ] + } + }, + "TestUser": { + "id": "TestUser", + "path": "TestStack/TestUser", + "children": { + "Resource": { + "id": "Resource", + "path": "TestStack/TestUser/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::IAM::User", + "aws:cdk:cloudformation:props": {} + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_iam.CfnUser", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_iam.User", + "version": "0.0.0", + "metadata": [ + "*" + ] + } + }, + "DestroyBucket": { + "id": "DestroyBucket", + "path": "TestStack/DestroyBucket", + "children": { + "Default": { + "id": "Default", + "path": "TestStack/DestroyBucket/Default", + "children": { + "Resource": { + "id": "Resource", + "path": "TestStack/DestroyBucket/Default/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::S3::Bucket", + "aws:cdk:cloudformation:props": {} + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_s3.CfnBucket", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_s3.Bucket", + "version": "0.0.0", + "metadata": [ + "*" + ] + } + } + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.4.2" + } + }, + "MissingPoliciesTest": { + "id": "MissingPoliciesTest", + "path": "TestStack/MissingPoliciesTest", + "children": { + "PreConfigured": { + "id": "PreConfigured", + "path": "TestStack/MissingPoliciesTest/PreConfigured", + "children": { + "Resource": { + "id": "Resource", + "path": "TestStack/MissingPoliciesTest/PreConfigured/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::S3::Bucket", + "aws:cdk:cloudformation:props": {} + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_s3.CfnBucket", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_s3.Bucket", + "version": "0.0.0", + "metadata": [ + { + "removalPolicy": "destroy" + } + ] + } + }, + "NotConfigured": { + "id": "NotConfigured", + "path": "TestStack/MissingPoliciesTest/NotConfigured", + "children": { + "Resource": { + "id": "Resource", + "path": "TestStack/MissingPoliciesTest/NotConfigured/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::S3::Bucket", + "aws:cdk:cloudformation:props": {} + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_s3.CfnBucket", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_s3.Bucket", + "version": "0.0.0", + "metadata": [ + "*" + ] + } + } + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.4.2" + } + }, + "FilteredMissingPoliciesTest": { + "id": "FilteredMissingPoliciesTest", + "path": "TestStack/FilteredMissingPoliciesTest", + "children": { + "BucketToRetain": { + "id": "BucketToRetain", + "path": "TestStack/FilteredMissingPoliciesTest/BucketToRetain", + "children": { + "Resource": { + "id": "Resource", + "path": "TestStack/FilteredMissingPoliciesTest/BucketToRetain/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::S3::Bucket", + "aws:cdk:cloudformation:props": {} + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_s3.CfnBucket", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_s3.Bucket", + "version": "0.0.0", + "metadata": [ + "*" + ] + } + }, + "TableToSkip": { + "id": "TableToSkip", + "path": "TestStack/FilteredMissingPoliciesTest/TableToSkip", + "children": { + "Resource": { + "id": "Resource", + "path": "TestStack/FilteredMissingPoliciesTest/TableToSkip/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::DynamoDB::Table", + "aws:cdk:cloudformation:props": { + "attributeDefinitions": [ + { + "attributeName": "id", + "attributeType": "S" + } + ], + "keySchema": [ + { + "attributeName": "id", + "keyType": "HASH" + } + ], + "provisionedThroughput": { + "readCapacityUnits": 5, + "writeCapacityUnits": 5 + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_dynamodb.CfnTable", + "version": "0.0.0" + } + }, + "ScalingRole": { + "id": "ScalingRole", + "path": "TestStack/FilteredMissingPoliciesTest/TableToSkip/ScalingRole", + "constructInfo": { + "fqn": "aws-cdk-lib.Resource", + "version": "0.0.0", + "metadata": [ + "*" + ] + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_dynamodb.Table", + "version": "0.0.0", + "metadata": [ + { + "partitionKey": { + "name": "*", + "type": "S" + } + } + ] + } + } + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.4.2" + } + }, + "BootstrapVersion": { + "id": "BootstrapVersion", + "path": "TestStack/BootstrapVersion", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnParameter", + "version": "0.0.0" + } + }, + "CheckBootstrapVersion": { + "id": "CheckBootstrapVersion", + "path": "TestStack/CheckBootstrapVersion", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnRule", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.Stack", + "version": "0.0.0" + } + }, + "RemovalPoliciesTest": { + "id": "RemovalPoliciesTest", + "path": "RemovalPoliciesTest", + "children": { + "DefaultTest": { + "id": "DefaultTest", + "path": "RemovalPoliciesTest/DefaultTest", + "children": { + "Default": { + "id": "Default", + "path": "RemovalPoliciesTest/DefaultTest/Default", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.4.2" + } + }, + "DeployAssert": { + "id": "DeployAssert", + "path": "RemovalPoliciesTest/DefaultTest/DeployAssert", + "children": { + "BootstrapVersion": { + "id": "BootstrapVersion", + "path": "RemovalPoliciesTest/DefaultTest/DeployAssert/BootstrapVersion", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnParameter", + "version": "0.0.0" + } + }, + "CheckBootstrapVersion": { + "id": "CheckBootstrapVersion", + "path": "RemovalPoliciesTest/DefaultTest/DeployAssert/CheckBootstrapVersion", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnRule", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.Stack", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests-alpha.IntegTestCase", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests-alpha.IntegTest", + "version": "0.0.0" + } + }, + "Tree": { + "id": "Tree", + "path": "Tree", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.4.2" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.App", + "version": "0.0.0" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/core/test/integ.removal-policies.ts b/packages/@aws-cdk-testing/framework-integ/test/core/test/integ.removal-policies.ts new file mode 100644 index 0000000000000..a7b71e937a83a --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/core/test/integ.removal-policies.ts @@ -0,0 +1,53 @@ +import { App, MissingRemovalPolicies, RemovalPolicies, RemovalPolicy, Stack } from 'aws-cdk-lib'; +import * as dynamodb from 'aws-cdk-lib/aws-dynamodb'; +import * as iam from 'aws-cdk-lib/aws-iam'; +import * as s3 from 'aws-cdk-lib/aws-s3'; +import * as integ from '@aws-cdk/integ-tests-alpha'; +import { Construct } from 'constructs'; + +const app = new App(); +const stack = new Stack(app, 'TestStack'); + +new s3.Bucket(stack, 'TestBucket'); + +new dynamodb.Table(stack, 'TestTable', { + partitionKey: { name: 'id', type: dynamodb.AttributeType.STRING }, +}); + +new iam.User(stack, 'TestUser'); + +const destroyBucket = new Construct(stack, 'DestroyBucket'); +new s3.Bucket(destroyBucket, 'Default'); + +RemovalPolicies.of(stack).retain({ + priority: 50, +}); +RemovalPolicies.of(destroyBucket).destroy({ + priority: 100, +}); + +// Missing Policies +const missingPoliciesTest = new Construct(stack, 'MissingPoliciesTest'); +new s3.Bucket(missingPoliciesTest, 'PreConfigured', { + removalPolicy: RemovalPolicy.DESTROY, +}); + +new s3.Bucket(missingPoliciesTest, 'NotConfigured'); + +MissingRemovalPolicies.of(missingPoliciesTest).retain(); + +const filteredTest = new Construct(stack, 'FilteredMissingPoliciesTest'); +new s3.Bucket(filteredTest, 'BucketToRetain'); +new dynamodb.Table(filteredTest, 'TableToSkip', { + partitionKey: { name: 'id', type: dynamodb.AttributeType.STRING }, +}); + +MissingRemovalPolicies.of(filteredTest).snapshot({ + applyToResourceTypes: [ + 's3.CfnBucket', + ], +}); + +new integ.IntegTest(app, 'RemovalPoliciesTest', { + testCases: [stack], +}); diff --git a/packages/aws-cdk-lib/core/README.md b/packages/aws-cdk-lib/core/README.md index 2b32f279b0ec3..5f4ebd35f2a8d 100644 --- a/packages/aws-cdk-lib/core/README.md +++ b/packages/aws-cdk-lib/core/README.md @@ -1800,4 +1800,72 @@ warning by the `id`. Annotations.of(this).acknowledgeWarning('IAM:Group:MaxPoliciesExceeded', 'Account has quota increased to 20'); ``` +## RemovalPolicies + +The `RemovalPolicies` class provides a convenient way to manage removal policies for AWS CDK resources within a construct scope. It allows you to apply removal policies to multiple resources at once, with options to include or exclude specific resource types. + +### Usage + +```typescript +import { RemovalPolicies, MissingRemovalPolicies } from 'aws-cdk-lib'; + +// Apply DESTROY policy to all resources in a scope +RemovalPolicies.of(scope).destroy(); + +// Apply RETAIN policy to all resources in a scope +RemovalPolicies.of(scope).retain(); + +// Apply SNAPSHOT policy to all resources in a scope +RemovalPolicies.of(scope).snapshot(); + +// Apply RETAIN_ON_UPDATE_OR_DELETE policy to all resources in a scope +RemovalPolicies.of(scope).retainOnUpdateOrDelete(); + +// Apply RETAIN policy only to specific resource types +RemovalPolicies.of(parent).retain({ + applyToResourceTypes: [ + 'AWS::DynamoDB::Table', + bucket.cfnResourceType, // 'AWS::S3::Bucket' + CfnDBInstance.CFN_RESOURCE_TYPE_NAME, // 'AWS::RDS::DBInstance' + ], +}); + +// Apply SNAPSHOT policy excluding specific resource types +RemovalPolicies.of(scope).snapshot({ + excludeResourceTypes: ['AWS::Test::Resource'], +}); +``` + +### RemovalPolicies vs MissingRemovalPolicies + +CDK provides two different classes for managing removal policies: + +- RemovalPolicies: Always applies the specified removal policy, overriding any existing policies. +- MissingRemovalPolicies: Applies the removal policy only to resources that don't already have a policy set. + +```typescript +// Override any existing policies +RemovalPolicies.of(scope).retain(); + +// Only apply to resources without existing policies +MissingRemovalPolicies.of(scope).retain(); +``` + +### Aspect Priority + +Both RemovalPolicies and MissingRemovalPolicies are implemented as Aspects. You can control the order in which they're applied using the priority parameter: + +```typescript +// Apply in a specific order based on priority +RemovalPolicies.of(stack).retain({ priority: 100 }); +RemovalPolicies.of(stack).destroy({ priority: 200 }); // This will override the RETAIN policy +``` + +For RemovalPolicies, the policies are applied in order of aspect execution, with the last applied policy overriding previous ones. The priority only affects the order in which aspects are applied during synthesis. + +#### Note + +When using MissingRemovalPolicies with priority, a warning will be issued as this can lead to unexpected behavior. This is because MissingRemovalPolicies only applies to resources without existing policies, making priority less relevant. + + diff --git a/packages/aws-cdk-lib/core/lib/index.ts b/packages/aws-cdk-lib/core/lib/index.ts index de8f05498ff9a..8f94a53ee8f92 100644 --- a/packages/aws-cdk-lib/core/lib/index.ts +++ b/packages/aws-cdk-lib/core/lib/index.ts @@ -29,6 +29,7 @@ export * from './cfn-dynamic-reference'; export * from './cfn-tag'; export * from './cfn-json'; export * from './removal-policy'; +export * from './removal-policies'; export * from './arn'; export * from './duration'; export * from './expiration'; diff --git a/packages/aws-cdk-lib/core/lib/removal-policies.ts b/packages/aws-cdk-lib/core/lib/removal-policies.ts new file mode 100644 index 0000000000000..8b89923c023a0 --- /dev/null +++ b/packages/aws-cdk-lib/core/lib/removal-policies.ts @@ -0,0 +1,249 @@ +import { IConstruct } from 'constructs'; +import { Annotations } from './annotations'; +import { Aspects, IAspect, AspectPriority } 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. + * Can be a CloudFormation resource type string (e.g., 'AWS::S3::Bucket'). + * @default - apply to all resources + */ + readonly applyToResourceTypes?: string[]; + + /** + * Exclude specific resource types from the removal policy. + * Can be a CloudFormation resource type string (e.g., 'AWS::S3::Bucket'). + * @default - no exclusions + */ + readonly excludeResourceTypes?: string[]; + + /** + * The priority to use when applying this policy. + * + * The priority affects only the order in which aspects are applied during synthesis. + * For RemovalPolicies, the last applied policy will override previous ones. + * + * NOTE: Priority does NOT determine which policy "wins" when there are conflicts. + * The order of application determines the final policy, with later policies + * overriding earlier ones. + * + * @default - AspectPriority.MUTATING + */ + readonly priority?: number; +} + +/** + * Base class for removal policy aspects + */ +abstract class BaseRemovalPolicyAspect implements IAspect { + constructor( + protected readonly policy: RemovalPolicy, + protected readonly props: RemovalPolicyProps = {}, + ) {} + + /** + * Checks if the given resource type matches any of the patterns + */ + protected resourceTypeMatchesPatterns(resourceType: string, patterns?: string[]): boolean { + if (!patterns || patterns.length === 0) { + return false; + } + return patterns.includes(resourceType); + } + + /** + * Determines if the removal policy should be applied to the given resource + */ + protected abstract shouldApplyPolicy(cfnResource: CfnResource): boolean; + + public visit(node: IConstruct): void { + if (!CfnResource.isCfnResource(node)) { + return; + } + + const cfnResource = node as CfnResource; + const resourceType = cfnResource.cfnResourceType; + + if (this.resourceTypeMatchesPatterns(resourceType, this.props.excludeResourceTypes)) { + return; + } + + if ( + this.props.applyToResourceTypes?.length && + !this.resourceTypeMatchesPatterns(resourceType, this.props.applyToResourceTypes) + ) { + return; + } + + if (this.shouldApplyPolicy(cfnResource)) { + // Apply the removal policy + cfnResource.applyRemovalPolicy(this.policy); + } + } +} + +/** + * The RemovalPolicyAspect handles applying a removal policy to resources, + * overriding any existing policies + */ +class RemovalPolicyAspect extends BaseRemovalPolicyAspect { + protected shouldApplyPolicy(_cfnResource: CfnResource): boolean { + // RemovalPolicyAspect always applies the policy, regardless of existing policies + return true; + } +} + +/** + * The MissingRemovalPolicyAspect handles applying a removal policy only to resources + * that don't already have a policy set + */ +class MissingRemovalPolicyAspect extends BaseRemovalPolicyAspect { + protected shouldApplyPolicy(cfnResource: CfnResource): boolean { + // For MissingRemovalPolicies, we only apply the policy if one doesn't already exist + const userAlreadySetPolicy = + cfnResource.cfnOptions.deletionPolicy !== undefined || + cfnResource.cfnOptions.updateReplacePolicy !== undefined; + + return !userAlreadySetPolicy; + } +} + +/** + * Manages removal policies for all resources within a construct scope, + * overriding any existing policies by default + */ +export class RemovalPolicies { + /** + * Returns the removal policies API for the given scope + * @param scope The scope + */ + public static of(scope: IConstruct): RemovalPolicies { + return new RemovalPolicies(scope); + } + + private constructor(private readonly scope: IConstruct) {} + + /** + * Apply a removal policy to all resources within this scope, + * overriding any existing policies + * + * @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), { + priority: props.priority ?? AspectPriority.MUTATING, + }); + } + + /** + * 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); + } +} + +/** + * Manages removal policies for resources without existing policies within a construct scope + */ +export class MissingRemovalPolicies { + /** + * Returns the missing removal policies API for the given scope + * @param scope The scope + */ + public static of(scope: IConstruct): MissingRemovalPolicies { + return new MissingRemovalPolicies(scope); + } + + private constructor(private readonly scope: IConstruct) {} + + /** + * Apply a removal policy only to resources without existing policies 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 MissingRemovalPolicyAspect(policy, props), { + priority: props.priority ?? AspectPriority.MUTATING, + }); + + if (props.priority !== undefined) { + Annotations.of(this.scope).addWarningV2( + `Warning MissingRemovalPolicies with priority in ${this.scope.node.path}`, + 'Applying a MissingRemovalPolicy with `priority` can lead to unexpected behavior since it only applies to resources without existing policies. Please refer to the documentation for more details.', + ); + } + } + + /** + * Apply DESTROY removal policy only to resources without existing policies within this scope + * + * @param props Configuration options + */ + public destroy(props: RemovalPolicyProps = {}) { + this.apply(RemovalPolicy.DESTROY, props); + } + + /** + * Apply RETAIN removal policy only to resources without existing policies within this scope + * + * @param props Configuration options + */ + public retain(props: RemovalPolicyProps = {}) { + this.apply(RemovalPolicy.RETAIN, props); + } + + /** + * Apply SNAPSHOT removal policy only to resources without existing policies within this scope + * + * @param props Configuration options + */ + public snapshot(props: RemovalPolicyProps = {}) { + this.apply(RemovalPolicy.SNAPSHOT, props); + } + + /** + * Apply RETAIN_ON_UPDATE_OR_DELETE removal policy only to resources without existing policies within this scope + * + * @param props Configuration options + */ + public retainOnUpdateOrDelete(props: RemovalPolicyProps = {}) { + this.apply(RemovalPolicy.RETAIN_ON_UPDATE_OR_DELETE, props); + } +} diff --git a/packages/aws-cdk-lib/core/test/aspect.test.ts b/packages/aws-cdk-lib/core/test/aspect.test.ts index feade4c79f10f..034a0c2010969 100644 --- a/packages/aws-cdk-lib/core/test/aspect.test.ts +++ b/packages/aws-cdk-lib/core/test/aspect.test.ts @@ -1,9 +1,13 @@ import { Construct, IConstruct } from 'constructs'; +import { getWarnings } from './util'; import { Template } from '../../assertions'; import { Bucket, CfnBucket } from '../../aws-s3'; import * as cxschema from '../../cloud-assembly-schema'; import { App, CfnResource, Stack, Tag, Tags } from '../lib'; import { IAspect, Aspects, AspectPriority } from '../lib/aspect'; +import { MissingRemovalPolicies, RemovalPolicies } from '../lib/removal-policies'; +import { RemovalPolicy } from '../lib/removal-policy'; + class MyConstruct extends Construct { public static IsMyConstruct(x: any): x is MyConstruct { return x.visitCounter !== undefined; @@ -300,7 +304,7 @@ describe('aspect', () => { test('Infinitely recursing Aspect is caught with error', () => { const app = new App({ context: { '@aws-cdk/core:aspectStabilization': true } }); const root = new Stack(app, 'My-Stack'); - const child = new MyConstruct(root, 'MyConstruct'); + new MyConstruct(root, 'MyConstruct'); Aspects.of(root).add(new InfiniteAspect()); @@ -343,4 +347,113 @@ describe('aspect', () => { app.synth(); }).not.toThrow(); }); + + test('RemovalPolicy: higher priority wins', () => { + const app = new App(); + const stack = new Stack(app, 'My-Stack'); + new Bucket(stack, 'my-bucket', { + removalPolicy: RemovalPolicy.RETAIN, + }); + + RemovalPolicies.of(stack).apply(RemovalPolicy.DESTROY, { + priority: 100, + }); + + RemovalPolicies.of(stack).apply(RemovalPolicy.RETAIN, { + priority: 200, + }); + + Template.fromStack(stack).hasResource('AWS::S3::Bucket', { + UpdateReplacePolicy: 'Retain', + DeletionPolicy: 'Retain', + }); + }); + + test('RemovalPolicy: last one wins when priorities are equal', () => { + const app = new App(); + const stack = new Stack(app, 'My-Stack'); + new Bucket(stack, 'my-bucket', { + removalPolicy: RemovalPolicy.RETAIN, + }); + + RemovalPolicies.of(stack).apply(RemovalPolicy.DESTROY, { + priority: 100, + }); + + RemovalPolicies.of(stack).apply(RemovalPolicy.RETAIN, { + priority: 100, + }); + + Template.fromStack(stack).hasResource('AWS::S3::Bucket', { + UpdateReplacePolicy: 'Retain', + DeletionPolicy: 'Retain', + }); + }); + + test('MissingRemovalPolicy: default removal policy is respected', () => { + const app = new App(); + const stack = new Stack(app, 'My-Stack'); + new Bucket(stack, 'my-bucket', { + removalPolicy: RemovalPolicy.RETAIN, + }); + + MissingRemovalPolicies.of(stack).apply(RemovalPolicy.DESTROY, { + priority: 100, + }); + + Template.fromStack(stack).hasResource('AWS::S3::Bucket', { + UpdateReplacePolicy: 'Retain', + DeletionPolicy: 'Retain', + }); + }); + + test('RemovalPolicy: multiple aspects in chain', () => { + const app = new App(); + const stack = new Stack(app, 'My-Stack'); + new Bucket(stack, 'my-bucket', { + removalPolicy: RemovalPolicy.RETAIN, + }); + + RemovalPolicies.of(stack).apply(RemovalPolicy.DESTROY, { + priority: 100, + }); + + RemovalPolicies.of(stack).apply(RemovalPolicy.RETAIN, { + priority: 200, + }); + + RemovalPolicies.of(stack).apply(RemovalPolicy.SNAPSHOT, { + priority: 300, + }); + + Template.fromStack(stack).hasResource('AWS::S3::Bucket', { + UpdateReplacePolicy: 'Snapshot', + DeletionPolicy: 'Snapshot', + }); + }); + + test('RemovalPolicy: different resource type', () => { + const app = new App(); + const stack = new Stack(app, 'My-Stack'); + new CfnResource(stack, 'my-resource', { + type: 'AWS::EC2::Instance', + properties: { + ImageId: 'ami-1234567890abcdef0', + InstanceType: 't2.micro', + }, + }).applyRemovalPolicy(RemovalPolicy.DESTROY); + + RemovalPolicies.of(stack).apply(RemovalPolicy.RETAIN, { + priority: 100, + }); + + Template.fromStack(stack).hasResource('AWS::EC2::Instance', { + Properties: { + ImageId: 'ami-1234567890abcdef0', + InstanceType: 't2.micro', + }, + UpdateReplacePolicy: 'Retain', + DeletionPolicy: 'Retain', + }); + }); }); diff --git a/packages/aws-cdk-lib/core/test/removal-policies.test.ts b/packages/aws-cdk-lib/core/test/removal-policies.test.ts new file mode 100644 index 0000000000000..c79b29eefdead --- /dev/null +++ b/packages/aws-cdk-lib/core/test/removal-policies.test.ts @@ -0,0 +1,353 @@ +import { Construct } from 'constructs'; +import { getWarnings } from './util'; +import { App, CfnResource, Stack } from '../lib'; +import { synthesize } from '../lib/private/synthesis'; +import { RemovalPolicies, MissingRemovalPolicies } from '../lib/removal-policies'; +import { RemovalPolicy } from '../lib/removal-policy'; + +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-policies', () => { + 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 + RemovalPolicies.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', () => { + // 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 + RemovalPolicies.of(parent).retain({ + applyToResourceTypes: [ + bucket.cfnResourceType, // 'AWS::S3::Bucket' + TestTableResource.CFN_RESOURCE_TYPE_NAME, // 'AWS::DynamoDB::Table' + ], + }); + + // 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', () => { + // 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 + RemovalPolicies.of(parent).snapshot({ + excludeResourceTypes: [ + TestResource.CFN_RESOURCE_TYPE_NAME, // 'AWS::Test::Resource' + ], + }); + + // 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 + RemovalPolicies.of(destroy).destroy(); + RemovalPolicies.of(retain).retain(); + RemovalPolicies.of(snapshot).snapshot(); + RemovalPolicies.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('RemovalPolicies overrides existing policies by default', () => { + // GIVEN + const stack = new Stack(); + const resource = new TestResource(stack, 'Resource'); + + // WHEN + RemovalPolicies.of(resource).destroy(); + synthesize(stack); + expect(resource.cfnOptions.deletionPolicy).toBe('Delete'); + + RemovalPolicies.of(resource).retain(); + synthesize(stack); + expect(resource.cfnOptions.deletionPolicy).toBe('Retain'); + + RemovalPolicies.of(resource).snapshot(); + synthesize(stack); + expect(resource.cfnOptions.deletionPolicy).toBe('Snapshot'); + }); + + test('child scope can override parent scope removal policy by default', () => { + // 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 + RemovalPolicies.of(parent).destroy(); + RemovalPolicies.of(child).retain(); + + // THEN + synthesize(stack); + expect(parentResource.cfnOptions.deletionPolicy).toBe('Delete'); + expect(childResource.cfnOptions.deletionPolicy).toBe('Retain'); + }); + + test('RemovalPolicies applies policies in order, with the last one overriding previous ones regardless of priority', () => { + // GIVEN + const stack = new Stack(); + const resource = new TestResource(stack, 'PriorityResource'); + + // WHEN - despite higher priority (10), destroy is applied first and gets overridden by retainOnUpdateOrDelete + RemovalPolicies.of(stack).destroy({ priority: 10 }); + RemovalPolicies.of(stack).retainOnUpdateOrDelete({ priority: 250 }); + + // THEN + synthesize(stack); + expect(resource.cfnOptions.deletionPolicy).toBe('RetainExceptOnCreate'); + }); + + test('RemovalPolicies application order determines the final policy, not priority', () => { + // GIVEN + const stack = new Stack(); + const resource = new TestResource(stack, 'PriorityResource'); + + // WHEN + RemovalPolicies.of(stack).retainOnUpdateOrDelete({ priority: 10 }); + RemovalPolicies.of(stack).destroy({ priority: 250 }); + + // THEN + synthesize(stack); + expect(resource.cfnOptions.deletionPolicy).toBe('Delete'); + }); +}); + +describe('missing-removal-policies', () => { + test('applies removal policy only to resources without existing policies', () => { + // GIVEN + const stack = new Stack(); + const parent = new Construct(stack, 'Parent'); + const resource1 = new TestResource(parent, 'Resource1'); + const resource2 = new TestResource(parent, 'Resource2'); + + // Set a policy on resource1 + resource1.applyRemovalPolicy(RemovalPolicy.RETAIN); + + // WHEN + MissingRemovalPolicies.of(parent).destroy(); + // THEN + synthesize(stack); + expect(resource1.cfnOptions.deletionPolicy).toBe('Retain'); // Unchanged + expect(resource2.cfnOptions.deletionPolicy).toBe('Delete'); // Applied + }); + + test('applies removal policy only to specified resource types without existing policies', () => { + // GIVEN + const stack = new Stack(); + const parent = new Construct(stack, 'Parent'); + const bucket1 = new TestBucketResource(parent, 'Bucket1'); + const bucket2 = new TestBucketResource(parent, 'Bucket2'); + const table = new TestTableResource(parent, 'Table'); + + // Set a policy on bucket1 + bucket1.applyRemovalPolicy(RemovalPolicy.RETAIN); + + // WHEN + MissingRemovalPolicies.of(parent).snapshot({ + applyToResourceTypes: [ + TestBucketResource.CFN_RESOURCE_TYPE_NAME, // 'AWS::S3::Bucket' + ], + }); + // THEN + synthesize(stack); + expect(bucket1.cfnOptions.deletionPolicy).toBe('Retain'); // Unchanged + expect(bucket2.cfnOptions.deletionPolicy).toBe('Snapshot'); // Applied + expect(table.cfnOptions.deletionPolicy).toBeUndefined(); // Not applied (wrong type) + }); + + test('excludes specified resource types from missing removal policies', () => { + // 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 + MissingRemovalPolicies.of(parent).retain({ + excludeResourceTypes: [ + TestTableResource.CFN_RESOURCE_TYPE_NAME, // 'AWS::DynamoDB::Table' + ], + }); + + // THEN + synthesize(stack); + expect(bucket.cfnOptions.deletionPolicy).toBe('Retain'); + expect(table.cfnOptions.deletionPolicy).toBeUndefined(); + expect(resource.cfnOptions.deletionPolicy).toBe('Retain'); + }); + + test('applies different missing 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 + MissingRemovalPolicies.of(destroy).destroy(); + MissingRemovalPolicies.of(retain).retain(); + MissingRemovalPolicies.of(snapshot).snapshot(); + MissingRemovalPolicies.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('MissingRemovalPolicies does not override existing policies', () => { + // GIVEN + const stack = new Stack(); + const resource = new TestResource(stack, 'Resource'); + // WHEN + resource.applyRemovalPolicy(RemovalPolicy.RETAIN); + synthesize(stack); + expect(resource.cfnOptions.deletionPolicy).toBe('Retain'); + + MissingRemovalPolicies.of(resource).destroy(); + synthesize(stack); + expect(resource.cfnOptions.deletionPolicy).toBe('Retain'); // Unchanged + + MissingRemovalPolicies.of(resource).snapshot(); + synthesize(stack); + expect(resource.cfnOptions.deletionPolicy).toBe('Retain'); // Still unchanged + }); + + test('child scope MissingRemovalPolicies does not override parent scope RemovalPolicies', () => { + // GIVEN + const stack = new Stack(); + const parent = new Construct(stack, 'Parent'); + const child = new Construct(parent, 'Child'); + const childResource = new TestResource(child, 'ChildResource'); + // WHEN + RemovalPolicies.of(parent).destroy(); + MissingRemovalPolicies.of(child).retain(); + + // THEN + synthesize(stack); + expect(childResource.cfnOptions.deletionPolicy).toBe('Delete'); // Parent policy applied + }); + + test('parent scope MissingRemovalPolicies does not override child scope RemovalPolicies', () => { + // GIVEN + const stack = new Stack(); + const parent = new Construct(stack, 'Parent'); + const child = new Construct(parent, 'Child'); + const childResource = new TestResource(child, 'ChildResource'); + // WHEN + MissingRemovalPolicies.of(parent).destroy(); + RemovalPolicies.of(child).retain(); + + // THEN + synthesize(stack); + expect(childResource.cfnOptions.deletionPolicy).toBe('Retain'); // Child policy applied + }); + + test('RemovalPolicy aspect overrides where MissingRemovalPolicy does not', () => { + // GIVEN + const stack = new Stack(); + const bucket = new TestBucketResource(stack, 'Bucket'); + // WHEN - this is the example from the discussion + // const stack = new Stack(app); + // new MyThirdPartyBucket(stack, 'Bucket'); + // RemovalPolicies.of(stack).apply(RemovalPolicy.RETAIN); + // Simulate the bucket already having a policy (as if set by MyThirdPartyBucket) + bucket.applyRemovalPolicy(RemovalPolicy.DESTROY); + + // Apply the policy using RemovalPolicies (overrides) + RemovalPolicies.of(stack).retain(); + + // THEN + synthesize(stack); + expect(bucket.cfnOptions.deletionPolicy).toBe('Retain'); // Overridden + + // WHEN - reset and try with MissingRemovalPolicies + const stack2 = new Stack(); + const bucket2 = new TestBucketResource(stack2, 'Bucket'); + + // Simulate the bucket already having a policy (as if set by MyThirdPartyBucket) + bucket2.applyRemovalPolicy(RemovalPolicy.DESTROY); + + // Apply the policy using MissingRemovalPolicies (doesn't override) + MissingRemovalPolicies.of(stack2).retain(); + + // THEN + synthesize(stack2); + expect(bucket2.cfnOptions.deletionPolicy).toBe('Delete'); // Not overridden + }); +});