Skip to content

Commit

Permalink
Fixes #5 Allow resource sets to be different in aliases.
Browse files Browse the repository at this point in the history
  • Loading branch information
Frank Schmid committed Mar 6, 2017
1 parent 21021c1 commit fe288d0
Show file tree
Hide file tree
Showing 2 changed files with 158 additions and 4 deletions.
159 changes: 156 additions & 3 deletions lib/aliasRestructureStack.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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');

}

Expand All @@ -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());
Expand Down
3 changes: 2 additions & 1 deletion lib/stackInformation.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ module.exports = {
},

/**
* Load all deployed alias stack templates.
* Load all deployed alias stack templates excluding the current alias.
*/
aliasStackLoadAliasTemplates() {

Expand All @@ -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 = {
Expand Down

0 comments on commit fe288d0

Please sign in to comment.