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

Issue:
bug: Resource Extractor can't handle multiple references to the same Resource
### What is the problem?

Currently, the resource extractor is using the same name for a resource to Value Share to the future stacks. For example, if a KMS Key is being extracted, and a stack wants to know the `keyId` and the `keyArn`, only the first exported value works. In this example, it would only export the `keyId`.

### Reproduction Steps

```typescript
    // Lambda function references two values from a KMS Key
    const key = new Key(stack, 'TestKey');
    new Function(stack, 'TestLambda', {
      code: Code.fromInline(`def handler(event, context)\n    print(event)`),
      handler: 'index.handler',
      runtime: Runtime.PYTHON_3_9,
      environment: {
        KEY_ARN: key.keyArn,
        KEY_ID: key.keyId,
      },
    });

    const synthedApp = app.synth();
    Aspects.of(app).add(
      new ResourceExtractor({
        extractDestinationStack: extractedStack,
        stackArtifacts: synthedApp.stacks,
        valueShareMethod: ResourceExtractorShareMethod.CFN_OUTPUT,
        resourceTypesToExtract,
      })
    );
    app.synth({ force: true });
```

### What did you expect to happen?

Both values are correctly referenced.

### What actually happened?

Both values reference the Key ID

### cdk-enterprise-iac version

latest

### 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 8169758..90d0da3 100644
--- a/src/patches/resource-extractor/resourceExtractor.ts
+++ b/src/patches/resource-extractor/resourceExtractor.ts
@@ -440,18 +440,29 @@ export class ResourceExtractor implements IAspect {
    */
   private exportValue(stack: Stack, name: string, value: any): string {
     const stackName = stack.stackName;
-    const shareName = `${stackName}:${name}`;
+    let shareName = `${stackName}:${name}`;
+
+    // Intrinsic Function will resolve the value upon deployment
+    const intrinsic = stack.resolve(value);
+
+    // Support for multiple references to the same object
+    if ('Fn::GetAtt' in intrinsic) {
+      const attribute = intrinsic['Fn::GetAtt'][1];
+      shareName = shareName + `:${attribute}`;
+    }
 
     if (this.valueShareMethod == ResourceExtractorShareMethod.SSM_PARAMETER) {
-      const paramName = `/${shareName.replace(':', '/')}`;
-      const paramLogicalId = `p${name}`;
+      shareName = shareName.replace(/:/g, '/');
+      const paramName = `/${shareName}`;
+      const paramLogicalId = `p${shareName}`;
+
       if (!this.extractDestinationStack.node.tryFindChild(paramLogicalId)) {
         new CfnResource(this.extractDestinationStack, paramLogicalId, {
           type: 'AWS::SSM::Parameter',
           properties: {
             Name: paramName,
             Type: 'String',
-            Value: stack.resolve(value),
+            Value: intrinsic,
           },
         });
       }
@@ -459,11 +470,9 @@ export class ResourceExtractor implements IAspect {
     } else {
       // CFN_OUTPUT and API_LOOKUP share the same method of exporting values
       if (
-        !this.extractDestinationStack.node.tryFindChild(
-          `Export${stackName}:${name}`
-        )
+        !this.extractDestinationStack.node.tryFindChild(`Export${shareName}`)
       ) {
-        return this.extractDestinationStack.exportValue(stack.resolve(value), {
+        return this.extractDestinationStack.exportValue(intrinsic, {
           name: shareName,
         });
       } else {


Test patch:
diff --git a/test/patches/resource-extractor/resourceExtractor.test.ts b/test/patches/resource-extractor/resourceExtractor.test.ts
index febea35..48959a5 100644
--- a/test/patches/resource-extractor/resourceExtractor.test.ts
+++ b/test/patches/resource-extractor/resourceExtractor.test.ts
@@ -264,6 +264,56 @@ describe('Extracting resources from stack', () => {
     appTemplate.resourceCountIs('AWS::S3::Bucket', 1);
     appTemplate2.resourceCountIs('AWS::S3::Bucket', 1);
   });
+
+  test('Multiple values can be referenced from an Exported Resource', () => {
+    // Lambda function references two values from a KMS Key
+    const key = new Key(stack, 'TestKey');
+    new Function(stack, 'TestLambda', {
+      code: Code.fromInline(`def handler(event, context)\n    print(event)`),
+      handler: 'index.handler',
+      runtime: Runtime.PYTHON_3_9,
+      environment: {
+        KEY_ARN: key.keyArn,
+        KEY_ID: key.keyId,
+      },
+    });
+
+    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 kmsLogicalId = Object.keys(
+      extractedTemplate.findResources('AWS::KMS::Key')
+    )[0];
+
+    // Extracted stack has IAM resources
+    extractedTemplate.resourceCountIs('AWS::IAM::Role', 1);
+    extractedTemplate.resourceCountIs('AWS::KMS::Key', 1);
+
+    // Templates have synthesized and contains the function
+    appTemplate.resourceCountIs('AWS::Lambda::Function', 1);
+
+    // Extracted stack has multiple outputs for 1 resource (KMS Key)
+    extractedTemplate.hasOutput(`Export${appStackName}${kmsLogicalId}`, {
+      Export: {
+        Name: `${appStackName}:${kmsLogicalId}`,
+      },
+    });
+    extractedTemplate.hasOutput(`Export${appStackName}${kmsLogicalId}Arn`, {
+      Export: {
+        Name: `${appStackName}:${kmsLogicalId}:Arn`,
+      },
+    });
+  });
 });
 
 describe('Sharing Methods - CFN_OUTPUT', () => {
@@ -303,14 +353,14 @@ describe('Sharing Methods - CFN_OUTPUT', () => {
       extractedTemplate.findResources('AWS::IAM::Role')
     )[0];
 
-    extractedTemplate.hasOutput(`Export${appStackName}${roleLogicalId}`, {
+    extractedTemplate.hasOutput(`Export${appStackName}${roleLogicalId}Arn`, {
       Export: {
-        Name: `${appStackName}:${roleLogicalId}`,
+        Name: `${appStackName}:${roleLogicalId}:Arn`,
       },
     });
     appTemplate.hasResourceProperties('AWS::Lambda::Function', {
       Role: {
-        'Fn::ImportValue': `${appStackName}:${roleLogicalId}`,
+        'Fn::ImportValue': `${appStackName}:${roleLogicalId}:Arn`,
       },
     });
   });
@@ -498,7 +548,7 @@ describe('Sharing Methods - SSM_PARAMETER', () => {
     appTemplate.resourceCountIs('AWS::S3::Bucket', 1);
     appTemplate.resourceCountIs('AWS::Lambda::Function', 1);
     appTemplate.hasResourceProperties('AWS::Lambda::Function', {
-      Role: `dummy-value-for-/${appStackName}/${roleLogicalId}`,
+      Role: `dummy-value-for-/${appStackName}/${roleLogicalId}/Arn`,
     });
   });
 
@@ -586,7 +636,7 @@ describe('Sharing Methods - API_LOOKUP', () => {
     appTemplate.resourceCountIs('AWS::S3::Bucket', 1);
     appTemplate.resourceCountIs('AWS::Lambda::Function', 1);
     appTemplate.hasResourceProperties('AWS::Lambda::Function', {
-      Role: `dummy-value-for-${appStackName}:${roleLogicalId}`,
+      Role: `dummy-value-for-${appStackName}:${roleLogicalId}:Arn`,
     });
   });