diff --git a/lib/plugins/aws/deploy/lib/extendedValidate.js b/lib/plugins/aws/deploy/lib/extendedValidate.js index 1bdc696a7..db4266176 100644 --- a/lib/plugins/aws/deploy/lib/extendedValidate.js +++ b/lib/plugins/aws/deploy/lib/extendedValidate.js @@ -3,6 +3,7 @@ const path = require('path'); const BbPromise = require('bluebird'); const _ = require('lodash'); +const findReferences = require('../../utils/findReferences'); module.exports = { extendedValidate() { @@ -17,7 +18,11 @@ module.exports = { } const state = this.serverless .utils.readFileSync(serviceStateFilePath); + const selfReferences = findReferences(state.service, '${self:}'); + _.forEach(selfReferences, ref => _.set(state.service, ref, this.serverless.service)); + _.assign(this.serverless.service, state.service); + this.serverless.service.package.artifactDirectoryName = state.package.artifactDirectoryName; // only restore the default artifact path if the user is not using a custom path if (!_.isEmpty(state.package.artifact) && this.serverless.service.artifact) { diff --git a/lib/plugins/aws/package/lib/saveServiceState.js b/lib/plugins/aws/package/lib/saveServiceState.js index 8ebcb1b0f..46756bd62 100644 --- a/lib/plugins/aws/package/lib/saveServiceState.js +++ b/lib/plugins/aws/package/lib/saveServiceState.js @@ -3,6 +3,7 @@ const BbPromise = require('bluebird'); const path = require('path'); const _ = require('lodash'); +const findReferences = require('../../utils/findReferences'); module.exports = { saveServiceState() { @@ -20,8 +21,14 @@ module.exports = { ) ); + const strippedService = _.assign( + {}, _.omit(this.serverless.service, ['serverless', 'package']) + ); + const selfReferences = findReferences(strippedService, this.serverless.service); + _.forEach(selfReferences, refPath => _.set(strippedService, refPath, '${self:}')); + const state = { - service: _.assign({}, _.omit(this.serverless.service, ['serverless', 'package'])), + service: strippedService, package: { individually: this.serverless.service.package.individually, artifactDirectoryName: this.serverless.service.package.artifactDirectoryName, diff --git a/lib/plugins/aws/package/lib/saveServiceState.test.js b/lib/plugins/aws/package/lib/saveServiceState.test.js index e4a2e4a61..33f7ab1ac 100644 --- a/lib/plugins/aws/package/lib/saveServiceState.test.js +++ b/lib/plugins/aws/package/lib/saveServiceState.test.js @@ -66,4 +66,38 @@ describe('#saveServiceState()', () => { .to.equal(true); }); }); + + it('should remove self references correctly', () => { + const filePath = path.join( + awsPackage.serverless.config.servicePath, + '.serverless', + 'service-state.json' + ); + + serverless.service.custom = { + mySelfRef: serverless.service, + }; + + return awsPackage.saveServiceState().then(() => { + const expectedStateFileContent = { + service: { + provider: { + compiledCloudFormationTemplate: 'compiled content', + }, + custom: { + mySelfRef: '${self:}', + }, + }, + package: { + individually: false, + artifactDirectoryName: 'artifact-directory', + artifact: 'service.zip', + }, + }; + + expect(getServiceStateFileNameStub.calledOnce).to.equal(true); + expect(writeFileSyncStub.calledWithExactly(filePath, expectedStateFileContent)) + .to.equal(true); + }); + }); }); diff --git a/lib/plugins/aws/utils/findReferences.js b/lib/plugins/aws/utils/findReferences.js new file mode 100644 index 000000000..0f70c70e7 --- /dev/null +++ b/lib/plugins/aws/utils/findReferences.js @@ -0,0 +1,44 @@ +'use strict'; + +const _ = require('lodash'); + +/** + * Find all objects with a given value within a given root object. + * The search is implemented non-recursive to prevent stackoverflows and will + * do a complete deep search including arrays. + * @param root {Object} Root object for search + * @param value {Object} Value to search + * @returns {Array} Paths to all self references found within the object + */ +function findReferences(root, value) { + const visitedObjects = []; + const resourcePaths = []; + const stack = [{ propValue: root, path: '' }]; + + while (!_.isEmpty(stack)) { + const property = stack.pop(); + + _.forOwn(property.propValue, (propValue, key) => { + let propKey; + if (_.isArray(property.propValue)) { + propKey = `[${key}]`; + } else { + propKey = _.isEmpty(property.path) ? `${key}` : `.${key}`; + } + if (propValue === value) { + resourcePaths.push(`${property.path}${propKey}`); + } else if (_.isObject(propValue)) { + // Prevent circular references + if (_.includes(visitedObjects, propValue)) { + return; + } + visitedObjects.push(propValue); + stack.push({ propValue, path: `${property.path}${propKey}` }); + } + }); + } + + return resourcePaths; +} + +module.exports = findReferences; diff --git a/lib/plugins/aws/utils/findReferences.test.js b/lib/plugins/aws/utils/findReferences.test.js new file mode 100644 index 000000000..cc994712d --- /dev/null +++ b/lib/plugins/aws/utils/findReferences.test.js @@ -0,0 +1,88 @@ +'use strict'; + +const expect = require('chai').expect; +const _ = require('lodash'); +const findReferences = require('./findReferences'); + +describe('#findReferences()', () => { + it('should succeed on invalid input', () => { + const withoutArgs = findReferences(); + const nullArgs = findReferences(null); + + expect(withoutArgs).to.be.a('Array').to.have.lengthOf(0); + expect(nullArgs).to.be.a('Array').have.lengthOf(0); + }); + + it('should return paths', () => { + const testObject = { + prop1: 'test', + array1: [ + { + prop1: 'hit', + prop2: 4, + }, + 'hit', + [ + { + prop1: null, + prop2: 'hit', + }, + ], + ], + prop2: { + prop1: 'foo', + prop2: { + prop1: 'hit', + }, + }, + }; + + const expectedResult = [ + 'array1[0].prop1', + 'array1[1]', + 'array1[2][0].prop2', + 'prop2.prop2.prop1', + ]; + const paths = findReferences(testObject, 'hit'); + + expect(paths).to.be.a('Array').to.have.lengthOf(4); + expect(_.every(paths, path => _.includes(expectedResult, path))).to.equal(true); + }); + + it('should not fail with circular references', () => { + const testObject = { + prop1: 'test', + array1: [ + { + prop1: 'hit', + prop2: 4, + }, + 'hit', + [ + { + prop1: null, + prop2: 'hit', + }, + ], + ], + prop2: { + prop1: 'foo', + prop2: { + prop1: 'hit', + }, + }, + }; + testObject.array1.push(testObject.prop2); + + const expectedResult = [ + 'array1[0].prop1', + 'array1[1]', + 'array1[2][0].prop2', + 'prop2.prop2.prop1', + ]; + const paths = findReferences(testObject, 'hit'); + + expect(paths).to.be.a('Array').to.have.lengthOf(4); + expect(_.every(paths, path => _.includes(expectedResult, path))).to.equal(true); + }); +});