Skip to content

Commit 91f19ee

Browse files
author
Frank Schmid
committed
Keep functions for current alias in case of a rollback
1 parent feb6f10 commit 91f19ee

File tree

4 files changed

+199
-134
lines changed

4 files changed

+199
-134
lines changed

lib/aliasRestructureStack.js

Lines changed: 46 additions & 130 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,14 @@ const utils = require('./utils');
2020
* @param currentTemplate {Object} Currently deployed main stack template
2121
* @param aliasStackTemplates {Array<Object>} Currently deployed and references aliases
2222
*/
23-
function mergeAliases(stackName, newTemplate, currentTemplate, aliasStackTemplates) {
23+
function mergeAliases(stackName, newTemplate, currentTemplate, aliasStackTemplates, currentAliasStackTemplate, removedResources) {
24+
25+
const allAliasTemplates = _.concat(aliasStackTemplates, currentAliasStackTemplate);
2426

2527
// Get all referenced function logical resource ids
2628
const aliasedFunctions =
2729
_.flatMap(
28-
aliasStackTemplates,
30+
allAliasTemplates,
2931
template => _.compact(_.map(
3032
template.Resources,
3133
(resource, name) => {
@@ -55,36 +57,22 @@ function mergeAliases(stackName, newTemplate, currentTemplate, aliasStackTemplat
5557
_.forEach(usedFunctionElements.Resources, resources => _.defaults(newTemplate.Resources, resources));
5658
_.forEach(usedFunctionElements.Outputs, outputs => _.defaults(newTemplate.Outputs, outputs));
5759

58-
}
60+
// Set references to obsoleted resources in fct env to "REMOVED" in case
61+
// the alias that is removed was the last deployment of the stage.
62+
// This will change the function definition, but that does not matter
63+
// as is is neither aliased nor versioned
64+
_.forEach(_.filter(newTemplate.Resources, [ 'Type', 'AWS::Lambda::Function' ]), func => {
65+
const refs = utils.findReferences(func, removedResources);
66+
_.forEach(refs, ref => _.set(func, ref, "REMOVED"));
67+
});
5968

60-
const defaultAliasFlags = {
61-
hasRole: false
62-
};
69+
}
6370

6471
module.exports = {
6572

66-
aliasInit(currentTemplate, aliasStackTemplates) {
67-
const aliasStack = this._serverless.service.provider.compiledCloudFormationAliasTemplate;
68-
69-
// Prepare flags
70-
aliasStack.Outputs.AliasFlags = {
71-
Description: 'Alias flags.',
72-
Value: _.assign({}, defaultAliasFlags)
73-
};
74-
75-
_.forEach(aliasStackTemplates, aliasTemplate => {
76-
const flags = _.get(aliasTemplate, 'Outputs.AliasFlags', '{}');
77-
try {
78-
_.set(aliasTemplate, 'Outputs.AliasFlags.Value', _.defaults(JSON.parse(flags), defaultAliasFlags));
79-
} catch (e) {
80-
// Not handled
81-
}
82-
});
83-
84-
return BbPromise.resolve([ currentTemplate, aliasStackTemplates ]);
85-
},
73+
aliasInit: require('./stackops/init'),
8674

87-
aliasHandleFunctions(currentTemplate, aliasStackTemplates) {
75+
aliasHandleFunctions(currentTemplate, aliasStackTemplates, currentAliasStackTemplate) {
8876

8977
this.options.verbose && this._serverless.cli.log('Processing functions');
9078

@@ -155,16 +143,16 @@ module.exports = {
155143
}
156144

157145
// Merge function aliases and versions
158-
mergeAliases(stackName, stageStack, currentTemplate, aliasStackTemplates);
146+
mergeAliases(stackName, stageStack, currentTemplate, aliasStackTemplates, currentAliasStackTemplate, this.removedResourceKeys);
159147

160148
// FIXME: Resource handling
161149
// mergeResources()
162150

163151
// Promote the parsed templates to the promise chain.
164-
return BbPromise.resolve([ currentTemplate, aliasStackTemplates ]);
152+
return BbPromise.resolve([ currentTemplate, aliasStackTemplates, currentAliasStackTemplate ]);
165153
},
166154

167-
aliasHandleApiGateway(currentTemplate, aliasStackTemplates) {
155+
aliasHandleApiGateway(currentTemplate, aliasStackTemplates, currentAliasStackTemplate) {
168156

169157
const stackName = this._provider.naming.getStackName();
170158
const stageStack = this._serverless.service.provider.compiledCloudFormationTemplate;
@@ -303,105 +291,16 @@ module.exports = {
303291

304292
_.forEach(aliasResources, resource => _.assign(aliasStack.Resources, resource));
305293

306-
return BbPromise.resolve([ currentTemplate, aliasStackTemplates ]);
294+
return BbPromise.resolve([ currentTemplate, aliasStackTemplates, currentAliasStackTemplate ]);
307295
},
308296

309-
aliasHandleUserResources(currentTemplate, aliasStackTemplates) {
310-
311-
const stageStack = this._serverless.service.provider.compiledCloudFormationTemplate;
312-
const aliasStack = this._serverless.service.provider.compiledCloudFormationAliasTemplate;
313-
const userResources = _.get(this._serverless.service, 'resources', { Resources: {}, Outputs: {} });
314-
315-
this.options.verbose && this._serverless.cli.log('Processing custom resources');
316-
317-
// Retrieve all resources referenced from other aliases
318-
const aliasDependencies = _.reduce(aliasStackTemplates, (result, template) => {
319-
try {
320-
const resourceRefs = JSON.parse(_.get(template, 'Outputs.AliasResources.Value', "[]"));
321-
const outputRefs = JSON.parse(_.get(template, 'Outputs.AliasOutputs.Value', "[]"));
322-
const resources = _.assign({}, _.pick(_.get(currentTemplate, 'Resources'), resourceRefs, {}));
323-
const outputs = _.assign({}, _.pick(_.get(currentTemplate, 'Outputs'), outputRefs, {}));
324-
325-
// Check if there are IAM policy references for the alias resources and integrate them into
326-
// the lambda policy.
327-
328-
_.assign(result.Resources, resources);
329-
_.assign(result.Outputs, outputs);
330-
return result;
331-
} catch (e) {
332-
return result;
333-
}
334-
}, { Resources: {}, Outputs: {} });
335-
336-
// Logical resource ids are unique per stage
337-
// Alias stacks reference the used resources through an Output reference
338-
// On deploy, the plugin checks if a resource is already deployed from a stack
339-
// and does a validation of the resource properties
340-
// All used resources are copied from the current template
341-
342-
// Extract the user resources that are not overrides of existing Serverless resources
343-
const currentResources =
344-
_.assign({},
345-
_.omitBy(_.get(userResources, 'Resources', {}), (value, name) => _.includes(_.keys(stageStack.Resources), name)));
346-
347-
const currentOutputs = _.get(userResources, 'Outputs', {});
348-
349-
// Add the alias resources as output to the alias stack
350-
aliasStack.Outputs.AliasResources = {
351-
Description: 'Custom resource references',
352-
Value: JSON.stringify(_.keys(currentResources))
353-
};
354-
355-
// Add the outputs as output to the alias stack
356-
aliasStack.Outputs.AliasOutputs = {
357-
Description: 'Custom output references',
358-
Value: JSON.stringify(_.keys(currentOutputs))
359-
};
360-
361-
// FIXME: Deployments to the master (stage) alias should be allowed to reconfigure
362-
// resources and outputs. Otherwise a "merge" of feature branches into a
363-
// release branch would not be possible as resources would be rendered
364-
// immutable otherwise.
365-
366-
// Check if the resource is already used anywhere else with a different definition
367-
_.forOwn(currentResources, (resource, name) => {
368-
if (_.has(aliasDependencies.Resources, name) && !_.isMatch(aliasDependencies.Resources[name], resource)) {
369-
370-
// If we deploy the master alias, allow reconfiguration of resources
371-
if (this._alias === this._stage && resource.Type === aliasDependencies.Resources[name].Type) {
372-
this._serverless.cli.log(`Reconfigure resource ${name}. Remember to update it in other aliases too.`);
373-
} else {
374-
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.`));
375-
}
376-
}
377-
});
378-
379-
// Check if the output is already used anywhere else with a different definition
380-
_.forOwn(currentOutputs, (output, name) => {
381-
if (_.has(aliasDependencies.Outputs, name) && !_.isMatch(aliasDependencies.Outputs[name], output)) {
382-
if (this._alias === this._stage) {
383-
this._serverless.cli.log(`Reconfigure output ${name}. Remember to update it in other aliases too.`);
384-
} else {
385-
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.`));
386-
}
387-
}
388-
});
389-
390-
// Merge used alias resources and outputs into the stage
391-
_.defaults(stageStack.Resources, aliasDependencies.Resources);
392-
_.defaults(stageStack.Outputs, aliasDependencies.Outputs);
393-
394-
//console.log(JSON.stringify(aliasDependencies, null, 2));
395-
//throw new Error('iwzgeiug');
396-
397-
return BbPromise.resolve([ currentTemplate, aliasStackTemplates ]);
398-
},
297+
aliasHandleUserResources: require('./stackops/userResources'),
399298

400299
/**
401300
* Merge alias and current stack policies, so that all alias policy statements
402301
* are present and active
403302
*/
404-
aliasHandleLambdaRole(currentTemplate, aliasStackTemplates) {
303+
aliasHandleLambdaRole(currentTemplate, aliasStackTemplates, currentAliasStackTemplate) {
405304

406305
const stageStack = this._serverless.service.provider.compiledCloudFormationTemplate;
407306
const aliasStack = this._serverless.service.provider.compiledCloudFormationAliasTemplate;
@@ -427,7 +326,7 @@ module.exports = {
427326

428327
aliasStack.Outputs.AliasFlags.Value.hasRole = true;
429328

430-
return BbPromise.resolve([ currentTemplate, aliasStackTemplates ]);
329+
return BbPromise.resolve([ currentTemplate, aliasStackTemplates, currentAliasStackTemplate ]);
431330
}
432331

433332
// For now we only merge the first policy document and exit if SLS changes this behavior.
@@ -453,6 +352,23 @@ module.exports = {
453352
}
454353
});
455354

355+
// Remove all resource references of removed resources
356+
const voidResourceRefs = utils.findReferences(stageRolePolicyStatements, this.removedResourceKeys);
357+
const voidResourcePtrs = _.compact(_.map(voidResourceRefs, ref => {
358+
const ptrs = /\[([0-9]+)\].Resource\[([0-9]+)\].*/.exec(ref);
359+
if (ptrs && ptrs.length === 3) {
360+
return { s: ptrs[1], r: ptrs[2] };
361+
}
362+
return null;
363+
}));
364+
_.forEach(voidResourcePtrs, ptr => {
365+
const statement = stageRolePolicyStatements[ptr.s];
366+
_.pullAt(statement.Resource, [ ptr.r ]);
367+
if (_.isEmpty(statement.Resource)) {
368+
_.pullAt(stageRolePolicyStatements, [ ptr.s ]);
369+
}
370+
});
371+
456372
// Insert statement dependencies
457373
const dependencies = _.reject((() => {
458374
const result = [];
@@ -477,10 +393,10 @@ module.exports = {
477393
stageStack.Resources[dependency] = currentTemplate.Resources[dependency];
478394
});
479395

480-
return BbPromise.resolve([ currentTemplate, aliasStackTemplates ]);
396+
return BbPromise.resolve([ currentTemplate, aliasStackTemplates, currentAliasStackTemplate ]);
481397
},
482398

483-
aliasHandleEvents(currentTemplate, aliasStackTemplates) {
399+
aliasHandleEvents(currentTemplate, aliasStackTemplates, currentAliasStackTemplate) {
484400

485401
const stageStack = this._serverless.service.provider.compiledCloudFormationTemplate;
486402
const aliasStack = this._serverless.service.provider.compiledCloudFormationAliasTemplate;
@@ -508,26 +424,26 @@ module.exports = {
508424
_.defaults(aliasStack.Resources, subscriptions);
509425

510426
// Forward inputs to the promise chain
511-
return BbPromise.resolve([ currentTemplate, aliasStackTemplates ]);
427+
return BbPromise.resolve([ currentTemplate, aliasStackTemplates, currentAliasStackTemplate ]);
512428
},
513429

514-
aliasFinalize(currentTemplate, aliasStackTemplates) {
430+
aliasFinalize(currentTemplate, aliasStackTemplates, currentAliasStackTemplate) {
515431
const aliasStack = this._serverless.service.provider.compiledCloudFormationAliasTemplate;
516432

517433
aliasStack.Outputs.AliasFlags.Value = JSON.stringify(aliasStack.Outputs.AliasFlags.Value);
518434

519-
return BbPromise.resolve([ currentTemplate, aliasStackTemplates ]);
435+
return BbPromise.resolve([ currentTemplate, aliasStackTemplates, currentAliasStackTemplate ]);
520436
},
521437

522-
aliasRestructureStack(currentTemplate, aliasStackTemplates) {
438+
aliasRestructureStack(currentTemplate, aliasStackTemplates, currentAliasStackTemplate) {
523439

524440
this._serverless.cli.log('Preparing aliase ...');
525441

526442
if (_.isEmpty(aliasStackTemplates) && this._stage !== this._alias) {
527443
throw new this._serverless.classes.Error(new Error('You have to deploy the master alias at least once with "serverless deploy"'));
528444
}
529445

530-
return BbPromise.resolve([ currentTemplate, aliasStackTemplates ]).bind(this)
446+
return BbPromise.resolve([ currentTemplate, aliasStackTemplates, currentAliasStackTemplate ]).bind(this)
531447
.spread(this.aliasInit)
532448
.spread(this.aliasHandleUserResources)
533449
.spread(this.aliasHandleLambdaRole)

lib/stackInformation.js

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
*/
44

55
const BbPromise = require('bluebird');
6+
const _ = require('lodash');
67

78
module.exports = {
89

@@ -78,8 +79,8 @@ module.exports = {
7879
aliasStackLoadAliasTemplates() {
7980

8081
return this.aliasStackGetAliasStackNames() // eslint-disable-line lodash/prefer-lodash-method
81-
.filter(stack => stack !== `${this._provider.naming.getStackName()}-${this._alias}`)
82-
.mapSeries(stack => this.aliasStackLoadTemplate(stack))
82+
.mapSeries(stack => BbPromise.join(BbPromise.resolve(stack), this.aliasStackLoadTemplate(stack)))
83+
.map(stackInfo => ({ stack: stackInfo[0], template: stackInfo[1] }))
8384
.catch(err => {
8485
if (err.statusCode === 400) {
8586
// The export is not yet there. Can happen on the very first alias stack deployment.
@@ -133,11 +134,18 @@ module.exports = {
133134
BbPromise.bind(this).then(this.aliasStackLoadAliasTemplates)
134135
)
135136
.spread((currentTemplate, aliasStackTemplates) => {
137+
const currentAliasStackTemplate =
138+
_.get(
139+
_.first(_.remove(aliasStackTemplates, ['stack', `${this._provider.naming.getStackName()}-${this._alias}`])),
140+
'template',
141+
{});
142+
const deployedAliasStackTemplates = _.map(aliasStackTemplates, template => template.template);
143+
136144
this._serverless.service.provider.deployedCloudFormationTemplate = currentTemplate;
145+
this._serverless.service.provider.deployedCloudFormationAliasTemplate = currentAliasStackTemplate;
137146
this._serverless.service.provider.deployedAliasTemplates = aliasStackTemplates;
138-
return BbPromise.resolve([ currentTemplate, aliasStackTemplates ]);
147+
return BbPromise.resolve([ currentTemplate, deployedAliasStackTemplates, currentAliasStackTemplate ]);
139148
});
140-
141149
},
142150

143151
};

lib/stackops/init.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
'use strict';
2+
3+
/**
4+
* Initialize and prepare stack restructuring
5+
*/
6+
7+
const _ = require('lodash');
8+
const BbPromise = require('bluebird');
9+
10+
const defaultAliasFlags = {
11+
hasRole: false
12+
};
13+
14+
module.exports = function(currentTemplate, aliasStackTemplates, currentAliasStackTemplate) {
15+
const aliasStack = this._serverless.service.provider.compiledCloudFormationAliasTemplate;
16+
17+
// Prepare flags
18+
aliasStack.Outputs.AliasFlags = {
19+
Description: 'Alias flags.',
20+
Value: _.assign({}, defaultAliasFlags)
21+
};
22+
23+
_.forEach(aliasStackTemplates, aliasTemplate => {
24+
const flags = _.get(aliasTemplate, 'Outputs.AliasFlags', '{}');
25+
try {
26+
_.set(aliasTemplate, 'Outputs.AliasFlags.Value', _.defaults(JSON.parse(flags), defaultAliasFlags));
27+
} catch (e) {
28+
// Not handled
29+
}
30+
});
31+
32+
return BbPromise.resolve([ currentTemplate, aliasStackTemplates, currentAliasStackTemplate ]);
33+
};

0 commit comments

Comments
 (0)