Repository: cdklabs/cdk-enterprise-iac (language: python)

Issue:
bug: Resource Extractor does not work with RetentionPolicy
### What is the problem?

When using Retention Policies on a Resource that is going to be extracted, the retention policy properties are trashed and do not end up in the extracted stack.

### Reproduction Steps

Add a RetentionPolicy to an extracted resource.

### What did you expect to happen?

The "DeletionPolicy" and "UpdateReplacePolicy" props are on the resource in the extracted stack.

### What actually happened?

These properties are removed

### cdk-enterprise-iac version

0.0.233

### Language

Typescript, Python

### Other information

_No response_


Discussion / hints:

Fix patch:
diff --git a/src/patches/resource-extractor/resourceExtractor.ts b/src/patches/resource-extractor/resourceExtractor.ts
index 8354594..18980b9 100644
--- a/src/patches/resource-extractor/resourceExtractor.ts
+++ b/src/patches/resource-extractor/resourceExtractor.ts
@@ -157,7 +157,19 @@ export class ResourceExtractor implements IAspect {
   private processCfnResource(node: CfnResource): void {
     const stackName = node.stack.stackName;
     const logicalId = node.stack.resolve(node.logicalId);
-    const props = this.cfn.templates[stackName].Resources[logicalId].Properties;
+    const cfnResourceJson = this.cfn.templates[stackName].Resources[logicalId];
+    const props = cfnResourceJson.Properties;
+
+    // Create a new object that contains other allowed top level properties
+    // other than `Type` and `Properties` so that we can ensure that they
+    // are correctly set on the extracted object
+    const allowedTopLevelKeys = ['UpdateReplacePolicy', 'DeletionPolicy'];
+    const topLevelProps = Object.keys(cfnResourceJson)
+      .filter((k) => allowedTopLevelKeys.includes(k))
+      .reduce((obj, key) => {
+        obj[key] = cfnResourceJson[key];
+        return obj;
+      }, {} as Json);
 
     // Check if props include references to resources that _aren't_ being
     // extracted
@@ -184,10 +196,13 @@ export class ResourceExtractor implements IAspect {
         node.stack.resolve(logicalId)
       )
     ) {
-      new CfnResource(this.extractDestinationStack, logicalId, {
+      const r = new CfnResource(this.extractDestinationStack, logicalId, {
         type: node.cfnResourceType,
         properties: modifiedProps || props,
       });
+      Object.keys(topLevelProps).forEach((k) => {
+        r.addOverride(k, topLevelProps[k]);
+      });
     }
 
     // Look in the node's stack for any refs to the resource being extracted


Test patch:
diff --git a/test/patches/resource-extractor/resourceExtractor.test.ts b/test/patches/resource-extractor/resourceExtractor.test.ts
index e47c3e6..0a2feea 100644
--- a/test/patches/resource-extractor/resourceExtractor.test.ts
+++ b/test/patches/resource-extractor/resourceExtractor.test.ts
@@ -155,6 +155,33 @@ describe('Extracting resources from stack', () => {
     });
   });
 
+  test('Allowed top level properties are extracted correctly', () => {
+    const role = new Role(stack, 'TestRole', {
+      assumedBy: new ServicePrincipal('lambda.amazonaws.com'),
+    });
+    role.applyRemovalPolicy(RemovalPolicy.RETAIN);
+
+    const synthedApp = app.synth();
+    Aspects.of(app).add(
+      new ResourceExtractor({
+        extractDestinationStack: extractedStack,
+        stackArtifacts: synthedApp.stacks,
+        valueShareMethod: ResourceExtractorShareMethod.CFN_OUTPUT,
+        resourceTypesToExtract,
+      })
+    );
+    app.synth({ force: true });
+
+    const extractedTemplate = Template.fromStack(extractedStack);
+
+    // Extracted stack has IAM resources
+    extractedTemplate.resourceCountIs('AWS::IAM::Role', 1);
+    extractedTemplate.hasResource('AWS::IAM::Role', {
+      DeletionPolicy: 'Retain',
+      UpdateReplacePolicy: 'Retain',
+    });
+  });
+
   test('Dynamically build string with exported values', () => {
     const role = new Role(stack, 'TestRole', {
       assumedBy: new ServicePrincipal('lambda.amazonaws.com'),