Skip to content

Commit

Permalink
feat(Config Schema): Schema for functions[] properties (#8222)
Browse files Browse the repository at this point in the history
  • Loading branch information
fredericbarthelet committed Sep 21, 2020
1 parent 3e9e6aa commit feece9a
Show file tree
Hide file tree
Showing 6 changed files with 228 additions and 381 deletions.
3 changes: 2 additions & 1 deletion lib/classes/ConfigSchemaHandler/index.js
Expand Up @@ -50,7 +50,8 @@ class ConfigSchemaHandler {
this.serverless = serverless;
this.schema = _.cloneDeep(schema);

deepFreeze(this.schema.properties.service);
// TODO: Switch back to deepFreeze(this.schema.properties.service) once awsKmsKeyArn property is removed, see https://github.com/serverless/serverless/issues/8261
Object.freeze(this.schema.properties.service.name);
deepFreeze(this.schema.properties.plugins);
deepFreeze(this.schema.properties.package);
Object.freeze(this.schema.properties.layers);
Expand Down
7 changes: 6 additions & 1 deletion lib/configSchema.js
Expand Up @@ -9,7 +9,7 @@ const schema = {
type: 'object',
properties: {
name: { pattern: '^[a-zA-Z][0-9a-zA-Z-]+$' },
awsKmsKeyArn: { pattern: '^arn:(aws[a-zA-Z-]*)?:kms:[a-z0-9-]+-\\d+:\\d{12}:[^\\s]+$' },
awsKmsKeyArn: { $ref: '#/definitions/awsKmsArn' },
},
additionalProperties: false,
},
Expand Down Expand Up @@ -125,6 +125,11 @@ const schema = {
additionalProperties: false,
required: ['provider', 'service'],
definitions: {
// TODO: awsKmsArn definition to be moved to lib/plugins/aws/provider/awsProvider.js once service.awsKmsKeyArn moved to provider.awsKmsKeyArn, see https://github.com/serverless/serverless/issues/8261
// TODO: awsKmsArn to include #/definitions/awsCfFunction instead of type: object as one of the possible definition, see https://github.com/serverless/serverless/issues/8261
awsKmsArn: {
anyOf: [{ type: 'object' }, { type: 'string', pattern: '^arn:aws[a-z-]*:kms' }],
},
errorCode: {
type: 'string',
pattern: '^[A-Z0-9_]+$',
Expand Down
3 changes: 1 addition & 2 deletions lib/configSchema.test.js
Expand Up @@ -21,8 +21,7 @@ describe('#configSchema', () => {
},
{
isValid: false,
errorMessage:
'should match pattern "^arn:(aws[a-zA-Z-]*)?:kms:[a-z0-9-]+-\\d+:\\d{12}:[^\\s]+$',
errorMessage: 'should match pattern "^arn:aws[a-z-]*:kms',
description: 'service awsKmsKeyArn',
mutation: {
service: {
Expand Down
265 changes: 79 additions & 186 deletions lib/plugins/aws/package/compile/functions/index.js
Expand Up @@ -35,40 +35,25 @@ class AwsCompileFunctions {

compileRole(newFunction, role) {
const compiledFunction = newFunction;
const unsupportedRoleError = new this.serverless.classes.Error(
`Unsupported role provided: "${JSON.stringify(role)}"`
);

switch (typeof role) {
case 'object':
if ('Fn::GetAtt' in role) {
// role is an "Fn::GetAtt" object
compiledFunction.Properties.Role = role;
compiledFunction.DependsOn = (compiledFunction.DependsOn || []).concat(
role['Fn::GetAtt'][0]
);
} else if ('Fn::ImportValue' in role) {
// role is an "Fn::ImportValue" object
compiledFunction.Properties.Role = role;
} else {
throw unsupportedRoleError;
}
break;
case 'string':
if (role.startsWith('arn:aws')) {
// role is a statically defined iam arn
compiledFunction.Properties.Role = role;
} else if (role === 'IamRoleLambdaExecution') {
// role is the default role generated by the framework
compiledFunction.Properties.Role = { 'Fn::GetAtt': [role, 'Arn'] };
} else {
// role is a Logical Role Name
compiledFunction.Properties.Role = { 'Fn::GetAtt': [role, 'Arn'] };
compiledFunction.DependsOn = (compiledFunction.DependsOn || []).concat(role);
}
break;
default:
throw unsupportedRoleError;
if (typeof role === 'string') {
if (role.startsWith('arn:aws')) {
// role is a statically defined iam arn
compiledFunction.Properties.Role = role;
} else if (role === 'IamRoleLambdaExecution') {
// role is the default role generated by the framework
compiledFunction.Properties.Role = { 'Fn::GetAtt': [role, 'Arn'] };
} else {
// role is a Logical Role Name
compiledFunction.Properties.Role = { 'Fn::GetAtt': [role, 'Arn'] };
compiledFunction.DependsOn = (compiledFunction.DependsOn || []).concat(role);
}
} else if ('Fn::GetAtt' in role) {
// role is an "Fn::GetAtt" object
compiledFunction.Properties.Role = role;
compiledFunction.DependsOn = (compiledFunction.DependsOn || []).concat(role['Fn::GetAtt'][0]);
} else {
// role is an "Fn::ImportValue" object
compiledFunction.Properties.Role = role;
}
}

Expand Down Expand Up @@ -164,11 +149,8 @@ class AwsCompileFunctions {
const Handler = functionObject.handler;
const FunctionName = functionObject.name;
const MemorySize =
Number(functionObject.memorySize) ||
Number(this.serverless.service.provider.memorySize) ||
1024;
const Timeout =
Number(functionObject.timeout) || Number(this.serverless.service.provider.timeout) || 6;
functionObject.memorySize || this.serverless.service.provider.memorySize || 1024;
const Timeout = functionObject.timeout || this.serverless.service.provider.timeout || 6;
const Runtime = this.provider.getRuntime(functionObject.runtime);

functionResource.Properties.Handler = Handler;
Expand Down Expand Up @@ -208,96 +190,50 @@ class AwsCompileFunctions {
const arn = functionObject.onError;

if (typeof arn === 'string') {
const splittedArn = arn.split(':');
if (splittedArn[0] === 'arn' && (splittedArn[2] === 'sns' || splittedArn[2] === 'sqs')) {
const dlqType = splittedArn[2];
const iamRoleLambdaExecution = cfTemplate.Resources.IamRoleLambdaExecution;
let stmt;

functionResource.Properties.DeadLetterConfig = {
TargetArn: arn,
};

if (dlqType === 'sns') {
stmt = {
Effect: 'Allow',
Action: ['sns:Publish'],
Resource: [arn],
};
} else if (dlqType === 'sqs') {
const errorMessage = [
'onError currently only supports SNS topic arns due to a',
' race condition when using SQS queue arns and updating the IAM role.',
' Please check the docs for more info.',
].join('');
throw new this.serverless.classes.Error(errorMessage);
}
const iamRoleLambdaExecution = cfTemplate.Resources.IamRoleLambdaExecution;
functionResource.Properties.DeadLetterConfig = {
TargetArn: arn,
};

// update the PolicyDocument statements (if default policy is used)
if (iamRoleLambdaExecution) {
iamRoleLambdaExecution.Properties.Policies[0].PolicyDocument.Statement.push(stmt);
}
} else {
const errorMessage = 'onError config must be a SNS topic arn or SQS queue arn';
throw new this.serverless.classes.Error(errorMessage);
// update the PolicyDocument statements (if default policy is used)
if (iamRoleLambdaExecution) {
iamRoleLambdaExecution.Properties.Policies[0].PolicyDocument.Statement.push({
Effect: 'Allow',
Action: ['sns:Publish'],
Resource: [arn],
});
}
} else if (this.isArnRefGetAttOrImportValue(arn)) {
} else {
functionResource.Properties.DeadLetterConfig = {
TargetArn: arn,
};
} else {
const errorMessage = [
'onError config must be provided as an arn string,',
' Ref, Fn::GetAtt or Fn::ImportValue',
].join('');
throw new this.serverless.classes.Error(errorMessage);
}
}

let kmsKeyArn;
const serviceObj = this.serverless.service.serviceObject;
if ('awsKmsKeyArn' in functionObject) {
kmsKeyArn = functionObject.awsKmsKeyArn;
} else if (serviceObj && 'awsKmsKeyArn' in serviceObj) {
kmsKeyArn = serviceObj.awsKmsKeyArn;
}

if (kmsKeyArn) {
const arn = kmsKeyArn;
if (functionObject.awsKmsKeyArn || (serviceObj && serviceObj.awsKmsKeyArn)) {
const arn = functionObject.awsKmsKeyArn || (serviceObj && serviceObj.awsKmsKeyArn);

if (typeof arn === 'string') {
const splittedArn = arn.split(':');
if (splittedArn[0] === 'arn' && splittedArn[2] === 'kms') {
const iamRoleLambdaExecution = cfTemplate.Resources.IamRoleLambdaExecution;

functionResource.Properties.KmsKeyArn = arn;
functionResource.Properties.KmsKeyArn = arn;

const stmt = {
Effect: 'Allow',
Action: ['kms:Decrypt'],
Resource: [arn],
};

// update the PolicyDocument statements (if default policy is used)
if (iamRoleLambdaExecution) {
iamRoleLambdaExecution.Properties.Policies[0].PolicyDocument.Statement = _.unionWith(
iamRoleLambdaExecution.Properties.Policies[0].PolicyDocument.Statement,
[stmt],
_.isEqual
);
}
} else {
const errorMessage = 'awsKmsKeyArn config must be a KMS key arn';
throw new this.serverless.classes.Error(errorMessage);
// update the PolicyDocument statements (if default policy is used)
const iamRoleLambdaExecution = cfTemplate.Resources.IamRoleLambdaExecution;
if (iamRoleLambdaExecution) {
iamRoleLambdaExecution.Properties.Policies[0].PolicyDocument.Statement = _.unionWith(
iamRoleLambdaExecution.Properties.Policies[0].PolicyDocument.Statement,
[
{
Effect: 'Allow',
Action: ['kms:Decrypt'],
Resource: [arn],
},
],
_.isEqual
);
}
} else if (this.isArnRefGetAttOrImportValue(arn)) {
functionResource.Properties.KmsKeyArn = arn;
} else {
const errorMessage = [
'awsKmsKeyArn config must be provided as an arn string,',
' Ref, Fn::GetAtt or Fn::ImportValue',
].join('');
throw new this.serverless.classes.Error(errorMessage);
functionResource.Properties.KmsKeyArn = arn;
}
}

Expand All @@ -307,37 +243,31 @@ class AwsCompileFunctions {
this.serverless.service.provider.tracing.lambda);

if (tracing) {
if (typeof tracing === 'boolean' || typeof tracing === 'string') {
let mode = tracing;
let mode = tracing;

if (typeof tracing === 'boolean') {
mode = 'Active';
}
if (typeof tracing === 'boolean') {
mode = 'Active';
}

const iamRoleLambdaExecution = cfTemplate.Resources.IamRoleLambdaExecution;
const iamRoleLambdaExecution = cfTemplate.Resources.IamRoleLambdaExecution;

functionResource.Properties.TracingConfig = {
Mode: mode,
};
functionResource.Properties.TracingConfig = {
Mode: mode,
};

const stmt = {
Effect: 'Allow',
Action: ['xray:PutTraceSegments', 'xray:PutTelemetryRecords'],
Resource: ['*'],
};
const stmt = {
Effect: 'Allow',
Action: ['xray:PutTraceSegments', 'xray:PutTelemetryRecords'],
Resource: ['*'],
};

// update the PolicyDocument statements (if default policy is used)
if (iamRoleLambdaExecution) {
iamRoleLambdaExecution.Properties.Policies[0].PolicyDocument.Statement = _.unionWith(
iamRoleLambdaExecution.Properties.Policies[0].PolicyDocument.Statement,
[stmt],
_.isEqual
);
}
} else {
const errorMessage =
'tracing requires a boolean value or the "mode" provided as a string';
throw new this.serverless.classes.Error(errorMessage);
// update the PolicyDocument statements (if default policy is used)
if (iamRoleLambdaExecution) {
iamRoleLambdaExecution.Properties.Policies[0].PolicyDocument.Statement = _.unionWith(
iamRoleLambdaExecution.Properties.Policies[0].PolicyDocument.Statement,
[stmt],
_.isEqual
);
}
}

Expand All @@ -348,37 +278,12 @@ class AwsCompileFunctions {
this.serverless.service.provider.environment,
functionObject.environment
);

let invalidEnvVar = null;
Object.keys(functionResource.Properties.Environment.Variables).forEach(key => {
// taken from the bash man pages
if (!key.match(/^[A-Za-z_][a-zA-Z0-9_]*$/)) {
invalidEnvVar = `Invalid characters in environment variable ${key}`;
return false; // break loop with lodash
}
const value = functionResource.Properties.Environment.Variables[key];
if (_.isObject(value)) {
const isCFRef =
_.isObject(value) &&
!Object.keys(value).some(k => k !== 'Ref' && !k.startsWith('Fn::'));
if (!isCFRef) {
invalidEnvVar = `Environment variable ${key} must contain string`;
return false;
}
}
return true;
});

if (invalidEnvVar) throw new this.serverless.classes.Error(invalidEnvVar);
}

if ('role' in functionObject) {
this.compileRole(functionResource, functionObject.role);
} else if ('role' in this.serverless.service.provider) {
this.compileRole(functionResource, this.serverless.service.provider.role);
} else {
this.compileRole(functionResource, 'IamRoleLambdaExecution');
}
this.compileRole(
functionResource,
functionObject.role || this.serverless.service.provider.role || 'IamRoleLambdaExecution'
);

if (!functionObject.vpc) functionObject.vpc = {};
if (!this.serverless.service.provider.vpc) this.serverless.service.provider.vpc = {};
Expand Down Expand Up @@ -434,9 +339,8 @@ class AwsCompileFunctions {
}

if (functionObject.reservedConcurrency || functionObject.reservedConcurrency === 0) {
// Try convert reservedConcurrency to integer
const reservedConcurrency = Number(functionObject.reservedConcurrency);
functionResource.Properties.ReservedConcurrentExecutions = reservedConcurrency;
functionResource.Properties.ReservedConcurrentExecutions =
functionObject.reservedConcurrency;
}

if (!functionObject.disableLogs) {
Expand All @@ -445,12 +349,9 @@ class AwsCompileFunctions {
].concat(functionResource.DependsOn || []);
}

if (functionObject.layers && Array.isArray(functionObject.layers)) {
if (functionObject.layers) {
functionResource.Properties.Layers = functionObject.layers;
} else if (
this.serverless.service.provider.layers &&
Array.isArray(this.serverless.service.provider.layers)
) {
} else if (this.serverless.service.provider.layers) {
functionResource.Properties.Layers = this.serverless.service.provider.layers;
}

Expand Down Expand Up @@ -680,14 +581,6 @@ class AwsCompileFunctions {
return BbPromise.each(allFunctions, functionName => this.compileFunction(functionName));
}

// helper functions
isArnRefGetAttOrImportValue(arn) {
return (
typeof arn === 'object' &&
Object.keys(arn).some(k => ['Ref', 'Fn::GetAtt', 'Fn::ImportValue'].includes(k))
);
}

cfLambdaFunctionTemplate() {
return {
Type: 'AWS::Lambda::Function',
Expand Down

0 comments on commit feece9a

Please sign in to comment.