Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(servicecatalog): allow creating a CFN Product Version with CDK code #17144

Merged
merged 24 commits into from
Nov 2, 2021
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 38 additions & 2 deletions packages/@aws-cdk/aws-servicecatalog/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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:
Expand All @@ -149,7 +153,39 @@ const product = new servicecatalog.CloudFormationProduct(this, 'MyFirstProduct',
productVersionName: "v2",
cloudFormationTemplate: servicecatalog.CloudFormationTemplate.fromAsset(path.join(__dirname, 'development-environment.template.json')),
},
]
],
});
```

The product versions do not need to be of the same type, a Product can support product versions from url, assets, and stacks.
arcrank marked this conversation as resolved.
Show resolved Hide resolved

### 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';

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.fromStack(new S3BucketProduct(this, 'S3BucketProduct')),
},
],
});
```

Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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 fromStack(productStack: ProductStack): CloudFormationTemplate {
arcrank marked this conversation as resolved.
Show resolved Hide resolved
return new CloudFormationStackTemplate(productStack);
}

/**
* Called when the product is initialized to allow this object to bind
* to the stack, add resources and have fun.
Expand Down Expand Up @@ -88,3 +96,21 @@ class CloudFormationAssetTemplate extends CloudFormationTemplate {
};
}
}

/**
* Template from a CDK defined product stack.
*/
class CloudFormationStackTemplate extends CloudFormationTemplate {
arcrank marked this conversation as resolved.
Show resolved Hide resolved
/**
* @param stack A service catalog product stack.
*/
constructor(public readonly productStack: ProductStack) {
super();
}

public bind(_scope: Construct): CloudFormationTemplateConfig {
return {
httpUrl: this.productStack._getTemplateUrl(),
};
}
}
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 @@ -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:
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
skinny85 marked this conversation as resolved.
Show resolved Hide resolved
}
}
77 changes: 77 additions & 0 deletions packages/@aws-cdk/aws-servicecatalog/lib/product-stack.ts
Original file line number Diff line number Diff line change
@@ -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;
skinny85 marked this conversation as resolved.
Show resolved Hide resolved
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');
}
}
4 changes: 3 additions & 1 deletion packages/@aws-cdk/aws-servicecatalog/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,9 @@
"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",
"construct-ctor:@aws-cdk/aws-servicecatalog.ProductStack.<initializer>.params[0]"
skinny85 marked this conversation as resolved.
Show resolved Hide resolved
]
},
"maturity": "experimental",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,57 @@
]
}
}
},
{
"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"
}
]
}
]
}
]
]
}
}
}
]
}
Expand Down Expand Up @@ -142,6 +193,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\""
}
}
}
12 changes: 12 additions & 0 deletions packages/@aws-cdk/aws-servicecatalog/test/integ.product.ts
Original file line number Diff line number Diff line change
@@ -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',
Expand All @@ -20,6 +29,9 @@ new servicecatalog.CloudFormationProduct(stack, 'TestProduct', {
{
cloudFormationTemplate: servicecatalog.CloudFormationTemplate.fromAsset(path.join(__dirname, 'product2.template.json')),
},
{
cloudFormationTemplate: servicecatalog.CloudFormationTemplate.fromStack(new TestProductStack(stack, 'SNSTopicProduct')),
},
arcrank marked this conversation as resolved.
Show resolved Hide resolved
],
});

Expand Down
Loading