Skip to content

ServiceCatalog: Not possible to deploy two products within the same portfolio both of which have custom assets (lambdas) #25189

@ViktarKhomich

Description

@ViktarKhomich

Describe the bug

I am facing an issue while working with the service catalog. Specifically, I am trying to deploy two service catalog products within the same portfolio, both of which have custom assets (lambdas in this case). However, I am encountering an error during the synth process.

Error: There is already a Construct with name 'AssetsBucketDeployment' in PortfolioStack [ServiceCatalogProductsStack]
    at Node.addChild (.../node_modules/constructs/src/construct.ts:403:13)
    at new Node (.../node_modules/constructs/src/construct.ts:71:17)
    at new Construct (.../node_modules/constructs/src/construct.ts:463:17)
    at new BucketDeployment (.../node_modules/aws-cdk-lib/aws-s3-deployment/lib/bucket-deployment.ts:261:5)
    at ProductStackSynthesizer.addFileAsset (.../node_modules/aws-cdk-lib/aws-servicecatalog/lib/private/product-stack-synthesizer.ts:33:31)
    at new Asset (.../node_modules/aws-cdk-lib/aws-s3-assets/lib/asset.ts:146:40)

When I am using custom assets only for one product, then everything works fine.

Expected Behavior

I expect to be able to deploy two or more products within the same portfolio which can have the custom assets.

Current Behavior

I am able to use custom assets in every product within the same portfolio.

Reproduction Steps

cdk app with which bug can be easily reproduced:

import * as s3 from 'aws-cdk-lib/aws-s3';
import * as servicecatalog from 'aws-cdk-lib/aws-servicecatalog';
import * as nodejs from 'aws-cdk-lib/aws-lambda-nodejs';
import * as logs from 'aws-cdk-lib/aws-logs';
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';

export const handler = async () => {};

class ProductAWithCustomAsset extends servicecatalog.ProductStack {
  constructor(scope: Construct, id: string, props?: servicecatalog.ProductStackProps) {
    super(scope, id, props);

    new nodejs.NodejsFunction(this, 'LambdaA', {
      entry: 'lib/cdk-stack.ts',
      handler: 'handler',
      timeout: cdk.Duration.minutes(3),
      logRetention: logs.RetentionDays.TWO_WEEKS,
    });
  }
}

class ProductBWithCustomAsset extends servicecatalog.ProductStack {
  constructor(scope: Construct, id: string, props?: servicecatalog.ProductStackProps) {
    super(scope, id, props);
   
   // if you comment this out then cdk synth will work
    new nodejs.NodejsFunction(this, 'LambdaB', {
      entry: 'lib/cdk-stack.ts',
      handler: 'handler',
      timeout: cdk.Duration.minutes(3),
      logRetention: logs.RetentionDays.TWO_WEEKS,
    });
  }
}

export class ExamplePortfolioStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const portfolio = new servicecatalog.Portfolio(this, 'Portfolio', {
      displayName: 'MyPortfolio',
      providerName: 'MyProvider',
    });

    const assetBucket = new s3.Bucket(this, 'ProductAssetBucket', {
      bucketName: 'products-asset-bucket',
      encryption: s3.BucketEncryption.S3_MANAGED,
      enforceSSL: true,
      blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
    });

    const productAStackHistory = new servicecatalog.ProductStackHistory(this, 'ProductAStackHistory', {
      productStack: new ProductAWithCustomAsset(this, 'ProductAWithCustomAsset', { assetBucket }),
      currentVersionName: 'v1',
      currentVersionLocked: false,
    });

    const productA = new servicecatalog.CloudFormationProduct(this, 'ProductAId', {
      productName: 'Product A',
      owner: 'Owner A',
      productVersions: [
        productAStackHistory.currentVersion(),
      ],
    });


    portfolio.addProduct(productA);

    const productBStackHistory = new servicecatalog.ProductStackHistory(this, 'ProductBStackHistory', {
      productStack: new ProductBWithCustomAsset(this, 'ProductBWithCustomAsset', { assetBucket }),
      currentVersionName: 'v1',
      currentVersionLocked: false,
    });

    const productB = new servicecatalog.CloudFormationProduct(this, 'ProductBId', {
      productName: 'Product B',
      owner: 'Owner B',
      productVersions: [
        productBStackHistory.currentVersion(),
      ],
    });

    portfolio.addProduct(productB);

  }
}


const app = new cdk.App();
new ExamplePortfolioStack(app, 'ExamplePortfolioStackId');

app.synth();

Possible Solution

If I am right, then in the ProductStackSynthesizer.js file in addFileAsset method the name of the assets bucket deployment (marked with bold in the code snippet below) should be generated dynamically (now it is static):

addFileAsset(asset) {
        if (!this.assetBucket) {
            throw new Error('An Asset Bucket must be provided to use Assets');
        }
        const outdir = cdk.App.of(this.boundStack)?.outdir ?? 'cdk.out';
        const assetPath = `${outdir}/${asset.fileName}`;
        if (!this.bucketDeployment) {
            const parentStack = this.boundStack._getParentStack();
            if (!cdk.Resource.isOwnedResource(this.assetBucket)) {
                cdk.Annotations.of(parentStack).addWarning('[WARNING] Bucket Policy Permissions cannot be added to' +
                    ' referenced Bucket. Please make sure your bucket has the correct permissions');
            }
            this.bucketDeployment = new aws_s3_deployment_1.BucketDeployment(parentStack, '**AssetsBucketDeployment**', {
                sources: [aws_s3_deployment_1.Source.asset(assetPath)],
                destinationBucket: this.assetBucket,
                extract: false,
                prune: false,
            });
        }

Additional Information/Context

No response

CDK CLI Version

2.75.1

Framework Version

No response

Node.js Version

16.17.0

OS

MacOS

Language

Typescript

Language Version

4.8.4

Other information

No response

Metadata

Metadata

Assignees

Labels

@aws-cdk/aws-servicecatalogRelated to AWS Service CatalogbugThis issue is a bug.effort/mediumMedium work item – several days of effortp2

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions