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(cfn-include): add support for Hooks #10143

Merged
merged 4 commits into from
Sep 4, 2020
Merged
Show file tree
Hide file tree
Changes from all 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
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