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

Issue:
bug: Resource Extractor fails to deal with constructs that create the same resource name twice
### What is the problem?

When utilizing the Resource Extractor, if you are using a construct (in different stacks) that creates underlying resources with a specific name, the Resource Extractor fails to process them

### Reproduction Steps

Create two stacks, with the resource extractor extracting IAM Roles and Policies

Stack 1:

Create a bucket with the following:

```typescript
        const bucket = new s3.Bucket(this, 'TestBucket1', {
            autoDeleteObjects: true,
            removalPolicy: RemovalPolicy.DESTROY
        });
```

Stack 2:

Create a bucket with the following:

```typescript
        const bucket = new s3.Bucket(this, 'TestBucket2', {
            autoDeleteObjects: true,
            removalPolicy: RemovalPolicy.DESTROY
        });
```

### What did you expect to happen?

Resource Extractor runs successfully and the CDK app synthesizes correctly.

### What actually happened?

The following error is thrown:

```
RuntimeError: TypeError: Cannot read properties of undefined (reading 'stack')
```

### cdk-enterprise-iac version

v0.0.86

### 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 49463be..8169758 100644
--- a/src/patches/resource-extractor/resourceExtractor.ts
+++ b/src/patches/resource-extractor/resourceExtractor.ts
@@ -159,81 +159,82 @@ export class ResourceExtractor implements IAspect {
     const logicalId = node.stack.resolve(node.logicalId);
     const props = this.cfn.templates[stackName].Resources[logicalId].Properties;
 
-    /** Recreate resource in extracted stack and export value */
+    // Check if props include references to resources that _aren't_ being
+    // extracted
+    const flattenedProps = Flattener.flattenObject(props);
+    const propsToAdjust = Object.keys(flattenedProps).filter((key) =>
+      Object.keys(this.logicalIdsNotGettingExtracted).includes(
+        flattenedProps[key]
+      )
+    );
+
+    // If properties being extracted to another stack contain
+    // references to resources in the original stack, we need to adjust
+    let modifiedProps: Json = {};
+    if (propsToAdjust) {
+      modifiedProps = this.modifyExtractedResourceProperties({
+        props,
+        propsToAdjust,
+      });
+    }
+
+    // Add to extracted stack
     if (
       !this.extractDestinationStack.node.tryFindChild(
         node.stack.resolve(logicalId)
       )
     ) {
-      // Check if props include references to resources that _aren't_ being
-      // extracted
-      const flattenedProps = Flattener.flattenObject(props);
-      const propsToAdjust = Object.keys(flattenedProps).filter((key) =>
-        Object.keys(this.logicalIdsNotGettingExtracted).includes(
-          flattenedProps[key]
-        )
-      );
-
-      // If properties being extracted to another stack contain
-      // references to resources in the original stack, we need to adjust
-      let modifiedProps: Json = {};
-      if (propsToAdjust) {
-        modifiedProps = this.modifyExtractedResourceProperties({
-          props,
-          propsToAdjust,
-        });
-      }
-
-      // Add to extracted stack
-      const res = new CfnResource(this.extractDestinationStack, logicalId, {
+      new CfnResource(this.extractDestinationStack, logicalId, {
         type: node.cfnResourceType,
         properties: modifiedProps || props,
       });
+    }
 
-      // Look in all existing stacks for any refs to the resource being
-      // extracted
-      const foundRefs = Object.keys(this.cfn.flatTemplates).filter(
-        (key) => this.cfn.flatTemplates[key] === logicalId
-      );
+    // Look in the node's stack for any refs to the resource being extracted
+    const foundRefsAllStacks = Object.keys(this.cfn.flatTemplates).filter(
+      (key) => this.cfn.flatTemplates[key] === logicalId
+    );
+    const foundRefs = foundRefsAllStacks.filter(
+      (k) => k.split('.')[0] === stackName
+    );
 
-      // loop through resources in stack with Cfn intrinsic functions
-      // referencing extracted resources
-      for (const foundRef of foundRefs) {
-        // See if the found ref is also getting extracted. Ignore if so.
-        if (this.isRefAlsoGettingExtracted(foundRef)) continue;
-
-        // Get the CfnResource that is referencing this extracted resource
-        const foundRefNode = this.getResourceFromFoundRef(node, foundRef) as
-          | CfnResource
-          | CfnOutput;
-
-        // Figure out the pattern of how extracted resource is being referenced
-        // e.g. using `Fn::GetAtt` or `Ref`
-        const exportValue = this.cfn.determineExportValue(node, foundRef);
-        if (exportValue) {
-          // Generate export in ExportedStack, and return the `Fn::ImportValue`
-          // method to use when referencing in the App stack
-          const importValue = this.exportValue(
-            node.stack,
-            this.extractDestinationStack.resolve(res.logicalId),
-            exportValue
-          );
+    // loop through resources in stack with Cfn intrinsic functions
+    // referencing extracted resources
+    for (const foundRef of foundRefs) {
+      // See if the found ref is also getting extracted. Ignore if so.
+      if (this.isRefAlsoGettingExtracted(foundRef)) continue;
+
+      // Get the CfnResource that is referencing this extracted resource
+      const foundRefNode = this.getResourceFromFoundRef(node, foundRef) as
+        | CfnResource
+        | CfnOutput;
+
+      // Figure out the pattern of how extracted resource is being referenced
+      // e.g. using `Fn::GetAtt` or `Ref`
+      const exportValue = this.cfn.determineExportValue(node, foundRef);
+      if (exportValue) {
+        // Generate export in ExportedStack, and return the `Fn::ImportValue`
+        // method to use when referencing in the App stack
+        const importValue = this.exportValue(
+          node.stack,
+          this.extractDestinationStack.resolve(logicalId),
+          exportValue
+        );
 
-          // Override any ref to extracted resource
-          this.overrideFoundRefWithImportValue({
-            foundRefNode,
-            importValue,
-            flattenedKey: foundRef,
-          });
-        }
+        // Override any ref to extracted resource
+        this.overrideFoundRefWithImportValue({
+          foundRefNode,
+          importValue,
+          flattenedKey: foundRef,
+        });
+      }
 
-        // Remove any DependsOn references
-        if (foundRefNode instanceof CfnResource) {
-          this.deletionOverrideDependsOn({
-            foundRefNode,
-            flattenedKey: foundRef,
-          });
-        }
+      // Remove any DependsOn references
+      if (foundRefNode instanceof CfnResource) {
+        this.deletionOverrideDependsOn({
+          foundRefNode,
+          flattenedKey: foundRef,
+        });
       }
     }
 


Test patch:
diff --git a/test/patches/resource-extractor/resourceExtractor.test.ts b/test/patches/resource-extractor/resourceExtractor.test.ts
index 9adc2ea..febea35 100644
--- a/test/patches/resource-extractor/resourceExtractor.test.ts
+++ b/test/patches/resource-extractor/resourceExtractor.test.ts
@@ -229,6 +229,41 @@ describe('Extracting resources from stack', () => {
     appTemplate.resourceCountIs('AWS::EC2::Instance', 1);
     appTemplate.resourceCountIs('AWS::RDS::DBInstance', 1);
   });
+
+  test('Constructs can create resources with the same name', () => {
+    const secondStack = new Stack(app, 'Stack2');
+    new Bucket(stack, 'TestBucket1', {
+      autoDeleteObjects: true,
+      removalPolicy: RemovalPolicy.DESTROY,
+    });
+
+    new Bucket(secondStack, 'TestBucket2', {
+      autoDeleteObjects: true,
+      removalPolicy: RemovalPolicy.DESTROY,
+    });
+
+    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);
+    const appTemplate = Template.fromStack(stack);
+    const appTemplate2 = Template.fromStack(secondStack);
+
+    // Extracted stack has IAM resources
+    extractedTemplate.resourceCountIs('AWS::IAM::Role', 1);
+
+    // Templates have synthesized and contain the buckets in the two stacks
+    appTemplate.resourceCountIs('AWS::S3::Bucket', 1);
+    appTemplate2.resourceCountIs('AWS::S3::Bucket', 1);
+  });
 });
 
 describe('Sharing Methods - CFN_OUTPUT', () => {