Skip to content

Commit

Permalink
feat: cloudformation condition chaining (#1494)
Browse files Browse the repository at this point in the history
Change `conditionXx` methods to accept an interface `IConditionExpression`
instead of concrete class and implement this interface by the `Condition`
construct.

This enables chaining conditions like so:

    const cond1, cond2, cond, cond4 = new cdk.Condition(...)
    Fn.conditionOr(cond1, cond2, Fn.conditionAnd(cond3, cond4))

Fixes #1457
  • Loading branch information
Elad Ben-Israel authored Jan 8, 2019
1 parent 81b4174 commit 2169015
Show file tree
Hide file tree
Showing 5 changed files with 116 additions and 51 deletions.
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;
}
46 changes: 26 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,19 @@ class FnCidr extends FnBase {
}
}

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

/**
* 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 +463,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 +481,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 +497,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 +512,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 +539,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 +554,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();
}
};

0 comments on commit 2169015

Please sign in to comment.