Skip to content

Commit 7cc0d82

Browse files
authored
Removal of master alias (#58)
* 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
1 parent 928a66c commit 7cc0d82

File tree

6 files changed

+566
-77
lines changed

6 files changed

+566
-77
lines changed

README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
[![Build Status](https://travis-ci.org/HyperBrain/serverless-aws-alias.svg?branch=master)](https://travis-ci.org/HyperBrain/serverless-aws-alias)
22
[![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)
33
[![npm version](https://badge.fury.io/js/serverless-aws-alias.svg)](https://badge.fury.io/js/serverless-aws-alias)
4+
[![npm](https://img.shields.io/npm/dt/serverless-aws-alias.svg)](https://www.npmjs.com/package/serverless-aws-alias)
45

56
# Serverless AWS alias plugin
67

@@ -47,6 +48,25 @@ with the alias name as option value.
4748
Example:
4849
`serverless deploy --alias myAlias`
4950

51+
## Remove an alias
52+
53+
See the `alias remove` command below.
54+
55+
## Remove a service
56+
57+
To remove a complete service, all deployed user aliases have to be removed first,
58+
using the `alias remove` command.
59+
60+
To finally remove the whole service (same outcome as `serverless remove`), you have
61+
to remove the master (stage) alias with `serverless alias remove --alias=MY_STAGE_NAME`.
62+
63+
This will trigger a removal of the master alias CF stack followed by a removal of
64+
the service stack. After the stacks have been removed, there should be no remains
65+
of the service.
66+
67+
The plugin will print reasonable error messages if you miss something so that you're
68+
guided through the removal.
69+
5070
## Aliases and API Gateway
5171

5272
In Serverless stages are, as above mentioned, parallel stacks with parallel resources.

index.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,13 @@ class AwsAlias {
124124
.then(this.validate)
125125
.then(this.listAliases),
126126

127+
'before:remove:remove': () => {
128+
if (!this._validated) {
129+
throw new this._serverless.classes.Error(`Use "serverless alias remove --alias=${this._stage}" to remove the service.`);
130+
}
131+
return BbPromise.resolve();
132+
},
133+
127134
// Override the logs command - must be, because the $LATEST filter
128135
// in the original logs command is not easy to change without hacks.
129136
'logs:logs': () => BbPromise.bind(this)

lib/removeAlias.js

Lines changed: 75 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -8,43 +8,9 @@ const NO_UPDATE_MESSAGE = 'No updates are to be performed.';
88

99
module.exports = {
1010

11-
aliasGetAliasStackTemplate() {
11+
aliasCreateStackChanges(currentTemplate, aliasStackTemplates, currentAliasStackTemplate) {
1212

13-
const stackName = `${this._provider.naming.getStackName()}-${this._alias}`;
14-
15-
// Get current aliasTemplate
16-
const params = {
17-
StackName: stackName,
18-
TemplateStage: 'Processed'
19-
};
20-
21-
return this._provider.request('CloudFormation',
22-
'getTemplate',
23-
params,
24-
this._options.stage,
25-
this._options.region)
26-
.then(cfData => {
27-
try {
28-
return BbPromise.resolve(JSON.parse(cfData.TemplateBody));
29-
} catch (e) {
30-
return BbPromise.reject(new Error('Received malformed response from CloudFormation'));
31-
}
32-
})
33-
.catch(err => {
34-
if (_.includes(err.message, 'does not exist')) {
35-
const message = `Alias ${this._alias} is not deployed.`;
36-
throw new this._serverless.classes.Error(new Error(message));
37-
}
38-
39-
throw new this._serverless.classes.Error(err);
40-
});
41-
42-
},
43-
44-
aliasCreateStackChanges(currentTemplate, aliasStackTemplates) {
45-
46-
return this.aliasGetAliasStackTemplate()
47-
.then(aliasTemplate => {
13+
return BbPromise.try(() => {
4814

4915
const usedFuncRefs = _.uniq(
5016
_.flatMap(aliasStackTemplates, template => {
@@ -72,7 +38,7 @@ module.exports = {
7238
const obsoleteFuncRefs = _.reject(_.map(
7339
_.assign({},
7440
_.pickBy(
75-
_.get(aliasTemplate, 'Resources', {}),
41+
_.get(currentAliasStackTemplate, 'Resources', {}),
7642
[ 'Type', 'AWS::Lambda::Alias' ])),
7743
(value, key) => {
7844
return _.replace(key, /Alias$/, '');
@@ -85,13 +51,39 @@ module.exports = {
8551
name => `${name}LambdaFunctionArn`);
8652

8753
const obsoleteResources = _.reject(
88-
JSON.parse(_.get(aliasTemplate, 'Outputs.AliasResources.Value', "[]")),
54+
JSON.parse(_.get(currentAliasStackTemplate, 'Outputs.AliasResources.Value', "[]")),
8955
resource => _.includes(usedResources, resource));
9056

9157
const obsoleteOutputs = _.reject(
92-
JSON.parse(_.get(aliasTemplate, 'Outputs.AliasOutputs.Value', "[]")),
58+
JSON.parse(_.get(currentAliasStackTemplate, 'Outputs.AliasOutputs.Value', "[]")),
9359
output => _.includes(usedOutputs, output));
9460

61+
// Check for aliased authorizers thhat reference a removed function
62+
_.forEach(obsoleteFuncRefs, obsoleteFuncRef => {
63+
const authorizerName = `${obsoleteFuncRef}ApiGatewayAuthorizer${this._alias}`;
64+
if (_.has(currentTemplate.Resources, authorizerName)) {
65+
// find obsolete references
66+
const authRefs = utils.findReferences(currentTemplate.Resources, authorizerName);
67+
_.forEach(authRefs, authRef => {
68+
if (_.endsWith(authRef, '.AuthorizerId')) {
69+
const parent = _.get(currentTemplate.Resources, _.replace(authRef, '.AuthorizerId', ''));
70+
delete parent.AuthorizerId;
71+
parent.AuthorizationType = "NONE";
72+
}
73+
});
74+
// find dependencies
75+
_.forOwn(currentTemplate.Resources, resource => {
76+
if (_.isArray(resource.DependsOn) && _.includes(resource.DependsOn, authorizerName)) {
77+
resource.DependsOn = _.without(resource.DependsOn, authorizerName);
78+
} else if (resource.DependsOn === authorizerName) {
79+
delete resource.DependsOn;
80+
}
81+
});
82+
// Add authorizer to obsolete resources
83+
obsoleteResources.push(authorizerName);
84+
}
85+
});
86+
9587
// Remove all alias references that are not used in other stacks
9688
_.assign(currentTemplate, {
9789
Resources: _.assign({}, _.omit(currentTemplate.Resources, obsoleteFuncResources, obsoleteResources)),
@@ -101,32 +93,22 @@ module.exports = {
10193
if (this.options.verbose) {
10294
this._serverless.cli.log(`Remove unused resources:`);
10395
_.forEach(obsoleteResources, resource => this._serverless.cli.log(` * ${resource}`));
104-
this.options.verbose && this._serverless.cli.log(`Adjust IAM policies`);
10596
}
10697

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

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

113-
// Remove all obsolete resource references from the IAM policy statements
114-
const emptyStatements = [];
115-
const statementResources = utils.findReferences(currentRolePolicyStatements, obsoleteRefs);
116-
_.forEach(statementResources, resourcePath => {
117-
const indices = /.*?\[([0-9]+)\].*?\[([0-9]+)\]/.exec(resourcePath);
118-
if (indices) {
119-
const statementIndex = indices[1];
120-
const resourceIndex = indices[2];
121-
122-
_.pullAt(currentRolePolicyStatements[statementIndex].Resource, resourceIndex);
123-
if (_.isEmpty(currentRolePolicyStatements[statementIndex].Resource)) {
124-
emptyStatements.push(statementIndex);
125-
}
126-
}
127-
});
128-
_.pullAt(currentRolePolicyStatements, emptyStatements);
129-
130112
// Set references to obsoleted resources in fct env to "REMOVED" in case
131113
// the alias that is removed was the last deployment of the stage.
132114
// This will change the function definition, but that does not matter
@@ -149,11 +131,11 @@ module.exports = {
149131
delete currentTemplate.Outputs.ServiceEndpoint;
150132
}
151133

152-
return BbPromise.resolve([ currentTemplate, aliasStackTemplates ]);
134+
return BbPromise.resolve([ currentTemplate, aliasStackTemplates, currentAliasStackTemplate ]);
153135
});
154136
},
155137

156-
aliasApplyStackChanges(currentTemplate, aliasStackTemplates) {
138+
aliasApplyStackChanges(currentTemplate, aliasStackTemplates, currentAliasStackTemplate) {
157139

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

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

162+
this.options.verbose && this._serverless.cli.log(`Checking stack policy`);
163+
180164
// Policy must have at least one statement, otherwise no updates would be possible at all
181165
if (this.serverless.service.provider.stackPolicy &&
182166
this.serverless.service.provider.stackPolicy.length) {
@@ -185,23 +169,23 @@ module.exports = {
185169
});
186170
}
187171

188-
return this.provider.request('CloudFormation',
172+
return this._provider.request('CloudFormation',
189173
'updateStack',
190174
params,
191175
this.options.stage,
192176
this.options.region)
193177
.then(cfData => this.monitorStack('update', cfData))
194-
.then(() => BbPromise.resolve([ currentTemplate, aliasStackTemplates ]))
178+
.then(() => BbPromise.resolve([ currentTemplate, aliasStackTemplates, currentAliasStackTemplate ]))
195179
.catch(err => {
196180
if (err.message === NO_UPDATE_MESSAGE) {
197-
return BbPromise.resolve([ currentTemplate, aliasStackTemplates ]);
181+
return BbPromise.resolve([ currentTemplate, aliasStackTemplates, currentAliasStackTemplate ]);
198182
}
199-
throw new this._serverless.classes.Error(err);
183+
throw err;
200184
});
201185

202186
},
203187

204-
aliasRemoveAliasStack(currentTemplate, aliasStackTemplates) {
188+
aliasRemoveAliasStack(currentTemplate, aliasStackTemplates, currentAliasStackTemplate) {
205189

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

@@ -218,33 +202,47 @@ module.exports = {
218202
return this.monitorStack('removal', cfData);
219203
})
220204
.then(() =>{
221-
return BbPromise.resolve([ currentTemplate, aliasStackTemplates ]);
205+
return BbPromise.resolve([ currentTemplate, aliasStackTemplates, currentAliasStackTemplate ]);
222206
})
223207
.catch(e => {
224208
if (_.includes(e.message, 'does not exist')) {
225209
const message = `Alias ${this._alias} is not deployed.`;
226-
throw new this._serverless.classes.Error(new Error(message));
210+
throw new this._serverless.classes.Error(message);
227211
}
228212

229-
throw new this._serverless.classes.Error(e);
213+
throw e;
230214
});
231215

232216
},
233217

234-
removeAlias(currentTemplate, aliasStackTemplates) {
235-
236-
if (this._stage && this._stage === this._alias) {
237-
const message = `Cannot delete the stage alias. Did you intend to remove the service instead?`;
238-
throw new this._serverless.classes.Error(new Error(message));
239-
}
218+
removeAlias(currentTemplate, aliasStackTemplates, currentAliasStackTemplate) {
240219

241220
if (this._options.noDeploy) {
221+
this._serverless.cli.log('noDeploy option active - will do nothing');
242222
return BbPromise.resolve();
243223
}
244224

225+
if (this._stage && this._stage === this._alias) {
226+
// Removal of the master alias is requested -> check if any other aliases are still deployed.
227+
const aliases = _.map(aliasStackTemplates, aliasTemplate => _.get(aliasTemplate, 'Outputs.ServerlessAliasName.Value'));
228+
if (!_.isEmpty(aliases)) {
229+
throw new this._serverless.classes.Error(`Remove the other deployed aliases before removing the service: ${_.without(aliases, this._alias)}`);
230+
}
231+
if (_.isEmpty(currentAliasStackTemplate)) {
232+
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.`);
233+
}
234+
235+
// We're ready for removal
236+
this._serverless.cli.log(`Removing master alias and stage ${this._alias} ...`);
237+
238+
return BbPromise.resolve([ currentTemplate, aliasStackTemplates, currentAliasStackTemplate ]).bind(this)
239+
.spread(this.aliasRemoveAliasStack)
240+
.then(() => this._serverless.pluginManager.spawn('remove'));
241+
}
242+
245243
this._serverless.cli.log(`Removing alias ${this._alias} ...`);
246244

247-
return BbPromise.resolve([ currentTemplate, aliasStackTemplates ]).bind(this)
245+
return BbPromise.resolve([ currentTemplate, aliasStackTemplates, currentAliasStackTemplate ]).bind(this)
248246
.spread(this.aliasCreateStackChanges)
249247
.spread(this.aliasRemoveAliasStack)
250248
.spread(this.aliasApplyStackChanges)

lib/validate.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ module.exports = {
2828
this._aliasResources = true;
2929
}
3030

31+
this._validated = true;
32+
3133
return BbPromise.resolve();
3234

3335
}

0 commit comments

Comments
 (0)