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

Commit

Permalink
Implement Fn::Split, remove cloudformation-js-yaml-schema (#148)
Browse files Browse the repository at this point in the history
* remove cloudformation-js-yaml-parser

* implement Fn::Split

* Update CHANGELOG.md
  • Loading branch information
akdor1154 authored and martysweet committed May 4, 2018
1 parent 257eeb2 commit f430012
Show file tree
Hide file tree
Showing 9 changed files with 237 additions and 59 deletions.
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']]
}]
};
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']

0 comments on commit f430012

Please sign in to comment.