Skip to content

Commit df65ed1

Browse files
authored
Merge pull request #37 from HyperBrain/iam-roles-per-alias
Use separate role per alias. Fix user resource detection.
2 parents c129c03 + a78d9b3 commit df65ed1

8 files changed

+117
-97
lines changed

index.js

+5
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ const BbPromise = require('bluebird')
1515
, stackInformation = require('./lib/stackInformation')
1616
, listAliases = require('./lib/listAliases')
1717
, removeAlias = require('./lib/removeAlias')
18+
, collectUserResources = require('./lib/collectUserResources')
1819
, uploadAliasArtifacts = require('./lib/uploadAliasArtifacts');
1920

2021
class AwsAlias {
@@ -51,6 +52,7 @@ class AwsAlias {
5152
_.assign(
5253
this,
5354
validate,
55+
collectUserResources,
5456
configureAliasStack,
5557
createAliasStack,
5658
updateAliasStack,
@@ -92,6 +94,9 @@ class AwsAlias {
9294
'before:package:initialize': () => BbPromise.bind(this)
9395
.then(this.validate),
9496

97+
'before:aws:package:finalize:mergeCustomProviderResources': () => BbPromise.bind(this)
98+
.then(this.collectUserResources),
99+
95100
'before:deploy:deploy': () => BbPromise.bind(this)
96101
.then(this.validate)
97102
.then(this.configureAliasStack),

lib/aliasRestructureStack.js

+37-7
Original file line numberDiff line numberDiff line change
@@ -22,19 +22,49 @@ const cwEvents = require('./stackops/cwEvents');
2222

2323
module.exports = {
2424

25-
aliasInit: init,
26-
aliasHandleFunctions: functions,
27-
aliasHandleApiGateway: apiGateway,
28-
aliasHandleUserResources: userResources,
29-
aliasHandleLambdaRole: lambdaRole,
30-
aliasHandleEvents: events,
31-
aliasHandleCWEvents: cwEvents,
25+
aliasInit(currentTemplate, aliasStackTemplates, currentAliasStackTemplate) {
26+
return init.call(this, currentTemplate, aliasStackTemplates, currentAliasStackTemplate);
27+
},
28+
29+
aliasHandleFunctions(currentTemplate, aliasStackTemplates, currentAliasStackTemplate) {
30+
return functions.call(this, currentTemplate, aliasStackTemplates, currentAliasStackTemplate);
31+
},
32+
33+
aliasHandleApiGateway(currentTemplate, aliasStackTemplates, currentAliasStackTemplate) {
34+
return apiGateway.call(this, currentTemplate, aliasStackTemplates, currentAliasStackTemplate);
35+
},
36+
37+
aliasHandleUserResources(currentTemplate, aliasStackTemplates, currentAliasStackTemplate) {
38+
return userResources.call(this, currentTemplate, aliasStackTemplates, currentAliasStackTemplate);
39+
},
40+
41+
aliasHandleLambdaRole(currentTemplate, aliasStackTemplates, currentAliasStackTemplate) {
42+
return lambdaRole.call(this, currentTemplate, aliasStackTemplates, currentAliasStackTemplate);
43+
},
44+
45+
aliasHandleEvents(currentTemplate, aliasStackTemplates, currentAliasStackTemplate) {
46+
return events.call(this, currentTemplate, aliasStackTemplates, currentAliasStackTemplate);
47+
},
48+
49+
aliasHandleCWEvents(currentTemplate, aliasStackTemplates, currentAliasStackTemplate) {
50+
return cwEvents.call(this, currentTemplate, aliasStackTemplates, currentAliasStackTemplate);
51+
},
3252

3353
aliasFinalize(currentTemplate, aliasStackTemplates, currentAliasStackTemplate) {
54+
const stageStack = this._serverless.service.provider.compiledCloudFormationTemplate;
3455
const aliasStack = this._serverless.service.provider.compiledCloudFormationAliasTemplate;
3556

3657
aliasStack.Outputs.AliasFlags.Value = JSON.stringify(aliasStack.Outputs.AliasFlags.Value);
3758

59+
// Check for missing dependencies and integrate them too
60+
_.forEach(_.filter(stageStack.Resources, resource => !_.isEmpty(resource.DependsOn)), parent => {
61+
_.forEach(parent.DependsOn, child => {
62+
if (!_.has(stageStack.Resources, child) && _.has(currentTemplate.Resources, child)) {
63+
stageStack.Resources[child] = currentTemplate.Resources[child];
64+
}
65+
});
66+
});
67+
3868
return BbPromise.resolve([ currentTemplate, aliasStackTemplates, currentAliasStackTemplate ]);
3969
},
4070

lib/collectUserResources.js

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
'use strict';
2+
/**
3+
* Persist the user resources.
4+
* This is now necessary as the package command merges them already.
5+
*/
6+
7+
const BbPromise = require('bluebird');
8+
const _ = require('lodash');
9+
10+
module.exports = {
11+
collectUserResources() {
12+
this._serverless.service.provider.aliasUserResources =
13+
_.cloneDeep(
14+
_.get(this._serverless.service, 'resources', { Resources: {}, Outputs: {} }));
15+
16+
return BbPromise.resolve();
17+
}
18+
};

lib/stackops/lambdaRole.js

+33-76
Original file line numberDiff line numberDiff line change
@@ -13,94 +13,51 @@ module.exports = function(currentTemplate, aliasStackTemplates, currentAliasStac
1313

1414
const stageStack = this._serverless.service.provider.compiledCloudFormationTemplate;
1515
const aliasStack = this._serverless.service.provider.compiledCloudFormationAliasTemplate;
16-
let stageRolePolicies = _.get(stageStack, 'Resources.IamRoleLambdaExecution.Properties.Policies', []);
17-
let currentRolePolicies = _.get(currentTemplate, 'Resources.IamRoleLambdaExecution.Properties.Policies', []);
18-
19-
// Older serverless versions (<1.7.0) do not use a inline policy.
20-
if (_.isEmpty(currentRolePolicies) && _.has(currentTemplate, 'Resources.IamPolicyLambdaExecution')) {
21-
this._serverless.cli.log('WARNING: Project created with SLS < 1.7.0. Using resources from policy.');
22-
currentRolePolicies = [ _.get(currentTemplate, 'Resources.IamPolicyLambdaExecution.Properties') ];
23-
}
24-
if (_.isEmpty(stageRolePolicies) && _.has(stageStack, 'Resources.IamPolicyLambdaExecution')) {
25-
stageRolePolicies = [ _.get(stageStack, 'Resources.IamPolicyLambdaExecution.Properties') ];
26-
}
2716

2817
// There can be a service role defined. In this case there is no embedded IAM role.
2918
if (_.has(this._serverless.service.provider, 'role')) {
3019
// Use the role if any of the aliases reference it
31-
if (!_.isEmpty(currentRolePolicies) &&
32-
_.some(aliasStackTemplates, template => !template.Outputs.AliasFlags.Value.hasRole)) {
33-
stageStack.Resources.IamRoleLambdaExecution = _.cloneDeep(currentTemplate.Resources.IamRoleLambdaExecution);
34-
}
35-
3620
aliasStack.Outputs.AliasFlags.Value.hasRole = true;
3721

38-
return BbPromise.resolve([ currentTemplate, aliasStackTemplates, currentAliasStackTemplate ]);
39-
}
40-
41-
// For now we only merge the first policy document and exit if SLS changes this behavior.
42-
if (stageRolePolicies.length !== 1) {
43-
return BbPromise.reject(new Error('Policy count should be 1! Please report this error to the alias plugin owner.'));
44-
}
22+
// Import all defined roles from the current template (without overwriting)
23+
const currentRoles = _.assign({}, _.pickBy(currentTemplate.Resources, (resource, name) => resource.Type === 'AWS::IAM::Role' && /^IamRoleLambdaExecution/.test(name)));
24+
_.defaults(stageStack.Resources, currentRoles);
4525

46-
const stageRolePolicyStatements = _.get(stageRolePolicies[0], 'PolicyDocument.Statement', []);
47-
const currentRolePolicyStatements = _.get(currentRolePolicies[0], 'PolicyDocument.Statement', []);
26+
// Remove old role for this alias
27+
delete stageStack.Resources[`IamRoleLambdaExecution${this._alias}`];
4828

49-
_.forEach(currentRolePolicyStatements, statement => {
50-
// Check if there is already a statement with the same actions and effect.
51-
const sameStageStatement = _.find(stageRolePolicyStatements, value => value.Effect === statement.Effect &&
52-
value.Action.length === statement.Action.length &&
53-
_.every(value.Action, action => _.includes(statement.Action, action)));
54-
55-
if (sameStageStatement) {
56-
// Merge the resources
57-
sameStageStatement.Resource = _.uniqWith(_.concat(sameStageStatement.Resource, statement.Resource), (a,b) => _.isEqual(a,b));
58-
} else {
59-
// Add the different statement
60-
stageRolePolicyStatements.push(statement);
61-
}
62-
});
29+
return BbPromise.resolve([ currentTemplate, aliasStackTemplates, currentAliasStackTemplate ]);
30+
}
6331

64-
// Remove all resource references of removed resources
65-
const voidResourceRefs = utils.findReferences(stageRolePolicyStatements, this.removedResourceKeys);
66-
const voidResourcePtrs = _.compact(_.map(voidResourceRefs, ref => {
67-
const ptrs = /\[([0-9]+)\].Resource\[([0-9]+)\].*/.exec(ref);
68-
if (ptrs && ptrs.length === 3) {
69-
return { s: ptrs[1], r: ptrs[2] };
70-
}
71-
return null;
72-
}));
73-
_.forEach(voidResourcePtrs, ptr => {
74-
const statement = stageRolePolicyStatements[ptr.s];
75-
_.pullAt(statement.Resource, [ ptr.r ]);
76-
if (_.isEmpty(statement.Resource)) {
77-
_.pullAt(stageRolePolicyStatements, [ ptr.s ]);
78-
}
32+
const roleName = `IamRoleLambdaExecution${this._alias}`;
33+
const role = stageStack.Resources.IamRoleLambdaExecution;
34+
35+
// Set role name
36+
_.last(role.Properties.RoleName['Fn::Join']).push(this._alias);
37+
38+
stageStack.Resources[roleName] = stageStack.Resources.IamRoleLambdaExecution;
39+
delete stageStack.Resources.IamRoleLambdaExecution;
40+
41+
// Replace references
42+
const functions = _.filter(stageStack.Resources, ['Type', 'AWS::Lambda::Function']);
43+
_.forEach(functions, func => {
44+
func.Properties.Role = {
45+
'Fn::GetAtt': [
46+
roleName,
47+
'Arn'
48+
]
49+
};
50+
const dependencyIndex = _.indexOf(func.DependsOn, 'IamRoleLambdaExecution');
51+
func.DependsOn[dependencyIndex] = roleName;
7952
});
8053

81-
// Insert statement dependencies
82-
const dependencies = _.reject((() => {
83-
const result = [];
84-
const stack = [ _.first(stageRolePolicyStatements) ];
85-
while (!_.isEmpty(stack)) {
86-
const statement = stack.pop();
87-
88-
_.forOwn(statement, (value, key) => {
89-
if (key === 'Ref') {
90-
result.push(value);
91-
} else if (key === 'Fn::GetAtt') {
92-
result.push(value[0]);
93-
} else if (_.isObject(value)) {
94-
stack.push(value);
95-
}
96-
});
97-
}
98-
return result;
99-
})(), dependency => _.has(stageStack.Resources, dependency));
54+
if (_.isEmpty(utils.findReferences(currentTemplate.Resources, 'IamRoleLambdaExecution')) && _.has(currentTemplate, 'Resources.IamRoleLambdaExecution')) {
55+
delete currentTemplate.Resources.IamRoleLambdaExecution;
56+
}
10057

101-
_.forEach(dependencies, dependency => {
102-
stageStack.Resources[dependency] = currentTemplate.Resources[dependency];
103-
});
58+
// Import all defined roles from the current template (without overwriting)
59+
const currentRoles = _.assign({}, _.pickBy(currentTemplate.Resources, (resource, name) => resource.Type === 'AWS::IAM::Role' && /^IamRoleLambdaExecution/.test(name)));
60+
_.defaults(stageStack.Resources, currentRoles);
10461

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

lib/stackops/userResources.js

+15-12
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ module.exports = function(currentTemplate, aliasStackTemplates, currentAliasStac
1313

1414
const stageStack = this._serverless.service.provider.compiledCloudFormationTemplate;
1515
const aliasStack = this._serverless.service.provider.compiledCloudFormationAliasTemplate;
16-
const userResources = _.get(this._serverless.service, 'resources', { Resources: {}, Outputs: {} });
16+
const userResources = _.get(this._serverless.service, 'provider.aliasUserResources', { Resources: {}, Outputs: {} });
1717

1818
this.options.verbose && this._serverless.cli.log('Processing custom resources');
1919

@@ -25,9 +25,6 @@ module.exports = function(currentTemplate, aliasStackTemplates, currentAliasStac
2525
const resources = _.assign({}, _.pick(_.get(currentTemplate, 'Resources'), resourceRefs, {}));
2626
const outputs = _.assign({}, _.pick(_.get(currentTemplate, 'Outputs'), outputRefs, {}));
2727

28-
// Check if there are IAM policy references for the alias resources and integrate them into
29-
// the lambda policy.
30-
3128
_.assign(result.Resources, resources);
3229
_.assign(result.Outputs, outputs);
3330
return result;
@@ -43,20 +40,26 @@ module.exports = function(currentTemplate, aliasStackTemplates, currentAliasStac
4340
// All used resources are copied from the current template
4441

4542
// Extract the user resources that are not overrides of existing Serverless resources
46-
const stageUserResources = _.get(userResources, 'Resources', {});
43+
const currentResources = _.get(userResources, 'Resources', {});
4744
const currentOutputs = _.get(userResources, 'Outputs', {});
4845

49-
// Store a list of all removed resources
50-
const allUsedResources = _.merge({}, aliasDependencies.Resources, stageUserResources, stageStack.Resources);
51-
const deployedResourceKeys = _.keys(currentTemplate.Resources);
52-
const allUsedResourceKeys = _.keys(allUsedResources);
53-
this.removedResourceKeys = _.filter(deployedResourceKeys, key => !_.includes(allUsedResourceKeys, key));
46+
const aliasResourceRefs = _.keys(aliasDependencies.Resources);
47+
//const aliasOutputRefs = _.keys(aliasDependencies.Outputs);
48+
const currentResourceRefs = _.keys(currentResources);
49+
//const currentOutputRefs = _.keys(currentOutputs);
50+
const oldResourceRefs = JSON.parse(_.get(currentAliasStackTemplate, 'Outputs.AliasResources.Value', "[]"));
51+
//const oldOutputRefs = JSON.parse(_.get(currentAliasStackTemplate, 'Outputs.AliasOutputs.Value', "[]"));
52+
53+
// Removed resources are resources that are no longer referenced
54+
55+
const removedResourceRefs = _.difference(_.difference(oldResourceRefs, currentResourceRefs), aliasResourceRefs);
56+
this.removedResourceKeys = removedResourceRefs;
5457
this._options.verbose && this._serverless.cli.log(`Removing resources: ${this.removedResourceKeys}`);
5558

5659
// Add the alias resources as output to the alias stack
5760
aliasStack.Outputs.AliasResources = {
5861
Description: 'Custom resource references',
59-
Value: JSON.stringify(_.keys(stageUserResources))
62+
Value: JSON.stringify(_.keys(currentResources))
6063
};
6164

6265
// Add the outputs as output to the alias stack
@@ -71,7 +74,7 @@ module.exports = function(currentTemplate, aliasStackTemplates, currentAliasStac
7174
// immutable otherwise.
7275

7376
// Check if the resource is already used anywhere else with a different definition
74-
_.forOwn(stageUserResources, (resource, name) => {
77+
_.forOwn(currentResources, (resource, name) => {
7578
if (_.has(aliasDependencies.Resources, name) && !_.isMatch(aliasDependencies.Resources[name], resource)) {
7679

7780
// If we deploy the master alias, allow reconfiguration of resources

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"url": "git@github.com:hyperbrain/serverless-aws-alias.git"
99
},
1010
"scripts": {
11-
"test": "istanbul cover -x test node_modules/mocha/bin/_mocha test/**/*.js -- -R spec --recursive",
11+
"test": "istanbul cover -x test node_modules/mocha/bin/_mocha 'test/**/*.js' -- -R spec --recursive",
1212
"eslint": "node node_modules/eslint/bin/eslint.js --ext .js lib"
1313
},
1414
"author": "Frank Schmid <fschmid740@googlemail.com>",

test/index.test.js

+8
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ describe('AwsAlias', () => {
7474
let setBucketNameStub;
7575
let uploadAliasArtifactsStub;
7676
let updateAliasStackStub;
77+
let collectUserResourcesStub;
7778

7879
before(() => {
7980
sandbox = sinon.sandbox.create();
@@ -89,6 +90,7 @@ describe('AwsAlias', () => {
8990
setBucketNameStub = sandbox.stub(awsAlias, 'setBucketName');
9091
uploadAliasArtifactsStub = sandbox.stub(awsAlias, 'uploadAliasArtifacts');
9192
updateAliasStackStub = sandbox.stub(awsAlias, 'updateAliasStack');
93+
collectUserResourcesStub = sandbox.stub(awsAlias, 'collectUserResources');
9294
});
9395

9496
afterEach(() => {
@@ -101,6 +103,12 @@ describe('AwsAlias', () => {
101103
.then(() => expect(validateStub).to.be.calledOnce);
102104
});
103105

106+
it('before:aws:package:finalize:mergeCustomProviderResources should resolve', () => {
107+
validateStub.returns(BbPromise.resolve());
108+
return expect(awsAlias.hooks['before:aws:package:finalize:mergeCustomProviderResources']()).to.eventually.be.fulfilled
109+
.then(() => expect(collectUserResourcesStub).to.be.calledOnce);
110+
});
111+
104112
it('before:deploy:deploy should resolve', () => {
105113
configureAliasStackStub.returns(BbPromise.resolve());
106114
return expect(awsAlias.hooks['before:deploy:deploy']()).to.eventually.be.fulfilled

test/stackops/lambdaRole.test.js

-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
*/
55

66
const getInstalledPath = require('get-installed-path');
7-
const BbPromise = require('bluebird');
87
const chai = require('chai');
98
const sinon = require('sinon');
109
const AWSAlias = require('../../index');

0 commit comments

Comments
 (0)