diff --git a/lib/classes/ConfigSchemaHandler/index.js b/lib/classes/ConfigSchemaHandler/index.js index 02cf3128d62..f5fa2d5001d 100644 --- a/lib/classes/ConfigSchemaHandler/index.js +++ b/lib/classes/ConfigSchemaHandler/index.js @@ -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); diff --git a/lib/configSchema.js b/lib/configSchema.js index 729890835cb..4fb47304729 100644 --- a/lib/configSchema.js +++ b/lib/configSchema.js @@ -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, }, @@ -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_]+$', diff --git a/lib/configSchema.test.js b/lib/configSchema.test.js index 312b2d90f0b..2cc6a631a62 100644 --- a/lib/configSchema.test.js +++ b/lib/configSchema.test.js @@ -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: { diff --git a/lib/plugins/aws/package/compile/functions/index.js b/lib/plugins/aws/package/compile/functions/index.js index 89dc9a7338e..fd007a279de 100644 --- a/lib/plugins/aws/package/compile/functions/index.js +++ b/lib/plugins/aws/package/compile/functions/index.js @@ -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; } } @@ -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; @@ -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; } } @@ -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 + ); } } @@ -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 = {}; @@ -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) { @@ -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; } @@ -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', diff --git a/lib/plugins/aws/package/compile/functions/index.test.js b/lib/plugins/aws/package/compile/functions/index.test.js index 60f7e1a0be6..e831f0600ac 100644 --- a/lib/plugins/aws/package/compile/functions/index.test.js +++ b/lib/plugins/aws/package/compile/functions/index.test.js @@ -75,21 +75,6 @@ describe('AwsCompileFunctions', () => { expect(awsCompileFunctions.provider).to.be.instanceof(AwsProvider)); }); - describe('#isArnRefGetAttOrImportValue()', () => { - it('should accept a Ref', () => - expect(awsCompileFunctions.isArnRefGetAttOrImportValue({ Ref: 'DLQ' })).to.equal(true)); - it('should accept a Fn::GetAtt', () => - expect( - awsCompileFunctions.isArnRefGetAttOrImportValue({ 'Fn::GetAtt': ['DLQ', 'Arn'] }) - ).to.equal(true)); - it('should accept a Fn::ImportValue', () => - expect( - awsCompileFunctions.isArnRefGetAttOrImportValue({ 'Fn::ImportValue': 'DLQ' }) - ).to.equal(true)); - it('should reject other objects', () => - expect(awsCompileFunctions.isArnRefGetAttOrImportValue({ Blah: 'vtha' })).to.equal(false)); - }); - describe('#downloadPackageArtifacts()', () => { let requestStub; let testFilePath; @@ -769,44 +754,6 @@ describe('AwsCompileFunctions', () => { s3FileName = awsCompileFunctions.serverless.service.package.artifact.split(path.sep).pop(); }); - it('should reject if config is provided as a number', () => { - awsCompileFunctions.serverless.service.functions = { - func: { - handler: 'func.function.handler', - name: 'new-service-dev-func', - onError: 12, - }, - }; - - return expect(awsCompileFunctions.compileFunctions()).to.be.rejectedWith(Error); - }); - - it('should reject if config is provided as an object', () => { - awsCompileFunctions.serverless.service.functions = { - func: { - handler: 'func.function.handler', - name: 'new-service-dev-func', - onError: { - foo: 'bar', - }, - }, - }; - - return expect(awsCompileFunctions.compileFunctions()).to.be.rejectedWith(Error); - }); - - it('should reject if config is not a SNS or SQS arn', () => { - awsCompileFunctions.serverless.service.functions = { - func: { - handler: 'func.function.handler', - name: 'new-service-dev-func', - onError: 'foo', - }, - }; - - return expect(awsCompileFunctions.compileFunctions()).to.be.rejectedWith(Error); - }); - describe('when IamRoleLambdaExecution is used', () => { beforeEach(() => { // pretend that the IamRoleLambdaExecution is used @@ -872,20 +819,6 @@ describe('AwsCompileFunctions', () => { }); }); - it('should throw an informative error message if a SQS arn is provided', () => { - awsCompileFunctions.serverless.service.functions = { - func: { - handler: 'func.function.handler', - name: 'new-service-dev-func', - onError: 'arn:aws:sqs:region:accountid:foo', - }, - }; - - return expect(awsCompileFunctions.compileFunctions()).to.be.rejectedWith( - 'only supports SNS' - ); - }); - it('should create necessary resources if a Ref is provided', () => { awsCompileFunctions.serverless.service.functions = { func: { @@ -1052,20 +985,6 @@ describe('AwsCompileFunctions', () => { expect(functionResource).to.deep.equal(compiledFunction); }); }); - - it('should reject with an informative error message if a SQS arn is provided', () => { - awsCompileFunctions.serverless.service.functions = { - func: { - handler: 'func.function.handler', - name: 'new-service-dev-func', - onError: 'arn:aws:sqs:region:accountid:foo', - }, - }; - - return expect(awsCompileFunctions.compileFunctions()).to.be.rejectedWith( - 'only supports SNS' - ); - }); }); }); @@ -1078,20 +997,6 @@ describe('AwsCompileFunctions', () => { s3FileName = awsCompileFunctions.serverless.service.package.artifact.split(path.sep).pop(); }); - it('should reject if config is provided as a number', () => { - awsCompileFunctions.serverless.service.functions = { - func: { - handler: 'func.function.handler', - name: 'new-service-dev-func', - awsKmsKeyArn: 12, - }, - }; - - return expect(awsCompileFunctions.compileFunctions()).to.be.rejectedWith( - 'provided as an arn string' - ); - }); - it('should allow if config is provided as a Fn::GetAtt', () => { awsCompileFunctions.serverless.service.functions = { func: { @@ -1203,34 +1108,6 @@ describe('AwsCompileFunctions', () => { }); }); - it('should reject if config is provided as an invalid object', () => { - awsCompileFunctions.serverless.service.functions = { - func: { - handler: 'func.function.handler', - name: 'new-service-dev-func', - awsKmsKeyArn: { - foo: 'bar', - }, - }, - }; - - return expect(awsCompileFunctions.compileFunctions()).to.be.rejectedWith( - 'provided as an arn string' - ); - }); - - it('should throw an error if config is not a KMS key arn', () => { - awsCompileFunctions.serverless.service.functions = { - func: { - handler: 'func.function.handler', - name: 'new-service-dev-func', - awsKmsKeyArn: 'foo', - }, - }; - - return expect(awsCompileFunctions.compileFunctions()).to.be.rejectedWith('KMS key arn'); - }); - it('should use a the service KMS key arn if provided', () => { awsCompileFunctions.serverless.service.serviceObject = { name: 'new-service', @@ -1448,18 +1325,6 @@ describe('AwsCompileFunctions', () => { s3FileName = awsCompileFunctions.serverless.service.package.artifact.split(path.sep).pop(); }); - it('should throw an error if config paramter is not a string', () => { - awsCompileFunctions.serverless.service.functions = { - func: { - handler: 'func.function.handler', - name: 'new-service-dev-func', - tracing: 123, - }, - }; - - return expect(awsCompileFunctions.compileFunctions()).to.be.rejectedWith('as a string'); - }); - it('should use a the provider wide tracing config if provided', () => { Object.assign(awsCompileFunctions.serverless.service.provider, { tracing: { @@ -1872,21 +1737,6 @@ describe('AwsCompileFunctions', () => { }); }); - it('should throw an error if environment variable has invalid name', () => { - awsCompileFunctions.serverless.service.functions = { - func: { - handler: 'func.function.handler', - name: 'new-service-dev-func', - environment: { - '1test1': 'test1', - 'test2': 'test2', - }, - }, - }; - - return expect(awsCompileFunctions.compileFunctions()).to.be.rejectedWith(Error); - }); - it('should accept an environment variable with a not-string value', () => { awsCompileFunctions.serverless.service.functions = { func: { @@ -1923,18 +1773,10 @@ describe('AwsCompileFunctions', () => { }; return expect(awsCompileFunctions.compileFunctions()).to.be.fulfilled.then(() => { - awsCompileFunctions.serverless.service.functions = { - func: { - handler: 'func.function.handler', - name: 'new-service-dev-func', - environment: { - counter: { - NotRef: 'TestVariable', - }, - }, - }, - }; - return expect(awsCompileFunctions.compileFunctions()).to.be.rejectedWith(Error); + expect( + awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate.Resources + .FuncLambdaFunction.Properties.Environment.Variables.counter + ).to.eql({ Ref: 'TestVariable' }); }); }); @@ -2464,29 +2306,6 @@ describe('AwsCompileFunctions', () => { }); }); - describe('Errors if unsupported object type is provided', () => { - it('should throw for object type { Ref: "Foo" }', () => { - const role = { Ref: 'Foo' }; - const resource = { Properties: {} }; - - expect(() => awsCompileFunctions.compileRole(resource, role)).to.throw(Error); - }); - - it('should throw for object type Buffer', () => { - const role = Buffer.from('Foo'); - const resource = { Properties: {} }; - - expect(() => awsCompileFunctions.compileRole(resource, role)).to.throw(Error); - }); - - it('should throw for object type Array', () => { - const role = [1, 2, 3]; - const resource = { Properties: {} }; - - expect(() => awsCompileFunctions.compileRole(resource, role)).to.throw(Error); - }); - }); - it('should not set unset properties when not specified in yml (layers, vpc, etc)', () => { const s3Folder = awsCompileFunctions.serverless.service.package.artifactDirectoryName; const s3FileName = awsCompileFunctions.serverless.service.package.artifact diff --git a/lib/plugins/aws/provider/awsProvider.js b/lib/plugins/aws/provider/awsProvider.js index 11f46d7a9c4..45bc0f1cb9a 100644 --- a/lib/plugins/aws/provider/awsProvider.js +++ b/lib/plugins/aws/provider/awsProvider.js @@ -222,21 +222,103 @@ class AwsProvider { required: ['Fn::Sub'], additionalProperties: false, }, + awsLambdaEnvironment: { + type: 'object', + patternProperties: { + '^[A-Za-z_][a-zA-Z0-9_]*$': { + anyOf: [ + { type: 'string' }, + { $ref: '#/definitions/awsCfImport' }, + { $ref: '#/definitions/awsCfRef' }, + ], + }, + }, + additionalProperties: false, + }, + awsLambdaLayers: { + type: 'array', + items: { $ref: '#/definitions/awsArn' }, + }, + awsLambdaMemorySize: { type: 'integer', multipleOf: 64, minimum: 128, maximum: 3008 }, + awsLambdaRole: { + anyOf: [ + { type: 'string', minLength: 1 }, + { $ref: '#/definitions/awsCfImport' }, + { $ref: '#/definitions/awsCfGetAtt' }, + ], + }, + awsLambdaRuntime: { + enum: [ + 'dotnetcore2.1', + 'dotnetcore3.1', + 'go1.x', + 'java11', + 'java8', + 'java8.al2', + 'nodejs10.x', + 'nodejs12.x', + 'provided', + 'provided.al2', + 'python2.7', + 'python3.6', + 'python3.7', + 'python3.8', + 'ruby2.5', + 'ruby2.7', + ], + }, + awsLambdaTimeout: { type: 'integer', minimum: 1, maximum: 900 }, + awsLambdaTracing: { anyOf: [{ enum: ['Active', 'PassThrough'] }, { type: 'boolean' }] }, + awsLambdaVersionning: { type: 'boolean' }, + awsLambdaVpcConfig: { + type: 'object', + properties: { + securityGroupIds: { + type: 'array', + items: { + type: 'string', + }, + maxItems: 5, + }, + subnetIds: { + type: 'array', + items: { + type: 'string', + }, + maxItems: 16, + }, + }, + }, + awsResourceCondition: { type: 'string' }, + awsResourceDependsOn: { + oneOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' } }], + }, awsResourceProperties: { Properties: { type: 'object' }, CreationPolicy: { type: 'object' }, DeletionPolicy: { type: 'string' }, - DependsOn: { - oneOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' } }], - }, + DependsOn: { $ref: '#/definitions/awsResourceDependsOn' }, Metadata: { type: 'object' }, UpdatePolicy: { type: 'object' }, UpdateReplacePolicy: { type: 'string' }, - Condition: { type: 'string' }, + Condition: { $ref: '#/definitions/awsResourceCondition' }, + }, + awsResourceTags: { + type: 'object', + patternProperties: { + '^(?!aws:)[\\w./=+-]{1,128}$': { + type: 'string', + pattern: '^(?!aws:)[\\w./=+-]*$', + maxLength: 256, + }, + }, + additionalProperties: false, }, }, provider: { properties: { + apiGateway: { type: 'object', properties: { websocketApiId: { type: 'string' } } }, + environment: { $ref: '#/definitions/awsLambdaEnvironment' }, httpApi: { type: 'object', properties: { @@ -299,6 +381,7 @@ class AwsProvider { }, additionalProperties: false, }, + layers: { $ref: '#/definitions/awsLambdaLayers' }, logs: { type: 'object', properties: { @@ -328,20 +411,39 @@ class AwsProvider { }, }, }, + memorySize: { $ref: '#/definitions/awsLambdaMemorySize' }, resourcePolicy: { type: 'array', items: { type: 'object', }, }, + role: { $ref: '#/definitions/awsLambdaRole' }, + runtime: { $ref: '#/definitions/awsLambdaRuntime' }, + tags: { $ref: '#/definitions/awsResourceTags' }, + timeout: { $ref: '#/definitions/awsLambdaTimeout' }, + tracing: { + type: 'object', + properties: { + apiGateway: { type: 'boolean' }, + lambda: { $ref: '#/definitions/awsLambdaTracing' }, + }, + additionalProperties: false, + }, + vpc: { $ref: '#/definitions/awsLambdaVpcConfig' }, + versionFunctions: { $ref: '#/definitions/awsLambdaVersionning' }, websocketsApiName: { type: 'string' }, websocketsApiRouteSelectionExpression: { type: 'string' }, - apiGateway: { type: 'object', properties: { websocketApiId: { type: 'string' } } }, }, }, function: { - // TODO: Complete schema, see https://github.com/serverless/serverless/issues/8017 properties: { + awsKmsKeyArn: { $ref: '#/definitions/awsKmsArn' }, + condition: { $ref: '#/definitions/awsResourceCondition' }, + dependsOn: { $ref: '#/definitions/awsResourceDependsOn' }, + description: { type: 'string', maxLength: 256 }, + disableLogs: { type: 'boolean' }, + environment: { $ref: '#/definitions/awsLambdaEnvironment' }, fileSystemConfig: { type: 'object', properties: { @@ -363,7 +465,35 @@ class AwsProvider { required: ['localMountPath', 'arn'], }, handler: { type: 'string' }, + layers: { $ref: '#/definitions/awsLambdaLayers' }, + memorySize: { $ref: '#/definitions/awsLambdaMemorySize' }, + onError: { + oneOf: [ + { type: 'string', pattern: '^arn:aws[a-z-]*:sns' }, + { $ref: '#/definitions/awsCfFunction' }, + ], + }, + package: { + type: 'object', + properties: { + artifact: { type: 'string' }, + exclude: { type: 'array', items: { type: 'string' } }, + include: { type: 'array', items: { type: 'string' } }, + individually: { type: 'boolean' }, + }, + additionalProperties: false, + }, + provisionedConcurrency: { type: 'integer', minimum: 1 }, + reservedConcurrency: { type: 'integer', minimum: 0 }, + role: { $ref: '#/definitions/awsLambdaRole' }, + runtime: { $ref: '#/definitions/awsLambdaRuntime' }, + tags: { $ref: '#/definitions/awsResourceTags' }, + timeout: { $ref: '#/definitions/awsLambdaTimeout' }, + tracing: { $ref: '#/definitions/awsLambdaTracing' }, + versionFunction: { $ref: '#/definitions/awsLambdaVersionning' }, + vpc: { $ref: '#/definitions/awsLambdaVpcConfig' }, }, + additionalProperties: false, }, resources: { properties: {