Skip to content

Commit c4fcc1e

Browse files
authored
Merge pull request #13 from HyperBrain/issue-#12-alias-remove
Remove unused resources on alias remove
2 parents e3f84fd + 68e3981 commit c4fcc1e

File tree

5 files changed

+311
-81
lines changed

5 files changed

+311
-81
lines changed

index.js

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ const BbPromise = require('bluebird')
1414
, aliasRestructureStack = require('./lib/aliasRestructureStack')
1515
, stackInformation = require('./lib/stackInformation')
1616
, listAliases = require('./lib/listAliases')
17-
, removeAliasStack = require('./lib/removeAliasStack')
17+
, removeAlias = require('./lib/removeAlias')
1818
, uploadAliasArtifacts = require('./lib/uploadAliasArtifacts');
1919

2020
class AwsAlias {
@@ -55,7 +55,7 @@ class AwsAlias {
5555
createAliasStack,
5656
updateAliasStack,
5757
listAliases,
58-
removeAliasStack,
58+
removeAlias,
5959
aliasRestructureStack,
6060
stackInformation,
6161
uploadAliasArtifacts,
@@ -78,13 +78,18 @@ class AwsAlias {
7878
remove: {
7979
usage: 'Remove a deployed alias',
8080
lifecycleEvents: [
81-
'removeStack'
81+
'remove'
8282
],
8383
options: {
8484
alias: {
8585
usage: 'Name of the alias',
8686
shortcut: 'a',
8787
required: true
88+
},
89+
verbose: {
90+
usage: 'Enable verbose output',
91+
shortcut: 'v',
92+
required: false
8893
}
8994
}
9095
}
@@ -112,8 +117,11 @@ class AwsAlias {
112117
// Setup provider configuration reuses some of the functions of the AwsDeploy plugin
113118
'after:deploy:setupProviderConfiguration': () => BbPromise.bind(this)
114119
.then(this.createAliasStack),
120+
115121
'before:deploy:deploy': () => BbPromise.bind(this)
116-
.then(this.aliasRestructureStack),
122+
.then(this.aliasStackLoadCurrentCFStackAndDependencies)
123+
.spread(this.aliasRestructureStack),
124+
117125
'after:deploy:deploy': () => BbPromise.bind(this)
118126
.then(this.setBucketName)
119127
.then(() => {
@@ -135,8 +143,11 @@ class AwsAlias {
135143
},
136144
'after:info:info': () => BbPromise.bind(this)
137145
.then(this.listAliases),
138-
'alias:remove:removeStack': () => BbPromise.bind(this)
139-
.then(this.removeAliasStack)
146+
147+
'alias:remove:remove': () => BbPromise.bind(this)
148+
.then(this.validate)
149+
.then(this.aliasStackLoadCurrentCFStackAndDependencies)
150+
.spread(this.removeAlias)
140151
};
141152
}
142153

lib/aliasRestructureStack.js

Lines changed: 7 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -58,20 +58,6 @@ function mergeAliases(stackName, newTemplate, currentTemplate, aliasStackTemplat
5858

5959
module.exports = {
6060

61-
aliasLoadCurrentCFStackAndDependencies() {
62-
63-
return BbPromise.join(
64-
BbPromise.bind(this).then(this.aliasStackLoadCurrentTemplate),
65-
BbPromise.bind(this).then(this.aliasStackLoadAliasTemplates)
66-
)
67-
.spread((currentTemplate, aliasStackTemplates) => {
68-
this._serverless.service.provider.deployedCloudFormationTemplate = currentTemplate;
69-
this._serverless.service.provider.deployedAliasTemplates = aliasStackTemplates;
70-
return BbPromise.resolve([ currentTemplate, aliasStackTemplates ]);
71-
});
72-
73-
},
74-
7561
aliasHandleFunctions(currentTemplate, aliasStackTemplates) {
7662

7763
this.options.verbose && this._serverless.cli.log('Processing functions');
@@ -433,12 +419,15 @@ module.exports = {
433419
return BbPromise.resolve([ currentTemplate, aliasStackTemplates ]);
434420
},
435421

436-
aliasRestructureStack() {
422+
aliasRestructureStack(currentTemplate, aliasStackTemplates) {
423+
424+
this._serverless.cli.log('Preparing aliase ...');
437425

438-
this._serverless.cli.log('Preparing aliases ...');
426+
if (_.isEmpty(aliasStackTemplates) && this._stage !== this._alias) {
427+
throw new this._serverless.classes.Error(new Error('You have to deploy the master alias at least once with "serverless deploy"'));
428+
}
439429

440-
return BbPromise.bind(this)
441-
.then(this.aliasLoadCurrentCFStackAndDependencies)
430+
return BbPromise.resolve([ currentTemplate, aliasStackTemplates ]).bind(this)
442431
.spread(this.aliasHandleUserResources)
443432
.spread(this.aliasHandleLambdaRole)
444433
.spread(this.aliasHandleFunctions)

lib/removeAlias.js

Lines changed: 271 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
1+
'use strict';
2+
3+
const BbPromise = require('bluebird');
4+
const _ = require('lodash');
5+
6+
const NO_UPDATE_MESSAGE = 'No updates are to be performed.';
7+
8+
function findReferences(root, references) {
9+
const resourcePaths = [];
10+
const stack = [ { parent: null, value: root, path:'' } ];
11+
12+
while (!_.isEmpty(stack)) {
13+
const property = stack.pop();
14+
15+
_.forOwn(property.value, (value, key) => {
16+
if (key === 'Ref' && _.includes(references, value) ||
17+
key === 'Fn::GetAtt' && _.includes(references, value[0])) {
18+
resourcePaths.push(property.path);
19+
} else if (_.isObject(value)) {
20+
key = _.isArray(property.value) ? `[${key}]` : (_.isEmpty(property.path) ? `${key}` : `.${key}`);
21+
stack.push({ parent: property, value, path: `${property.path}${key}` });
22+
}
23+
});
24+
}
25+
26+
return resourcePaths;
27+
}
28+
29+
module.exports = {
30+
31+
aliasGetAliasStackTemplate() {
32+
33+
const stackName = `${this._provider.naming.getStackName()}-${this._alias}`;
34+
35+
// Get current aliasTemplate
36+
const params = {
37+
StackName: stackName,
38+
TemplateStage: 'Processed'
39+
};
40+
41+
return this._provider.request('CloudFormation',
42+
'getTemplate',
43+
params,
44+
this._options.stage,
45+
this._options.region)
46+
.then(cfData => {
47+
try {
48+
return BbPromise.resolve(JSON.parse(cfData.TemplateBody));
49+
} catch (e) {
50+
return BbPromise.reject(new Error('Received malformed response from CloudFormation'));
51+
}
52+
})
53+
.catch(err => {
54+
if (_.includes(err.message, 'does not exist')) {
55+
const message = `Alias ${this._alias} is not deployed.`;
56+
throw new this._serverless.classes.Error(new Error(message));
57+
}
58+
59+
throw new this._serverless.classes.Error(err);
60+
});
61+
62+
},
63+
64+
aliasCreateStackChanges(currentTemplate, aliasStackTemplates) {
65+
66+
return this.aliasGetAliasStackTemplate()
67+
.then(aliasTemplate => {
68+
69+
const usedFuncRefs = _.uniq(
70+
_.flatMap(aliasStackTemplates, template => {
71+
const funcRefs = _.map(
72+
_.assign({},
73+
_.pickBy(
74+
_.get(template, 'Resources', {}),
75+
[ 'Type', 'AWS::Lambda::Alias' ])),
76+
(value, key) => {
77+
return key.replace(/Alias$/, '');
78+
});
79+
80+
return funcRefs;
81+
})
82+
);
83+
84+
const usedResources = _.flatMap(aliasStackTemplates, template => {
85+
return JSON.parse(_.get(template, 'Outputs.AliasResources.Value', "[]"));
86+
});
87+
88+
const usedOutputs = _.flatMap(aliasStackTemplates, template => {
89+
return JSON.parse(_.get(template, 'Outputs.AliasOutputs.Value', "[]"));
90+
});
91+
92+
const obsoleteFuncRefs = _.reject(_.map(
93+
_.assign({},
94+
_.pickBy(
95+
_.get(aliasTemplate, 'Resources', {}),
96+
[ 'Type', 'AWS::Lambda::Alias' ])),
97+
(value, key) => {
98+
return key.replace(/Alias$/, '');
99+
}), ref => _.includes(usedFuncRefs, ref));
100+
101+
const obsoleteFuncResources = _.flatMap(obsoleteFuncRefs,
102+
name => ([ `${name}LambdaFunction`, `${name}LogGroup` ]));
103+
104+
const obsoleteFuncOutputs = _.map(obsoleteFuncRefs,
105+
name => `${name}LambdaFunctionArn`);
106+
107+
const obsoleteResources = _.reject(
108+
JSON.parse(_.get(aliasTemplate, 'Outputs.AliasResources.Value', "[]")),
109+
resource => _.includes(usedResources, resource));
110+
111+
const obsoleteOutputs = _.reject(
112+
JSON.parse(_.get(aliasTemplate, 'Outputs.AliasOutputs.Value', "[]")),
113+
output => _.includes(usedOutputs, output));
114+
115+
// Remove all alias references that are not used in other stacks
116+
_.assign(currentTemplate, {
117+
Resources: _.assign({}, _.omit(currentTemplate.Resources, obsoleteFuncResources, obsoleteResources)),
118+
Outputs: _.assign({}, _.omit(currentTemplate.Outputs, obsoleteFuncOutputs, obsoleteOutputs))
119+
});
120+
121+
if (this.options.verbose) {
122+
this._serverless.cli.log(`Remove unused resources:`);
123+
_.forEach(obsoleteResources, resource => this._serverless.cli.log(` * ${resource}`));
124+
this.options.verbose && this._serverless.cli.log(`Adjust IAM policies`);
125+
}
126+
127+
// Adjust IAM policies
128+
const currentRolePolicies = _.get(currentTemplate, 'Resources.IamRoleLambdaExecution.Properties.Policies', []);
129+
const currentRolePolicyStatements = _.get(currentRolePolicies[0], 'PolicyDocument.Statement', []);
130+
131+
const obsoleteRefs = _.concat(obsoleteFuncResources, obsoleteResources);
132+
133+
// Remove all obsolete resource references from the IAM policy statements
134+
const statementResources = findReferences(currentRolePolicyStatements, obsoleteRefs);
135+
_.forEach(statementResources, resourcePath => {
136+
const indices = /.*?\[([0-9]+)\].*?\[([0-9]+)\]/.exec(resourcePath);
137+
if (indices) {
138+
const statementIndex = indices[1];
139+
const resourceIndex = indices[2];
140+
141+
_.pullAt(currentRolePolicyStatements[statementIndex].Resource, resourceIndex);
142+
_.pull(currentRolePolicyStatements[statementIndex], statement => _.isEmpty(statement.Resource));
143+
}
144+
});
145+
146+
// Set references to obsoleted resources in fct env to "REMOVED" in case
147+
// the alias that is removed was the last deployment of the stage.
148+
// This will change the function definition, but that does not matter
149+
// as is is neither aliased nor versioned
150+
_.forEach(_.filter(currentTemplate.Resources, [ 'Type', 'AWS::Lambda::Function' ]), func => {
151+
const refs = findReferences(func, obsoleteRefs);
152+
_.forEach(refs, ref => _.set(func, ref, "REMOVED"));
153+
});
154+
155+
// Check if API is still referenced and remove it otherwise
156+
const usesApi = _.some(aliasStackTemplates, template => {
157+
return _.some(_.get(template, 'Resources', {}), [ 'Type', 'AWS::ApiGateway::Deployment' ]);
158+
});
159+
if (!usesApi) {
160+
this.options.verbose && this._serverless.cli.log(`Remove API`);
161+
162+
delete currentTemplate.Resources.ApiGatewayRestApi;
163+
delete currentTemplate.Outputs.ApiGatewayRestApi;
164+
delete currentTemplate.Outputs.ApiGatewayRestApiRootResource;
165+
delete currentTemplate.Outputs.ServiceEndpoint;
166+
}
167+
168+
return BbPromise.resolve([ currentTemplate, aliasStackTemplates ]);
169+
});
170+
},
171+
172+
aliasApplyStackChanges(currentTemplate, aliasStackTemplates) {
173+
174+
const stackName = this._provider.naming.getStackName();
175+
176+
this.options.verbose && this._serverless.cli.log(`Apply changes for ${stackName}`);
177+
178+
let stackTags = { STAGE: this._stage };
179+
180+
// Merge additional stack tags
181+
if (_.isObject(this.serverless.service.provider.stackTags)) {
182+
stackTags = _.extend(stackTags, this.serverless.service.provider.stackTags);
183+
}
184+
185+
const params = {
186+
StackName: stackName,
187+
Capabilities: [
188+
'CAPABILITY_IAM',
189+
'CAPABILITY_NAMED_IAM',
190+
],
191+
Parameters: [],
192+
TemplateBody: JSON.stringify(currentTemplate),
193+
Tags: _.map(_.keys(stackTags), key => ({ Key: key, Value: stackTags[key] })),
194+
};
195+
196+
// Policy must have at least one statement, otherwise no updates would be possible at all
197+
if (this.serverless.service.provider.stackPolicy &&
198+
this.serverless.service.provider.stackPolicy.length) {
199+
params.StackPolicyBody = JSON.stringify({
200+
Statement: this.serverless.service.provider.stackPolicy,
201+
});
202+
}
203+
204+
return this.provider.request('CloudFormation',
205+
'updateStack',
206+
params,
207+
this.options.stage,
208+
this.options.region)
209+
.then(cfData => this.monitorStack('update', cfData))
210+
.then(() => BbPromise.resolve([ currentTemplate, aliasStackTemplates ]))
211+
.catch(e => {
212+
if (e.message === NO_UPDATE_MESSAGE) {
213+
return BbPromise.resolve([ currentTemplate, aliasStackTemplates ]);
214+
}
215+
throw new this._serverless.classes.Error(e);
216+
});
217+
218+
},
219+
220+
aliasRemoveAliasStack(currentTemplate, aliasStackTemplates) {
221+
222+
const stackName = `${this._provider.naming.getStackName()}-${this._alias}`;
223+
224+
this.options.verbose && this._serverless.cli.log(`Removing CF stack ${stackName}`);
225+
226+
return this._provider.request('CloudFormation',
227+
'deleteStack',
228+
{ StackName: stackName },
229+
this._options.stage,
230+
this._options.region)
231+
.then(cfData => {
232+
// monitorStack wants a StackId member
233+
cfData.StackId = stackName;
234+
return this.monitorStack('removal', cfData);
235+
})
236+
.then(() =>{
237+
return BbPromise.resolve([ currentTemplate, aliasStackTemplates ]);
238+
})
239+
.catch(e => {
240+
if (_.includes(e.message, 'does not exist')) {
241+
const message = `Alias ${this._alias} is not deployed.`;
242+
throw new this._serverless.classes.Error(new Error(message));
243+
}
244+
245+
throw new this._serverless.classes.Error(e);
246+
});
247+
248+
},
249+
250+
removeAlias(currentTemplate, aliasStackTemplates) {
251+
252+
if (this._stage && this._stage === this._alias) {
253+
const message = `Cannot delete the stage alias. Did you intend to remove the service instead?`;
254+
throw new this._serverless.classes.Error(new Error(message));
255+
}
256+
257+
if (this._options.noDeploy) {
258+
return BbPromise.resolve();
259+
}
260+
261+
this._serverless.cli.log(`Removing alias ${this._alias} ...`);
262+
263+
return BbPromise.resolve([ currentTemplate, aliasStackTemplates ]).bind(this)
264+
.spread(this.aliasCreateStackChanges)
265+
.spread(this.aliasRemoveAliasStack)
266+
.spread(this.aliasApplyStackChanges)
267+
.then(() => BbPromise.resolve());
268+
269+
}
270+
271+
};

0 commit comments

Comments
 (0)