-
Notifications
You must be signed in to change notification settings - Fork 4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(servicecatalog): initial implementation of the Product construct (…
…#15185) This is the first minimal release of an L2 construct for a service catalog product. In a future PR, functionality to associate with a Portfolio and add constraints will be added. Testing done ------------------ * `yarn build && yarn test` * `yarn integ` ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* Co-authored-by: Aidan Crank <arcrank@gmail.com> Co-authored-by: Dillon Ponzo <dponzo18@gmail.com>
- Loading branch information
1 parent
74da5c1
commit fe3e0f2
Showing
10 changed files
with
747 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
89 changes: 89 additions & 0 deletions
89
packages/@aws-cdk/aws-servicecatalog/lib/cloudformation-template.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
import * as s3_assets from '@aws-cdk/aws-s3-assets'; | ||
|
||
// 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 '@aws-cdk/core'; | ||
|
||
/** | ||
* Represents the Product Provisioning Artifact Template. | ||
*/ | ||
export abstract class CloudFormationTemplate { | ||
/** | ||
* Template from URL | ||
* @param url The url that points to the provisioning artifacts template | ||
*/ | ||
public static fromUrl(url: string): CloudFormationTemplate { | ||
return new CloudFormationUrlTemplate(url); | ||
} | ||
|
||
/** | ||
* Loads the provisioning artifacts template from a local disk path. | ||
* | ||
* @param path A file containing the provisioning artifacts | ||
*/ | ||
public static fromAsset(path: string, options?: s3_assets.AssetOptions): CloudFormationTemplate { | ||
return new CloudFormationAssetTemplate(path, options); | ||
} | ||
|
||
/** | ||
* Called when the product is initialized to allow this object to bind | ||
* to the stack, add resources and have fun. | ||
* | ||
* @param scope The binding scope. Don't be smart about trying to down-cast or | ||
* assume it's initialized. You may just use it as a construct scope. | ||
*/ | ||
public abstract bind(scope: Construct): CloudFormationTemplateConfig; | ||
} | ||
|
||
/** | ||
* Result of binding `Template` into a `Product`. | ||
*/ | ||
export interface CloudFormationTemplateConfig { | ||
/** | ||
* The http url of the template in S3. | ||
*/ | ||
readonly httpUrl: string; | ||
} | ||
|
||
/** | ||
* Template code from a Url. | ||
*/ | ||
class CloudFormationUrlTemplate extends CloudFormationTemplate { | ||
constructor(private readonly url: string) { | ||
super(); | ||
} | ||
|
||
public bind(_scope: Construct): CloudFormationTemplateConfig { | ||
return { | ||
httpUrl: this.url, | ||
}; | ||
} | ||
} | ||
|
||
/** | ||
* Template from a local file. | ||
*/ | ||
class CloudFormationAssetTemplate extends CloudFormationTemplate { | ||
private asset?: s3_assets.Asset; | ||
|
||
/** | ||
* @param path The path to the asset file. | ||
*/ | ||
constructor(public readonly path: string, private readonly options: s3_assets.AssetOptions = { }) { | ||
super(); | ||
} | ||
|
||
public bind(scope: Construct): CloudFormationTemplateConfig { | ||
// If the same AssetCode is used multiple times, retain only the first instantiation. | ||
if (!this.asset) { | ||
this.asset = new s3_assets.Asset(scope, 'Template', { | ||
path: this.path, | ||
...this.options, | ||
}); | ||
} | ||
|
||
return { | ||
httpUrl: this.asset.httpUrl, | ||
}; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,7 @@ | ||
export * from './common'; | ||
export * from './cloudformation-template'; | ||
export * from './portfolio'; | ||
export * from './product'; | ||
|
||
// AWS::ServiceCatalog CloudFormation Resources: | ||
export * from './servicecatalog.generated'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,212 @@ | ||
import { ArnFormat, IResource, Resource, Stack } from '@aws-cdk/core'; | ||
import { Construct } from 'constructs'; | ||
import { CloudFormationTemplate } from './cloudformation-template'; | ||
import { AcceptLanguage } from './common'; | ||
import { InputValidator } from './private/validation'; | ||
import { CfnCloudFormationProduct } from './servicecatalog.generated'; | ||
|
||
/** | ||
* A Service Catalog product, currently only supports type CloudFormationProduct | ||
*/ | ||
export interface IProduct extends IResource { | ||
/** | ||
* The ARN of the product. | ||
* @attribute | ||
*/ | ||
readonly productArn: string; | ||
|
||
/** | ||
* The id of the product | ||
* @attribute | ||
*/ | ||
readonly productId: string; | ||
} | ||
|
||
abstract class ProductBase extends Resource implements IProduct { | ||
public abstract readonly productArn: string; | ||
public abstract readonly productId: string; | ||
} | ||
|
||
/** | ||
* Properties of product version (also known as a provisioning artifact). | ||
*/ | ||
export interface CloudFormationProductVersion { | ||
/** | ||
* 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 S3 template that points to the provisioning version template | ||
*/ | ||
readonly cloudFormationTemplate: CloudFormationTemplate; | ||
|
||
/** | ||
* The name of the product version. | ||
* @default - No product version name provided | ||
*/ | ||
readonly productVersionName?: string; | ||
} | ||
|
||
/** | ||
* Properties for a Cloudformation Product | ||
*/ | ||
export interface CloudFormationProductProps { | ||
/** | ||
* The owner of the product. | ||
*/ | ||
readonly owner: string; | ||
|
||
/** | ||
* The name of the product. | ||
*/ | ||
readonly productName: string; | ||
|
||
/** | ||
* The configuration of the product version. | ||
*/ | ||
readonly productVersions: CloudFormationProductVersion[]; | ||
|
||
/** | ||
* The language code. | ||
* @default - No accept language provided | ||
*/ | ||
readonly acceptLanguage?: AcceptLanguage; | ||
|
||
/** | ||
* The description of the product. | ||
* @default - No description provided | ||
*/ | ||
readonly description?: string; | ||
|
||
/** | ||
* The distributor of the product. | ||
* @default - No distributor provided | ||
*/ | ||
readonly distributor?: string; | ||
|
||
/** | ||
* Whether to give provisioning artifacts a new unique identifier when the product attributes or provisioning artifacts is updated | ||
* @default false | ||
*/ | ||
readonly replaceProductVersionIds?: boolean; | ||
|
||
/** | ||
* The support information about the product | ||
* @default - No support description provided | ||
*/ | ||
readonly supportDescription?: string; | ||
|
||
/** | ||
* The contact email for product support. | ||
* @default - No support email provided | ||
*/ | ||
readonly supportEmail?: string; | ||
|
||
/** | ||
* The contact URL for product support. | ||
* @default - No support URL provided | ||
*/ | ||
readonly supportUrl?: string; | ||
} | ||
|
||
/** | ||
* Abstract class for Service Catalog Product. | ||
*/ | ||
export abstract class Product extends ProductBase { | ||
/** | ||
* Creates a Product construct that represents an external product. | ||
* @param scope The parent creating construct (usually `this`). | ||
* @param id The construct's name. | ||
* @param productArn Product Arn | ||
*/ | ||
public static fromProductArn(scope: Construct, id: string, productArn: string): IProduct { | ||
const arn = Stack.of(scope).splitArn(productArn, ArnFormat.SLASH_RESOURCE_NAME); | ||
const productId = arn.resourceName; | ||
|
||
if (!productId) { | ||
throw new Error('Missing required Portfolio ID from Portfolio ARN: ' + productArn); | ||
} | ||
|
||
return new class extends ProductBase { | ||
public readonly productId = productId!; | ||
public readonly productArn = productArn; | ||
}(scope, id); | ||
} | ||
} | ||
|
||
/** | ||
* A Service Catalog Cloudformation Product. | ||
*/ | ||
export class CloudFormationProduct extends Product { | ||
public readonly productArn: string; | ||
public readonly productId: string; | ||
|
||
constructor(scope: Construct, id: string, props: CloudFormationProductProps) { | ||
super(scope, id); | ||
|
||
this.validateProductProps(props); | ||
|
||
const product = new CfnCloudFormationProduct(this, 'Resource', { | ||
acceptLanguage: props.acceptLanguage, | ||
description: props.description, | ||
distributor: props.distributor, | ||
name: props.productName, | ||
owner: props.owner, | ||
provisioningArtifactParameters: this.renderProvisioningArtifacts(props), | ||
replaceProvisioningArtifacts: props.replaceProductVersionIds, | ||
supportDescription: props.supportDescription, | ||
supportEmail: props.supportEmail, | ||
supportUrl: props.supportUrl, | ||
}); | ||
|
||
this.productArn = Stack.of(this).formatArn({ | ||
service: 'catalog', | ||
resource: 'product', | ||
resourceName: product.ref, | ||
}); | ||
|
||
this.productId = product.ref; | ||
} | ||
|
||
private renderProvisioningArtifacts( | ||
props: CloudFormationProductProps): CfnCloudFormationProduct.ProvisioningArtifactPropertiesProperty[] { | ||
return props.productVersions.map(productVersion => { | ||
const template = productVersion.cloudFormationTemplate.bind(this); | ||
InputValidator.validateUrl(this.node.path, 'provisioning template url', template.httpUrl); | ||
return { | ||
name: productVersion.productVersionName, | ||
description: productVersion.description, | ||
disableTemplateValidation: productVersion.validateTemplate === false ? true : false, | ||
info: { | ||
LoadTemplateFromURL: template.httpUrl, | ||
}, | ||
}; | ||
}); | ||
}; | ||
|
||
private validateProductProps(props: CloudFormationProductProps) { | ||
InputValidator.validateLength(this.node.path, 'product product name', 1, 100, props.productName); | ||
InputValidator.validateLength(this.node.path, 'product owner', 1, 8191, props.owner); | ||
InputValidator.validateLength(this.node.path, 'product description', 0, 8191, props.description); | ||
InputValidator.validateLength(this.node.path, 'product distributor', 0, 8191, props.distributor); | ||
InputValidator.validateEmail(this.node.path, 'support email', props.supportEmail); | ||
InputValidator.validateUrl(this.node.path, 'support url', props.supportUrl); | ||
InputValidator.validateLength(this.node.path, 'support description', 0, 8191, props.supportDescription); | ||
if (props.productVersions.length == 0) { | ||
throw new Error(`Invalid product versions for resource ${this.node.path}, must contain at least 1 product version`); | ||
} | ||
props.productVersions.forEach(productVersion => { | ||
InputValidator.validateLength(this.node.path, 'provisioning artifact name', 0, 100, productVersion.productVersionName); | ||
InputValidator.validateLength(this.node.path, 'provisioning artifact description', 0, 8191, productVersion.description); | ||
}); | ||
} | ||
} |
Oops, something went wrong.