Skip to content

Commit

Permalink
feat(cfn-include): allow passing Parameters to the included template (#…
Browse files Browse the repository at this point in the history
…9543)

----
Closes #4994

Cfn-Include can now be passed a mapping of parameters and their values. Specified parameters will have all references to them replaced with the passed value at build time, and their definitions will be removed from the template. Unspecified parameters and references to them will not be modified. 

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
  • Loading branch information
comcalvi committed Aug 13, 2020
1 parent fb5068d commit cb6de0a
Show file tree
Hide file tree
Showing 11 changed files with 375 additions and 81 deletions.
14 changes: 14 additions & 0 deletions packages/@aws-cdk/cloudformation-include/README.md
Expand Up @@ -131,6 +131,20 @@ and any changes you make to it will be reflected in the resulting template:
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'
parameters: {
'MyParam': 'my-value',
},
});
```

This will replace all references to `MyParam` with the string 'my-value',
and `MyParam` will be removed from the Parameters section of the template.

## Conditions

If your template uses [CloudFormation Conditions](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/conditions-section-structure.html),
Expand Down
67 changes: 59 additions & 8 deletions packages/@aws-cdk/cloudformation-include/lib/cfn-include.ts
Expand Up @@ -23,8 +23,20 @@ export interface CfnIncludeProps {
* If you include a stack here with an ID that isn't in the template,
* or is in the template but is not a nested stack,
* template creation will fail and an error will be thrown.
*
* @default - no nested stacks will be included
*/
readonly nestedStacks?: { [stackName: string]: CfnIncludeProps };

/**
* Specifies parameters to be replaced by the values in this mapping.
* Any parameters in the template that aren't specified here will be left unmodified.
* If you include a parameter here with an ID that isn't in the template,
* template creation will fail and an error will be thrown.
*
* @default - no parameters will be replaced
*/
readonly parameters?: { [parameterName: string]: any };
}

/**
Expand Down Expand Up @@ -55,6 +67,7 @@ export class CfnInclude extends core.CfnElement {
private readonly conditionsScope: core.Construct;
private readonly resources: { [logicalId: string]: core.CfnResource } = {};
private readonly parameters: { [logicalId: string]: core.CfnParameter } = {};
private readonly parametersToReplace: { [parameterName: string]: any };
private readonly outputs: { [logicalId: string]: core.CfnOutput } = {};
private readonly nestedStacks: { [logicalId: string]: IncludedNestedStack } = {};
private readonly nestedStacksToInclude: { [name: string]: CfnIncludeProps };
Expand All @@ -64,12 +77,21 @@ export class CfnInclude extends core.CfnElement {
constructor(scope: core.Construct, id: string, props: CfnIncludeProps) {
super(scope, id);

this.parametersToReplace = props.parameters || {};

// read the template into a JS object
this.template = futils.readYamlSync(props.templateFile);

// ToDo implement preserveLogicalIds=false
this.preserveLogicalIds = true;

// check if all user specified parameter values exist in the template
for (const logicalId of Object.keys(this.parametersToReplace)) {
if (!(logicalId in (this.template.Parameters || {}))) {
throw new Error(`Parameter with logical ID '${logicalId}' was not found in the template`);
}
}

// instantiate all parameters
for (const logicalId of Object.keys(this.template.Parameters || {})) {
this.createParameter(logicalId);
Expand Down Expand Up @@ -203,17 +225,43 @@ export class CfnInclude extends core.CfnElement {
const ret: { [section: string]: any } = {};

for (const section of Object.keys(this.template)) {
// render all sections of the template unchanged,
// except Conditions, Resources, Parameters, and Outputs which will be taken care of by the created L1s
if (section !== 'Conditions' && section !== 'Resources' && section !== 'Parameters' && section !== 'Outputs') {
ret[section] = this.template[section];
const self = this;
const finder: cfn_parse.ICfnFinder = {
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];
},
};
const cfnParser = new cfn_parse.CfnParser({
finder,
parameters: this.parametersToReplace,
});

switch (section) {
case 'Conditions':
case 'Resources':
case 'Parameters':
case 'Outputs':
// these are rendered as a side effect of instantiating the L1s
break;
default:
ret[section] = cfnParser.parseValue(this.template[section]);
}
}

return ret;
}

private createParameter(logicalId: string): void {
if (logicalId in this.parametersToReplace) {
return;
}

const expression = new cfn_parse.CfnParser({
finder: {
findResource() { throw new Error('Using GetAtt expressions in Parameter definitions is not allowed'); },
Expand Down Expand Up @@ -253,6 +301,7 @@ export class CfnInclude extends core.CfnElement {
return undefined;
},
},
parameters: this.parametersToReplace,
}).parseValue(this.template.Outputs[logicalId]);
const cfnOutput = new core.CfnOutput(scope, logicalId, {
value: outputAttributes.Value,
Expand Down Expand Up @@ -294,6 +343,7 @@ export class CfnInclude extends core.CfnElement {
},
},
context: cfn_parse.CfnParsingContext.CONDITIONS,
parameters: this.parametersToReplace,
});
const cfnCondition = new core.CfnCondition(this.conditionsScope, conditionName, {
expression: cfnParser.parseValue(this.template.Conditions[conditionName]),
Expand Down Expand Up @@ -326,7 +376,7 @@ export class CfnInclude extends core.CfnElement {
}

const self = this;
const finder: core.ICfnFinder = {
const finder: cfn_parse.ICfnFinder = {
findCondition(conditionName: string): core.CfnCondition | undefined {
return self.conditions[conditionName];
},
Expand All @@ -348,6 +398,7 @@ export class CfnInclude extends core.CfnElement {
};
const cfnParser = new cfn_parse.CfnParser({
finder,
parameters: this.parametersToReplace,
});

let l1Instance: core.CfnResource;
Expand All @@ -356,13 +407,13 @@ export class CfnInclude extends core.CfnElement {
} else {
const l1ClassFqn = cfn_type_to_l1_mapping.lookup(resourceAttributes.Type);
if (l1ClassFqn) {
const options: core.FromCloudFormationOptions = {
finder,
const options: cfn_parse.FromCloudFormationOptions = {
parser: cfnParser,
};
const [moduleName, ...className] = l1ClassFqn.split('.');
const module = require(moduleName); // eslint-disable-line @typescript-eslint/no-require-imports
const jsClassFromModule = module[className.join('.')];
l1Instance = jsClassFromModule.fromCloudFormation(this, logicalId, resourceAttributes, options);
l1Instance = jsClassFromModule._fromCloudFormation(this, logicalId, resourceAttributes, options);
} else {
l1Instance = new core.CfnResource(this, logicalId, {
type: resourceAttributes.Type,
Expand Down
Expand Up @@ -45,4 +45,4 @@
}
}
}
}
}
@@ -0,0 +1,19 @@
{
"Parameters": {
"MyParam": {
"Type": "String"
}
},
"Resources": {
"Bucket": {
"Type": "AWS::S3::Bucket",
"Properties": {
"BucketName": {
"Fn::Sub": [
"${MyParam}"
]
}
}
}
}
}
@@ -0,0 +1,22 @@
{
"Parameters": {
"MyParam": {
"Type": "String"
}
},
"Resources": {
"Bucket": {
"Type": "AWS::S3::Bucket",
"Properties": {
"BucketName": {
"Fn::Sub": [
"${MyParam}",
{
"MyParam": { "Ref" : "MyParam" }
}
]
}
}
}
}
}
@@ -0,0 +1,54 @@
{
"Transform" : {
"Name" : "AWS::Include",
"Parameters" : {
"Location" : {
"Ref": "MyParam"
}
}
},
"Parameters": {
"MyParam": {
"Type": "String",
"Default": "MyValue"
}
},
"Conditions": {
"AlwaysFalse": {
"Fn::Equals": [ { "Ref": "MyParam" }, "Invalid?BucketName"]
}
},
"Metadata": {
"Field": {
"Fn::If": [
"AlwaysFalse",
"AWS::NoValue",
{
"Ref": "MyParam"
}
]
}
},
"Resources": {
"Bucket": {
"Type": "AWS::S3::Bucket",
"Metadata": {
"Field": {
"Ref": "MyParam"
}
},
"Properties": {
"BucketName": {
"Ref": "MyParam"
}
}
}
},
"Outputs": {
"MyOutput": {
"Value": {
"Ref": "MyParam"
}
}
}
}

0 comments on commit cb6de0a

Please sign in to comment.