diff --git a/lib/aliasRestructureStack.js b/lib/aliasRestructureStack.js index 13dfef4..90e0157 100644 --- a/lib/aliasRestructureStack.js +++ b/lib/aliasRestructureStack.js @@ -74,7 +74,7 @@ module.exports = { aliasHandleFunctions(currentTemplate, aliasStackTemplates) { - this._serverless.cli.log('Resolving function versions ...'); + this.options.verbose && this._serverless.cli.log('Processing functions'); const stackName = this._provider.naming.getStackName(); const stageStack = this._serverless.service.provider.compiledCloudFormationTemplate; @@ -173,6 +173,8 @@ module.exports = { if (exposeApi) { + this.options.verbose && this._serverless.cli.log('Processing API'); + // Export the API for the alias stacks stageStack.Outputs.ApiGatewayRestApi = { Description: 'API Gateway API', @@ -272,8 +274,6 @@ module.exports = { aliasResources.push(apiResources); aliasResources.push(apiMethods); aliasResources.push(apiLambdaPermissions); - //console.log(JSON.stringify(apiLambdaPermissions, null, 2)); - //throw new Error('iwzgeiug'); } @@ -282,12 +282,165 @@ module.exports = { return BbPromise.resolve([ currentTemplate, aliasStackTemplates ]); }, + aliasHandleUserResources(currentTemplate, aliasStackTemplates) { + + const stageStack = this._serverless.service.provider.compiledCloudFormationTemplate; + const aliasStack = this._serverless.service.provider.compiledCloudFormationAliasTemplate; + const userResources = _.get(this._serverless.service, 'resources', { Resources: {}, Outputs: {} }); + + this.options.verbose && this._serverless.cli.log('Processing custom resources'); + + // Retrieve all resources referenced from other aliases + const aliasDependencies = _.reduce(aliasStackTemplates, (result, template) => { + try { + const resourceRefs = JSON.parse(_.get(template, 'Outputs.AliasResources.Value', "[]")); + const outputRefs = JSON.parse(_.get(template, 'Outputs.AliasOutputs.Value', "[]")); + const resources = _.assign({}, _.pick(_.get(currentTemplate, 'Resources'), resourceRefs, {})); + const outputs = _.assign({}, _.pick(_.get(currentTemplate, 'Outputs'), outputRefs, {})); + + // Check if there are IAM policy references for the alias resources and integrate them into + // the lambda policy. + + _.assign(result.Resources, resources); + _.assign(result.Outputs, outputs); + return result; + } catch (e) { + return result; + } + }, { Resources: {}, Outputs: {} }); + + // Logical resource ids are unique per stage + // Alias stacks reference the used resources through an Output reference + // On deploy, the plugin checks if a resource is already deployed from a stack + // and does a validation of the resource properties + // All used resources are copied from the current template + + // Extract the user resources that are not overrides of existing Serverless resources + const currentResources = + _.assign({}, + _.omitBy(_.get(userResources, 'Resources', {}), (value, name) => _.includes(_.keys(stageStack.Resources), name))); + + const currentOutputs = _.get(userResources, 'Outputs', {}); + + // Add the alias resources as output to the alias stack + aliasStack.Outputs.AliasResources = { + Description: 'Custom resource references', + Value: JSON.stringify(_.keys(currentResources)) + }; + + // Add the outputs as output to the alias stack + aliasStack.Outputs.AliasOutputs = { + Description: 'Custom output references', + Value: JSON.stringify(_.keys(currentOutputs)) + }; + + // FIXME: Deployments to the master (stage) alias should be allowed to reconfigure + // resources and outputs. Otherwise a "merge" of feature branches into a + // release branch would not be possible as resources would be rendered + // immutable otherwise. + + // Check if the resource is already used anywhere else with a different definition + _.forOwn(currentResources, (resource, name) => { + if (_.has(aliasDependencies.Resources, name) && !_.isMatch(aliasDependencies.Resources[name], resource)) { + + // If we deploy the master alias, allow reconfiguration of resources + if (this._alias === this._stage && resource.Type === aliasDependencies.Resources[name].Type) { + this._serverless.cli.log(`Reconfigure resource ${name}. Remember to update it in other aliases too.`); + } else { + return BbPromise.reject(new Error(`Resource ${name} is already deployed in another alias with a different configuration. Either you change your resource to match the other definition, or you change the logical resource id to deploy your resource separately.`)); + } + } + }); + + // Check if the output is already used anywhere else with a different definition + _.forOwn(currentOutputs, (output, name) => { + if (_.has(aliasDependencies.Outputs, name) && !_.isMatch(aliasDependencies.Outputs[name], output)) { + if (this._alias === this._stage) { + this._serverless.cli.log(`Reconfigure output ${name}. Remember to update it in other aliases too.`); + } else { + return BbPromise.reject(new Error(`Output ${name} is already deployed in another alias with a different configuration. Either you change your output to match the other definition, or you change the logical resource id to deploy your output separately.`)); + } + } + }); + + // Merge used alias resources and outputs into the stage + _.defaults(stageStack.Resources, aliasDependencies.Resources); + _.defaults(stageStack.Outputs, aliasDependencies.Outputs); + + //console.log(JSON.stringify(aliasDependencies, null, 2)); + //throw new Error('iwzgeiug'); + + return BbPromise.resolve([ currentTemplate, aliasStackTemplates ]); + }, + + /** + * Merge alias and current stack policies, so that all alias policy statements + * are present and active + */ + aliasHandleLambdaRole(currentTemplate, aliasStackTemplates) { + + const stageStack = this._serverless.service.provider.compiledCloudFormationTemplate; + const stageRolePolicies = _.get(stageStack, 'Resources.IamRoleLambdaExecution.Properties.Policies', []); + const currentRolePolicies = _.get(currentTemplate, 'Resources.IamRoleLambdaExecution.Properties.Policies', []); + + // For now we only merge the first policy document and exit if SLS changes this behavior. + if (stageRolePolicies.length !== 1 || currentRolePolicies.length !== 1) { + return BbPromise.reject(new Error('Policy count should be 1! Please report this error to the alias plugin owner.')); + } + + const stageRolePolicyStatements = _.get(stageRolePolicies[0], 'PolicyDocument.Statement', []); + const currentRolePolicyStatements = _.get(currentRolePolicies[0], 'PolicyDocument.Statement', []); + + _.forEach(currentRolePolicyStatements, statement => { + // Check if there is already a statement with the same actions and effect. + const sameStageStatement = _.find(stageRolePolicyStatements, value => value.Effect === statement.Effect && + value.Action.length === statement.Action.length && + _.every(value.Action, action => _.includes(statement.Action, action))); + + if (sameStageStatement) { + // Merge the resources + sameStageStatement.Resource = _.unionWith(sameStageStatement.Resource, statement.Resource, (a,b) => _.isMatch(a,b)); + } else { + // Add the different statement + stageRolePolicyStatements.push(statement); + } + }); + + // Insert statement dependencies + const dependencies = _.reject((() => { + const result = []; + const stack = [ _.first(stageRolePolicyStatements) ]; + while (!_.isEmpty(stack)) { + const statement = stack.pop(); + + _.forOwn(statement, (value, key) => { + if (key === 'Ref') { + result.push(value); + } else if (key === 'Fn::GetAtt') { + result.push(value[0]); + } else if (_.isObject(value)) { + stack.push(value); + } + }); + } + return result; + })(), dependency => _.has(stageStack.Resources, dependency)); + + _.forEach(dependencies, dependency => { + stageStack.Resources[dependency] = currentTemplate.Resources[dependency]; + }); + + return BbPromise.resolve([ currentTemplate, aliasStackTemplates ]); + }, + aliasRestructureStack() { this._serverless.cli.log('Preparing aliases ...'); return BbPromise.bind(this) .then(this.aliasLoadCurrentCFStackAndDependencies) + .spread(this.aliasHandleUserResources) + .spread(this.aliasHandleLambdaRole) .spread(this.aliasHandleFunctions) .spread(this.aliasHandleApiGateway) .then(() => BbPromise.resolve()); diff --git a/lib/stackInformation.js b/lib/stackInformation.js index 4e8726e..0f3398c 100644 --- a/lib/stackInformation.js +++ b/lib/stackInformation.js @@ -37,7 +37,7 @@ module.exports = { }, /** - * Load all deployed alias stack templates. + * Load all deployed alias stack templates excluding the current alias. */ aliasStackLoadAliasTemplates() { @@ -51,6 +51,7 @@ module.exports = { this._options.stage, this._options.region) .then(cfData => BbPromise.resolve(cfData.Imports)) + .filter(stack => stack !== `${this._provider.naming.getStackName()}-${this._alias}`) .mapSeries(stack => { const importParams = {