Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add base definition of properties #8222

Merged
merged 22 commits into from Sep 21, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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