From 769e347b508ffd2a4838fbb72f30596c6df5cf44 Mon Sep 17 00:00:00 2001 From: Erik Erikson Date: Thu, 8 Sep 2016 12:52:30 -0700 Subject: [PATCH] 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. --- .../aws/deploy/compile/functions/index.js | 162 +++++++++++------- .../deploy/compile/functions/tests/index.js | 122 +++++++++++++ 2 files changed, 222 insertions(+), 62 deletions(-) diff --git a/lib/plugins/aws/deploy/compile/functions/index.js b/lib/plugins/aws/deploy/compile/functions/index.js index e851221e7..8c6a0324a 100644 --- a/lib/plugins/aws/deploy/compile/functions/index.js +++ b/lib/plugins/aws/deploy/compile/functions/index.js @@ -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); }); } } diff --git a/lib/plugins/aws/deploy/compile/functions/tests/index.js b/lib/plugins/aws/deploy/compile/functions/tests/index.js index 1af70fbb2..4a806020f 100644 --- a/lib/plugins/aws/deploy/compile/functions/tests/index.js +++ b/lib/plugins/aws/deploy/compile/functions/tests/index.js @@ -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: {