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 retrieving Output objects from the template #8821

Merged
merged 51 commits into from
Jul 2, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
db60c27
doc: added slack link to readme
comcalvi Jun 4, 2020
5924f3b
Merge branch 'master' into master
comcalvi Jun 5, 2020
f0c85a3
Update README.md
comcalvi Jun 5, 2020
f37e60e
Merge branch 'master' into master
mergify[bot] Jun 5, 2020
83ed743
Merge branch 'master' of https://github.com/aws/aws-cdk
comcalvi Jun 15, 2020
c6664af
added support for Fn::Select, Fn::FindInMap, Fn::Cidr, Fn::GetAZs, Fn…
comcalvi Jun 15, 2020
d0f64d2
added support for the 'Fn::Transform' cloudformation intrinsic function.
comcalvi Jun 16, 2020
d69e46d
added support for the Fn::Base64 Intrinsic Function
comcalvi Jun 16, 2020
df1446a
tested more complex combinations of conditional and non-conditional i…
comcalvi Jun 16, 2020
132cb4d
fixed linter issues
comcalvi Jun 16, 2020
ae7c570
implmented Adam's requests and fixed additional linter issues
comcalvi Jun 17, 2020
7ca707e
updated README to reflect the newly supported cloudformation functions
comcalvi Jun 17, 2020
63c27a7
removed quotes from the type of Transform's parameter argument, modif…
comcalvi Jun 17, 2020
4179271
fixed teseting issue related to Fn::Select
comcalvi Jun 18, 2020
117ae63
Merge branch 'master' into CfnFunctions
comcalvi Jun 18, 2020
dbb9ef8
fixing merge conflicts
comcalvi Jun 18, 2020
bdab761
Merge branch 'master' of https://github.com/aws/aws-cdk
comcalvi Jun 18, 2020
d390580
Merge branch 'master' of https://github.com/aws/aws-cdk into CfnFunct…
comcalvi Jun 18, 2020
14a0d64
merge conflict resolution
comcalvi Jun 18, 2020
dfb42b9
Merge branch 'CfnFunctions' of github.com:comcalvi/aws-cdk into CfnFu…
comcalvi Jun 18, 2020
9f1b2d9
Merge branch 'master' of https://github.com/aws/aws-cdk into CfnFunct…
comcalvi Jun 18, 2020
4c9d1ec
fixed typo in condition name
comcalvi Jun 18, 2020
e15c187
Merge branch 'CfnFunctions'
comcalvi Jun 18, 2020
82c4317
Merge branch 'master' of https://github.com/aws/aws-cdk
comcalvi Jun 18, 2020
42c0228
removed parameters from _toCloudFormation()
comcalvi Jun 18, 2020
6a2df1e
added support for parameters in templates
comcalvi Jun 19, 2020
43198b7
fixed merge conflict
comcalvi Jun 19, 2020
cd27ff6
updated documentation
comcalvi Jun 19, 2020
1e72465
updated readme
comcalvi Jun 19, 2020
070f902
incorporated adam's comments
comcalvi Jun 19, 2020
b10a1ed
fixed spacing
comcalvi Jun 19, 2020
68d2fbc
fixed merge conflicts
comcalvi Jun 22, 2020
225d1ce
Merge branch 'master' of https://github.com/aws/aws-cdk
comcalvi Jun 25, 2020
badeae8
Merge branch 'master' of https://github.com/aws/aws-cdk
comcalvi Jun 26, 2020
5bf4760
Merge branch 'master' of https://github.com/aws/aws-cdk
comcalvi Jun 29, 2020
479931e
Merge branch 'master' of https://github.com/aws/aws-cdk
comcalvi Jun 29, 2020
faed26a
added outputs array
comcalvi Jun 29, 2020
4ee6c4b
added support for retrieving and modifying outputs
comcalvi Jun 30, 2020
13431f1
fixed linter issues
comcalvi Jun 30, 2020
2472235
updated README
comcalvi Jun 30, 2020
bd7e8a1
updated documentation
comcalvi Jun 30, 2020
ff02d26
removed unneeded line in tests
comcalvi Jun 30, 2020
45aefd6
added newline
comcalvi Jun 30, 2020
c574508
incorporated PR requests
comcalvi Jul 1, 2020
a16d283
updated the example in the readme
comcalvi Jul 1, 2020
a15413c
added support for common-named outputs. Fixed a bug in the export nam…
comcalvi Jul 1, 2020
cf3074a
added a negative test case and a new error message if an output refer…
comcalvi Jul 1, 2020
670e22e
Update getOutput() exception test description
skinny85 Jul 1, 2020
a17361f
Use core.Aws.NO_VALUE in the test instead of strings
skinny85 Jul 1, 2020
30b5725
Wrong assertion.
skinny85 Jul 1, 2020
18caef0
Merge branch 'master' into outputs
mergify[bot] Jul 2, 2020
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
20 changes: 19 additions & 1 deletion packages/@aws-cdk/cloudformation-include/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,24 @@ and any changes you make to it will be reflected in the resulting template:
condition.expression = core.Fn.conditionEquals(1, 2);
```

## Outputs

If your template uses [CloudFormation Outputs](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/outputs-section-structure.html),
you can retrieve them from your template:

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

const output: core.CfnOutput = cfnTemplate.getOutput('MyOutput');
```

The `CfnOutput` object is mutable,
and any changes you make to it will be reflected in the resulting template:

```typescript
output.value = cfnBucket.attrArn;
```

## Known limitations

This module is still in its early, experimental stage,
Expand All @@ -135,7 +153,7 @@ All items unchecked below are currently not supported.
- [x] Resources
- [x] Parameters
- [x] Conditions
- [ ] Outputs
- [x] Outputs

### [Resource attributes](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-product-attribute-reference.html):

Expand Down
64 changes: 62 additions & 2 deletions packages/@aws-cdk/cloudformation-include/lib/cfn-include.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export class CfnInclude extends core.CfnElement {
private readonly conditions: { [conditionName: string]: core.CfnCondition } = {};
private readonly resources: { [logicalId: string]: core.CfnResource } = {};
private readonly parameters: { [logicalId: string]: core.CfnParameter } = {};
private readonly outputs: { [logicalId: string]: core.CfnOutput } = {};
private readonly template: any;
private readonly preserveLogicalIds: boolean;

Expand All @@ -50,6 +51,12 @@ export class CfnInclude extends core.CfnElement {
for (const logicalId of Object.keys(this.template.Resources || {})) {
this.getOrCreateResource(logicalId);
}

const outputScope = new core.Construct(this, '$Ouputs');

for (const logicalId of Object.keys(this.template.Outputs || {})) {
this.createOutput(logicalId, outputScope);
}
}

/**
Expand Down Expand Up @@ -112,14 +119,32 @@ export class CfnInclude extends core.CfnElement {
return ret;
}

/**
* Returns the CfnOutput object from the 'Outputs'
* section of the included template
* Any modifications performed on that object will be reflected in the resulting CDK template.
*
* If an Output with the given name is not present in the template,
* throws an exception.
*
* @param logicalId the name of the output to retrieve
*/
public getOutput(logicalId: string): core.CfnOutput {
const ret = this.outputs[logicalId];
if (!ret) {
throw new Error(`Output with logical ID '${logicalId}' was not found in the template`);
}
return ret;
}

/** @internal */
public _toCloudFormation(): object {
const ret: { [section: string]: any } = {};

for (const section of Object.keys(this.template)) {
// render all sections of the template unchanged,
// except Conditions, Resources, and Parameters, which will be taken care of by the created L1s
if (section !== 'Conditions' && section !== 'Resources' && section !== 'Parameters') {
// 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];
}
}
Expand Down Expand Up @@ -152,6 +177,41 @@ export class CfnInclude extends core.CfnElement {
this.parameters[logicalId] = cfnParameter;
}

private createOutput(logicalId: string, scope: core.Construct): void {
const self = this;
const outputAttributes = 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(): undefined {
return undefined;
},
},
}).parseValue(this.template.Outputs[logicalId]);
const cfnOutput = new core.CfnOutput(scope, logicalId, {
value: outputAttributes.Value,
description: outputAttributes.Description,
exportName: outputAttributes.Export ? outputAttributes.Export.Name : undefined,
condition: (() => {
if (!outputAttributes.Condition) {
return undefined;
} else if (this.conditions[outputAttributes.Condition]) {
return self.getCondition(outputAttributes.Condition);
}

throw new Error(`Output with name '${logicalId}' refers to a Condition with name\
'${outputAttributes.Condition}' which was not found in this template`);
})(),
});

cfnOutput.overrideLogicalId(logicalId);
this.outputs[logicalId] = cfnOutput;
}

private createCondition(conditionName: string): void {
// ToDo condition expressions can refer to other conditions -
// will be important when implementing preserveLogicalIds=false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,12 @@ describe('CDK Include', () => {
includeTestTemplate(stack, 'getting-attribute-of-a-non-existent-resource.json');
}).toThrow(/Resource used in GetAtt expression with logical ID: 'DoesNotExist' not found/);
});

test("throws a validation exception when an output references a condition that doesn't exist", () => {
expect(() => {
includeTestTemplate(stack, 'output-referencing-nonexistant-condition.json');
}).toThrow(/Output with name 'SomeOutput' refers to a Condition with name 'NonexistantCondition' which was not found in this template/);
});
});

function includeTestTemplate(scope: core.Construct, testTemplate: string): inc.CfnInclude {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"Outputs": {
"SomeOutput": {
"Condition": "NonexistantCondition"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
{
"Conditions": {
"AlwaysFalseCond": {
"Fn::Equals": [
{
"Ref": "AWS::Region"
},
"completely-made-up-region"
]
}
},
"Parameters": {
"MyParam": {
"Type": "String"
}
},
"Resources": {
"Bucket": {
"Type": "AWS::S3::Bucket"
},
"Output1": {
"Type": "AWS::S3::Bucket"
}
},
"Outputs": {
"Output1": {
"Value": {
"Fn::Join": [ "", [
{ "Ref": "MyParam" },
{ "Fn::GetAtt": [ "Bucket", "Arn" ] } ]
]
},
"Description": {
"Ref": "Bucket"
},
"Condition": "AlwaysFalseCond",
"Export": {
"Name": "Bucket"
}
},
"OutputWithNoCondition": {
"Value": "some-value"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,7 @@ describe('CDK Include', () => {
);
});

test("correctly parses templates with parameters", () => {
test('correctly parses templates with parameters', () => {
const cfnTemplate = includeTestTemplate(stack, 'bucket-with-parameters.json');
const param = cfnTemplate.getParameter('BucketName');
new s3.CfnBucket(stack, 'NewBucket', {
Expand Down Expand Up @@ -396,8 +396,8 @@ describe('CDK Include', () => {
],
},
},
"Metadata" : {
"Object1" : "Location1",
"Metadata": {
"Object1": "Location1",
"KeyRef": { "Ref": "TotallyDifferentKey" },
"KeyArn": { "Fn::GetAtt": ["TotallyDifferentKey", "Arn"] },
},
Expand All @@ -414,6 +414,73 @@ describe('CDK Include', () => {
includeTestTemplate(stack, 'non-existent-resource-type.json');
}).toThrow(/Unrecognized CloudFormation resource type: 'AWS::FakeService::DoesNotExist'/);
});

test('can ingest a template that contains outputs and modify them', () => {
const cfnTemplate = includeTestTemplate(stack, 'outputs-with-references.json');

const output = cfnTemplate.getOutput('Output1');
output.value = 'a mutated value';
output.description = undefined;
output.exportName = 'an export';
output.condition = new core.CfnCondition(stack, 'MyCondition', {
expression: core.Fn.conditionIf('AlwaysFalseCond', core.Aws.NO_VALUE, true),
});

const originalTemplate = loadTestFileToJsObject('outputs-with-references.json');

expect(stack).toMatchTemplate({
"Conditions": {
...originalTemplate.Conditions,
"MyCondition": {
"Fn::If": [
"AlwaysFalseCond",
{ "Ref": "AWS::NoValue" },
true,
],
},
},
"Parameters": {
...originalTemplate.Parameters,
},
"Resources": {
...originalTemplate.Resources,
},
"Outputs": {
"Output1": {
"Value": "a mutated value",
"Export": {
"Name": "an export",
},
"Condition": "MyCondition",
},
"OutputWithNoCondition": {
"Value": "some-value",
},
},
});
});

test('can ingest a template that contains outputs and get those outputs', () => {
const cfnTemplate = includeTestTemplate(stack, 'outputs-with-references.json');
const output = cfnTemplate.getOutput('Output1');

expect(output.condition).toBe(cfnTemplate.getCondition('AlwaysFalseCond'));
expect(output.description).toBeDefined();
expect(output.value).toBeDefined();
expect(output.exportName).toBeDefined();

expect(stack).toMatchTemplate(
loadTestFileToJsObject('outputs-with-references.json'),
);
});

test("throws an exception when attempting to retrieve an Output that doesn't exist", () => {
const cfnTemplate = includeTestTemplate(stack, 'outputs-with-references.json');

expect(() => {
cfnTemplate.getOutput('FakeOutput');
}).toThrow(/Output with logical ID 'FakeOutput' was not found in the template/);
});
});

interface IncludeTestTemplateProps {
Expand Down
72 changes: 66 additions & 6 deletions packages/@aws-cdk/core/lib/cfn-output.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,10 @@ export interface CfnOutputProps {
}

export class CfnOutput extends CfnElement {
private readonly _description?: string;
private readonly _condition?: CfnCondition;
private readonly _value?: any;
private readonly _export?: string;
private _description?: string;
private _condition?: CfnCondition;
private _value?: any;
private _exportName?: string;

/**
* Creates an CfnOutput value for this stack.
Expand All @@ -56,7 +56,67 @@ export class CfnOutput extends CfnElement {
this._description = props.description;
this._value = props.value;
this._condition = props.condition;
this._export = props.exportName;
this._exportName = props.exportName;
}

/**
* Returns the description of this Output
*/
public get description() {
return this._description;
}

/**
* Sets this output's description to the parameter
* @param description the description to update this Output's description to
*/
public set description(description: string | undefined) {
this._description = description;
}

/**
* Returns the value of this Output
*/
public get value() {
return this._value;
}

/**
* Sets this output's value to the parameter
* @param value the value to update this Output's value to
*/
public set value(value: any) {
this._value = value;
}

/**
* Returns the condition of this Output
*/
public get condition() {
return this._condition;
}

/**
* Sets this output's condition to the parameter
* @param condition the condition to update this Output's condition to
*/
public set condition(condition: CfnCondition | undefined) {
this._condition = condition;
}

/**
* Returns the export of this Output
*/
public get exportName() {
return this._exportName;
}

/**
* Sets this output's export to the parameter
* @param exportName the export to update this Output's export to
*/
public set exportName(exportName: string | undefined) {
this._exportName = exportName;
}

/**
Expand All @@ -68,7 +128,7 @@ export class CfnOutput extends CfnElement {
[this.logicalId]: {
Description: this._description,
Value: this._value,
Export: this._export != null ? { Name: this._export } : undefined,
Export: this._exportName != null ? { Name: this._exportName } : undefined,
Condition: this._condition ? this._condition.logicalId : undefined,
},
},
Expand Down