Skip to content

Commit

Permalink
feat(cfn-include): add support for the Fn::Sub function (#9275)
Browse files Browse the repository at this point in the history
----

Closes  #9228 

*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 Jul 29, 2020
1 parent 239e686 commit 2a48495
Show file tree
Hide file tree
Showing 21 changed files with 415 additions and 55 deletions.
44 changes: 0 additions & 44 deletions packages/@aws-cdk/cloudformation-include/README.md
Expand Up @@ -218,47 +218,3 @@ bucketReadRole.addToPolicy(new iam.PolicyStatement({
resources: [bucket.attrArn],
}));
```

## Known limitations

This module is still in its early, experimental stage,
and so does not implement all features of CloudFormation templates.
All items unchecked below are currently not supported.

### Ability to retrieve CloudFormation objects from the template:

- [x] Resources
- [x] Parameters
- [x] Conditions
- [x] Outputs

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

- [x] Properties
- [x] Condition
- [x] DependsOn
- [x] CreationPolicy
- [x] UpdatePolicy
- [x] UpdateReplacePolicy
- [x] DeletionPolicy
- [x] Metadata

### [CloudFormation functions](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference.html):

- [x] Ref
- [x] Fn::GetAtt
- [x] Fn::Join
- [x] Fn::If
- [x] Fn::And
- [x] Fn::Equals
- [x] Fn::Not
- [x] Fn::Or
- [x] Fn::Base64
- [x] Fn::Cidr
- [x] Fn::FindInMap
- [x] Fn::GetAZs
- [x] Fn::ImportValue
- [x] Fn::Select
- [x] Fn::Split
- [ ] Fn::Sub
- [x] Fn::Transform
4 changes: 1 addition & 3 deletions packages/@aws-cdk/cloudformation-include/lib/file-utils.ts
Expand Up @@ -31,11 +31,9 @@ function makeTagForCfnIntrinsic(
}

const shortForms: yaml_types.Schema.CustomTag[] = [
'Base64', 'Cidr', 'FindInMap', 'GetAZs', 'ImportValue', 'Join',
'Base64', 'Cidr', 'FindInMap', 'GetAZs', 'ImportValue', 'Join', 'Sub',
'Select', 'Split', 'Transform', 'And', 'Equals', 'If', 'Not', 'Or',
].map(name => makeTagForCfnIntrinsic(name)).concat(
// ToDo: special logic for ImportValue will be needed when support for Fn::Sub is added. See
// https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-importvalue.html
makeTagForCfnIntrinsic('Ref', false),
makeTagForCfnIntrinsic('GetAtt', true, (_doc: yaml.Document, cstNode: yaml_cst.CST.Node): any => {
// The position of the leftmost period and opening bracket tell us what syntax is being used
Expand Down
Expand Up @@ -100,6 +100,18 @@ describe('CDK Include', () => {
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/);
});

test("throws a validation exception when Fn::Sub in string form uses a key that isn't in the template", () => {
expect(() => {
includeTestTemplate(stack, 'fn-sub-key-not-in-template-string.json');
}).toThrow(/Element referenced in Fn::Sub expression with logical ID: 'AFakeResource' was not found in the template/);
});

test('throws a validation exception when Fn::Sub has an empty ${} reference', () => {
expect(() => {
includeTestTemplate(stack, 'fn-sub-${}-only.json');
}).toThrow(/Element referenced in Fn::Sub expression with logical ID: '' was not found in the template/);
});
});

function includeTestTemplate(scope: core.Construct, testTemplate: string): inc.CfnInclude {
Expand Down
@@ -0,0 +1,55 @@
{
"Resources": {
"Bucket": {
"Type": "Custom::ManyStrings",
"Properties": {
"SymbolsOnly": {
"DollarSign": {
"Fn::Sub": "$"
},
"OpeningBrace": {
"Fn::Sub": "{"
},
"ClosingBrace": {
"Fn::Sub": "}"
},
"DollarOpeningBrace": {
"Fn::Sub": "${"
},
"DollarClosingBrace": {
"Fn::Sub": "$}"
},
"OpeningBraceDollar": {
"Fn::Sub": "{$"
},
"ClosingBraceDollar": {
"Fn::Sub": "}$"
}
},
"SymbolsAndResources": {
"DollarSign": {
"Fn::Sub": "DoesNotExist$DoesNotExist"
},
"OpeningBrace": {
"Fn::Sub": "DoesNotExist{DoesNotExist"
},
"ClosingBrace": {
"Fn::Sub": "DoesNotExist}DoesNotExist"
},
"DollarOpeningBrace": {
"Fn::Sub": "DoesNotExist${DoesNotExist"
},
"DollarClosingBrace": {
"Fn::Sub": "DoesNotExist$}DoesNotExist"
},
"OpeningBraceDollar": {
"Fn::Sub": "DoesNotExist{$DoesNotExist"
},
"ClosingBraceDollar": {
"Fn::Sub": "DoesNotExist}$DoesNotExist"
}
}
}
}
}
}
@@ -0,0 +1,10 @@
{
"Resources": {
"Bucket": {
"Type": "AWS::S3::Bucket",
"Properties": {
"BucketName": { "Fn::Sub": "some-bucket${!AWS::AccountId}7896${ ! DoesNotExist}${!Immediate}234" }
}
}
}
}
@@ -0,0 +1,25 @@
{
"Resources": {
"Bucket": {
"Type": "AWS::S3::Bucket",
"Properties": {
"BucketName": {
"Fn::Sub": "${ELB.SourceSecurityGroup.GroupName}"
}
}
},
"ELB": {
"Type": "AWS::ElasticLoadBalancing::LoadBalancer",
"Properties": {
"AvailabilityZones": [
"us-east-1a"
],
"Listeners": [{
"LoadBalancerPort": "80",
"InstancePort": "80",
"Protocol": "HTTP"
}]
}
}
}
}
@@ -0,0 +1,16 @@
{
"Resources": {
"Bucket": {
"Type": "AWS::S3::Bucket",
"Properties": {
"BucketName": "bucket"
}
},
"AnotherBucket": {
"Type": "AWS::S3::Bucket",
"Properties": {
"BucketName": { "Fn::Sub": "${Bucket}-${!Bucket}-${Bucket.DomainName}" }
}
}
}
}
Empty file.
@@ -0,0 +1,20 @@
{
"Resources": {
"Bucket": {
"Type": "AWS::S3::Bucket",
"Properties": {
"BucketName": {
"Fn::Sub": [
"${AnotherBucket.DomainName}",
{
"AnotherBucket": "whatever"
}
]
}
}
},
"AnotherBucket": {
"Type": "AWS::S3::Bucket"
}
}
}
@@ -0,0 +1,20 @@
{
"Resources": {
"Bucket": {
"Type": "AWS::S3::Bucket",
"Properties": {
"BucketName": {
"Fn::Sub": [
"${AnotherBucket}",
{
"AnotherBucket": { "Ref" : "AnotherBucket" }
}
]
}
}
},
"AnotherBucket": {
"Type": "AWS::S3::Bucket"
}
}
}
@@ -0,0 +1,16 @@
{
"Resources": {
"Bucket": {
"Type": "AWS::S3::Bucket",
"Properties": {
"BucketName": "bucket"
}
},
"AnotherBucket": {
"Type": "AWS::S3::Bucket",
"Properties": {
"BucketName": { "Fn::Sub": "1-${AWS::Region}-foo-${Bucket}-${!Literal}-${Bucket.DomainName}-${AWS::Region}" }
}
}
}
}
@@ -0,0 +1,12 @@
{
"Resources": {
"Bucket": {
"Type": "AWS::S3::Bucket",
"Properties": {
"BucketName": {
"Fn::Sub": "${}"
}
}
}
}
}
@@ -0,0 +1,10 @@
{
"Resources": {
"Bucket": {
"Type": "AWS::S3::Bucket",
"Properties": {
"AccessControl": { "Fn::Sub": "${AFakeResource}" }
}
}
}
}
@@ -0,0 +1,7 @@
Resources:
Bucket:
Type: AWS::S3::Bucket
Properties:
BucketName:
!ImportValue
!Sub ${AWS::Region}
Expand Up @@ -42,3 +42,11 @@ Resources:
Location: location,
AnotherParameter:
Fn::Base64: AnotherValue
AccessControl:
Fn::ImportValue:
Fn::Sub:
- "${Region}-foo-${!Immediate}-foo-${Vpc}-${Vpc.Id}-${Name}"
- Name:
Ref: Vpc
Region:
Fn::Base64: AWS::Region
@@ -0,0 +1,11 @@
Resources:
Bucket:
Type: AWS::S3::Bucket
Properties:
BucketName:
Fn::Sub: some-bucket${!AWS::AccountId}7896${ ! AWS::Region}1-1${!Immediate}234
AnotherBucket:
Type: AWS::S3::Bucket
Properties:
BucketName:
!Sub 1-${AWS::Region}-foo-${Bucket}-${!Literal}-${Bucket.DomainName}-${AWS::Region}
@@ -0,0 +1,15 @@
Resources:
Bucket:
Type: AWS::S3::Bucket
Properties:
BucketName:
!Sub
- "${Region}-foo-${!Immediate}-foo-${AnotherBucket}-${AnotherBucket.DomainName}-${Name}"
- Name:
Ref: AnotherBucket
Region:
Fn::Base64: AWS::Region
AnotherBucket:
Type: AWS::S3::Bucket
Properties:
BucketName: another-bucket

0 comments on commit 2a48495

Please sign in to comment.