Allow Custom Per-Function Roles

Resolves matter 1 of https://github.com/serverless/serverless/issues/1895

Allow each function to declare the ARN of the role that it is to execute within.  If any function has no role ARN but provider role ARN is defined then use that role for such functions.  If any function does not have a specified role (even by falling back to a provider-wide role) then add the default policy and role so that it can be used for such functions.
Break out the portions of the logic into their discrete units so that the plugin code is a readable summary.  Add comments to the discrete units.

Add tests that check that the default role and policy are not added if every function has a role.
Add tests that check that every function that has a declared role gets it assigned.
Add tests that check that every function that has no declared role but where a provider role is declared gets the provider role assigned.
This commit is contained in:
Erik Erikson 2016-09-08 12:52:30 -07:00
parent eada1ea9fa
commit 769e347b50
2 changed files with 222 additions and 62 deletions

View File

@ -14,43 +14,75 @@ class AwsCompileFunctions {
};
}
/**
* Compile the declared functions into the Cloud Formation Template (CFT)
*/
compileFunctions() {
if (typeof this.serverless.service.provider.iamRoleARN !== 'string') {
// merge in the iamRoleLambdaTemplate
const iamRoleLambdaExecutionTemplate = this.serverless.utils.readFileSync(
path.join(this.serverless.config.serverlessPath,
'plugins',
'aws',
'deploy',
'compile',
'functions',
'iam-role-lambda-execution-template.json')
);
if (this.anyFunctionHasNoRole()) {
this.attachDefaultRoleAndPolicy();
}
this.mergeFunctionsIntoCft();
}
_.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources,
/**
* Check whether any function is missing an assigned role.
* All functions have a role if a provider-wide iamRoleARN is declared. If one is not declared
* then each function must declare its own iamRoleARN. Each function may override a declared
* provider level iamRoleARN by declaring its own.
* @returns {boolean} Whether any of the declared functions did not have an iamRoleARN.
*/
anyFunctionHasNoRole() {
let ret = false;
if (typeof this.serverless.service.provider.iamRoleARN !== 'string') {
this.serverless.service.getAllFunctions().forEach((functionName) => {
const functionObject = this.serverless.service.getFunction(functionName);
if (!functionObject.iamRoleARN) {
ret = true;
}
});
}
return ret;
}
/**
* Attach the default role and policy to the provider for application into the CFT.
*/
attachDefaultRoleAndPolicy() {
// merge in the iamRoleLambdaTemplate
const iamRoleLambdaExecutionTemplate = this.serverless.utils.readFileSync(
path.join(this.serverless.config.serverlessPath,
'plugins',
'aws',
'deploy',
'compile',
'functions',
'iam-role-lambda-execution-template.json')
);
_.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources,
iamRoleLambdaExecutionTemplate);
// merge in the iamPolicyLambdaTemplate
const iamPolicyLambdaExecutionTemplate = this.serverless.utils.readFileSync(
// merge in the iamPolicyLambdaTemplate
const iamPolicyLambdaExecutionTemplate = this.serverless.utils.readFileSync(
path.join(this.serverless.config.serverlessPath,
'plugins',
'aws',
'deploy',
'compile',
'functions',
'iam-policy-lambda-execution-template.json')
);
'plugins',
'aws',
'deploy',
'compile',
'functions',
'iam-policy-lambda-execution-template.json')
);
_.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources,
_.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources,
iamPolicyLambdaExecutionTemplate);
// set the necessary variables for the IamPolicyLambda
this.serverless.service.provider.compiledCloudFormationTemplate
// set the necessary variables for the IamPolicyLambda
this.serverless.service.provider.compiledCloudFormationTemplate
.Resources
.IamPolicyLambdaExecution
.Properties
.PolicyName = `${this.options.stage}-${this.serverless.service.service}-lambda`;
this.serverless.service.provider.compiledCloudFormationTemplate
this.serverless.service.provider.compiledCloudFormationTemplate
.Resources
.IamPolicyLambdaExecution
.Properties
@ -58,10 +90,10 @@ class AwsCompileFunctions {
.Statement[0]
.Resource = `arn:aws:logs:${this.options.region}:*:*`;
// add custom iam role statements
if (this.serverless.service.provider.iamRoleStatements &&
// add custom iam role statements
if (this.serverless.service.provider.iamRoleStatements &&
this.serverless.service.provider.iamRoleStatements instanceof Array) {
this.serverless.service.provider.compiledCloudFormationTemplate
this.serverless.service.provider.compiledCloudFormationTemplate
.Resources
.IamPolicyLambdaExecution
.Properties
@ -72,41 +104,45 @@ class AwsCompileFunctions {
.Properties
.PolicyDocument
.Statement.concat(this.serverless.service.provider.iamRoleStatements);
}
}
}
/**
* Merge the function declarations into the CFT.
*/
mergeFunctionsIntoCft() {
const functionTemplate = `
{
"Type": "AWS::Lambda::Function",
"Properties": {
"Code": {
"S3Bucket": { "Ref": "ServerlessDeploymentBucket" },
"S3Key": "S3Key"
},
"FunctionName": "FunctionName",
"Handler": "Handler",
"MemorySize": "MemorySize",
"Role": "Role",
"Runtime": "Runtime",
"Timeout": "Timeout"
{
"Type": "AWS::Lambda::Function",
"Properties": {
"Code": {
"S3Bucket": { "Ref": "ServerlessDeploymentBucket" },
"S3Key": "S3Key"
},
"FunctionName": "FunctionName",
"Handler": "Handler",
"MemorySize": "MemorySize",
"Role": "Role",
"Runtime": "Runtime",
"Timeout": "Timeout"
}
}
}
`;
`;
const outputTemplate = `
{
"Description": "Lambda function info",
"Value": "Value"
}
`;
{
"Description": "Lambda function info",
"Value": "Value"
}
`;
this.serverless.service.getAllFunctions().forEach((functionName) => {
const newFunction = JSON.parse(functionTemplate);
const functionObject = this.serverless.service.getFunction(functionName);
const artifactFilePath = this.serverless.service.package.individually ?
functionObject.artifact :
this.serverless.service.package.artifact;
functionObject.artifact :
this.serverless.service.package.artifact;
if (!artifactFilePath) {
throw new Error(`No artifact path is set for function: ${functionName}`);
@ -124,19 +160,19 @@ class AwsCompileFunctions {
' Please check the docs for more info',
].join('');
throw new this.serverless.classes
.Error(errorMessage);
.Error(errorMessage);
}
const Handler = functionObject.handler;
const FunctionName = functionObject.name;
const MemorySize = Number(functionObject.memorySize)
|| Number(this.serverless.service.provider.memorySize)
|| 1024;
|| Number(this.serverless.service.provider.memorySize)
|| 1024;
const Timeout = Number(functionObject.timeout)
|| Number(this.serverless.service.provider.timeout)
|| 6;
|| Number(this.serverless.service.provider.timeout)
|| 6;
const Runtime = this.serverless.service.provider.runtime
|| 'nodejs4.3';
|| 'nodejs4.3';
newFunction.Properties.Handler = Handler;
newFunction.Properties.FunctionName = FunctionName;
@ -144,7 +180,9 @@ class AwsCompileFunctions {
newFunction.Properties.Timeout = Timeout;
newFunction.Properties.Runtime = Runtime;
if (typeof this.serverless.service.provider.iamRoleARN === 'string') {
if (typeof functionObject.iamRoleARN === 'string') {
newFunction.Properties.Role = functionObject.iamRoleARN;
} else if (typeof this.serverless.service.provider.iamRoleARN === 'string') {
newFunction.Properties.Role = this.serverless.service.provider.iamRoleARN;
} else {
newFunction.Properties.Role = { 'Fn::GetAtt': ['IamRoleLambdaExecution', 'Arn'] };
@ -155,12 +193,12 @@ class AwsCompileFunctions {
newFunction.Properties.VpcConfig = {
SecurityGroupIds: functionObject.vpc.securityGroupIds ||
this.serverless.service.provider.vpc.securityGroupIds,
this.serverless.service.provider.vpc.securityGroupIds,
SubnetIds: functionObject.vpc.subnetIds || this.serverless.service.provider.vpc.subnetIds,
};
if (!newFunction.Properties.VpcConfig.SecurityGroupIds
|| !newFunction.Properties.VpcConfig.SubnetIds) {
|| !newFunction.Properties.VpcConfig.SubnetIds) {
delete newFunction.Properties.VpcConfig;
}
@ -171,7 +209,7 @@ class AwsCompileFunctions {
};
_.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources,
newFunctionObject);
newFunctionObject);
// Add function to Outputs section
const newOutput = JSON.parse(outputTemplate);
@ -182,7 +220,7 @@ class AwsCompileFunctions {
};
_.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Outputs,
newOutputObject);
newOutputObject);
});
}
}

View File

@ -168,6 +168,128 @@ describe('AwsCompileFunctions', () => {
).to.deep.equal(awsCompileFunctions.serverless.service.provider.iamRoleARN);
});
it('should add function declared iamRoleARN', () => {
awsCompileFunctions.serverless.service.provider.name = 'aws';
awsCompileFunctions.serverless.service.functions = {
func0: {
handler: 'func.function.handler',
name: 'new-service-dev-func0',
iamRoleARN: 'some:aws:arn:xx0:*:*',
},
func1: {
handler: 'func.function.handler',
name: 'new-service-dev-func1',
iamRoleARN: 'some:aws:arn:xx1:*:*',
},
};
awsCompileFunctions.compileFunctions();
expect(awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate
.Resources.Func0LambdaFunction.Properties.Role
).to.deep.equal(awsCompileFunctions.serverless.service.functions.func0.iamRoleARN);
expect(awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate
.Resources.Func1LambdaFunction.Properties.Role
).to.deep.equal(awsCompileFunctions.serverless.service.functions.func1.iamRoleARN);
});
it('should add function declared iamRoleARN and fill in with provider iamRoleARN', () => {
awsCompileFunctions.serverless.service.provider.name = 'aws';
awsCompileFunctions.serverless.service.provider.iamRoleARN = 'some:aws:arn:xxx:*:*';
awsCompileFunctions.serverless.service.functions = {
func0: {
handler: 'func.function.handler',
name: 'new-service-dev-func0',
// iamRoleARN: 'some:aws:arn:xx0:*:*', // obtain from provider
},
func1: {
handler: 'func.function.handler',
name: 'new-service-dev-func1',
iamRoleARN: 'some:aws:arn:xx1:*:*',
},
};
awsCompileFunctions.compileFunctions();
expect(awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate
.Resources.Func0LambdaFunction.Properties.Role
).to.deep.equal(awsCompileFunctions.serverless.service.provider.iamRoleARN);
expect(awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate
.Resources.Func1LambdaFunction.Properties.Role
).to.deep.equal(awsCompileFunctions.serverless.service.functions.func1.iamRoleARN);
});
it('should not add the default role and policy if provider has an iamRoleARN', () => {
awsCompileFunctions.serverless.service.provider.name = 'aws';
awsCompileFunctions.serverless.service.provider.iamRoleARN = 'some:aws:arn:xxx:*:*';
awsCompileFunctions.compileFunctions();
// eslint-disable-next-line no-unused-expressions
expect(awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate
.Resources.IamPolicyLambdaExecution
).to.be.undefined;
// eslint-disable-next-line no-unused-expressions
expect(awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate
.Resources.IamRoleLambdaExecution
).to.be.undefined;
});
it('should not add the default role and policy if all functions have an iamRoleARN', () => {
awsCompileFunctions.serverless.service.provider.name = 'aws';
awsCompileFunctions.serverless.service.functions = {
func0: {
handler: 'func.function.handler',
name: 'new-service-dev-func0',
iamRoleARN: 'some:aws:arn:xx0:*:*',
},
func1: {
handler: 'func.function.handler',
name: 'new-service-dev-func1',
iamRoleARN: 'some:aws:arn:xx1:*:*',
},
};
awsCompileFunctions.compileFunctions();
// eslint-disable-next-line no-unused-expressions
expect(awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate
.Resources.IamPolicyLambdaExecution
).to.be.undefined;
// eslint-disable-next-line no-unused-expressions
expect(awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate
.Resources.IamRoleLambdaExecution
).to.be.undefined;
});
it('should not add default role/policy if all functions can has iamRoleARN', () => {
awsCompileFunctions.serverless.service.provider.name = 'aws';
awsCompileFunctions.serverless.service.provider.iamRoleARN = 'some:aws:arn:xxx:*:*';
awsCompileFunctions.serverless.service.functions = {
func0: {
handler: 'func.function.handler',
name: 'new-service-dev-func0',
// iamRoleARN: 'some:aws:arn:xx0:*:*', // obtain from provider
},
func1: {
handler: 'func.function.handler',
name: 'new-service-dev-func1',
iamRoleARN: 'some:aws:arn:xx1:*:*',
},
};
awsCompileFunctions.compileFunctions();
// eslint-disable-next-line no-unused-expressions
expect(awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate
.Resources.IamPolicyLambdaExecution
).to.be.undefined;
// eslint-disable-next-line no-unused-expressions
expect(awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate
.Resources.IamRoleLambdaExecution
).to.be.undefined;
});
it('should throw an error if the function handler is not present', () => {
awsCompileFunctions.serverless.service.functions = {
func: {