Skip to content

Commit

Permalink
Addressed comments and created ProductStackHistory
Browse files Browse the repository at this point in the history
  • Loading branch information
wanjacki committed May 18, 2022
1 parent 3e3f05e commit d2e2af0
Show file tree
Hide file tree
Showing 17 changed files with 290 additions and 168 deletions.
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-servicecatalog/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
*.d.ts
tsconfig.json
node_modules
product-stack-snapshots
*.generated.ts
dist
.jsii
Expand Down
60 changes: 35 additions & 25 deletions packages/@aws-cdk/aws-servicecatalog/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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';
Expand All @@ -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';
Expand All @@ -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')
],
});
```
Expand Down
54 changes: 29 additions & 25 deletions packages/@aws-cdk/aws-servicecatalog/lib/cloudformation-template.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
}

/**
Expand Down Expand Up @@ -128,40 +130,42 @@ 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,
};
}
}

/**
* 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,
};
}
}
39 changes: 13 additions & 26 deletions packages/@aws-cdk/aws-servicecatalog/lib/common.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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 {
/**
Expand All @@ -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 {
/**
Expand All @@ -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;
}
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-servicecatalog/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
78 changes: 78 additions & 0 deletions packages/@aws-cdk/aws-servicecatalog/lib/product-stack-history.ts
Original file line number Diff line number Diff line change
@@ -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,
};
}
}
Loading

0 comments on commit d2e2af0

Please sign in to comment.