Skip to content
This repository has been archived by the owner on Apr 16, 2022. It is now read-only.

Implement Fn::Split, remove cloudformation-js-yaml-schema #148

Merged
merged 4 commits into from May 4, 2018
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Expand Up @@ -11,6 +11,10 @@ Versioning](http://semver.org/spec/v2.0.0.html).

### Changed
- Merge PR #157, adding licence attribute to package.json
- Merge PR #148, removing cloudformation-js-yaml-schema in favour of custom handling of intrinsic functions

### Added
- Merge PR #148, adding Fn::split functionality


## [1.6.2] - 2018-04-26
Expand Down
2 changes: 1 addition & 1 deletion data/aws_intrinsic_functions.json
Expand Up @@ -13,7 +13,7 @@
"Fn::Select": {},
"Fn::Select::Index": {"supportedFunctions": ["Ref", "Fn::FindInMap"]},
"Fn::Select::List": {"supportedFunctions" : ["Fn::FindInMap", "Fn::GetAtt", "Fn::GetAZs", "Fn::If", "Fn::Split", "Ref"] },
"Fn::Split": {},
"Fn::Split": {"supportedFunctions": ["Fn::Base64", "Fn::FindInMap", "Fn::GetAtt", "Fn::If", "Fn::Join", "Fn::Select", "Ref"]},
"Fn::Sub": { "supportedFunctions": ["Fn::Base64", "Fn::FindInMap", "Fn::GetAtt", "Fn::GetAZs", "Fn::If", "Fn::Join", "Fn::Select", "Ref"]},
"Ref": {}
}
1 change: 0 additions & 1 deletion package.json
Expand Up @@ -14,7 +14,6 @@
"url": "https://github.com/martysweet/cfn-lint/issues"
},
"dependencies": {
"cloudformation-js-yaml-schema": "0.3.0",
"colors": "^1.2.1",
"commander": "^2.15.0",
"core-js": "^2.5.1",
Expand Down
61 changes: 4 additions & 57 deletions src/parser.ts
@@ -1,6 +1,8 @@
import yaml = require('js-yaml');
const { CLOUDFORMATION_SCHEMA } = require('cloudformation-js-yaml-schema');
import fs = require('fs');
import buildYamlSchema from './yamlSchema';

const yamlSchema = buildYamlSchema();

export function openFile(path: string){

Expand Down Expand Up @@ -30,15 +32,12 @@ function openYaml(path: string){
// Try and load the Yaml
let yamlParse = yaml.safeLoad(fs.readFileSync(path, 'utf8'), {
filename: path,
schema: CLOUDFORMATION_SCHEMA,
schema: yamlSchema,
onWarning: (warning) => {
console.error(warning);
}
});

lastPlaceInTemplate = yamlParse;
cleanupYaml(yamlParse);

if(typeof yamlParse == 'object'){
return yamlParse
}
Expand All @@ -53,55 +52,3 @@ function openJson(path: string){
return JSON.parse(fs.readFileSync(path, 'utf8'));

}

let lastPlaceInTemplate = null;
let lastKeyInTemplate = null;
function cleanupYaml(ref: any){

// Step into next attribute
for(let i=0; i < Object.keys(ref).length; i++){
let key = Object.keys(ref)[i];

// Resolve the function
if( ref[key] !== null && ref[key].hasOwnProperty('class') && ref[key].hasOwnProperty('data')){

// We have a Yaml generated object

// Define the name of the intrinsic function
let outputKeyName = "Ref";
if(ref[key]["class"] != "Ref"){
outputKeyName = "Fn::" + ref[key]["class"];
}

// Convert the object to expected object type
let outputData = null;
let data = ref[key]['data'];
// Specify the data of the key outputKeyName: {}
if(typeof data == 'string'){
// Split . into array if Fn::GetAtt
if(outputKeyName == "Fn::GetAtt"){
outputData = data.split('.');
}else {
outputData = data;
}
}else{
// If Data is a yaml resolution object, check it doesn't need resolving
lastPlaceInTemplate = ref[key];
lastKeyInTemplate = 'data';
cleanupYaml(data);
// Set the resolved object
outputData = data;
}

ref[key] = {};
ref[key][outputKeyName] = outputData;

}else if(key != 'Attributes' && typeof ref[key] == "object" && ref[key] !== null){
lastPlaceInTemplate = ref;
lastKeyInTemplate = key;
cleanupYaml(ref[key]);
}


}
}
91 changes: 91 additions & 0 deletions src/test/validatorTest.ts
Expand Up @@ -536,6 +536,97 @@ describe('validator', () => {

});

describe('Fn::Split', () => {
it('should split a basic string', () => {
const input = {
'Fn::Split': ['-', 'asdf-fdsa']
};
const result = validator.doInstrinsicSplit(input, 'Fn::Split');
expect(result).to.deep.equal(['asdf', 'fdsa']);
});

it('should split a string that doesn\'t contain the delimiter', () => {
const input = {
'Fn::Split': ['-', 'asdffdsa']
};
const result = validator.doInstrinsicSplit(input, 'Fn::Split');
expect(result).to.deep.equal(['asdffdsa']);
});

it('should resolve an intrinsic function', () => {
const input = {
'Fn::Split': ['-', {
'Fn::Select': [1, ['0-0', '1-1', '2-2']]
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice

}]
};
const result = validator.doInstrinsicSplit(input, 'Fn::Split');
expect(result).to.deep.equal(['1', '1']);
});

it('should reject a parameter that is an object', () => {
const input = {
'Fn::Split': {}
};
const result = validator.doInstrinsicSplit(input, 'Fn::Split');
expect(result).to.deep.equal(['INVALID_SPLIT']);
});

it('should reject a parameter that is a string', () => {
const input = {
'Fn::Split': 'split-me-plz'
};
const result = validator.doInstrinsicSplit(input, 'Fn::Split');
expect(result).to.deep.equal(['INVALID_SPLIT']);
});

it('should reject a parameter that is an empty array', () => {
const input = {
'Fn::Split': []
};
const result = validator.doInstrinsicSplit(input, 'Fn::Split');
expect(result).to.deep.equal(['INVALID_SPLIT']);
});

it('should reject a parameter that is a single length array', () => {
const input = {
'Fn::Split': ['delim']
};
const result = validator.doInstrinsicSplit(input, 'Fn::Split');
expect(result).to.deep.equal(['INVALID_SPLIT']);
});

it('should reject a delimiter that isn\'t a string', () => {
const input = {
'Fn::Split': [{}, 'asd-asd-asd']
};
const result = validator.doInstrinsicSplit(input, 'Fn::Split');
expect(result).to.deep.equal(['INVALID_SPLIT']);
});

describe('validator test', () => {
let result: validator.ErrorObject;
before(() => {
validator.resetValidator();
const input = './testData/valid/yaml/split.yaml';
result = validator.validateFile(input);
});
it('should have no errors', () => {
console.dir(result['errors']);
expect(result).to.have.deep.property('templateValid', true);
expect(result['errors']['crit']).to.have.lengthOf(0);
});
it('should resolve a simple split', () => {
expect(result['outputs']['Simple']).to.deep.equal(['asdf', 'fdsa']);
});
it('should resolve a split of a join', () => {
expect(result['outputs']['Nested']).to.deep.equal(['asdf', 'fdsa_asdf', 'fdsa']);
});
it('should resolve a select of a split', () => {
expect(result['outputs']['SelectASplit']).to.deep.equal('b');
});
});
})

describe('templateVersion', () => {

it('1 invalid template version should return an object with validTemplate = false, 1 crit errors', () => {
Expand Down
38 changes: 38 additions & 0 deletions src/test/yamlSchema.ts
@@ -0,0 +1,38 @@
import buildYamlSchema, * as yamlSchema from '../yamlSchema';
import yaml = require('js-yaml');
import assert = require('assert');

describe('yamlSchema', () => {
describe('buildYamlSchema', () => {
it('should build a yaml schema', () => {
assert(buildYamlSchema() instanceof yaml.Schema, 'yamlSchema didn\'t return a yaml schema');
})
});

describe('functionTag', () => {
it('should work on a Fn::Thing', () => {
assert.strictEqual(yamlSchema.functionTag('Fn::Name'), 'Name');
})
it('should work on a Thing', () => {
assert.strictEqual(yamlSchema.functionTag('Name'), 'Name');
})
});

describe('buildYamlType', () => {
it('should return a type that builds the JSON representation of the yaml tag', () => {
const type = yamlSchema.buildYamlType('Fn::Join', 'sequence');
const input = ['asdf', 'asdf'];
const representation = type.construct(input);
assert.deepStrictEqual(representation, {'Fn::Join': ['asdf', 'asdf']});
});

it('should special-case Fn::GetAtt', () => {
const type = yamlSchema.buildYamlType('Fn::GetAtt', 'scalar');
const input = 'Resource.Attribute';
const representation = type.construct(input);
assert.deepStrictEqual(representation, {'Fn::GetAtt': ['Resource', 'Attribute']});
})
})


})
41 changes: 41 additions & 0 deletions src/validator.ts
Expand Up @@ -552,6 +552,8 @@ function resolveIntrinsicFunction(ref: any, key: string) : string | boolean | st
return doIntrinsicImportValue(ref, key);
case 'Fn::Select':
return doIntrinsicSelect(ref, key);
case 'Fn::Split':
return doInstrinsicSplit(ref, key);
default:
addError("warn", `Unhandled Intrinsic Function ${key}, this needs implementing. Some errors might be missed.`, placeInTemplate, "Functions");
return null;
Expand Down Expand Up @@ -1047,8 +1049,47 @@ function doIntrinsicImportValue(ref: any, key: string){
addError('warn', `Something went wrong when resolving references for a Fn::ImportValue`, placeInTemplate, 'Fn::ImportValue');
return 'INVALID_FN_IMPORTVALUE';
}
}

export function doInstrinsicSplit(ref: any, key: string): string[] {
const args = ref[key];

if (!Array.isArray(args) || args.length !== 2) {
addError('crit', 'Invalid parameter for Fn::Split. It needs an Array of length 2.', placeInTemplate, 'Fn::Split');
return ['INVALID_SPLIT'];
}

const delimiter: string = args[0];

if (typeof delimiter !== 'string') {
addError ('crit', `Invalid parameter for Fn::Split. The delimiter, ${util.inspect(delimiter)}, needs to be a string.`, placeInTemplate, 'Fn::Split');
return ['INVALID_SPLIT'];
}

const stringOrInstrinsic = args[1];
let stringToSplit: string;

if (typeof stringOrInstrinsic === 'object') {
const fnName = Object.keys(stringOrInstrinsic)[0];

if (awsIntrinsicFunctions['Fn::Split']['supportedFunctions'].indexOf(fnName) == -1) {
addError('crit', `Fn::Split does not support function '${fnName}' here`, placeInTemplate, 'Fn::Split');
return ['INVALID_SPLIT'];
}

stringToSplit = resolveIntrinsicFunction(stringOrInstrinsic, fnName) as string;
} else if (typeof stringOrInstrinsic === 'string') {
stringToSplit = stringOrInstrinsic;
} else {
addError('crit', `Invalid parameters for Fn::Split. The parameter, ${stringOrInstrinsic}, needs to be a string or a supported intrinsic function.`, placeInTemplate, 'Fn::Split');
return ['INVALID_SPLIT'];
}

return fnSplit(delimiter, stringToSplit);
}

function fnSplit(delimiter: string, stringToSplit: string): string[] {
return stringToSplit.split(delimiter);
}

function fnJoin(join: any, parts: any){
Expand Down
36 changes: 36 additions & 0 deletions src/yamlSchema.ts
@@ -0,0 +1,36 @@
import yaml = require('js-yaml');

export function functionTag(functionName: string) {
const splitFunctionName = functionName.split('::');
return splitFunctionName[splitFunctionName.length-1];
}

export default function buildYamlSchema() {
const intrinsicFunctions = require('../data/aws_intrinsic_functions.json');
const yamlTypes = [];
for (const fn in intrinsicFunctions) {
yamlTypes.push(...buildYamlTypes(fn));
}
return yaml.Schema.create(yamlTypes);
}

export type YamlKind = 'scalar' | 'mapping' | 'sequence';
const kinds: YamlKind[] = ['scalar', 'mapping', 'sequence'];

export function buildYamlTypes(fnName: string) {
return kinds.map((kind) => buildYamlType(fnName, kind));
}

export function buildYamlType(fnName: string, kind: YamlKind) {
const tagName = functionTag(fnName);
const tag = `!${tagName}`;

const constructFn = (fnName === 'Fn::GetAtt')
? (data: any) => ({'Fn::GetAtt': data.split('.')})
: (data: any) => ({[fnName]: data});

return new yaml.Type(tag, {
kind,
construct: constructFn
});
}
22 changes: 22 additions & 0 deletions testData/valid/yaml/split.yaml
@@ -0,0 +1,22 @@
AWSTemplateFormatVersion: '2010-09-09'

Resources:
WebServerSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Desc
SecurityGroupIngress:
- CidrIp: 0.0.0.0/0
FromPort: '80'
IpProtocol: tcp
ToPort: '80'

Outputs:
Simple:
Value: !Split ['-', 'asdf-fdsa']
Nested:
Value: !Split ['^', !Join ['_', [asdf^fdsa, asdf^fdsa]]]
SelectASplit:
Value: !Select
- 1,
- !Split ['@', 'a@b@c']