From 864c50ed2f3ae133af0cffd17ed77a6cf32ac6f4 Mon Sep 17 00:00:00 2001 From: Nick Lynch Date: Tue, 2 Nov 2021 15:54:59 +0000 Subject: [PATCH 1/4] fix(cli): cdk ls --long outputs less-friendly stack IDs for nested assemblies (#17263) Since #14379, `cdk ls` has outputted friendlier stack names for nested assemblies (e.g., with pipelines). However, `cdk ls --long` still outputs the less-friendly stack IDs. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/aws-cdk/lib/cdk-toolkit.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/aws-cdk/lib/cdk-toolkit.ts b/packages/aws-cdk/lib/cdk-toolkit.ts index 6bc109dd37822..65aa443190a57 100644 --- a/packages/aws-cdk/lib/cdk-toolkit.ts +++ b/packages/aws-cdk/lib/cdk-toolkit.ts @@ -282,7 +282,7 @@ export class CdkToolkit { const long = []; for (const stack of stacks.stackArtifacts) { long.push({ - id: stack.id, + id: stack.hierarchicalId, name: stack.stackName, environment: stack.environment, }); From d4952c3cbe12e7c8c27e1bca7f9d8536d93fd3cb Mon Sep 17 00:00:00 2001 From: Peter Woodworth <44349620+peterwoodworth@users.noreply.github.com> Date: Tue, 2 Nov 2021 10:35:39 -0700 Subject: [PATCH 2/4] fix(ec2): functions addIngressRule and addEgressRule detect unresolved tokens as duplicates (#17221) fixes #17201 The issue is when the same security group uses these functions, so I added a private counter to `SecurityGroupBase`. However, to modify this private counter, `determineRuleScope` and `renderPeer` need to be member functions. These originally weren't member functions for a reason, and that's because `SecurityGroup` also uses these functions. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../@aws-cdk/aws-ec2/lib/security-group.ts | 152 +++++++++--------- .../aws-ec2/test/security-group.test.ts | 24 +++ 2 files changed, 104 insertions(+), 72 deletions(-) diff --git a/packages/@aws-cdk/aws-ec2/lib/security-group.ts b/packages/@aws-cdk/aws-ec2/lib/security-group.ts index 54695f2d17b71..692df3629ebbf 100644 --- a/packages/@aws-cdk/aws-ec2/lib/security-group.ts +++ b/packages/@aws-cdk/aws-ec2/lib/security-group.ts @@ -68,6 +68,8 @@ abstract class SecurityGroupBase extends Resource implements ISecurityGroup { public readonly connections: Connections = new Connections({ securityGroups: [this] }); public readonly defaultPort?: Port; + private peerAsTokenCount: number = 0; + constructor(scope: Construct, id: string, props?: ResourceProps) { super(scope, id, props); @@ -83,7 +85,7 @@ abstract class SecurityGroupBase extends Resource implements ISecurityGroup { description = `from ${peer.uniqueId}:${connection}`; } - const [scope, id] = determineRuleScope(this, peer, connection, 'from', remoteRule); + const [scope, id] = this.determineRuleScope(peer, connection, 'from', remoteRule); // Skip duplicates if (scope.node.tryFindChild(id) === undefined) { @@ -101,7 +103,7 @@ abstract class SecurityGroupBase extends Resource implements ISecurityGroup { description = `to ${peer.uniqueId}:${connection}`; } - const [scope, id] = determineRuleScope(this, peer, connection, 'to', remoteRule); + const [scope, id] = this.determineRuleScope(peer, connection, 'to', remoteRule); // Skip duplicates if (scope.node.tryFindChild(id) === undefined) { @@ -121,75 +123,82 @@ abstract class SecurityGroupBase extends Resource implements ISecurityGroup { public toEgressRuleConfig(): any { return { destinationSecurityGroupId: this.securityGroupId }; } -} -/** - * Determine where to parent a new ingress/egress rule - * - * A SecurityGroup rule is parented under the group it's related to, UNLESS - * we're in a cross-stack scenario with another Security Group. In that case, - * we respect the 'remoteRule' flag and will parent under the other security - * group. - * - * This is necessary to avoid cyclic dependencies between stacks, since both - * ingress and egress rules will reference both security groups, and a naive - * parenting will lead to the following situation: - * - * ╔════════════════════╗ ╔════════════════════╗ - * ║ ┌───────────┐ ║ ║ ┌───────────┐ ║ - * ║ │ GroupA │◀────╬─┐ ┌───╬───▶│ GroupB │ ║ - * ║ └───────────┘ ║ │ │ ║ └───────────┘ ║ - * ║ ▲ ║ │ │ ║ ▲ ║ - * ║ │ ║ │ │ ║ │ ║ - * ║ │ ║ │ │ ║ │ ║ - * ║ ┌───────────┐ ║ └───┼───╬────┌───────────┐ ║ - * ║ │ EgressA │─────╬─────┘ ║ │ IngressB │ ║ - * ║ └───────────┘ ║ ║ └───────────┘ ║ - * ║ ║ ║ ║ - * ╚════════════════════╝ ╚════════════════════╝ - * - * By having the ability to switch the parent, we avoid the cyclic reference by - * keeping all rules in a single stack. - * - * If this happens, we also have to change the construct ID, because - * otherwise we might have two objects with the same ID if we have - * multiple reversed security group relationships. - * - * ╔═══════════════════════════════════╗ - * ║┌───────────┐ ║ - * ║│ GroupB │ ║ - * ║└───────────┘ ║ - * ║ ▲ ║ - * ║ │ ┌───────────┐ ║ - * ║ ├────"from A"──│ IngressB │ ║ - * ║ │ └───────────┘ ║ - * ║ │ ┌───────────┐ ║ - * ║ ├─────"to B"───│ EgressA │ ║ - * ║ │ └───────────┘ ║ - * ║ │ ┌───────────┐ ║ - * ║ └─────"to B"───│ EgressC │ ║ <-- oops - * ║ └───────────┘ ║ - * ╚═══════════════════════════════════╝ - */ -function determineRuleScope( - group: SecurityGroupBase, - peer: IPeer, - connection: Port, - fromTo: 'from' | 'to', - remoteRule?: boolean): [SecurityGroupBase, string] { - - if (remoteRule && SecurityGroupBase.isSecurityGroup(peer) && differentStacks(group, peer)) { - // Reversed - const reversedFromTo = fromTo === 'from' ? 'to' : 'from'; - return [peer, `${group.uniqueId}:${connection} ${reversedFromTo}`]; - } else { - // Regular (do old ID escaping to in order to not disturb existing deployments) - return [group, `${fromTo} ${renderPeer(peer)}:${connection}`.replace('/', '_')]; + /** + * Determine where to parent a new ingress/egress rule + * + * A SecurityGroup rule is parented under the group it's related to, UNLESS + * we're in a cross-stack scenario with another Security Group. In that case, + * we respect the 'remoteRule' flag and will parent under the other security + * group. + * + * This is necessary to avoid cyclic dependencies between stacks, since both + * ingress and egress rules will reference both security groups, and a naive + * parenting will lead to the following situation: + * + * ╔════════════════════╗ ╔════════════════════╗ + * ║ ┌───────────┐ ║ ║ ┌───────────┐ ║ + * ║ │ GroupA │◀────╬─┐ ┌───╬───▶│ GroupB │ ║ + * ║ └───────────┘ ║ │ │ ║ └───────────┘ ║ + * ║ ▲ ║ │ │ ║ ▲ ║ + * ║ │ ║ │ │ ║ │ ║ + * ║ │ ║ │ │ ║ │ ║ + * ║ ┌───────────┐ ║ └───┼───╬────┌───────────┐ ║ + * ║ │ EgressA │─────╬─────┘ ║ │ IngressB │ ║ + * ║ └───────────┘ ║ ║ └───────────┘ ║ + * ║ ║ ║ ║ + * ╚════════════════════╝ ╚════════════════════╝ + * + * By having the ability to switch the parent, we avoid the cyclic reference by + * keeping all rules in a single stack. + * + * If this happens, we also have to change the construct ID, because + * otherwise we might have two objects with the same ID if we have + * multiple reversed security group relationships. + * + * ╔═══════════════════════════════════╗ + * ║┌───────────┐ ║ + * ║│ GroupB │ ║ + * ║└───────────┘ ║ + * ║ ▲ ║ + * ║ │ ┌───────────┐ ║ + * ║ ├────"from A"──│ IngressB │ ║ + * ║ │ └───────────┘ ║ + * ║ │ ┌───────────┐ ║ + * ║ ├─────"to B"───│ EgressA │ ║ + * ║ │ └───────────┘ ║ + * ║ │ ┌───────────┐ ║ + * ║ └─────"to B"───│ EgressC │ ║ <-- oops + * ║ └───────────┘ ║ + * ╚═══════════════════════════════════╝ + */ + + protected determineRuleScope( + peer: IPeer, + connection: Port, + fromTo: 'from' | 'to', + remoteRule?: boolean): [SecurityGroupBase, string] { + + if (remoteRule && SecurityGroupBase.isSecurityGroup(peer) && differentStacks(this, peer)) { + // Reversed + const reversedFromTo = fromTo === 'from' ? 'to' : 'from'; + return [peer, `${this.uniqueId}:${connection} ${reversedFromTo}`]; + } else { + // Regular (do old ID escaping to in order to not disturb existing deployments) + return [this, `${fromTo} ${this.renderPeer(peer)}:${connection}`.replace('/', '_')]; + } } -} -function renderPeer(peer: IPeer) { - return Token.isUnresolved(peer.uniqueId) ? '{IndirectPeer}' : peer.uniqueId; + private renderPeer(peer: IPeer) { + if (Token.isUnresolved(peer.uniqueId)) { + // Need to return a unique value each time a peer + // is an unresolved token, else the duplicate skipper + // in `sg.addXxxRule` can detect unique rules as duplicates + return this.peerAsTokenCount++ ? `'{IndirectPeer${this.peerAsTokenCount}}'` : '{IndirectPeer}'; + } else { + return peer.uniqueId; + } + } } function differentStacks(group1: SecurityGroupBase, group2: SecurityGroupBase) { @@ -565,13 +574,12 @@ export class SecurityGroup extends SecurityGroupBase { */ private removeNoTrafficRule() { if (this.disableInlineRules) { - const [scope, id] = determineRuleScope( - this, + const [scope, id] = this.determineRuleScope( NO_TRAFFIC_PEER, NO_TRAFFIC_PORT, 'to', - false); - + false, + ); scope.node.tryRemoveChild(id); } else { const i = this.directEgressRules.findIndex(r => egressRulesEqual(r, MATCH_NO_TRAFFIC)); diff --git a/packages/@aws-cdk/aws-ec2/test/security-group.test.ts b/packages/@aws-cdk/aws-ec2/test/security-group.test.ts index 09ccc6cdc682e..70dbe64cef335 100644 --- a/packages/@aws-cdk/aws-ec2/test/security-group.test.ts +++ b/packages/@aws-cdk/aws-ec2/test/security-group.test.ts @@ -208,6 +208,30 @@ describe('security group', () => { }); + test('can add multiple rules using tokens on same security group', () => { + // GIVEN + const stack = new Stack(undefined, 'TestStack', { env: { account: '12345678', region: 'dummy' } }); + const vpc = new Vpc(stack, 'VPC'); + const sg = new SecurityGroup(stack, 'SG', { vpc }); + + const p1 = Lazy.string({ produce: () => 'dummyid1' }); + const p2 = Lazy.string({ produce: () => 'dummyid2' }); + const peer1 = Peer.prefixList(p1); + const peer2 = Peer.prefixList(p2); + + // WHEN + sg.addIngressRule(peer1, Port.tcp(5432), 'Rule 1'); + sg.addIngressRule(peer2, Port.tcp(5432), 'Rule 2'); + + // THEN -- no crash + expect(stack).toHaveResourceLike('AWS::EC2::SecurityGroupIngress', { + Description: 'Rule 1', + }); + expect(stack).toHaveResourceLike('AWS::EC2::SecurityGroupIngress', { + Description: 'Rule 2', + }); + }); + test('if tokens are used in ports, `canInlineRule` should be false to avoid cycles', () => { // GIVEN const p1 = Lazy.number({ produce: () => 80 }); From f8d0ef550df07e43aeab35dde4406c92f7551ed0 Mon Sep 17 00:00:00 2001 From: arcrank Date: Tue, 2 Nov 2021 14:30:17 -0400 Subject: [PATCH 3/4] feat(servicecatalog): allow creating a CFN Product Version with CDK code (#17144) Add ability to define a product version entirely within CDK as opposed to referencing templates or local assets. The service catalog `ProductStack` is similar to `NestedStacks` that do not deploy themselves but rather are referenced by the parent stacks. The resources defined in your product are added to the product stack like any other cdk app. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* Co-authored-by: Dillon Ponzo --- .../@aws-cdk/aws-servicecatalog/README.md | 39 +++++- .../lib/cloudformation-template.ts | 26 ++++ .../@aws-cdk/aws-servicecatalog/lib/index.ts | 1 + .../lib/private/product-stack-synthesizer.ts | 34 ++++++ .../aws-servicecatalog/lib/product-stack.ts | 77 ++++++++++++ .../@aws-cdk/aws-servicecatalog/package.json | 3 +- .../rosetta/basic-portfolio.ts-fixture | 6 +- .../test/integ.product.expected.json | 114 ++++++++++++++++++ .../aws-servicecatalog/test/integ.product.ts | 15 +++ .../test/product-stack.test.ts | 88 ++++++++++++++ .../aws-servicecatalog/test/product.test.ts | 88 ++++++++++++++ 11 files changed, 485 insertions(+), 6 deletions(-) create mode 100644 packages/@aws-cdk/aws-servicecatalog/lib/private/product-stack-synthesizer.ts create mode 100644 packages/@aws-cdk/aws-servicecatalog/lib/product-stack.ts create mode 100644 packages/@aws-cdk/aws-servicecatalog/test/product-stack.test.ts diff --git a/packages/@aws-cdk/aws-servicecatalog/README.md b/packages/@aws-cdk/aws-servicecatalog/README.md index 4bb6885c601eb..2d7694d3e84b6 100644 --- a/packages/@aws-cdk/aws-servicecatalog/README.md +++ b/packages/@aws-cdk/aws-servicecatalog/README.md @@ -30,6 +30,8 @@ enables organizations to create and manage catalogs of products for their end us - [Granting access to a portfolio](#granting-access-to-a-portfolio) - [Sharing a portfolio with another AWS account](#sharing-a-portfolio-with-another-aws-account) - [Product](#product) + - [Creating a product from a local asset](#creating-a-product-from-local-asset) + - [Creating a product from a stack](#creating-a-product-from-a-stack) - [Adding a product to a portfolio](#adding-a-product-to-a-portfolio) - [TagOptions](#tag-options) - [Constraints](#constraints) @@ -125,10 +127,12 @@ const product = new servicecatalog.CloudFormationProduct(this, 'MyFirstProduct', cloudFormationTemplate: servicecatalog.CloudFormationTemplate.fromUrl( 'https://raw.githubusercontent.com/awslabs/aws-cloudformation-templates/master/aws/services/ServiceCatalog/Product.yaml'), }, - ] + ], }); ``` +### Creating a product from a local asset + A `CloudFormationProduct` can also be created using a Cloudformation template from an Asset. Assets are files that are uploaded to an S3 Bucket before deployment. `CloudFormationTemplate.fromAsset` can be utilized to create a Product by passing the path to a local template file on your disk: @@ -149,7 +153,38 @@ const product = new servicecatalog.CloudFormationProduct(this, 'MyFirstProduct', productVersionName: "v2", cloudFormationTemplate: servicecatalog.CloudFormationTemplate.fromAsset(path.join(__dirname, 'development-environment.template.json')), }, - ] + ], +}); +``` + +### Creating a product from a stack + +You can define a service catalog `CloudFormationProduct` entirely within CDK using a service catalog `ProductStack`. +A separate child stack for your product is created and you can add resources like you would for any other CDK stack, +such as an S3 Bucket, IAM roles, and EC2 instances. This stack is passed in as a product version to your +product. This will not create a separate stack during deployment. + +```ts +import * as s3 from '@aws-cdk/aws-s3'; +import * as cdk from '@aws-cdk/core'; + +class S3BucketProduct extends servicecatalog.ProductStack { + constructor(scope: cdk.Construct, id: string) { + super(scope, id); + + new s3.Bucket(this, 'BucketProduct'); + } +} + +const product = new servicecatalog.CloudFormationProduct(this, 'MyFirstProduct', { + productName: "My Product", + owner: "Product Owner", + productVersions: [ + { + productVersionName: "v1", + cloudFormationTemplate: servicecatalog.CloudFormationTemplate.fromProductStack(new S3BucketProduct(this, 'S3BucketProduct')), + }, + ], }); ``` diff --git a/packages/@aws-cdk/aws-servicecatalog/lib/cloudformation-template.ts b/packages/@aws-cdk/aws-servicecatalog/lib/cloudformation-template.ts index be0cb9adf2022..4086db6655fda 100644 --- a/packages/@aws-cdk/aws-servicecatalog/lib/cloudformation-template.ts +++ b/packages/@aws-cdk/aws-servicecatalog/lib/cloudformation-template.ts @@ -1,5 +1,6 @@ import * as s3_assets from '@aws-cdk/aws-s3-assets'; import { hashValues } from './private/util'; +import { ProductStack } from './product-stack'; // keep this import separate from other imports to reduce chance for merge conflicts with v2-main // eslint-disable-next-line no-duplicate-imports, import/order @@ -26,6 +27,13 @@ export abstract class CloudFormationTemplate { return new CloudFormationAssetTemplate(path, options); } + /** + * Creates a product with the resources defined in the given product stack. + */ + public static fromProductStack(productStack: ProductStack): CloudFormationTemplate { + return new CloudFormationProductStackTemplate(productStack); + } + /** * Called when the product is initialized to allow this object to bind * to the stack, add resources and have fun. @@ -88,3 +96,21 @@ class CloudFormationAssetTemplate extends CloudFormationTemplate { }; } } + +/** + * Template from a CDK defined product stack. + */ +class CloudFormationProductStackTemplate extends CloudFormationTemplate { + /** + * @param stack A service catalog product stack. + */ + constructor(public readonly productStack: ProductStack) { + super(); + } + + public bind(_scope: Construct): CloudFormationTemplateConfig { + return { + httpUrl: this.productStack._getTemplateUrl(), + }; + } +} diff --git a/packages/@aws-cdk/aws-servicecatalog/lib/index.ts b/packages/@aws-cdk/aws-servicecatalog/lib/index.ts index 8a7b0f0ff9ac6..cc26e880fdd2e 100644 --- a/packages/@aws-cdk/aws-servicecatalog/lib/index.ts +++ b/packages/@aws-cdk/aws-servicecatalog/lib/index.ts @@ -3,6 +3,7 @@ export * from './constraints'; export * from './cloudformation-template'; export * from './portfolio'; export * from './product'; +export * from './product-stack'; export * from './tag-options'; // AWS::ServiceCatalog CloudFormation Resources: diff --git a/packages/@aws-cdk/aws-servicecatalog/lib/private/product-stack-synthesizer.ts b/packages/@aws-cdk/aws-servicecatalog/lib/private/product-stack-synthesizer.ts new file mode 100644 index 0000000000000..4840a7e756fe5 --- /dev/null +++ b/packages/@aws-cdk/aws-servicecatalog/lib/private/product-stack-synthesizer.ts @@ -0,0 +1,34 @@ +import * as cdk from '@aws-cdk/core'; + +/** + * Deployment environment for an AWS Service Catalog product stack. + * + * Interoperates with the StackSynthesizer of the parent stack. + */ +export class ProductStackSynthesizer extends cdk.StackSynthesizer { + private stack?: cdk.Stack; + + public bind(stack: cdk.Stack): void { + if (this.stack !== undefined) { + throw new Error('A Stack Synthesizer can only be bound once, create a new instance to use with a different Stack'); + } + this.stack = stack; + } + + public addFileAsset(_asset: cdk.FileAssetSource): cdk.FileAssetLocation { + throw new Error('Service Catalog Product Stacks cannot use Assets'); + } + + public addDockerImageAsset(_asset: cdk.DockerImageAssetSource): cdk.DockerImageAssetLocation { + throw new Error('Service Catalog Product Stacks cannot use Assets'); + } + + public synthesize(session: cdk.ISynthesisSession): void { + if (!this.stack) { + throw new Error('You must call bindStack() first'); + } + // Synthesize the template, but don't emit as a cloud assembly artifact. + // It will be registered as an S3 asset of its parent instead. + this.synthesizeStackTemplate(this.stack, session); + } +} diff --git a/packages/@aws-cdk/aws-servicecatalog/lib/product-stack.ts b/packages/@aws-cdk/aws-servicecatalog/lib/product-stack.ts new file mode 100644 index 0000000000000..b96224a8b2c60 --- /dev/null +++ b/packages/@aws-cdk/aws-servicecatalog/lib/product-stack.ts @@ -0,0 +1,77 @@ +import * as crypto from 'crypto'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as cdk from '@aws-cdk/core'; +import { ProductStackSynthesizer } from './private/product-stack-synthesizer'; + +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct } from 'constructs'; + +/** + * A Service Catalog product stack, which is similar in form to a Cloudformation nested stack. + * You can add the resources to this stack that you want to define for your service catalog product. + * + * This stack will not be treated as an independent deployment + * artifact (won't be listed in "cdk list" or deployable through "cdk deploy"), + * but rather only synthesized as a template and uploaded as an asset to S3. + * + */ +export class ProductStack extends cdk.Stack { + public readonly templateFile: string; + private _templateUrl?: string; + private _parentStack: cdk.Stack; + + constructor(scope: Construct, id: string) { + super(scope, id, { + synthesizer: new ProductStackSynthesizer(), + }); + + this._parentStack = findParentStack(scope); + + // this is the file name of the synthesized template file within the cloud assembly + this.templateFile = `${cdk.Names.uniqueId(this)}.product.template.json`; + } + + /** + * Fetch the template URL. + * + * @internal + */ + public _getTemplateUrl(): string { + return cdk.Lazy.uncachedString({ produce: () => this._templateUrl }); + } + + /** + * Synthesize the product stack template, overrides the `super` class method. + * + * Defines an asset at the parent stack which represents the template of this + * product stack. + * + * @internal + */ + public _synthesizeTemplate(session: cdk.ISynthesisSession): void { + const cfn = JSON.stringify(this._toCloudFormation(), undefined, 2); + const templateHash = crypto.createHash('sha256').update(cfn).digest('hex'); + + this._templateUrl = this._parentStack.synthesizer.addFileAsset({ + packaging: cdk.FileAssetPackaging.FILE, + sourceHash: templateHash, + fileName: this.templateFile, + }).httpUrl; + + fs.writeFileSync(path.join(session.assembly.outdir, this.templateFile), cfn); + } +} + +/** + * Validates the scope for a product stack, which must be defined within the scope of another `Stack`. + */ +function findParentStack(scope: Construct): cdk.Stack { + try { + const parentStack = cdk.Stack.of(scope); + return parentStack as cdk.Stack; + } catch (e) { + throw new Error('Product stacks must be defined within scope of another non-product stack'); + } +} diff --git a/packages/@aws-cdk/aws-servicecatalog/package.json b/packages/@aws-cdk/aws-servicecatalog/package.json index de3bfcfb667a5..3d96fbe55edcc 100644 --- a/packages/@aws-cdk/aws-servicecatalog/package.json +++ b/packages/@aws-cdk/aws-servicecatalog/package.json @@ -104,7 +104,8 @@ "resource-attribute:@aws-cdk/aws-servicecatalog.CloudFormationProduct.cloudFormationProductProvisioningArtifactNames", "props-physical-name:@aws-cdk/aws-servicecatalog.CloudFormationProductProps", "resource-attribute:@aws-cdk/aws-servicecatalog.Portfolio.portfolioName", - "props-physical-name:@aws-cdk/aws-servicecatalog.PortfolioProps" + "props-physical-name:@aws-cdk/aws-servicecatalog.PortfolioProps", + "props-physical-name:@aws-cdk/aws-servicecatalog.ProductStack" ] }, "maturity": "experimental", diff --git a/packages/@aws-cdk/aws-servicecatalog/rosetta/basic-portfolio.ts-fixture b/packages/@aws-cdk/aws-servicecatalog/rosetta/basic-portfolio.ts-fixture index d8925e6645aa7..3029872ea1f0d 100644 --- a/packages/@aws-cdk/aws-servicecatalog/rosetta/basic-portfolio.ts-fixture +++ b/packages/@aws-cdk/aws-servicecatalog/rosetta/basic-portfolio.ts-fixture @@ -1,9 +1,9 @@ // Fixture with packages imported, but nothing else -import { Construct, Stack } from '@aws-cdk/core'; +import * as cdk from '@aws-cdk/core'; import * as servicecatalog from '@aws-cdk/aws-servicecatalog'; -class Fixture extends Stack { - constructor(scope: Construct, id: string) { +class Fixture extends cdk.Stack { + constructor(scope: cdk.Construct, id: string) { super(scope, id); const portfolio = new servicecatalog.Portfolio(this, "MyFirstPortfolio", { diff --git a/packages/@aws-cdk/aws-servicecatalog/test/integ.product.expected.json b/packages/@aws-cdk/aws-servicecatalog/test/integ.product.expected.json index 786fdcad0f8fc..f54d640e1d0ca 100644 --- a/packages/@aws-cdk/aws-servicecatalog/test/integ.product.expected.json +++ b/packages/@aws-cdk/aws-servicecatalog/test/integ.product.expected.json @@ -113,6 +113,108 @@ ] } } + }, + { + "DisableTemplateValidation": false, + "Info": { + "LoadTemplateFromURL": { + "Fn::Join": [ + "", + [ + "https://s3.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "AssetParametersdd2d087eeb6ede1d2a9166639ccbde7bd1b10eef9ba2b4cb3d9855faa4fe8c1fS3BucketB4751C98" + }, + "/", + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParametersdd2d087eeb6ede1d2a9166639ccbde7bd1b10eef9ba2b4cb3d9855faa4fe8c1fS3VersionKeyEB38C6F9" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParametersdd2d087eeb6ede1d2a9166639ccbde7bd1b10eef9ba2b4cb3d9855faa4fe8c1fS3VersionKeyEB38C6F9" + } + ] + } + ] + } + ] + ] + } + } + }, + { + "DisableTemplateValidation": false, + "Info": { + "LoadTemplateFromURL": { + "Fn::Join": [ + "", + [ + "https://s3.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "AssetParametersdd2d087eeb6ede1d2a9166639ccbde7bd1b10eef9ba2b4cb3d9855faa4fe8c1fS3BucketB4751C98" + }, + "/", + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParametersdd2d087eeb6ede1d2a9166639ccbde7bd1b10eef9ba2b4cb3d9855faa4fe8c1fS3VersionKeyEB38C6F9" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParametersdd2d087eeb6ede1d2a9166639ccbde7bd1b10eef9ba2b4cb3d9855faa4fe8c1fS3VersionKeyEB38C6F9" + } + ] + } + ] + } + ] + ] + } + } } ] } @@ -142,6 +244,18 @@ "AssetParameters6412a5f4524c6b41d26fbeee226c68c2dad735393940a51008d77e6f8b1038f5ArtifactHashDC26AFAC": { "Type": "String", "Description": "Artifact hash for asset \"6412a5f4524c6b41d26fbeee226c68c2dad735393940a51008d77e6f8b1038f5\"" + }, + "AssetParametersdd2d087eeb6ede1d2a9166639ccbde7bd1b10eef9ba2b4cb3d9855faa4fe8c1fS3BucketB4751C98": { + "Type": "String", + "Description": "S3 bucket for asset \"dd2d087eeb6ede1d2a9166639ccbde7bd1b10eef9ba2b4cb3d9855faa4fe8c1f\"" + }, + "AssetParametersdd2d087eeb6ede1d2a9166639ccbde7bd1b10eef9ba2b4cb3d9855faa4fe8c1fS3VersionKeyEB38C6F9": { + "Type": "String", + "Description": "S3 key for asset version \"dd2d087eeb6ede1d2a9166639ccbde7bd1b10eef9ba2b4cb3d9855faa4fe8c1f\"" + }, + "AssetParametersdd2d087eeb6ede1d2a9166639ccbde7bd1b10eef9ba2b4cb3d9855faa4fe8c1fArtifactHash5C1F9228": { + "Type": "String", + "Description": "Artifact hash for asset \"dd2d087eeb6ede1d2a9166639ccbde7bd1b10eef9ba2b4cb3d9855faa4fe8c1f\"" } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-servicecatalog/test/integ.product.ts b/packages/@aws-cdk/aws-servicecatalog/test/integ.product.ts index 5c4192cc9b25c..7a88c98a466d1 100644 --- a/packages/@aws-cdk/aws-servicecatalog/test/integ.product.ts +++ b/packages/@aws-cdk/aws-servicecatalog/test/integ.product.ts @@ -1,10 +1,19 @@ import * as path from 'path'; +import * as sns from '@aws-cdk/aws-sns'; import * as cdk from '@aws-cdk/core'; import * as servicecatalog from '../lib'; const app = new cdk.App(); const stack = new cdk.Stack(app, 'integ-servicecatalog-product'); +class TestProductStack extends servicecatalog.ProductStack { + constructor(scope: any, id: string) { + super(scope, id); + + new sns.Topic(this, 'TopicProduct'); + } +} + new servicecatalog.CloudFormationProduct(stack, 'TestProduct', { productName: 'testProduct', owner: 'testOwner', @@ -20,6 +29,12 @@ new servicecatalog.CloudFormationProduct(stack, 'TestProduct', { { cloudFormationTemplate: servicecatalog.CloudFormationTemplate.fromAsset(path.join(__dirname, 'product2.template.json')), }, + { + cloudFormationTemplate: servicecatalog.CloudFormationTemplate.fromProductStack(new TestProductStack(stack, 'SNSTopicProduct1')), + }, + { + cloudFormationTemplate: servicecatalog.CloudFormationTemplate.fromProductStack(new TestProductStack(stack, 'SNSTopicProduct2')), + }, ], }); diff --git a/packages/@aws-cdk/aws-servicecatalog/test/product-stack.test.ts b/packages/@aws-cdk/aws-servicecatalog/test/product-stack.test.ts new file mode 100644 index 0000000000000..e024421d724dc --- /dev/null +++ b/packages/@aws-cdk/aws-servicecatalog/test/product-stack.test.ts @@ -0,0 +1,88 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import * as s3_assets from '@aws-cdk/aws-s3-assets'; +import * as sns from '@aws-cdk/aws-sns'; +import * as cdk from '@aws-cdk/core'; +import * as servicecatalog from '../lib'; + +/* eslint-disable quote-props */ +describe('ProductStack', () => { + test('fails to add asset to a product stack', () => { + // GIVEN + const app = new cdk.App(); + const mainStack = new cdk.Stack(app, 'MyStack'); + const productStack = new servicecatalog.ProductStack(mainStack, 'MyProductStack'); + + // THEN + expect(() => { + new s3_assets.Asset(productStack, 'testAsset', { + path: path.join(__dirname, 'product1.template.json'), + }); + }).toThrow(/Service Catalog Product Stacks cannot use Assets/); + }), + + test('fails if defined at root', () => { + // GIVEN + const app = new cdk.App(); + + // THEN + expect(() => { + new servicecatalog.ProductStack(app, 'ProductStack'); + }).toThrow(/must be defined within scope of another non-product stack/); + }), + + test('fails if defined without a parent stack', () => { + // GIVEN + const app = new cdk.App(); + const group = new cdk.Construct(app, 'group'); + + // THEN + expect(() => { + new servicecatalog.ProductStack(group, 'ProductStack'); + }).toThrow(/must be defined within scope of another non-product stack/); + }), + + test('can be defined as a direct child or an indirect child of a Stack', () => { + // GIVEN + const parent = new cdk.Stack(); + + // THEN + expect(() => { + new servicecatalog.ProductStack(parent, 'direct'); + }).not.toThrow(); + }); + + test('product stack is not synthesized as a stack artifact into the assembly', () => { + // GIVEN + const app = new cdk.App(); + const parentStack = new cdk.Stack(app, 'ParentStack'); + new servicecatalog.ProductStack(parentStack, 'ProductStack'); + + // WHEN + const assembly = app.synth(); + + // THEN + expect(assembly.artifacts.length).toEqual(2); + }); + + test('the template of the product stack is synthesized into the cloud assembly', () => { + // GIVEN + const app = new cdk.App(); + const parent = new cdk.Stack(app, 'ParentStack'); + const productStack = new servicecatalog.ProductStack(parent, 'ProductStack'); + new sns.Topic(productStack, 'SNSTopicProduct'); + + // WHEN + const assembly = app.synth(); + + // THEN + const template = JSON.parse(fs.readFileSync(path.join(assembly.directory, productStack.templateFile), 'utf-8')); + expect(template).toEqual({ + Resources: { + SNSTopicProduct20605D98: { + Type: 'AWS::SNS::Topic', + }, + }, + }); + }); +}); diff --git a/packages/@aws-cdk/aws-servicecatalog/test/product.test.ts b/packages/@aws-cdk/aws-servicecatalog/test/product.test.ts index f3e485b30e951..f399a79dcdb83 100644 --- a/packages/@aws-cdk/aws-servicecatalog/test/product.test.ts +++ b/packages/@aws-cdk/aws-servicecatalog/test/product.test.ts @@ -1,5 +1,6 @@ import * as path from 'path'; import { Match, Template } from '@aws-cdk/assertions'; +import * as sns from '@aws-cdk/aws-sns'; import * as cdk from '@aws-cdk/core'; import * as servicecatalog from '../lib'; @@ -101,6 +102,93 @@ describe('Product', () => { expect(synthesized.assets.length).toEqual(2); }), + test('product test from product stack', () => { + const productStack = new servicecatalog.ProductStack(stack, 'ProductStack'); + + new sns.Topic(productStack, 'SNSTopicProductStack'); + + new servicecatalog.CloudFormationProduct(stack, 'MyProduct', { + productName: 'testProduct', + owner: 'testOwner', + productVersions: [ + { + productVersionName: 'v1', + cloudFormationTemplate: servicecatalog.CloudFormationTemplate.fromProductStack(productStack), + }, + ], + }); + + const assembly = app.synth(); + expect(assembly.artifacts.length).toEqual(2); + expect(assembly.stacks[0].assets.length).toBe(1); + expect(assembly.stacks[0].assets[0].path).toEqual('ProductStack.product.template.json'); + }), + + test('multiple product versions from product stack', () => { + const productStackVersion1 = new servicecatalog.ProductStack(stack, 'ProductStackV1'); + const productStackVersion2 = new servicecatalog.ProductStack(stack, 'ProductStackV2'); + + new sns.Topic(productStackVersion1, 'SNSTopicProductStack1'); + + new sns.Topic(productStackVersion2, 'SNSTopicProductStack2', { + displayName: 'a test', + }); + + new servicecatalog.CloudFormationProduct(stack, 'MyProduct', { + productName: 'testProduct', + owner: 'testOwner', + productVersions: [ + { + productVersionName: 'v1', + cloudFormationTemplate: servicecatalog.CloudFormationTemplate.fromProductStack(productStackVersion1), + }, + { + productVersionName: 'v2', + cloudFormationTemplate: servicecatalog.CloudFormationTemplate.fromProductStack(productStackVersion2), + }, + ], + }); + + const assembly = app.synth(); + + expect(assembly.stacks[0].assets.length).toBe(2); + expect(assembly.stacks[0].assets[0].path).toEqual('ProductStackV1.product.template.json'); + expect(assembly.stacks[0].assets[1].path).toEqual('ProductStackV2.product.template.json'); + }), + + test('identical product versions from product stack creates one asset', () => { + class TestProductStack extends servicecatalog.ProductStack { + constructor(scope: any, id: string) { + super(scope, id); + + new sns.Topic(this, 'TopicProduct'); + } + } + + new servicecatalog.CloudFormationProduct(stack, 'MyProduct', { + productName: 'testProduct', + owner: 'testOwner', + productVersions: [ + { + productVersionName: 'v1', + cloudFormationTemplate: servicecatalog.CloudFormationTemplate.fromProductStack(new TestProductStack(stack, 'v1')), + }, + { + productVersionName: 'v2', + cloudFormationTemplate: servicecatalog.CloudFormationTemplate.fromProductStack(new TestProductStack(stack, 'v2')), + }, + { + productVersionName: 'v3', + cloudFormationTemplate: servicecatalog.CloudFormationTemplate.fromProductStack(new TestProductStack(stack, 'v3')), + }, + ], + }); + + const assembly = app.synth(); + + expect(assembly.stacks[0].assets.length).toBe(1); + }), + test('product test from multiple sources', () => { new servicecatalog.CloudFormationProduct(stack, 'MyProduct', { productName: 'testProduct', From 1e2218941af13297b6ddaf02a1d8e07606ed3058 Mon Sep 17 00:00:00 2001 From: Adam Ruka Date: Tue, 2 Nov 2021 12:24:16 -0700 Subject: [PATCH 4/4] chore: add the new aws-iot-actions module to the assignment GitHub Action (#17259) We've created a new module in https://github.com/aws/aws-cdk/pull/17112, so now we need to add it to our assignment GitHub Action. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .github/workflows/issue-label-assign.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/issue-label-assign.yml b/.github/workflows/issue-label-assign.yml index 3309598c4e5e3..4cc8f06bbbf5e 100644 --- a/.github/workflows/issue-label-assign.yml +++ b/.github/workflows/issue-label-assign.yml @@ -126,6 +126,7 @@ jobs: {"area":"@aws-cdk/aws-imagebuilder","keywords":["aws-imagebuilder","imagebuilder"],"labels":["@aws-cdk/aws-imagebuilder"],"assignees":["skinny85"]}, {"area":"@aws-cdk/aws-inspector","keywords":["aws-inspector","inspector"],"labels":["@aws-cdk/aws-inspector"],"assignees":["skinny85"]}, {"area":"@aws-cdk/aws-iot","keywords":["internet-of-things","aws-iot","iot"],"labels":["@aws-cdk/aws-iot"],"assignees":["skinny85"]}, + {"area":"@aws-cdk/aws-iot-actions","keywords":["aws-iot-actions","iot-actions",],"labels":["@aws-cdk/aws-iot-actions"],"assignees":["skinny85"]}, {"area":"@aws-cdk/aws-iot1click","keywords":["aws-iot1click","iot1click"],"labels":["@aws-cdk/aws-iot1click"],"assignees":["skinny85"]}, {"area":"@aws-cdk/aws-iotanalytics","keywords":["aws-iotanalytics","iotanalytics"],"labels":["@aws-cdk/aws-iotanalytics"],"assignees":["skinny85"]}, {"area":"@aws-cdk/aws-iotevents","keywords":["aws-iotevents","iotevents"],"labels":["@aws-cdk/aws-iotevents"],"assignees":["skinny85"]},