Skip to content

Commit

Permalink
feat(servicecatalog): add CloudFormation Parameter constraint (#15770)
Browse files Browse the repository at this point in the history
Add ability to configure parameter options for when launching a product.

----

*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 <dponzo18@gmail.com>
  • Loading branch information
arcrank and dponzo authored Jul 30, 2021
1 parent b86062f commit 58fda91
Show file tree
Hide file tree
Showing 9 changed files with 300 additions and 33 deletions.
29 changes: 28 additions & 1 deletion packages/@aws-cdk/aws-servicecatalog/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ enables organizations to create and manage catalogs of products for their end us
- [Constraints](#constraints)
- [Tag update constraint](#tag-update-constraint)
- [Notify on stack events](#notify-on-stack-events)
- [CloudFormation parameters constraint](#cloudformation-parameters-constraint)
- [Set launch role](#set-launch-role)
- [Deploy with StackSets](#deploy-with-stacksets)

Expand Down Expand Up @@ -162,7 +163,7 @@ A product can be added to multiple portfolios depending on your resource and org
portfolio.addProduct(product);
```

### Tag Options
## Tag Options

TagOptions allow administrators to easily manage tags on provisioned products by creating a selection of tags for end users to choose from.
For example, an end user can choose an `ec2` for the instance type size.
Expand Down Expand Up @@ -228,6 +229,32 @@ portfolio.notifyOnStackEvents(product, topic2, {
});
```

### CloudFormation parameters constraint

CloudFormation parameters constraints allow you to configure the that are available to end users when they launch a product via defined rules.
A rule consists of one or more assertions that narrow the allowable values for parameters in a product.
You can configure multiple parameter constraints to govern the different parameters and parameter options in your products.
For example, a rule might define the various instance types that users can choose from when launching a stack that includes EC2 instances.
A parameter rule has an optional `condition` field that allows ability to configure when rules are applied.
If a `condition` is specified, all the assertions will be applied if the condition evalutates to true.
For information on rule-specific intrinsic functions to define rule conditions and assertions,
see [AWS Rule Functions](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-rules.html).

```ts fixture=portfolio-product
import * as cdk from '@aws-cdk/core';

portfolio.constrainCloudFormationParameters(product, {
rule: {
ruleName: 'testInstanceType',
condition: cdk.Fn.conditionEquals(cdk.Fn.ref('Environment'), 'test'),
assertions: [{
assert: cdk.Fn.conditionContains(['t2.micro', 't2.small'], cdk.Fn.ref('InstanceType')),
description: 'For test environment, the instance type should be small',
}],
},
});
```

### Set launch role

Allows you to configure a specific AWS `IAM` role that a user must assume when launching a product.
Expand Down
48 changes: 48 additions & 0 deletions packages/@aws-cdk/aws-servicecatalog/lib/constraints.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as iam from '@aws-cdk/aws-iam';
import * as cdk from '@aws-cdk/core';
import { MessageLanguage } from './common';

/**
Expand Down Expand Up @@ -62,4 +63,51 @@ export interface TagUpdateConstraintOptions extends CommonConstraintOptions {
* @default true
*/
readonly allow?: boolean;
}

/**
* An assertion within a template rule, defined by intrinsic functions.
*/
export interface TemplateRuleAssertion {
/**
* The assertion condition.
*/
readonly assert: cdk.ICfnRuleConditionExpression;

/**
* The description for the asssertion.
* @default - no description provided for the assertion.
*/
readonly description?: string;
}

/**
* Defines the provisioning template constraints.
*/
export interface TemplateRule {
/**
* Name of the rule.
*/
readonly ruleName: string;

/**
* Specify when to apply rule with a rule-specific intrinsic function.
* @default - no rule condition provided
*/
readonly condition?: cdk.ICfnRuleConditionExpression;

/**
* A list of assertions that make up the rule.
*/
readonly assertions: TemplateRuleAssertion[];
}

/**
* Properties for provisoning rule constraint.
*/
export interface CloudFormationRuleConstraintOptions extends CommonConstraintOptions {
/**
* The rule with condition and assertions to apply to template.
*/
readonly rule: TemplateRule;
}
18 changes: 16 additions & 2 deletions packages/@aws-cdk/aws-servicecatalog/lib/portfolio.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ import * as iam from '@aws-cdk/aws-iam';
import * as sns from '@aws-cdk/aws-sns';
import * as cdk from '@aws-cdk/core';
import { MessageLanguage } from './common';
import { CommonConstraintOptions, StackSetsConstraintOptions, TagUpdateConstraintOptions } from './constraints';
import {
CloudFormationRuleConstraintOptions, CommonConstraintOptions,
StackSetsConstraintOptions, TagUpdateConstraintOptions,
} from './constraints';
import { AssociationManager } from './private/association-manager';
import { hashValues } from './private/util';
import { InputValidator } from './private/validation';
Expand Down Expand Up @@ -100,6 +103,13 @@ export interface IPortfolio extends cdk.IResource {
*/
notifyOnStackEvents(product: IProduct, topic: sns.ITopic, options?: CommonConstraintOptions): void;

/**
* Set provisioning rules for the product.
* @param product A service catalog product.
* @param options options for the constraint.
*/
constrainCloudFormationParameters(product:IProduct, options: CloudFormationRuleConstraintOptions): void;

/**
* Force users to assume a certain role when launching a product.
*
Expand Down Expand Up @@ -136,7 +146,7 @@ abstract class PortfolioBase extends cdk.Resource implements IPortfolio {
}

public addProduct(product: IProduct): void {
AssociationManager.associateProductWithPortfolio(this, product);
AssociationManager.associateProductWithPortfolio(this, product, undefined);
}

public shareWithAccount(accountId: string, options: PortfolioShareOptions = {}): void {
Expand All @@ -161,6 +171,10 @@ abstract class PortfolioBase extends cdk.Resource implements IPortfolio {
AssociationManager.notifyOnStackEvents(this, product, topic, options);
}

public constrainCloudFormationParameters(product: IProduct, options: CloudFormationRuleConstraintOptions): void {
AssociationManager.constrainCloudFormationParameters(this, product, options);
}

public setLaunchRole(product: IProduct, launchRole: iam.IRole, options: CommonConstraintOptions = {}): void {
AssociationManager.setLaunchRole(this, product, launchRole, options);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import * as iam from '@aws-cdk/aws-iam';
import * as sns from '@aws-cdk/aws-sns';
import * as cdk from '@aws-cdk/core';
import { CommonConstraintOptions, StackSetsConstraintOptions, TagUpdateConstraintOptions } from '../constraints';
import {
CloudFormationRuleConstraintOptions, CommonConstraintOptions, StackSetsConstraintOptions,
TagUpdateConstraintOptions, TemplateRule, TemplateRuleAssertion,
} from '../constraints';
import { IPortfolio } from '../portfolio';
import { IProduct } from '../product';
import {
CfnLaunchNotificationConstraint, CfnLaunchRoleConstraint, CfnPortfolioProductAssociation,
CfnLaunchNotificationConstraint, CfnLaunchRoleConstraint, CfnLaunchTemplateConstraint, CfnPortfolioProductAssociation,
CfnResourceUpdateConstraint, CfnStackSetConstraint, CfnTagOption, CfnTagOptionAssociation,
} from '../servicecatalog.generated';
import { TagOptions } from '../tag-options';
Expand All @@ -14,8 +17,9 @@ import { InputValidator } from './validation';

export class AssociationManager {
public static associateProductWithPortfolio(
portfolio: IPortfolio, product: IProduct,
portfolio: IPortfolio, product: IProduct, options: CommonConstraintOptions | undefined,
): { associationKey: string, cfnPortfolioProductAssociation: CfnPortfolioProductAssociation } {
InputValidator.validateLength(this.prettyPrintAssociation(portfolio, product), 'description', 0, 2000, options?.description);
const associationKey = hashValues(portfolio.node.addr, product.node.addr, product.stack.node.addr);
const constructId = `PortfolioProductAssociation${associationKey}`;
const existingAssociation = portfolio.node.tryFindChild(constructId);
Expand All @@ -33,8 +37,7 @@ export class AssociationManager {
}

public static constrainTagUpdates(portfolio: IPortfolio, product: IProduct, options: TagUpdateConstraintOptions): void {
this.validateCommonConstraintOptions(portfolio, product, options);
const association = this.associateProductWithPortfolio(portfolio, product);
const association = this.associateProductWithPortfolio(portfolio, product, options);
const constructId = `ResourceUpdateConstraint${association.associationKey}`;

if (!portfolio.node.tryFindChild(constructId)) {
Expand All @@ -54,8 +57,7 @@ export class AssociationManager {
}

public static notifyOnStackEvents(portfolio: IPortfolio, product: IProduct, topic: sns.ITopic, options: CommonConstraintOptions): void {
this.validateCommonConstraintOptions(portfolio, product, options);
const association = this.associateProductWithPortfolio(portfolio, product);
const association = this.associateProductWithPortfolio(portfolio, product, options);
const constructId = `LaunchNotificationConstraint${hashValues(topic.node.addr, topic.stack.node.addr, association.associationKey)}`;

if (!portfolio.node.tryFindChild(constructId)) {
Expand All @@ -74,9 +76,31 @@ export class AssociationManager {
}
}

public static constrainCloudFormationParameters(
portfolio: IPortfolio, product: IProduct,
options: CloudFormationRuleConstraintOptions,
): void {
const association = this.associateProductWithPortfolio(portfolio, product, options);
const constructId = `LaunchTemplateConstraint${hashValues(association.associationKey, options.rule.ruleName)}`;

if (!portfolio.node.tryFindChild(constructId)) {
const constraint = new CfnLaunchTemplateConstraint(portfolio as unknown as cdk.Resource, constructId, {
acceptLanguage: options.messageLanguage,
description: options.description,
portfolioId: portfolio.portfolioId,
productId: product.productId,
rules: this.formatTemplateRule(portfolio.stack, options.rule),
});

// Add dependsOn to force proper order in deployment.
constraint.addDependsOn(association.cfnPortfolioProductAssociation);
} else {
throw new Error(`Provisioning rule ${options.rule.ruleName} already configured on association ${this.prettyPrintAssociation(portfolio, product)}`);
}
}

public static setLaunchRole(portfolio: IPortfolio, product: IProduct, launchRole: iam.IRole, options: CommonConstraintOptions): void {
this.validateCommonConstraintOptions(portfolio, product, options);
const association = this.associateProductWithPortfolio(portfolio, product);
const association = this.associateProductWithPortfolio(portfolio, product, options);
// Check if a stackset deployment constraint has already been configured.
if (portfolio.node.tryFindChild(this.stackSetConstraintLogicalId(association.associationKey))) {
throw new Error(`Cannot set launch role when a StackSet rule is already defined for association ${this.prettyPrintAssociation(portfolio, product)}`);
Expand All @@ -100,8 +124,7 @@ export class AssociationManager {
}

public static deployWithStackSets(portfolio: IPortfolio, product: IProduct, options: StackSetsConstraintOptions) {
this.validateCommonConstraintOptions(portfolio, product, options);
const association = this.associateProductWithPortfolio(portfolio, product);
const association = this.associateProductWithPortfolio(portfolio, product, options);
// Check if a launch role has already been set.
if (portfolio.node.tryFindChild(this.launchRoleConstraintLogicalId(association.associationKey))) {
throw new Error(`Cannot configure StackSet deployment when a launch role is already defined for association ${this.prettyPrintAssociation(portfolio, product)}`);
Expand Down Expand Up @@ -168,7 +191,25 @@ export class AssociationManager {
return `- Portfolio: ${portfolio.node.path} | Product: ${product.node.path}`;
}

private static validateCommonConstraintOptions(portfolio: IPortfolio, product: IProduct, options: CommonConstraintOptions): void {
InputValidator.validateLength(this.prettyPrintAssociation(portfolio, product), 'description', 0, 2000, options.description);
private static formatTemplateRule(stack: cdk.Stack, rule: TemplateRule): string {
return JSON.stringify({
[rule.ruleName]: {
Assertions: this.formatAssertions(stack, rule.assertions),
RuleCondition: rule.condition ? stack.resolve(rule.condition) : undefined,
},
});
}

private static formatAssertions(
stack: cdk.Stack, assertions : TemplateRuleAssertion[],
): { Assert: string, AssertDescription: string | undefined }[] {
return assertions.reduce((formattedAssertions, assertion) => {
formattedAssertions.push( {
Assert: stack.resolve(assertion.assert),
AssertDescription: assertion.description,
});
return formattedAssertions;
}, new Array<{ Assert: string, AssertDescription: string | undefined }>());
};
}

Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,21 @@
"TestPortfolioPortfolioProductAssociationa0185761d231B0D998A7"
]
},
"TestPortfolioLaunchTemplateConstraintfac7b49c426e599F9FFF": {
"Type": "AWS::ServiceCatalog::LaunchTemplateConstraint",
"Properties": {
"PortfolioId": {
"Ref": "TestPortfolio4AC794EB"
},
"ProductId": {
"Ref": "TestProduct7606930B"
},
"Rules": "{\"SubnetsinVPC\":{\"Assertions\":[{\"Assert\":{\"Fn::EachMemberIn\":[{\"Fn::ValueOfAll\":[\"AWs::EC2::Subnet::Id\",\"VpcId\"]},{\"Fn::RefAll\":\"AWS::EC2::VPC::Id\"}]},\"AssertDescription\":\"test description\"}]}}"
},
"DependsOn": [
"TestPortfolioPortfolioProductAssociationa0185761d231B0D998A7"
]
},
"TagOptionc0d88a3c4b8b": {
"Type": "AWS::ServiceCatalog::TagOption",
"Properties": {
Expand Down
18 changes: 15 additions & 3 deletions packages/@aws-cdk/aws-servicecatalog/test/integ.portfolio.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import * as iam from '@aws-cdk/aws-iam';
import * as sns from '@aws-cdk/aws-sns';
import { App, Stack } from '@aws-cdk/core';
import * as cdk from '@aws-cdk/core';
import * as servicecatalog from '../lib';

const app = new App();
const stack = new Stack(app, 'integ-servicecatalog-portfolio');
const app = new cdk.App();
const stack = new cdk.Stack(app, 'integ-servicecatalog-portfolio');

const role = new iam.Role(stack, 'TestRole', {
assumedBy: new iam.AccountRootPrincipal(),
Expand Down Expand Up @@ -79,4 +79,16 @@ secondPortfolio.deployWithStackSets(product, {
allowStackSetInstanceOperations: true,
});

portfolio.constrainCloudFormationParameters(product, {
rule: {
ruleName: 'SubnetsinVPC',
assertions: [{
assert: cdk.Fn.conditionEachMemberIn(
cdk.Fn.valueOfAll('AWs::EC2::Subnet::Id', 'VpcId'),
cdk.Fn.refAll('AWS::EC2::VPC::Id')),
description: 'test description',
}],
},
});

app.synth();
Loading

0 comments on commit 58fda91

Please sign in to comment.