diff --git a/packages/@aws-cdk/aws-servicecatalog/.gitignore b/packages/@aws-cdk/aws-servicecatalog/.gitignore index 6d05bba61dfa7..7c868a499059a 100644 --- a/packages/@aws-cdk/aws-servicecatalog/.gitignore +++ b/packages/@aws-cdk/aws-servicecatalog/.gitignore @@ -3,6 +3,7 @@ *.d.ts tsconfig.json node_modules +product-stack-snapshots *.generated.ts dist .jsii diff --git a/packages/@aws-cdk/aws-servicecatalog/README.md b/packages/@aws-cdk/aws-servicecatalog/README.md index a4abdbd8d4464..2f1cc0f847f76 100644 --- a/packages/@aws-cdk/aws-servicecatalog/README.md +++ b/packages/@aws-cdk/aws-servicecatalog/README.md @@ -22,7 +22,7 @@ enables organizations to create and manage catalogs of products for their end us - [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) - - [Creating a Product from a stack with immutable versions](#creating-a-product-from-a-stack-with-immutable-versions) + - [Creating a Product from a stack with a history of previous versions](#creating-a-product-from-a-stack-with-a-history-of-all-previous-versions) - [Adding a product to a portfolio](#adding-a-product-to-a-portfolio) - [TagOptions](#tag-options) - [Constraints](#constraints) @@ -185,14 +185,19 @@ const product = new servicecatalog.CloudFormationProduct(this, 'Product', { }); ``` -### Creating a Product from a stack with immutable versions +### Creating a Product from a stack with a history of previous versions The default behavior of Service Catalog is to overwrite each product version upon deployment. This applies to Product Stacks as well, where only the latest changes to your Product Stack will be deployed. -We would need to create a separate Product Stack to store that version of Product Stack in code. -If instead you want to never overwrite existing versions, but only add new versions, you can use the -`RetentionStrategy.RETAIN` strategy for your deployment. +To keep a history of the revisions of a ProductStack available in Service Catalog, +you would need to define a ProductStack for each historical copy. + +You can instead create a `ProductStackHistory` to maintain snapshots of all previous versions. +The `ProductStackHistory` can be created by passing the base `productStack`, +a `currentVersionName` for your current version and a `locked` boolean. +The `locked` boolean which when set to true will prevent your `currentVersionName` +from being overwritten when there is an existing snapshot for that version. ```ts import * as s3 from '@aws-cdk/aws-s3'; @@ -206,28 +211,33 @@ class S3BucketProduct extends servicecatalog.ProductStack { } } -const product = new servicecatalog.CloudFormationProduct(this, 'Product', { +const productStackHistory = new ProductStackHistory(this, 'ProductStackHistory', { + productStack: new S3BucketProduct(this, 'S3BucketProduct'), + currentVersionName: 'v1', + locked: true +}); +``` + +We can deploy the current version `v1` by using `productStackHistory.currentVersion()` + +```ts +const product = new servicecatalog.CloudFormationProduct(this, 'MyFirstProduct', { productName: "My Product", owner: "Product Owner", productVersions: [ - { - productVersionName: "v1", - cloudFormationTemplate: servicecatalog.CloudFormationTemplate.fromProductStack(new S3BucketProduct(this, 'S3BucketProduct'), servicecatalog.RetentionStrategy.RETAIN), - }, + productStackHistory.currentVersion(), ], }); ``` -Using the `RetentionStrategy.RETAIN` strategy all deployed templates for the ProductStack will be written to disk, +Using `ProductStackHistory` all deployed templates for the ProductStack will be written to disk, so that they will still be available in the future as the definition of the `ProductStack` subclass changes over time. **It is very important** that you commit these old versions to source control as these versions -determine whether a version has already been deployed and can be used also be deployed themselves -using `fromProductStackSnapshot`. +determine whether a version has already been deployed and can also be deployed themselves. -After using the `RetentionStrategy.RETAIN` strategy to deploy version `v1` of your `ProductStack`, we -make changes to the `ProductStack` and update it to `v2`. -We also want our `v1` version to still be deployed, so we reference it using `fromProductStackSnapshot` -and passing the corresponding `ProductStack` id. +After using `ProductStackHistory` to deploy version `v1` of your `ProductStack`, we +make changes to the `ProductStack` and update the `currentVersionName` to `v2`. +We still want our `v1` version to still be deployed, so we reference it by calling `productStackHistory.versionFromSnapshot('v1')`. ```ts import * as s3 from '@aws-cdk/aws-s3'; @@ -241,18 +251,18 @@ class S3BucketProduct extends servicecatalog.ProductStack { } } +const productStackHistory = new ProductStackHistory(this, 'ProductStackHistory', { + productStack: new S3BucketProduct(this, 'S3BucketProduct'), + currentVersionName: 'v2', + locked: true +}); + const product = new servicecatalog.CloudFormationProduct(this, 'MyFirstProduct', { productName: "My Product", owner: "Product Owner", productVersions: [ - { - productVersionName: "v2", - cloudFormationTemplate: servicecatalog.CloudFormationTemplate.fromProductStack(new S3BucketProduct(this, 'S3BucketProduct'), servicecatalog.RetentionStrategy.RETAIN), - }, - { - productVersionName: "v1", - cloudFormationTemplate: servicecatalog.CloudFormationTemplate.fromProductStackSnapshot('S3BucketProduct'), - }, + productStackHistory.currentVersion(), + productStackHistory.versionFromSnapshot('v1') ], }); ``` diff --git a/packages/@aws-cdk/aws-servicecatalog/lib/cloudformation-template.ts b/packages/@aws-cdk/aws-servicecatalog/lib/cloudformation-template.ts index 6592ee80c0163..0a1ff0408de9b 100644 --- a/packages/@aws-cdk/aws-servicecatalog/lib/cloudformation-template.ts +++ b/packages/@aws-cdk/aws-servicecatalog/lib/cloudformation-template.ts @@ -1,5 +1,5 @@ import * as s3_assets from '@aws-cdk/aws-s3-assets'; -import { ProductVersionDetails, TemplateType, RetentionStrategy } from './common'; +import { ProductVersionDetails, TemplateType } from './common'; import { hashValues } from './private/util'; import { ProductStack } from './product-stack'; @@ -31,18 +31,25 @@ export abstract class CloudFormationTemplate { /** * Creates a product with the resources defined in the given product stack. */ - public static fromProductStack(productStack: ProductStack, retentionStrategy?: RetentionStrategy): CloudFormationTemplate { - return new CloudFormationProductStackTemplate(productStack, retentionStrategy); + public static fromProductStack(productStack: ProductStack): CloudFormationTemplate { + return new CloudFormationProductStackTemplate(productStack); } /** - * Creates a product from a previously deployed productStack template. - * The previous template must have been retained using RetentionStrategy.RETAIN + * Creates a product with the resources defined in the given product stack and retains all previously deployed product stack versions. */ - public static fromProductStackSnapshot(baseProductStackId: string): CloudFormationTemplate { - return new CloudFormationProductStackContextTemplate(baseProductStackId); + public static fromProductStackHistory(productStack: ProductStack, locked: boolean, directory?: string ): CloudFormationTemplate { + return new CloudFormationProductStackTemplate(productStack, locked, directory); } + /** + * Creates a product from a previously deployed product stack snapshot. + */ + public static fromProductStackSnapshot(productStack: ProductStack, directory?: string ): CloudFormationTemplate { + return new CloudFormationProductStackSnapshotTemplate(productStack, directory); + } + + /** * Called when the product is initialized to allow this object to bind * to the stack, add resources and have fun. @@ -70,11 +77,6 @@ export interface CloudFormationTemplateConfig { * The type of the template source. */ readonly templateType: TemplateType; - /** - * Versioning Strategy to use for deployment - * @default DEFAULT - */ - readonly retentionStrategy?: RetentionStrategy } /** @@ -128,17 +130,20 @@ class CloudFormationAssetTemplate extends CloudFormationTemplate { class CloudFormationProductStackTemplate extends CloudFormationTemplate { /** * @param stack A service catalog product stack. - */ - constructor(public readonly productStack: ProductStack, public readonly retentionStrategy?: RetentionStrategy) { + */ + constructor(public readonly productStack: ProductStack, + public readonly locked?: boolean, public readonly directory?: string) { super(); + const productVersionDetails = productStack._getProductVersionDetails(); + productVersionDetails.locked = this.locked; + productVersionDetails.directory = this.directory; } public bind(_scope: Construct): CloudFormationTemplateConfig { return { - productVersionDetails: this.productStack._getProductVersionDetails(), httpUrl: this.productStack._getTemplateUrl(), + productVersionDetails: this.productStack._getProductVersionDetails(), templateType: TemplateType.PRODUCT_STACK, - retentionStrategy: this.retentionStrategy, }; } } @@ -146,22 +151,21 @@ class CloudFormationProductStackTemplate extends CloudFormationTemplate { /** * Template from a previously deployed product stack. */ -class CloudFormationProductStackContextTemplate extends CloudFormationTemplate { - private readonly productVersionDetails: ProductVersionDetails; +class CloudFormationProductStackSnapshotTemplate extends CloudFormationTemplate { /** - * @param baseProductStackId The id of the product stack where the version was deployed from. + * @param stack A service catalog product stack. */ - constructor(public readonly baseProductStackId: string) { + constructor(public readonly productStack: ProductStack, public readonly directory?: string) { super(); - this.productVersionDetails = new ProductVersionDetails(); - this.productVersionDetails.productStackId = this.baseProductStackId; + const productVersionDetails = productStack._getProductVersionDetails(); + productVersionDetails.directory = this.directory; } public bind(_scope: Construct): CloudFormationTemplateConfig { return { - httpUrl: '', - productVersionDetails: this.productVersionDetails, - templateType: TemplateType.PRODUCT_STACK_CONTEXT, + httpUrl: this.productStack._getTemplateUrl(), + productVersionDetails: this.productStack._getProductVersionDetails(), + templateType: TemplateType.PRODUCT_STACK_SNAPSHOT, }; } } diff --git a/packages/@aws-cdk/aws-servicecatalog/lib/common.ts b/packages/@aws-cdk/aws-servicecatalog/lib/common.ts index c8bcc25c6f16d..6f9aa1e3d9453 100644 --- a/packages/@aws-cdk/aws-servicecatalog/lib/common.ts +++ b/packages/@aws-cdk/aws-servicecatalog/lib/common.ts @@ -1,7 +1,7 @@ /** - * Constant for the context directory to store retained ProductStack templates. + * Constant for the default directory to store ProductStack snapshots. */ -export const PRODUCT_STACK_SNAPSHOT_DIRECTORY = 'product-stack-snapshots'; +export const DEFAULT_PRODUCT_STACK_SNAPSHOT_DIRECTORY = 'product-stack-snapshots'; /** * The language code. @@ -27,7 +27,7 @@ export enum MessageLanguage { /** * The types of cloudFormationTemplates for product versions. - * Used to determine the source of a cloudFormationTemplate and apply logic accordingly + * Used to determine the source of a cloudFormationTemplate and apply logic accordingly. */ export enum TemplateType { /** @@ -46,31 +46,13 @@ export enum TemplateType { PRODUCT_STACK = 'ProductStackTemplate', /** - * ProductStackContextTemplate + * ProductStackSnapshotTemplate */ - PRODUCT_STACK_CONTEXT = 'ProductStackContextTemplate' + PRODUCT_STACK_SNAPSHOT = 'ProductStackSnapshotTemplate' } /** - * The strategy to use for a ProductStack deployment. - * Determines how a productVersion is saved and deployed. - */ -export enum RetentionStrategy { - /** - * Default Strategy for ProductStack deployment. - * This strategy will overwrite existing versions when deployed. - */ - OVERRIDE = 'Override', - - /** - * Retain previously deployed ProductStacks in a local context directory. - * This strategy will not overwrite existing versions when deployed. - */ - RETAIN = 'Retain', -} - -/** - * A wrapper class containing useful metadata about the product version + * A wrapper class containing useful metadata about the product version. */ export class ProductVersionDetails { /** @@ -89,7 +71,12 @@ export class ProductVersionDetails { public productVersionName?: string; /** - * Versioning strategy to use for a ProductStack deployment. + * Directory to store snapshots + */ + public directory?: string; + + /** + * Whether to overwrite existing version */ - public retentionStrategy?: RetentionStrategy + public locked?: boolean; } diff --git a/packages/@aws-cdk/aws-servicecatalog/lib/index.ts b/packages/@aws-cdk/aws-servicecatalog/lib/index.ts index cc26e880fdd2e..334177bca33a2 100644 --- a/packages/@aws-cdk/aws-servicecatalog/lib/index.ts +++ b/packages/@aws-cdk/aws-servicecatalog/lib/index.ts @@ -4,6 +4,7 @@ export * from './cloudformation-template'; export * from './portfolio'; export * from './product'; export * from './product-stack'; +export * from './product-stack-history'; export * from './tag-options'; // AWS::ServiceCatalog CloudFormation Resources: diff --git a/packages/@aws-cdk/aws-servicecatalog/lib/product-stack-history.ts b/packages/@aws-cdk/aws-servicecatalog/lib/product-stack-history.ts new file mode 100644 index 0000000000000..b0a36d5495832 --- /dev/null +++ b/packages/@aws-cdk/aws-servicecatalog/lib/product-stack-history.ts @@ -0,0 +1,78 @@ +import { Construct } from 'constructs'; +import { CloudFormationTemplate } from './cloudformation-template'; +import { CloudFormationProductVersion } from './product'; +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 +import { Construct as CoreConstruct } from '@aws-cdk/core'; + +/** + * Properties for a ProductStackHistory. + */ +export interface ProductStackHistoryProps { + /** + * The base Product Stack. + */ + readonly productStack: ProductStack; + /** + * The current version name of the ProductStack. + */ + readonly currentVersionName: string; + /** + * If this is set to true, the ProductStack will not be overwritten if a snapshot is found for the currentVersionName. + */ + readonly locked: boolean + /** + * The description of the product version + * @default - No description provided + */ + readonly description?: string; + /** + * Whether the specified product template will be validated by CloudFormation. + * If turned off, an invalid template configuration can be stored. + * @default true + */ + readonly validateTemplate?: boolean; + /** + * The directory where template snapshots will be stored + * @default - product-stack-snapshots + */ + readonly directory?: string +} + +/** + * A Construct that contains a Service Catalog product stack with its previous deployments maintained. + */ +export class ProductStackHistory extends CoreConstruct { + private readonly props: ProductStackHistoryProps + constructor(scope: Construct, id: string, props: ProductStackHistoryProps) { + super(scope, id); + this.props = props; + } + + /** + * Retains product stack template as a snapshot when deployed and + * retrieves a CloudFormationProductVersion for the current product version. + */ + public currentVersion() : CloudFormationProductVersion { + return { + cloudFormationTemplate: CloudFormationTemplate.fromProductStackHistory( + this.props.productStack, this.props.locked, this.props.directory), + productVersionName: this.props.currentVersionName, + description: this.props.description, + }; + } + + /** + * Retrieves a CloudFormationProductVersion from a previously deployed productVersionName. + */ + public versionFromSnapshot(versionName: string) : CloudFormationProductVersion { + return { + cloudFormationTemplate: CloudFormationTemplate.fromProductStackSnapshot( + this.props.productStack, this.props.directory), + productVersionName: versionName, + description: this.props.description, + }; + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-servicecatalog/lib/product-stack.ts b/packages/@aws-cdk/aws-servicecatalog/lib/product-stack.ts index 73f3bfd2cc8c4..7dfeac68ee617 100644 --- a/packages/@aws-cdk/aws-servicecatalog/lib/product-stack.ts +++ b/packages/@aws-cdk/aws-servicecatalog/lib/product-stack.ts @@ -2,7 +2,7 @@ import * as crypto from 'crypto'; import * as fs from 'fs'; import * as path from 'path'; import * as cdk from '@aws-cdk/core'; -import { PRODUCT_STACK_SNAPSHOT_DIRECTORY, ProductVersionDetails, RetentionStrategy } from './common'; +import { DEFAULT_PRODUCT_STACK_SNAPSHOT_DIRECTORY, ProductVersionDetails } from './common'; import { ProductStackSynthesizer } from './private/product-stack-synthesizer'; // keep this import separate from other imports to reduce chance for merge conflicts with v2-main @@ -32,7 +32,6 @@ export class ProductStack extends cdk.Stack { this._parentStack = findParentStack(scope); this._productVersionDetails = new ProductVersionDetails(); this._productVersionDetails.productStackId = id; - // this is the file name of the synthesized template file within the cloud assembly this.templateFile = `${cdk.Names.uniqueId(this)}.product.template.json`; } @@ -42,7 +41,7 @@ export class ProductStack extends cdk.Stack { * * @internal */ - public _getProductVersionDetails(): ProductVersionDetails | undefined { + public _getProductVersionDetails(): ProductVersionDetails { return this._productVersionDetails; } @@ -73,37 +72,37 @@ export class ProductStack extends cdk.Stack { fileName: this.templateFile, }).httpUrl; - if (this._productVersionDetails.retentionStrategy == RetentionStrategy.RETAIN) { - this.writeTemplateToContext(cfn, templateHash); + if (this._productVersionDetails.locked !== undefined) { + this.writeTemplateToSnapshot(cfn); } fs.writeFileSync(path.join(session.assembly.outdir, this.templateFile), cfn); } /** - * Writes current template generated from Product Stack to a context directory. + * Writes current template generated from Product Stack to a snapshot directory. * * @internal */ - private writeTemplateToContext(cfn: string, templateHash: string) { - if (!fs.existsSync(PRODUCT_STACK_SNAPSHOT_DIRECTORY)) { - fs.mkdirSync(PRODUCT_STACK_SNAPSHOT_DIRECTORY); + private writeTemplateToSnapshot(cfn: string) { + const productStackSnapshotDirectory = this._productVersionDetails.directory || DEFAULT_PRODUCT_STACK_SNAPSHOT_DIRECTORY; + if (!fs.existsSync(productStackSnapshotDirectory)) { + fs.mkdirSync(productStackSnapshotDirectory); } const templateFileKey = `${this._productVersionDetails.productPathUniqueId}.${this._productVersionDetails.productStackId}.${this._productVersionDetails.productVersionName}.product.template.json`; - const templateFilePath = path.join(PRODUCT_STACK_SNAPSHOT_DIRECTORY, templateFileKey); + const templateFilePath = path.join(productStackSnapshotDirectory, templateFileKey); if (fs.existsSync(templateFilePath)) { - const previousTemplateHash = crypto.createHash('sha256').update(fs.readFileSync(templateFilePath)).digest('hex'); - if (templateHash !== previousTemplateHash) { + const previousCfn = fs.readFileSync(templateFilePath).toString(); + if (previousCfn !== cfn && this._productVersionDetails.locked) { throw new Error(`Template has changed for ProductStack Version ${this._productVersionDetails.productVersionName}. - ${this._productVersionDetails.productVersionName} already exist in ${PRODUCT_STACK_SNAPSHOT_DIRECTORY}. - Either update the productVersionName to deploy a new version or deploy existing ProductStack from context using: - CloudFormationTemplate.fromProductStackSnapshot('${this._productVersionDetails.productStackId}'); + ${this._productVersionDetails.productVersionName} already exist in ${productStackSnapshotDirectory}. + Since locked has been set to ${this._productVersionDetails.locked}, + Either update the currentVersionName to deploy a new version or deploy the existing ProductStack snapshot. If ${this._productVersionDetails.productVersionName} was unintentionally synthesized and not deployed, - delete the corresponding version from ${PRODUCT_STACK_SNAPSHOT_DIRECTORY} and redeploy.`); + delete the corresponding version from ${productStackSnapshotDirectory} and redeploy.`); } - } else { - fs.writeFileSync(templateFilePath, cfn); } + fs.writeFileSync(templateFilePath, cfn); } } diff --git a/packages/@aws-cdk/aws-servicecatalog/lib/product.ts b/packages/@aws-cdk/aws-servicecatalog/lib/product.ts index 9ef43861db093..7ec964b90dbfe 100644 --- a/packages/@aws-cdk/aws-servicecatalog/lib/product.ts +++ b/packages/@aws-cdk/aws-servicecatalog/lib/product.ts @@ -4,7 +4,7 @@ import * as s3_assets from '@aws-cdk/aws-s3-assets'; import { ArnFormat, IResource, Names, Resource, Stack } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { CloudFormationTemplate } from './cloudformation-template'; -import { MessageLanguage, PRODUCT_STACK_SNAPSHOT_DIRECTORY, TemplateType } from './common'; +import { MessageLanguage, DEFAULT_PRODUCT_STACK_SNAPSHOT_DIRECTORY, TemplateType } from './common'; import { AssociationManager } from './private/association-manager'; import { hashValues } from './private/util'; import { InputValidator } from './private/validation'; @@ -218,14 +218,14 @@ export class CloudFormationProduct extends Product { if (template.productVersionDetails) { template.productVersionDetails.productPathUniqueId = this.productPathUniqueId; template.productVersionDetails.productVersionName = productVersion.productVersionName; - template.productVersionDetails.retentionStrategy = template.retentionStrategy; } break; - case TemplateType.PRODUCT_STACK_CONTEXT: + case TemplateType.PRODUCT_STACK_SNAPSHOT: + const productStackSnapshotDirectory = template.productVersionDetails?.directory || DEFAULT_PRODUCT_STACK_SNAPSHOT_DIRECTORY; const templateFileKey = `${this.productPathUniqueId}.${template.productVersionDetails?.productStackId}.${productVersion.productVersionName}.product.template.json`; - const templateFilePath = path.join(PRODUCT_STACK_SNAPSHOT_DIRECTORY, templateFileKey); + const templateFilePath = path.join(productStackSnapshotDirectory, templateFileKey); if (!fs.existsSync(templateFilePath)) { - throw new Error(`Template ${templateFileKey} cannot be found in ${PRODUCT_STACK_SNAPSHOT_DIRECTORY}`); + throw new Error(`Template ${templateFileKey} cannot be found in ${productStackSnapshotDirectory}`); } httpUrl = new s3_assets.Asset(this, `Template${hashValues(templateFileKey)}`, { path: templateFilePath, @@ -243,8 +243,7 @@ export class CloudFormationProduct extends Product { info: { LoadTemplateFromURL: httpUrl, }, - }, - ); + }); } return productVersions; }; diff --git a/packages/@aws-cdk/aws-servicecatalog/test/integ.product.ts b/packages/@aws-cdk/aws-servicecatalog/test/integ.product.ts index 50914f7a20e03..a0c8bbfb68828 100644 --- a/packages/@aws-cdk/aws-servicecatalog/test/integ.product.ts +++ b/packages/@aws-cdk/aws-servicecatalog/test/integ.product.ts @@ -2,6 +2,7 @@ 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'; +import { ProductStackHistory } from '../lib'; const app = new cdk.App(); const stack = new cdk.Stack(app, 'integ-servicecatalog-product'); @@ -14,6 +15,12 @@ class TestProductStack extends servicecatalog.ProductStack { } } +const productStackHistory = new ProductStackHistory(stack, 'ProductStackHistory', { + productStack: new TestProductStack(stack, 'SNSTopicProduct3'), + currentVersionName: 'v1', + locked: true, +}); + const product = new servicecatalog.CloudFormationProduct(stack, 'TestProduct', { productName: 'testProduct', owner: 'testOwner', @@ -35,9 +42,7 @@ const product = new servicecatalog.CloudFormationProduct(stack, 'TestProduct', { { cloudFormationTemplate: servicecatalog.CloudFormationTemplate.fromProductStack(new TestProductStack(stack, 'SNSTopicProduct2')), }, - { - cloudFormationTemplate: servicecatalog.CloudFormationTemplate.fromProductStack(new TestProductStack(stack, 'SNSTopicProduct3'), servicecatalog.RetentionStrategy.RETAIN), - }, + productStackHistory.currentVersion(), ], }); diff --git a/packages/@aws-cdk/aws-servicecatalog/test/portfolio.integ.snapshot/cdk.out b/packages/@aws-cdk/aws-servicecatalog/test/portfolio.integ.snapshot/cdk.out index 90bef2e09ad39..2efc89439fab8 100644 --- a/packages/@aws-cdk/aws-servicecatalog/test/portfolio.integ.snapshot/cdk.out +++ b/packages/@aws-cdk/aws-servicecatalog/test/portfolio.integ.snapshot/cdk.out @@ -1 +1 @@ -{"version":"17.0.0"} \ No newline at end of file +{"version":"18.0.0"} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-servicecatalog/test/portfolio.integ.snapshot/integ.json b/packages/@aws-cdk/aws-servicecatalog/test/portfolio.integ.snapshot/integ.json index ab86cddb1a16a..09a045531f14f 100644 --- a/packages/@aws-cdk/aws-servicecatalog/test/portfolio.integ.snapshot/integ.json +++ b/packages/@aws-cdk/aws-servicecatalog/test/portfolio.integ.snapshot/integ.json @@ -1,7 +1,7 @@ { "version": "18.0.0", "testCases": { - "aws-servicecatalog/test/integ.portfolio": { + "integ.portfolio": { "stacks": [ "integ-servicecatalog-portfolio" ], diff --git a/packages/@aws-cdk/aws-servicecatalog/test/portfolio.integ.snapshot/manifest.json b/packages/@aws-cdk/aws-servicecatalog/test/portfolio.integ.snapshot/manifest.json index 881a78a773602..9abbe12a0c3aa 100644 --- a/packages/@aws-cdk/aws-servicecatalog/test/portfolio.integ.snapshot/manifest.json +++ b/packages/@aws-cdk/aws-servicecatalog/test/portfolio.integ.snapshot/manifest.json @@ -1,5 +1,5 @@ { - "version": "17.0.0", + "version": "18.0.0", "artifacts": { "Tree": { "type": "cdk:tree", diff --git a/packages/@aws-cdk/aws-servicecatalog/test/product.integ.snapshot/integ-servicecatalog-product.template.json b/packages/@aws-cdk/aws-servicecatalog/test/product.integ.snapshot/integ-servicecatalog-product.template.json index 2c4701743382e..11578cd12197b 100644 --- a/packages/@aws-cdk/aws-servicecatalog/test/product.integ.snapshot/integ-servicecatalog-product.template.json +++ b/packages/@aws-cdk/aws-servicecatalog/test/product.integ.snapshot/integ-servicecatalog-product.template.json @@ -265,7 +265,8 @@ ] ] } - } + }, + "Name": "v1" } ] } diff --git a/packages/@aws-cdk/aws-servicecatalog/test/product.integ.snapshot/integservicecatalogproductSNSTopicProduct3B51CF591.product.template.json b/packages/@aws-cdk/aws-servicecatalog/test/product.integ.snapshot/integservicecatalogproductSNSTopicProduct3B51CF591.product.template.json new file mode 100644 index 0000000000000..2f2f4704a22ad --- /dev/null +++ b/packages/@aws-cdk/aws-servicecatalog/test/product.integ.snapshot/integservicecatalogproductSNSTopicProduct3B51CF591.product.template.json @@ -0,0 +1,7 @@ +{ + "Resources": { + "TopicProductD757E287": { + "Type": "AWS::SNS::Topic" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-servicecatalog/test/product.integ.snapshot/manifest.json b/packages/@aws-cdk/aws-servicecatalog/test/product.integ.snapshot/manifest.json index d7ecd5e893be5..c8d093eaf8a0c 100644 --- a/packages/@aws-cdk/aws-servicecatalog/test/product.integ.snapshot/manifest.json +++ b/packages/@aws-cdk/aws-servicecatalog/test/product.integ.snapshot/manifest.json @@ -43,7 +43,7 @@ { "type": "aws:cdk:asset", "data": { - "path": "integservicecatalogproductSNSTopicProduct1B8D03934.product.template.json", + "path": "integservicecatalogproductSNSTopicProduct3B51CF591.product.template.json", "id": "dd2d087eeb6ede1d2a9166639ccbde7bd1b10eef9ba2b4cb3d9855faa4fe8c1f", "packaging": "file", "sourceHash": "dd2d087eeb6ede1d2a9166639ccbde7bd1b10eef9ba2b4cb3d9855faa4fe8c1f", diff --git a/packages/@aws-cdk/aws-servicecatalog/test/product.integ.snapshot/tree.json b/packages/@aws-cdk/aws-servicecatalog/test/product.integ.snapshot/tree.json index 48b0a9c4de946..7916b0fe8b481 100644 --- a/packages/@aws-cdk/aws-servicecatalog/test/product.integ.snapshot/tree.json +++ b/packages/@aws-cdk/aws-servicecatalog/test/product.integ.snapshot/tree.json @@ -16,17 +16,17 @@ "id": "integ-servicecatalog-product", "path": "integ-servicecatalog-product", "children": { - "SNSTopicProduct1": { - "id": "SNSTopicProduct1", - "path": "integ-servicecatalog-product/SNSTopicProduct1", + "SNSTopicProduct3": { + "id": "SNSTopicProduct3", + "path": "integ-servicecatalog-product/SNSTopicProduct3", "children": { "TopicProduct": { "id": "TopicProduct", - "path": "integ-servicecatalog-product/SNSTopicProduct1/TopicProduct", + "path": "integ-servicecatalog-product/SNSTopicProduct3/TopicProduct", "children": { "Resource": { "id": "Resource", - "path": "integ-servicecatalog-product/SNSTopicProduct1/TopicProduct/Resource", + "path": "integ-servicecatalog-product/SNSTopicProduct3/TopicProduct/Resource", "attributes": { "aws:cdk:cloudformation:type": "AWS::SNS::Topic", "aws:cdk:cloudformation:props": {} @@ -48,17 +48,25 @@ "version": "0.0.0" } }, - "SNSTopicProduct2": { - "id": "SNSTopicProduct2", - "path": "integ-servicecatalog-product/SNSTopicProduct2", + "ProductStackHistory": { + "id": "ProductStackHistory", + "path": "integ-servicecatalog-product/ProductStackHistory", + "constructInfo": { + "fqn": "@aws-cdk/aws-servicecatalog.ProductStackHistory", + "version": "0.0.0" + } + }, + "SNSTopicProduct1": { + "id": "SNSTopicProduct1", + "path": "integ-servicecatalog-product/SNSTopicProduct1", "children": { "TopicProduct": { "id": "TopicProduct", - "path": "integ-servicecatalog-product/SNSTopicProduct2/TopicProduct", + "path": "integ-servicecatalog-product/SNSTopicProduct1/TopicProduct", "children": { "Resource": { "id": "Resource", - "path": "integ-servicecatalog-product/SNSTopicProduct2/TopicProduct/Resource", + "path": "integ-servicecatalog-product/SNSTopicProduct1/TopicProduct/Resource", "attributes": { "aws:cdk:cloudformation:type": "AWS::SNS::Topic", "aws:cdk:cloudformation:props": {} @@ -80,17 +88,17 @@ "version": "0.0.0" } }, - "SNSTopicProduct3": { - "id": "SNSTopicProduct3", - "path": "integ-servicecatalog-product/SNSTopicProduct3", + "SNSTopicProduct2": { + "id": "SNSTopicProduct2", + "path": "integ-servicecatalog-product/SNSTopicProduct2", "children": { "TopicProduct": { "id": "TopicProduct", - "path": "integ-servicecatalog-product/SNSTopicProduct3/TopicProduct", + "path": "integ-servicecatalog-product/SNSTopicProduct2/TopicProduct", "children": { "Resource": { "id": "Resource", - "path": "integ-servicecatalog-product/SNSTopicProduct3/TopicProduct/Resource", + "path": "integ-servicecatalog-product/SNSTopicProduct2/TopicProduct/Resource", "attributes": { "aws:cdk:cloudformation:type": "AWS::SNS::Topic", "aws:cdk:cloudformation:props": {} @@ -294,6 +302,7 @@ "info": {} }, { + "name": "v1", "disableTemplateValidation": false, "info": {} } diff --git a/packages/@aws-cdk/aws-servicecatalog/test/product.test.ts b/packages/@aws-cdk/aws-servicecatalog/test/product.test.ts index 2abf0faab2b05..af76fdcc93b21 100644 --- a/packages/@aws-cdk/aws-servicecatalog/test/product.test.ts +++ b/packages/@aws-cdk/aws-servicecatalog/test/product.test.ts @@ -4,7 +4,8 @@ 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'; -import { RetentionStrategy, PRODUCT_STACK_SNAPSHOT_DIRECTORY } from '../lib'; +import { DEFAULT_PRODUCT_STACK_SNAPSHOT_DIRECTORY } from '../lib'; +import { ProductStackHistory } from '../lib/product-stack-history'; /* eslint-disable quote-props */ describe('Product', () => { @@ -191,19 +192,22 @@ describe('Product', () => { expect(assembly.stacks[0].assets.length).toBe(1); }), - test('product test from product stack with retention strategy override', () => { + test('product test from product stack history', () => { const productStack = new servicecatalog.ProductStack(stack, 'ProductStack'); + const productStackHistory = new ProductStackHistory(stack, 'MyProductStackHistory', { + productStack: productStack, + currentVersionName: 'v1', + locked: false, + }); + new sns.Topic(productStack, 'SNSTopicProductStack'); new servicecatalog.CloudFormationProduct(stack, 'MyProduct', { productName: 'testProduct', owner: 'testOwner', productVersions: [ - { - productVersionName: 'v1', - cloudFormationTemplate: servicecatalog.CloudFormationTemplate.fromProductStack(productStack, RetentionStrategy.OVERRIDE), - }, + productStackHistory.currentVersion(), ], }); @@ -213,83 +217,100 @@ describe('Product', () => { expect(assembly.stacks[0].assets[0].path).toEqual('ProductStack.product.template.json'); const expectedTemplateFileKey = 'MyProduct.ProductStack.v1.product.template.json'; - const fileExistsContext = fs.existsSync(path.join(PRODUCT_STACK_SNAPSHOT_DIRECTORY, expectedTemplateFileKey)); - expect(fileExistsContext).toBe(false); + const snapshotExists = fs.existsSync(path.join(DEFAULT_PRODUCT_STACK_SNAPSHOT_DIRECTORY, expectedTemplateFileKey)); + expect(snapshotExists).toBe(true); }), - test('product test from product stack with retention strategy retain', () => { + test('fails product test from product stack when template changes and locked', () => { const productStack = new servicecatalog.ProductStack(stack, 'ProductStack'); - new sns.Topic(productStack, 'SNSTopicProductStack'); + const productStackHistory = new ProductStackHistory(stack, 'MyProductStackHistory', { + productStack: productStack, + currentVersionName: 'v1', + locked: true, + }); + + new sns.Topic(productStack, 'SNSTopicProductStack2'); new servicecatalog.CloudFormationProduct(stack, 'MyProduct', { productName: 'testProduct', owner: 'testOwner', productVersions: [ - { - productVersionName: 'v2', - cloudFormationTemplate: servicecatalog.CloudFormationTemplate.fromProductStack(productStack, RetentionStrategy.RETAIN), - }, + productStackHistory.currentVersion(), ], }); - - 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'); - - const expectedTemplateFileKey = 'MyProduct.ProductStack.v2.product.template.json'; - const fileExistsContext = fs.existsSync(path.join(PRODUCT_STACK_SNAPSHOT_DIRECTORY, expectedTemplateFileKey)); - expect(fileExistsContext).toBe(true); + expect(() => { + app.synth(); + }).toThrowError('Template has changed for ProductStack Version v1'); }), - - test('fails product test from product stack with retention strategy retain when template changes', () => { + test('product test from product stack history when template changes and unlocked', () => { const productStack = new servicecatalog.ProductStack(stack, 'ProductStack'); + + const productStackHistory = new ProductStackHistory(stack, 'MyProductStackHistory', { + productStack: productStack, + currentVersionName: 'v1', + locked: false, + }); + new sns.Topic(productStack, 'SNSTopicProductStack2'); + new servicecatalog.CloudFormationProduct(stack, 'MyProduct', { productName: 'testProduct', owner: 'testOwner', productVersions: [ - { - productVersionName: 'v2', - cloudFormationTemplate: servicecatalog.CloudFormationTemplate.fromProductStack(productStack, RetentionStrategy.RETAIN), - }, + productStackHistory.currentVersion(), ], }); - expect(() => { - app.synth(); - }).toThrowError('Template has changed for ProductStack Version v2'); + + 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'); + + const expectedTemplateFileKey = 'MyProduct.ProductStack.v1.product.template.json'; + const snapshotExists = fs.existsSync(path.join(DEFAULT_PRODUCT_STACK_SNAPSHOT_DIRECTORY, expectedTemplateFileKey)); + expect(snapshotExists).toBe(true); }), - test('product test from product stack snapshot', () => { + test('product test from product stack history snapshot', () => { + const productStack = new servicecatalog.ProductStack(stack, 'ProductStack'); + + const productStackHistory = new ProductStackHistory(stack, 'MyProductStackHistory', { + productStack: productStack, + currentVersionName: 'v2', + locked: false, + }); + new servicecatalog.CloudFormationProduct(stack, 'MyProduct', { productName: 'testProduct', owner: 'testOwner', productVersions: [ - { - productVersionName: 'v2', - cloudFormationTemplate: servicecatalog.CloudFormationTemplate.fromProductStackSnapshot('ProductStack'), - }, + productStackHistory.versionFromSnapshot('v1'), ], }); 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('asset.c0d25a2f0dec85d4ebb373daf067bb05f6c35c669e6dddf499e1931d794d547f.json'); + expect(assembly.stacks[0].assets.length).toBe(2); + expect(assembly.stacks[0].assets[0].path).toEqual('asset.434625edc7e017d93f388b5f116c2ebcf11a38457cfb89a9b00d4e551c0bf731.json'); }), - test('fails product from product stack snapshot not found', () => { + test('fails product from product stack history snapshot not found', () => { + const productStack = new servicecatalog.ProductStack(stack, 'ProductStack'); + + const productStackHistory = new ProductStackHistory(stack, 'MyProductStackHistory', { + productStack: productStack, + currentVersionName: 'v2', + locked: false, + }); + expect(() => { new servicecatalog.CloudFormationProduct(stack, 'MyProduct', { productName: 'testProduct', owner: 'testOwner', productVersions: [ - { - productVersionName: 'v3', - cloudFormationTemplate: servicecatalog.CloudFormationTemplate.fromProductStackSnapshot('ProductStack'), - }, + productStackHistory.versionFromSnapshot('v3'), ], }); }).toThrowError('Template MyProduct.ProductStack.v3.product.template.json cannot be found in product-stack-snapshots');