Skip to content

Commit

Permalink
fix: aws-amplify#7243 yaml parsing should support all cfn functions (a…
Browse files Browse the repository at this point in the history
…ws-amplify#7245)

* fix: aws-amplify#7243 yaml parsing should support all cfn functions
  • Loading branch information
Attila Hajdrik authored and cjihrig-aws committed Jul 12, 2021
1 parent bbb16fc commit 3db8d3d
Show file tree
Hide file tree
Showing 2 changed files with 149 additions and 36 deletions.
90 changes: 89 additions & 1 deletion packages/amplify-cli-core/src/__tests__/cfnUtilities.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as fs from 'fs-extra';
import { CFNTemplateFormat, JSONUtilities, readCFNTemplate, writeCFNTemplate } from '../../lib';
import { CFNTemplateFormat, JSONUtilities, readCFNTemplate, writeCFNTemplate } from '../..';

jest.mock('fs-extra');

Expand All @@ -22,28 +22,35 @@ type TwoArgReadFile = (p: string, e: string) => Promise<string>;

describe('readCFNTemplate', () => {
beforeEach(() => jest.clearAllMocks());

it('throws if specified file does not exist', async () => {
fs_mock.existsSync.mockReturnValueOnce(false);
await expect(readCFNTemplate(testPath)).rejects.toMatchInlineSnapshot(
`[Error: No CloudFormation template found at /this/is/a/test/path.json]`,
);

fs_mock.existsSync.mockReturnValueOnce(true);
fs_mock.statSync.mockReturnValueOnce(({ isFile: false } as unknown) as fs.Stats);

await expect(readCFNTemplate(testPath)).rejects.toMatchInlineSnapshot(
`[Error: No CloudFormation template found at /this/is/a/test/path.json]`,
);
});

it('returns template with json format', async () => {
((fs_mock.readFile as unknown) as jest.MockedFunction<TwoArgReadFile>).mockResolvedValueOnce(jsonContent);

const result = await readCFNTemplate(testPath);

expect(result.templateFormat).toEqual(CFNTemplateFormat.JSON);
expect(result.cfnTemplate).toEqual(testTemplate);
});

it('returns template with yaml format', async () => {
((fs_mock.readFile as unknown) as jest.MockedFunction<TwoArgReadFile>).mockResolvedValueOnce(yamlContent);

const result = await readCFNTemplate(testPath);

expect(result.templateFormat).toEqual(CFNTemplateFormat.YAML);
expect(result.cfnTemplate).toEqual(testTemplate);
});
Expand All @@ -52,9 +59,11 @@ describe('readCFNTemplate', () => {
const yamlContent = `
!GetAtt myResource.output.someProp
`;

((fs_mock.readFile as unknown) as jest.MockedFunction<TwoArgReadFile>).mockResolvedValueOnce(yamlContent);

const result = await readCFNTemplate(testPath);

expect(result.cfnTemplate).toMatchInlineSnapshot(`
Object {
"Fn::GetAtt": Array [
Expand All @@ -68,20 +77,99 @@ describe('readCFNTemplate', () => {

describe('writeCFNTemplate', () => {
beforeEach(() => jest.clearAllMocks());

it('creates destination if it does not exist', async () => {
await writeCFNTemplate(testTemplate, testPath);

expect(fs_mock.ensureDir.mock.calls[0][0]).toEqual('/this/is/a/test');
});

it('writes json templates by default', async () => {
await writeCFNTemplate(testTemplate, testPath);

expect(fs_mock.writeFile.mock.calls[0][0]).toEqual(testPath);
expect(fs_mock.writeFile.mock.calls[0][1]).toEqual(jsonContent);
});

it('writes yaml templates if specified', async () => {
await writeCFNTemplate(testTemplate, testPath, { templateFormat: CFNTemplateFormat.YAML });

expect(fs_mock.writeFile.mock.calls[0][0]).toEqual(testPath);
expect(fs_mock.writeFile.mock.calls[0][1]).toEqual(yamlContent);
});
});

describe('roundtrip CFN Templates to object and back', () => {
beforeEach(() => jest.clearAllMocks());

it('roundtripped yml input should result in same object', async () => {
const yamlContent = `
Properties:
B64: !Base64 AWS CloudFormation
SimpleCidr: !Cidr [ "192.168.0.0/24", 6, 5 ]
NestedCidr: !Select [ 0, !Cidr [ !GetAtt ExampleVpc.CidrBlock, 1, 8 ]]
And: !And [C1, C2]
Equals: !Equals [C1, !Ref RefC2]
Or: !Or [C1, C2]
Size: !If [CreateLargeSize, 100, 10]
SizeRef: !If [CreateLargeSize, !Ref Value1, !Ref Value2]
AutoScalingRollingUpdate:
!If
- RollingUpdates
-
MaxBatchSize: 2
MinInstancesInService: 2
PauseTime: PT0M30S
- !Ref AWS::NoValue
Not: !Not [CreateLargeSize, !Ref Value1, !Ref Value2]
ImageId: !FindInMap
- RegionMap
- !Ref AWS::Region
- HVM64
GetAtt1: !GetAtt DNSName
GetAtt2: !GetAtt myELB.DNSName
GetAtt3: !GetAtt myELB.DNSName.Foo
GetAZs1: !GetAZs ""
GetAZs2: !GetAZs
Ref: AWS::Region
GetAZs3: !GetAZs us-east-1
ImportValue1: SomeValue
ImportValue2:
- !ImportValue
Fn::Sub: NetworkStackNameParameter-SecurityGroupID
Join1: !Join [ ":", [ a, b, c ] ]
Join2: !Join
- "-"
- - "arn:"
- !Ref AWS::Partition
- :s3:::elasticbeanstalk-*-
- !Ref AWS::AccountId
Select: !Select [ "1", [ "apples", "grapes", "oranges", "mangoes" ] ]
Split: !Split [ "|" , "a|b|c" ]
Sub1: !Sub
- www.\${Domain}
- { Domain: !Ref RootDomainName }
Sub2: !Sub "arn:aws:ec2:\${AWS::Region}:\${AWS::AccountId}:vpc/\${vpc}"
Transform:
!Transform
Name: "AWS::Include"
Parameters:
Location: Loc
Ref1: !Ref SomeOtherValue
`;

((fs_mock.readFile as unknown) as jest.MockedFunction<TwoArgReadFile>).mockResolvedValueOnce(yamlContent);

const result = await readCFNTemplate(testPath);

await writeCFNTemplate(result.cfnTemplate, testPath, { templateFormat: CFNTemplateFormat.YAML });

const writtenYaml = fs_mock.writeFile.mock.calls[0][1];

((fs_mock.readFile as unknown) as jest.MockedFunction<TwoArgReadFile>).mockResolvedValueOnce(writtenYaml);

const roundtrippedYaml = await readCFNTemplate(testPath);

expect(result).toMatchObject(roundtrippedYaml);
});
});
95 changes: 60 additions & 35 deletions packages/amplify-cli-core/src/cfnUtilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,41 +48,42 @@ export async function writeCFNTemplate(template: object, filePath: string, optio
}

// Register custom tags for yaml parser
// Order and definition based on docs: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference.html
const CF_SCHEMA = new yaml.Schema([
new yaml.Type('!Ref', {
new yaml.Type('!Base64', {
kind: 'scalar',
construct: function (data) {
return { Ref: data };
return { 'Fn::Base64': data };
},
}),
new yaml.Type('!Condition', {
kind: 'sequence',
new yaml.Type('!Base64', {
kind: 'mapping',
construct: function (data) {
return { Condition: data };
return { 'Fn::Base64': data };
},
}),
new yaml.Type('!Equals', {
new yaml.Type('!Cidr', {
kind: 'sequence',
construct: function (data) {
return { 'Fn::Equals': data };
return { 'Fn::Cidr': data };
},
}),
new yaml.Type('!Not', {
kind: 'sequence',
new yaml.Type('!Cidr', {
kind: 'mapping',
construct: function (data) {
return { 'Fn::Not': data };
return { 'Fn::Cidr': data };
},
}),
new yaml.Type('!Sub', {
kind: 'scalar',
new yaml.Type('!And', {
kind: 'sequence',
construct: function (data) {
return { 'Fn::Sub': data };
return { 'Fn::And': data };
},
}),
new yaml.Type('!Sub', {
new yaml.Type('!Equals', {
kind: 'sequence',
construct: function (data) {
return { 'Fn::Sub': data };
return { 'Fn::Equals': data };
},
}),
new yaml.Type('!If', {
Expand All @@ -91,16 +92,22 @@ const CF_SCHEMA = new yaml.Schema([
return { 'Fn::If': data };
},
}),
new yaml.Type('!Join', {
new yaml.Type('!Not', {
kind: 'sequence',
construct: function (data) {
return { 'Fn::Join': data };
return { 'Fn::Not': data };
},
}),
new yaml.Type('!Select', {
new yaml.Type('!Or', {
kind: 'sequence',
construct: function (data) {
return { 'Fn::Select': data };
return { 'Fn::Or': data };
},
}),
new yaml.Type('!Condition', {
kind: 'scalar',
construct: function (data) {
return { Condition: data };
},
}),
new yaml.Type('!FindInMap', {
Expand Down Expand Up @@ -145,46 +152,64 @@ const CF_SCHEMA = new yaml.Schema([
return { 'Fn::GetAZs': data };
},
}),
new yaml.Type('!Base64', {
new yaml.Type('!GetAZs', {
kind: 'mapping',
construct: function (data) {
return { 'Fn::Base64': data };
return { 'Fn::GetAZs': data };
},
}),
new yaml.Type('!Split', {
kind: 'sequence',
new yaml.Type('!ImportValue', {
kind: 'scalar',
construct: function (data) {
return { 'Fn::Split': data };
return { 'Fn::ImportValue': data };
},
}),
new yaml.Type('!Cidr', {
kind: 'sequence',
new yaml.Type('!ImportValue', {
kind: 'mapping',
construct: function (data) {
return { 'Fn::Cidr': data };
return { 'Fn::ImportValue': data };
},
}),
new yaml.Type('!ImportValue', {
new yaml.Type('!Join', {
kind: 'sequence',
construct: function (data) {
return { 'Fn::ImportValue': data };
return { 'Fn::Join': data };
},
}),
new yaml.Type('!Transform', {
new yaml.Type('!Select', {
kind: 'sequence',
construct: function (data) {
return { 'Fn::Transform': data };
return { 'Fn::Select': data };
},
}),
new yaml.Type('!And', {
new yaml.Type('!Split', {
kind: 'sequence',
construct: function (data) {
return { 'Fn::And': data };
return { 'Fn::Split': data };
},
}),
new yaml.Type('!Or', {
new yaml.Type('!Sub', {
kind: 'scalar',
construct: function (data) {
return { 'Fn::Sub': data };
},
}),
new yaml.Type('!Sub', {
kind: 'sequence',
construct: function (data) {
return { 'Fn::Or': data };
return { 'Fn::Sub': data };
},
}),
new yaml.Type('!Transform', {
kind: 'mapping',
construct: function (data) {
return { 'Fn::Transform': data };
},
}),
new yaml.Type('!Ref', {
kind: 'scalar',
construct: function (data) {
return { Ref: data };
},
}),
]);
Expand Down

0 comments on commit 3db8d3d

Please sign in to comment.