diff --git a/CHANGELOG.md b/CHANGELOG.md index ba109cd..17adf7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/data/aws_intrinsic_functions.json b/data/aws_intrinsic_functions.json index bb9a262..9473c39 100644 --- a/data/aws_intrinsic_functions.json +++ b/data/aws_intrinsic_functions.json @@ -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": {} } diff --git a/package.json b/package.json index c3e54cb..e8df735 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/parser.ts b/src/parser.ts index 53de5e3..564f385 100644 --- a/src/parser.ts +++ b/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){ @@ -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 } @@ -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]); - } - - - } -} diff --git a/src/test/validatorTest.ts b/src/test/validatorTest.ts index 5552dc0..bc4b2ff 100644 --- a/src/test/validatorTest.ts +++ b/src/test/validatorTest.ts @@ -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']] + }] + }; + 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', () => { diff --git a/src/test/yamlSchema.ts b/src/test/yamlSchema.ts new file mode 100644 index 0000000..0044fa2 --- /dev/null +++ b/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']}); + }) + }) + + +}) \ No newline at end of file diff --git a/src/validator.ts b/src/validator.ts index 696ccc9..5dec1df 100644 --- a/src/validator.ts +++ b/src/validator.ts @@ -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; @@ -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){ diff --git a/src/yamlSchema.ts b/src/yamlSchema.ts new file mode 100644 index 0000000..834f666 --- /dev/null +++ b/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 + }); +} diff --git a/testData/valid/yaml/split.yaml b/testData/valid/yaml/split.yaml new file mode 100644 index 0000000..b48c1dc --- /dev/null +++ b/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']