Skip to content

Commit

Permalink
feat(cfn-include): add support for Hooks (#10143)
Browse files Browse the repository at this point in the history
Add support for creating and retrieving the new Hook
CloudFormation objects used in blue-green deployments.
In order to do that, add new CfnHook and
CfnCodeDeployBlueGreenHook classes to core.

Closes #9713

----

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
  • Loading branch information
skinny85 committed Sep 4, 2020
1 parent ec15485 commit 4de68c0
Show file tree
Hide file tree
Showing 9 changed files with 800 additions and 17 deletions.
33 changes: 27 additions & 6 deletions packages/@aws-cdk/cloudformation-include/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,8 +134,8 @@ param.default = 'MyDefault';
You can also provide values for them when including the template:

```typescript
new inc.CfnInclude(stack, 'includeTemplate', {
templateFile: 'path/to/my/template'
new inc.CfnInclude(this, 'includeTemplate', {
templateFile: 'path/to/my/template',
parameters: {
'MyParam': 'my-value',
},
Expand Down Expand Up @@ -218,6 +218,25 @@ and any changes you make to it will be reflected in the resulting template:
output.value = cfnBucket.attrArn;
```

## Hooks

If your template uses [Hooks for blue-green deployments](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/blue-green.html),
you can retrieve them from your template:

```typescript
import * as core from '@aws-cdk/core';

const hook: core.CfnHook = cfnTemplate.getHook('MyOutput');
```

The `CfnHook` object can be mutated,
and any changes you make to it will be reflected in the resulting template:

```typescript
const codeDeployHook = hook as core.CfnCodeDeployBlueGreenHook;
codeDeployHook.serviceRole = myRole.roleArn;
```

## Nested Stacks

This module also support templates that use [nested stacks](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-nested-stacks.html).
Expand Down Expand Up @@ -249,10 +268,11 @@ where the child template pointed to by `https://my-s3-template-source.s3.amazona
}
```

You can include both the parent stack and the nested stack in your CDK application as follows:
You can include both the parent stack,
and the nested stack in your CDK application as follows:

```typescript
const parentTemplate = new inc.CfnInclude(stack, 'ParentStack', {
const parentTemplate = new inc.CfnInclude(this, 'ParentStack', {
templateFile: 'path/to/my-parent-template.json',
nestedStacks: {
'ChildStack': {
Expand All @@ -270,7 +290,8 @@ const childStack: core.NestedStack = includedChildStack.stack;
const childTemplate: cfn_inc.CfnInclude = includedChildStack.includedTemplate;
```

Now you can reference resources from `ChildStack` and modify them like any other included template:
Now you can reference resources from `ChildStack`,
and modify them like any other included template:

```typescript
const cfnBucket = childTemplate.getResource('MyBucket') as s3.CfnBucket;
Expand All @@ -295,7 +316,7 @@ role.addToPolicy(new iam.PolicyStatement({
In many cases, there are existing CloudFormation templates that are not entire applications,
but more like specialized fragments, implementing a particular pattern or best practice.
If you have templates like that,
you can use the `CfnInclude` class to vend them as a CDK Constructs:
you can use the `CfnInclude` class to vend them as CDK Constructs:

```ts
import * as path from 'path';
Expand Down
67 changes: 67 additions & 0 deletions packages/@aws-cdk/cloudformation-include/lib/cfn-include.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ export class CfnInclude extends core.CfnElement {
private readonly mappings: { [mappingName: string]: core.CfnMapping } = {};
private readonly rules: { [ruleName: string]: core.CfnRule } = {};
private readonly rulesScope: core.Construct;
private readonly hooks: { [hookName: string]: core.CfnHook } = {};
private readonly hooksScope: core.Construct;
private readonly outputs: { [logicalId: string]: core.CfnOutput } = {};
private readonly nestedStacks: { [logicalId: string]: IncludedNestedStack } = {};
private readonly nestedStacksToInclude: { [name: string]: CfnIncludeProps };
Expand Down Expand Up @@ -144,6 +146,12 @@ export class CfnInclude extends core.CfnElement {
}
}

// instantiate the Hooks
this.hooksScope = new core.Construct(this, '$Hooks');
for (const hookName of Object.keys(this.template.Hooks || {})) {
this.createHook(hookName);
}

const outputScope = new core.Construct(this, '$Ouputs');
for (const logicalId of Object.keys(this.template.Outputs || {})) {
this.createOutput(logicalId, outputScope);
Expand Down Expand Up @@ -263,6 +271,24 @@ export class CfnInclude extends core.CfnElement {
return ret;
}

/**
* Returns the CfnHook object from the 'Hooks'
* section of the included CloudFormation template with the given logical ID.
* Any modifications performed on the returned object will be reflected in the resulting CDK template.
*
* If a Hook with the given logical ID is not present in the template,
* an exception will be thrown.
*
* @param hookLogicalId the logical ID of the Hook in the included CloudFormation template's 'Hooks' section
*/
public getHook(hookLogicalId: string): core.CfnHook {
const ret = this.hooks[hookLogicalId];
if (!ret) {
throw new Error(`Hook with logical ID '${hookLogicalId}' was not found in the template`);
}
return ret;
}

/**
* Returns the NestedStack with name logicalId.
* For a nested stack to be returned by this method, it must be specified in the {@link CfnIncludeProps.nestedStacks}
Expand Down Expand Up @@ -314,6 +340,7 @@ export class CfnInclude extends core.CfnElement {
case 'Resources':
case 'Parameters':
case 'Rules':
case 'Hooks':
case 'Outputs':
// these are rendered as a side effect of instantiating the L1s
break;
Expand Down Expand Up @@ -400,6 +427,46 @@ export class CfnInclude extends core.CfnElement {
this.overrideLogicalIdIfNeeded(rule, ruleName);
}

private createHook(hookName: string): void {
const self = this;
const cfnParser = new cfn_parse.CfnParser({
finder: {
findResource(lId): core.CfnResource | undefined {
return self.resources[lId];
},
findRefTarget(elementName: string): core.CfnElement | undefined {
return self.resources[elementName] ?? self.parameters[elementName];
},
findCondition(conditionName: string): core.CfnCondition | undefined {
return self.conditions[conditionName];
},
findMapping(mappingName): core.CfnMapping | undefined {
return self.mappings[mappingName];
},
},
parameters: this.parametersToReplace,
});
const hookAttributes = this.template.Hooks[hookName];

let hook: core.CfnHook;
switch (hookAttributes.Type) {
case 'AWS::CodeDeploy::BlueGreen':
hook = (core.CfnCodeDeployBlueGreenHook as any)._fromCloudFormation(this.hooksScope, hookName, hookAttributes, {
parser: cfnParser,
});
break;
default: {
const hookProperties = cfnParser.parseValue(hookAttributes.Properties) ?? {};
hook = new core.CfnHook(this.hooksScope, hookName, {
type: hookAttributes.Type,
properties: hookProperties,
});
}
}
this.hooks[hookName] = hook;
this.overrideLogicalIdIfNeeded(hook, hookName);
}

private createOutput(logicalId: string, scope: core.Construct): void {
const self = this;
const outputAttributes = new cfn_parse.CfnParser({
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
{
"Hooks": {
"RandomHook": {
"Type": "UnknownToday"
},
"EcsBlueGreenCodeDeployHook": {
"Type": "AWS::CodeDeploy::BlueGreen",
"Properties": {
"ServiceRole": "CodeDeployServiceRoleName",
"Applications": [
{
"Target": {
"Type": "AWS::ECS::Service",
"LogicalID": "MyService"
},
"ECSAttributes": {
"TaskDefinitions": [
"MyTaskDefinition", "MyTaskDefinition"
],
"TaskSets": [
"MyTaskSet", "MyTaskSet"
],
"TrafficRouting": {
"ProdTrafficRoute": {
"Type": "AWS::ElasticLoadBalancingV2::Listener",
"LogicalID": "AlbListener"
},
"TestTrafficRoute": {
"Type": "AWS::ElasticLoadBalancingV2::Listener",
"LogicalID": "AlbListener"
},
"TargetGroups": [
"AlbTargetGroup", "AlbTargetGroup"
]
}
}
}
],
"TrafficRoutingConfig": {
"Type": "AllAtOnce",
"TimeBasedCanary": {
"StepPercentage": 1,
"BakeTimeMins": "2"
},
"TimeBasedLinear": {
"StepPercentage": "3",
"BakeTimeMins": 4
}
},
"AdditionalOptions": {
"TerminationWaitTimeInMinutes": 5
},
"LifecycleEventHooks": {
"BeforeInstall": "f1",
"AfterInstall": "f2",
"AfterAllowTestTraffic": "f3",
"BeforeAllowTraffic": "f4",
"AfterAllowTraffic": "f5"
}
}
}
},
"Resources": {
"MyService": {
"Type": "AWS::ECS::Service"
},
"MyTaskDefinition": {
"Type": "AWS::ECS::TaskDefinition"
},
"MyTaskSet": {
"Type": "AWS::ECS::TaskSet",
"Properties": {
"Cluster": "my-cluster",
"Service": { "Ref": "MyService" },
"TaskDefinition": { "Fn::Sub": "${MyTaskDefinition}" }
}
},
"AlbTargetGroup": {
"Type": "AWS::ElasticLoadBalancingV2::TargetGroup"
},
"AlbListener": {
"Type": "AWS::ElasticLoadBalancingV2::Listener",
"Properties": {
"Port": 80,
"Protocol": "HTTP",
"DefaultActions": [
{
"Type": "forward"
}
],
"LoadBalancerArn": "my-lb"
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -786,6 +786,24 @@ describe('CDK Include', () => {
}).toThrow(/Rule with name 'DoesNotExist' was not found in the template/);
});

test('can ingest a template that contains Hooks, and allows retrieving those Hooks', () => {
const cfnTemplate = includeTestTemplate(stack, 'hook-code-deploy-blue-green-ecs.json');
const hook = cfnTemplate.getHook('EcsBlueGreenCodeDeployHook');

expect(hook).toBeDefined();
expect(stack).toMatchTemplate(
loadTestFileToJsObject('hook-code-deploy-blue-green-ecs.json'),
);
});

test("throws an exception when attempting to retrieve a Hook that doesn't exist in the template", () => {
const cfnTemplate = includeTestTemplate(stack, 'hook-code-deploy-blue-green-ecs.json');

expect(() => {
cfnTemplate.getHook('DoesNotExist');
}).toThrow(/Hook with logical ID 'DoesNotExist' was not found in the template/);
});

test('replaces references to parameters with the user-specified values in Resources, Conditions, Metadata, and Options sections', () => {
includeTestTemplate(stack, 'parameter-references.json', {
parameters: {
Expand Down
Loading

0 comments on commit 4de68c0

Please sign in to comment.