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: cloudformation condition chaining #1494

Merged
merged 4 commits into from
Jan 8, 2019
Merged
Show file tree
Hide file tree
Changes from 3 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
15 changes: 13 additions & 2 deletions docs/src/cloudformation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -149,18 +149,29 @@ Outputs
Conditions
----------

.. NEEDS SOME INTRO TEXT
`cdk.Condition` can be used to define CloudFormation "Condition" elements in the template.
The `cdk.Fn.conditionXx()` static methods can be used to produce "condition expressions".

.. code-block:: js

import sqs = require('@aws-cdk/aws-sqs');
import cdk = require('@aws-cdk/cdk');

const param = new cdk.Parameter(this, 'Param1', { type: 'String' });
const cond1 = new cdk.Condition(this, 'Condition1', { expression: cdk.Fn.conditionEquals("a", "b") });
const cond2 = new cdk.Condition(this, 'Condition2', { expression: cdk.Fn.conditionContains([ "a", "b", "c" ], "c") });
const cond3 = new cdk.Condition(this, 'Condition3', { expression: cdk.Fn.conditionEquals(param, "hello") });

const cond4 = new cdk.Condition(this, 'Condition4', {
expression: cdk.Fn.conditionOr(cond1, cond2, cdk.Fn.conditionNot(cond3))
});

const cond = new cdk.Condition(this, 'MyCondition', {
expression: new cdk.FnIf(...)
});

const queue = new sqs.CfnQueue(this, 'MyQueue');
queue.options.condition = cond;
queue.options.condition = cond4;

.. _intrinsic_functions:

Expand Down
64 changes: 40 additions & 24 deletions packages/@aws-cdk/cdk/lib/cloudformation/condition.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,19 @@
import { Construct } from '../core/construct';
import { CloudFormationToken } from './cloudformation-token';
import { Referenceable } from './stack';

export interface ConditionProps {
expression?: FnCondition;
expression?: IConditionExpression;
}

/**
* Represents a CloudFormation condition, for resources which must be conditionally created and
* the determination must be made at deploy time.
*/
export class Condition extends Referenceable {
export class Condition extends Referenceable implements IConditionExpression {
/**
* The condition statement.
*/
public expression?: FnCondition;
public expression?: IConditionExpression;

/**
* Build a new condition. The condition must be constructed with a condition token,
Expand All @@ -26,35 +25,52 @@ export class Condition extends Referenceable {
}

public toCloudFormation(): object {
if (!this.expression) {
return { };
}

return {
Conditions: {
[this.logicalId]: this.expression
}
};
}

/**
* Synthesizes the condition.
*/
public resolve(): any {
return { Condition: this.logicalId };
}
}

/**
* You can use intrinsic functions, such as ``Fn::If``, ``Fn::Equals``, and ``Fn::Not``, to conditionally
* create stack resources. These conditions are evaluated based on input parameters that you
* declare when you create or update a stack. After you define all your conditions, you can
* associate them with resources or resource properties in the Resources and Outputs sections
* of a template.
* Represents a CloudFormation element that can be used within a Condition.
*
* You define all conditions in the Conditions section of a template except for ``Fn::If`` conditions.
* You can use the ``Fn::If`` condition in the metadata attribute, update policy attribute, and property
* values in the Resources section and Outputs sections of a template.
* You can use intrinsic functions, such as ``Fn.conditionIf``,
* ``Fn.conditionEquals``, and ``Fn.conditionNot``, to conditionally create
* stack resources. These conditions are evaluated based on input parameters
* that you declare when you create or update a stack. After you define all your
* conditions, you can associate them with resources or resource properties in
* the Resources and Outputs sections of a template.
*
* You might use conditions when you want to reuse a template that can create resources in different
* contexts, such as a test environment versus a production environment. In your template, you can
* add an EnvironmentType input parameter, which accepts either prod or test as inputs. For the
* production environment, you might include Amazon EC2 instances with certain capabilities;
* however, for the test environment, you want to use less capabilities to save costs. With
* conditions, you can define which resources are created and how they're configured for each
* environment type.
* You define all conditions in the Conditions section of a template except for
* ``Fn.conditionIf`` conditions. You can use the ``Fn.conditionIf`` condition
* in the metadata attribute, update policy attribute, and property values in
* the Resources section and Outputs sections of a template.
*
* You might use conditions when you want to reuse a template that can create
* resources in different contexts, such as a test environment versus a
* production environment. In your template, you can add an EnvironmentType
* input parameter, which accepts either prod or test as inputs. For the
* production environment, you might include Amazon EC2 instances with certain
* capabilities; however, for the test environment, you want to use less
* capabilities to save costs. With conditions, you can define which resources
* are created and how they're configured for each environment type.
*/
export class FnCondition extends CloudFormationToken {
constructor(type: string, value: any) {
super({ [type]: value });
}
}
export interface IConditionExpression {
/**
* Returns a JSON node that represents this condition expression
*/
resolve(): any;
Copy link
Contributor

@rix0rrr rix0rrr Jan 8, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why resolve() and not a more specific fucntion name? If you wanted resolve() you could have accepted a Token, or a ConditionBase

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But I wanted the Condition construct to implement it, so I had to define it as an interface. And I want the recursive resolution behavior of tokens to apply to full expressions, so I just faked it.
We should probably define an IToken with a resolve method so (reopened #79)

}
50 changes: 30 additions & 20 deletions packages/@aws-cdk/cdk/lib/cloudformation/fn.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { CloudFormationToken, FnJoin } from './cloudformation-token';
import { FnCondition } from './condition';
import { IConditionExpression } from './condition';

// tslint:disable:max-line-length

Expand Down Expand Up @@ -148,7 +148,7 @@ export class Fn {
* @param conditions conditions to AND
* @returns an FnCondition token
*/
public static conditionAnd(...conditions: FnCondition[]): FnCondition {
public static conditionAnd(...conditions: IConditionExpression[]): IConditionExpression {
return new FnAnd(...conditions);
}

Expand All @@ -159,7 +159,7 @@ export class Fn {
* @param rhs A value of any type that you want to compare.
* @returns an FnCondition token
*/
public static conditionEquals(lhs: any, rhs: any): FnCondition {
public static conditionEquals(lhs: any, rhs: any): IConditionExpression {
return new FnEquals(lhs, rhs);
}

Expand All @@ -178,7 +178,7 @@ export class Fn {
* evaluates to false.
* @returns an FnCondition token
*/
public static conditionIf(conditionId: string, valueIfTrue: any, valueIfFalse: any): FnCondition {
public static conditionIf(conditionId: string, valueIfTrue: any, valueIfFalse: any): IConditionExpression {
return new FnIf(conditionId, valueIfTrue, valueIfFalse);
}

Expand All @@ -189,7 +189,7 @@ export class Fn {
* or false.
* @returns an FnCondition token
*/
public static conditionNot(condition: FnCondition): FnCondition {
public static conditionNot(condition: IConditionExpression): IConditionExpression {
return new FnNot(condition);
}

Expand All @@ -201,7 +201,7 @@ export class Fn {
* @param conditions conditions that evaluates to true or false.
* @returns an FnCondition token
*/
public static conditionOr(...conditions: FnCondition[]): FnCondition {
public static conditionOr(...conditions: IConditionExpression[]): IConditionExpression {
return new FnOr(...conditions);
}

Expand All @@ -212,7 +212,7 @@ export class Fn {
* @param value A string, such as "A", that you want to compare against a list of strings.
* @returns an FnCondition token
*/
public static conditionContains(listOfStrings: string[], value: string): FnCondition {
public static conditionContains(listOfStrings: string[], value: string): IConditionExpression {
return new FnContains(listOfStrings, value);
}

Expand All @@ -223,7 +223,7 @@ export class Fn {
* of strings.
* @returns an FnCondition token
*/
public conditionEachMemberEquals(listOfStrings: string[], value: string): FnCondition {
public conditionEachMemberEquals(listOfStrings: string[], value: string): IConditionExpression {
return new FnEachMemberEquals(listOfStrings, value);
}

Expand All @@ -238,7 +238,7 @@ export class Fn {
* strings_to_check parameter.
* @returns an FnCondition token
*/
public conditionEachMemberIn(stringsToCheck: string[], stringsToMatch: string): FnCondition {
public conditionEachMemberIn(stringsToCheck: string[], stringsToMatch: string): IConditionExpression {
return new FnEachMemberIn(stringsToCheck, stringsToMatch);
}

Expand Down Expand Up @@ -442,13 +442,23 @@ class FnCidr extends FnBase {
}
}

class FnConditionBase extends CloudFormationToken implements IConditionExpression {
constructor(type: string, value: any) {
super({ [type]: value });
}

public toConditionJson() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where is this called?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oops. will remove

return this.resolve();
}
}

/**
* Returns true if all the specified conditions evaluate to true, or returns false if any one
* of the conditions evaluates to false. ``Fn::And`` acts as an AND operator. The minimum number of
* conditions that you can include is 2, and the maximum is 10.
*/
class FnAnd extends FnCondition {
constructor(...condition: FnCondition[]) {
class FnAnd extends FnConditionBase {
constructor(...condition: IConditionExpression[]) {
super('Fn::And', condition);
}
}
Expand All @@ -457,7 +467,7 @@ class FnAnd extends FnCondition {
* Compares if two values are equal. Returns true if the two values are equal or false
* if they aren't.
*/
class FnEquals extends FnCondition {
class FnEquals extends FnConditionBase {
/**
* Creates an ``Fn::Equals`` condition function.
* @param lhs A value of any type that you want to compare.
Expand All @@ -475,7 +485,7 @@ class FnEquals extends FnCondition {
* in the Resources section and Outputs sections of a template. You can use the AWS::NoValue
* pseudo parameter as a return value to remove the corresponding property.
*/
class FnIf extends FnCondition {
class FnIf extends FnConditionBase {
/**
* Creates an ``Fn::If`` condition function.
* @param condition A reference to a condition in the Conditions section. Use the condition's name to reference it.
Expand All @@ -491,12 +501,12 @@ class FnIf extends FnCondition {
* Returns true for a condition that evaluates to false or returns false for a condition that evaluates to true.
* ``Fn::Not`` acts as a NOT operator.
*/
class FnNot extends FnCondition {
class FnNot extends FnConditionBase {
/**
* Creates an ``Fn::Not`` condition function.
* @param condition A condition such as ``Fn::Equals`` that evaluates to true or false.
*/
constructor(condition: FnCondition) {
constructor(condition: IConditionExpression) {
super('Fn::Not', [ condition ]);
}
}
Expand All @@ -506,20 +516,20 @@ class FnNot extends FnCondition {
* all of the conditions evaluates to false. ``Fn::Or`` acts as an OR operator. The minimum number
* of conditions that you can include is 2, and the maximum is 10.
*/
class FnOr extends FnCondition {
class FnOr extends FnConditionBase {
/**
* Creates an ``Fn::Or`` condition function.
* @param condition A condition that evaluates to true or false.
*/
constructor(...condition: FnCondition[]) {
constructor(...condition: IConditionExpression[]) {
super('Fn::Or', condition);
}
}

/**
* Returns true if a specified string matches at least one value in a list of strings.
*/
class FnContains extends FnCondition {
class FnContains extends FnConditionBase {
/**
* Creates an ``Fn::Contains`` function.
* @param listOfStrings A list of strings, such as "A", "B", "C".
Expand All @@ -533,7 +543,7 @@ class FnContains extends FnCondition {
/**
* Returns true if a specified string matches all values in a list.
*/
class FnEachMemberEquals extends FnCondition {
class FnEachMemberEquals extends FnConditionBase {
/**
* Creates an ``Fn::EachMemberEquals`` function.
* @param listOfStrings A list of strings, such as "A", "B", "C".
Expand All @@ -548,7 +558,7 @@ class FnEachMemberEquals extends FnCondition {
* Returns true if each member in a list of strings matches at least one value in a second
* list of strings.
*/
class FnEachMemberIn extends FnCondition {
class FnEachMemberIn extends FnConditionBase {
/**
* Creates an ``Fn::EachMemberIn`` function.
* @param stringsToCheck A list of strings, such as "A", "B", "C". AWS CloudFormation checks whether each member in the strings_to_check parameter is in the strings_to_match parameter.
Expand Down
10 changes: 5 additions & 5 deletions packages/@aws-cdk/cdk/lib/cloudformation/rule.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Construct } from '../core/construct';
import { capitalizePropertyNames } from '../core/util';
import { FnCondition } from './condition';
import { IConditionExpression } from './condition';
import { Referenceable } from './stack';

/**
Expand Down Expand Up @@ -29,7 +29,7 @@ export interface RuleProps {
* If the rule condition evaluates to false, the rule doesn't take effect.
* If the function in the rule condition evaluates to true, expressions in each assert are evaluated and applied.
*/
ruleCondition?: FnCondition;
ruleCondition?: IConditionExpression;

/**
* Assertions which define the rule.
Expand Down Expand Up @@ -57,7 +57,7 @@ export class Rule extends Referenceable {
* If the rule condition evaluates to false, the rule doesn't take effect.
* If the function in the rule condition evaluates to true, expressions in each assert are evaluated and applied.
*/
public ruleCondition?: FnCondition;
public ruleCondition?: IConditionExpression;

/**
* Assertions which define the rule.
Expand All @@ -81,7 +81,7 @@ export class Rule extends Referenceable {
* @param condition The expression to evaluation.
* @param description The description of the assertion.
*/
public addAssertion(condition: FnCondition, description: string) {
public addAssertion(condition: IConditionExpression, description: string) {
if (!this.assertions) {
this.assertions = [];
}
Expand Down Expand Up @@ -111,7 +111,7 @@ export interface RuleAssertion {
/**
* The assertion.
*/
assert: FnCondition;
assert: IConditionExpression;

/**
* The assertion description.
Expand Down
32 changes: 32 additions & 0 deletions packages/@aws-cdk/cdk/test/cloudformation/test.condition.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Test } from 'nodeunit';
import cdk = require('../../lib');

export = {
'chain conditions'(test: Test) {
// GIVEN
const stack = new cdk.Stack();
const param = new cdk.Parameter(stack, 'Param1', { type: 'String' });
const cond1 = new cdk.Condition(stack, 'Condition1', { expression: cdk.Fn.conditionEquals("a", "b") });
const cond2 = new cdk.Condition(stack, 'Condition2', { expression: cdk.Fn.conditionContains([ "a", "b", "c" ], "c") });
const cond3 = new cdk.Condition(stack, 'Condition3', { expression: cdk.Fn.conditionEquals(param, "hello") });

// WHEN
new cdk.Condition(stack, 'Condition4', {
expression: cdk.Fn.conditionOr(cond1, cond2, cdk.Fn.conditionNot(cond3))
});

// THEN
test.deepEqual(stack.toCloudFormation(), {
Parameters: { Param1: { Type: 'String' } },
Conditions: {
Condition1: { 'Fn::Equals': [ 'a', 'b' ] },
Condition2: { 'Fn::Contains': [ [ 'a', 'b', 'c' ], 'c' ] },
Condition3: { 'Fn::Equals': [ { Ref: 'Param1' }, 'hello' ] },
Condition4: { 'Fn::Or': [
{ Condition: 'Condition1' },
{ Condition: 'Condition2' },
{ 'Fn::Not': [ { Condition: 'Condition3' } ] } ] } } });

test.done();
}
};