Skip to content

Commit

Permalink
Removal of master alias (#58)
Browse files Browse the repository at this point in the history
* Added npm download badge

* Fixed IAM role cleanup. Added checks for valid master alias removal.

* Remove the alias stack

* Spawn sls remove after the alias stack has been removed.

* Fixed issue when removing a stack with authorizers

* Block serverless remove and point to alias remove to remove the service

* Added service removal to the README

* Added unit tests and minor fixes
  • Loading branch information
HyperBrain authored Jun 21, 2017
1 parent 928a66c commit 7cc0d82
Show file tree
Hide file tree
Showing 6 changed files with 566 additions and 77 deletions.
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
[![Build Status](https://travis-ci.org/HyperBrain/serverless-aws-alias.svg?branch=master)](https://travis-ci.org/HyperBrain/serverless-aws-alias)
[![Coverage Status](https://coveralls.io/repos/github/HyperBrain/serverless-aws-alias/badge.svg?branch=master)](https://coveralls.io/github/HyperBrain/serverless-aws-alias?branch=master)
[![npm version](https://badge.fury.io/js/serverless-aws-alias.svg)](https://badge.fury.io/js/serverless-aws-alias)
[![npm](https://img.shields.io/npm/dt/serverless-aws-alias.svg)](https://www.npmjs.com/package/serverless-aws-alias)

# Serverless AWS alias plugin

Expand Down Expand Up @@ -47,6 +48,25 @@ with the alias name as option value.
Example:
`serverless deploy --alias myAlias`

## Remove an alias

See the `alias remove` command below.

## Remove a service

To remove a complete service, all deployed user aliases have to be removed first,
using the `alias remove` command.

To finally remove the whole service (same outcome as `serverless remove`), you have
to remove the master (stage) alias with `serverless alias remove --alias=MY_STAGE_NAME`.

This will trigger a removal of the master alias CF stack followed by a removal of
the service stack. After the stacks have been removed, there should be no remains
of the service.

The plugin will print reasonable error messages if you miss something so that you're
guided through the removal.

## Aliases and API Gateway

In Serverless stages are, as above mentioned, parallel stacks with parallel resources.
Expand Down
7 changes: 7 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,13 @@ class AwsAlias {
.then(this.validate)
.then(this.listAliases),

'before:remove:remove': () => {
if (!this._validated) {
throw new this._serverless.classes.Error(`Use "serverless alias remove --alias=${this._stage}" to remove the service.`);
}
return BbPromise.resolve();
},

// Override the logs command - must be, because the $LATEST filter
// in the original logs command is not easy to change without hacks.
'logs:logs': () => BbPromise.bind(this)
Expand Down
152 changes: 75 additions & 77 deletions lib/removeAlias.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,43 +8,9 @@ const NO_UPDATE_MESSAGE = 'No updates are to be performed.';

module.exports = {

aliasGetAliasStackTemplate() {
aliasCreateStackChanges(currentTemplate, aliasStackTemplates, currentAliasStackTemplate) {

const stackName = `${this._provider.naming.getStackName()}-${this._alias}`;

// Get current aliasTemplate
const params = {
StackName: stackName,
TemplateStage: 'Processed'
};

return this._provider.request('CloudFormation',
'getTemplate',
params,
this._options.stage,
this._options.region)
.then(cfData => {
try {
return BbPromise.resolve(JSON.parse(cfData.TemplateBody));
} catch (e) {
return BbPromise.reject(new Error('Received malformed response from CloudFormation'));
}
})
.catch(err => {
if (_.includes(err.message, 'does not exist')) {
const message = `Alias ${this._alias} is not deployed.`;
throw new this._serverless.classes.Error(new Error(message));
}

throw new this._serverless.classes.Error(err);
});

},

aliasCreateStackChanges(currentTemplate, aliasStackTemplates) {

return this.aliasGetAliasStackTemplate()
.then(aliasTemplate => {
return BbPromise.try(() => {

const usedFuncRefs = _.uniq(
_.flatMap(aliasStackTemplates, template => {
Expand Down Expand Up @@ -72,7 +38,7 @@ module.exports = {
const obsoleteFuncRefs = _.reject(_.map(
_.assign({},
_.pickBy(
_.get(aliasTemplate, 'Resources', {}),
_.get(currentAliasStackTemplate, 'Resources', {}),
[ 'Type', 'AWS::Lambda::Alias' ])),
(value, key) => {
return _.replace(key, /Alias$/, '');
Expand All @@ -85,13 +51,39 @@ module.exports = {
name => `${name}LambdaFunctionArn`);

const obsoleteResources = _.reject(
JSON.parse(_.get(aliasTemplate, 'Outputs.AliasResources.Value', "[]")),
JSON.parse(_.get(currentAliasStackTemplate, 'Outputs.AliasResources.Value', "[]")),
resource => _.includes(usedResources, resource));

const obsoleteOutputs = _.reject(
JSON.parse(_.get(aliasTemplate, 'Outputs.AliasOutputs.Value', "[]")),
JSON.parse(_.get(currentAliasStackTemplate, 'Outputs.AliasOutputs.Value', "[]")),
output => _.includes(usedOutputs, output));

// Check for aliased authorizers thhat reference a removed function
_.forEach(obsoleteFuncRefs, obsoleteFuncRef => {
const authorizerName = `${obsoleteFuncRef}ApiGatewayAuthorizer${this._alias}`;
if (_.has(currentTemplate.Resources, authorizerName)) {
// find obsolete references
const authRefs = utils.findReferences(currentTemplate.Resources, authorizerName);
_.forEach(authRefs, authRef => {
if (_.endsWith(authRef, '.AuthorizerId')) {
const parent = _.get(currentTemplate.Resources, _.replace(authRef, '.AuthorizerId', ''));
delete parent.AuthorizerId;
parent.AuthorizationType = "NONE";
}
});
// find dependencies
_.forOwn(currentTemplate.Resources, resource => {
if (_.isArray(resource.DependsOn) && _.includes(resource.DependsOn, authorizerName)) {
resource.DependsOn = _.without(resource.DependsOn, authorizerName);
} else if (resource.DependsOn === authorizerName) {
delete resource.DependsOn;
}
});
// Add authorizer to obsolete resources
obsoleteResources.push(authorizerName);
}
});

// Remove all alias references that are not used in other stacks
_.assign(currentTemplate, {
Resources: _.assign({}, _.omit(currentTemplate.Resources, obsoleteFuncResources, obsoleteResources)),
Expand All @@ -101,32 +93,22 @@ module.exports = {
if (this.options.verbose) {
this._serverless.cli.log(`Remove unused resources:`);
_.forEach(obsoleteResources, resource => this._serverless.cli.log(` * ${resource}`));
this.options.verbose && this._serverless.cli.log(`Adjust IAM policies`);
}

// Adjust IAM policies
const currentRolePolicies = _.get(currentTemplate, 'Resources.IamRoleLambdaExecution.Properties.Policies', []);
const currentRolePolicyStatements = _.get(currentRolePolicies[0], 'PolicyDocument.Statement', []);
this.options.verbose && this._serverless.cli.log(`Remove alias IAM policy`);
// Remove the alias IAM policy if it is not referenced in the current stage stack
// We cannot remove it otherwise, because the $LATEST function versions might still reference it.
// Then it will be deleted on the next deployment or the stage removal, whatever happend first.
const aliasPolicyName = `IamRoleLambdaExecution${this._alias}`;
if (_.isEmpty(utils.findReferences(currentTemplate.Resources, aliasPolicyName))) {
delete currentTemplate.Resources[`IamRoleLambdaExecution${this._alias}`];
} else {
this._serverless.cli.log(`IAM policy removal delayed - will be removed on next deployment`);
}

// Adjust IAM policies
const obsoleteRefs = _.concat(obsoleteFuncResources, obsoleteResources);

// Remove all obsolete resource references from the IAM policy statements
const emptyStatements = [];
const statementResources = utils.findReferences(currentRolePolicyStatements, obsoleteRefs);
_.forEach(statementResources, resourcePath => {
const indices = /.*?\[([0-9]+)\].*?\[([0-9]+)\]/.exec(resourcePath);
if (indices) {
const statementIndex = indices[1];
const resourceIndex = indices[2];

_.pullAt(currentRolePolicyStatements[statementIndex].Resource, resourceIndex);
if (_.isEmpty(currentRolePolicyStatements[statementIndex].Resource)) {
emptyStatements.push(statementIndex);
}
}
});
_.pullAt(currentRolePolicyStatements, emptyStatements);

// Set references to obsoleted resources in fct env to "REMOVED" in case
// the alias that is removed was the last deployment of the stage.
// This will change the function definition, but that does not matter
Expand All @@ -149,11 +131,11 @@ module.exports = {
delete currentTemplate.Outputs.ServiceEndpoint;
}

return BbPromise.resolve([ currentTemplate, aliasStackTemplates ]);
return BbPromise.resolve([ currentTemplate, aliasStackTemplates, currentAliasStackTemplate ]);
});
},

aliasApplyStackChanges(currentTemplate, aliasStackTemplates) {
aliasApplyStackChanges(currentTemplate, aliasStackTemplates, currentAliasStackTemplate) {

const stackName = this._provider.naming.getStackName();

Expand All @@ -177,6 +159,8 @@ module.exports = {
Tags: _.map(_.keys(stackTags), key => ({ Key: key, Value: stackTags[key] })),
};

this.options.verbose && this._serverless.cli.log(`Checking stack policy`);

// Policy must have at least one statement, otherwise no updates would be possible at all
if (this.serverless.service.provider.stackPolicy &&
this.serverless.service.provider.stackPolicy.length) {
Expand All @@ -185,23 +169,23 @@ module.exports = {
});
}

return this.provider.request('CloudFormation',
return this._provider.request('CloudFormation',
'updateStack',
params,
this.options.stage,
this.options.region)
.then(cfData => this.monitorStack('update', cfData))
.then(() => BbPromise.resolve([ currentTemplate, aliasStackTemplates ]))
.then(() => BbPromise.resolve([ currentTemplate, aliasStackTemplates, currentAliasStackTemplate ]))
.catch(err => {
if (err.message === NO_UPDATE_MESSAGE) {
return BbPromise.resolve([ currentTemplate, aliasStackTemplates ]);
return BbPromise.resolve([ currentTemplate, aliasStackTemplates, currentAliasStackTemplate ]);
}
throw new this._serverless.classes.Error(err);
throw err;
});

},

aliasRemoveAliasStack(currentTemplate, aliasStackTemplates) {
aliasRemoveAliasStack(currentTemplate, aliasStackTemplates, currentAliasStackTemplate) {

const stackName = `${this._provider.naming.getStackName()}-${this._alias}`;

Expand All @@ -218,33 +202,47 @@ module.exports = {
return this.monitorStack('removal', cfData);
})
.then(() =>{
return BbPromise.resolve([ currentTemplate, aliasStackTemplates ]);
return BbPromise.resolve([ currentTemplate, aliasStackTemplates, currentAliasStackTemplate ]);
})
.catch(e => {
if (_.includes(e.message, 'does not exist')) {
const message = `Alias ${this._alias} is not deployed.`;
throw new this._serverless.classes.Error(new Error(message));
throw new this._serverless.classes.Error(message);
}

throw new this._serverless.classes.Error(e);
throw e;
});

},

removeAlias(currentTemplate, aliasStackTemplates) {

if (this._stage && this._stage === this._alias) {
const message = `Cannot delete the stage alias. Did you intend to remove the service instead?`;
throw new this._serverless.classes.Error(new Error(message));
}
removeAlias(currentTemplate, aliasStackTemplates, currentAliasStackTemplate) {

if (this._options.noDeploy) {
this._serverless.cli.log('noDeploy option active - will do nothing');
return BbPromise.resolve();
}

if (this._stage && this._stage === this._alias) {
// Removal of the master alias is requested -> check if any other aliases are still deployed.
const aliases = _.map(aliasStackTemplates, aliasTemplate => _.get(aliasTemplate, 'Outputs.ServerlessAliasName.Value'));
if (!_.isEmpty(aliases)) {
throw new this._serverless.classes.Error(`Remove the other deployed aliases before removing the service: ${_.without(aliases, this._alias)}`);
}
if (_.isEmpty(currentAliasStackTemplate)) {
throw new this._serverless.classes.Error(`Internal error: Stack for master alias ${this._alias} is not deployed. Try to solve the problem by manual interaction with the AWS console.`);
}

// We're ready for removal
this._serverless.cli.log(`Removing master alias and stage ${this._alias} ...`);

return BbPromise.resolve([ currentTemplate, aliasStackTemplates, currentAliasStackTemplate ]).bind(this)
.spread(this.aliasRemoveAliasStack)
.then(() => this._serverless.pluginManager.spawn('remove'));
}

this._serverless.cli.log(`Removing alias ${this._alias} ...`);

return BbPromise.resolve([ currentTemplate, aliasStackTemplates ]).bind(this)
return BbPromise.resolve([ currentTemplate, aliasStackTemplates, currentAliasStackTemplate ]).bind(this)
.spread(this.aliasCreateStackChanges)
.spread(this.aliasRemoveAliasStack)
.spread(this.aliasApplyStackChanges)
Expand Down
2 changes: 2 additions & 0 deletions lib/validate.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ module.exports = {
this._aliasResources = true;
}

this._validated = true;

return BbPromise.resolve();

}
Expand Down
Loading

0 comments on commit 7cc0d82

Please sign in to comment.