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: {