diff --git a/lib/plugins/aws/deploy/lib/createStack.js b/lib/plugins/aws/deploy/lib/createStack.js index 9142c6df812..54512a0a70c 100644 --- a/lib/plugins/aws/deploy/lib/createStack.js +++ b/lib/plugins/aws/deploy/lib/createStack.js @@ -11,7 +11,7 @@ module.exports = { let stackTags = { STAGE: this.provider.getStage() }; // Merge additional stack tags - if (typeof this.serverless.service.provider.stackTags === 'object') { + if (this.serverless.service.provider.stackTags) { const customKeys = Object.keys(this.serverless.service.provider.stackTags); const collisions = Object.keys(stackTags).filter(defaultKey => customKeys.some(key => defaultKey.toLowerCase() === key.toLowerCase()) diff --git a/lib/plugins/aws/lib/updateStack.js b/lib/plugins/aws/lib/updateStack.js index 3c3ec1e1258..79f83399dd7 100644 --- a/lib/plugins/aws/lib/updateStack.js +++ b/lib/plugins/aws/lib/updateStack.js @@ -17,7 +17,7 @@ module.exports = { const templateUrl = `https://${s3Endpoint}/${this.bucketName}/${this.serverless.service.package.artifactDirectoryName}/${compiledTemplateFileName}`; // Merge additional stack tags - if (typeof this.serverless.service.provider.stackTags === 'object') { + if (this.serverless.service.provider.stackTags) { const customKeys = Object.keys(this.serverless.service.provider.stackTags); const collisions = Object.keys(stackTags).filter(defaultKey => customKeys.some(key => defaultKey.toLowerCase() === key.toLowerCase()) @@ -74,7 +74,7 @@ module.exports = { let stackTags = { STAGE: this.provider.getStage() }; // Merge additional stack tags - if (typeof this.serverless.service.provider.stackTags === 'object') { + if (this.serverless.service.provider.stackTags) { const customKeys = Object.keys(this.serverless.service.provider.stackTags); const collisions = Object.keys(stackTags).filter(defaultKey => customKeys.some(key => defaultKey.toLowerCase() === key.toLowerCase()) diff --git a/lib/plugins/aws/lib/validateS3BucketName.js b/lib/plugins/aws/lib/validateS3BucketName.js deleted file mode 100644 index ce348323293..00000000000 --- a/lib/plugins/aws/lib/validateS3BucketName.js +++ /dev/null @@ -1,45 +0,0 @@ -'use strict'; - -const BbPromise = require('bluebird'); - -module.exports = { - /** - * Retrieved 9/27/2016 from http://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html - * Bucket names must be at least 3 and no more than 63 characters long. - * Bucket names must be a series of one or more labels. - * Adjacent labels are separated by a single period (.). - * Bucket names can contain lowercase letters, numbers, and hyphens. - * Each label must start and end with a lowercase letter or a number. - * Bucket names must not be formatted as an IP address (e.g., 192.168.5.4). - * @param bucketName - */ - validateS3BucketName(bucketName) { - return BbPromise.resolve().then(() => { - let error; - if (!bucketName) { - error = 'Bucket name cannot be undefined or empty'; - } else if (bucketName.length < 3) { - error = `Bucket name is shorter than 3 characters. ${bucketName}`; - } else if (bucketName.length > 63) { - error = `Bucket name is longer than 63 characters. ${bucketName}`; - } else if (/[A-Z]/.test(bucketName)) { - error = `Bucket name cannot contain uppercase letters. ${bucketName}`; - } else if (/^[^a-z0-9]/.test(bucketName)) { - error = `Bucket name must start with a letter or number. ${bucketName}`; - } else if (/[^a-z0-9]$/.test(bucketName)) { - error = `Bucket name must end with a letter or number. ${bucketName}`; - } else if (!/^[a-z0-9][a-z.0-9-]+[a-z0-9]$/.test(bucketName)) { - error = `Bucket name contains invalid characters, [a-z.0-9-] ${bucketName}`; - } else if (/\.{2,}/.test(bucketName)) { - error = `Bucket name cannot contain consecutive periods (.) ${bucketName}`; - } else if (/^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/.test(bucketName)) { - error = `Bucket name cannot look like an IPv4 address. ${bucketName}`; - } - - if (error) { - throw new this.serverless.classes.Error(error); - } - return true; - }); - }, -}; diff --git a/lib/plugins/aws/lib/validateS3BucketName.test.js b/lib/plugins/aws/lib/validateS3BucketName.test.js deleted file mode 100644 index c33498a6b89..00000000000 --- a/lib/plugins/aws/lib/validateS3BucketName.test.js +++ /dev/null @@ -1,114 +0,0 @@ -'use strict'; - -const expect = require('chai').expect; -const Serverless = require('../../../Serverless'); -const validateS3BucketName = require('./validateS3BucketName'); - -describe('#validateS3BucketName()', () => { - const serverless = new Serverless(); - const awsPlugin = {}; - - beforeEach(() => { - awsPlugin.serverless = serverless; - awsPlugin.options = { - stage: 'dev', - region: 'us-east-1', - }; - - awsPlugin.serverless.config.servicePath = true; - - Object.assign(awsPlugin, validateS3BucketName); - }); - - describe('#validateS3BucketName()', () => { - it('should reject an ip address as a name', () => - awsPlugin - .validateS3BucketName('127.0.0.1') - .then(() => { - throw new Error('Should not get here'); - }) - .catch(err => expect(err.message).to.contain('cannot look like an IPv4 address'))); - - it('should reject names that are too long', () => { - const bucketName = Array.from({ length: 64 }, () => 'j').join(''); - return awsPlugin - .validateS3BucketName(bucketName) - .then(() => { - throw new Error('Should not get here'); - }) - .catch(err => expect(err.message).to.contain('longer than 63 characters')); - }); - - it('should reject names that are too short', () => - awsPlugin - .validateS3BucketName('12') - .then(() => { - throw new Error('Should not get here'); - }) - .catch(err => expect(err.message).to.contain('shorter than 3 characters'))); - - it('should reject names that contain invalid characters', () => - awsPlugin - .validateS3BucketName('this has b@d characters') - .then(() => { - throw new Error('Should not get here'); - }) - .catch(err => expect(err.message).to.contain('contains invalid characters'))); - - it('should reject names that have consecutive periods', () => - awsPlugin - .validateS3BucketName('otherwise..valid.name') - .then(() => { - throw new Error('Should not get here'); - }) - .catch(err => expect(err.message).to.contain('cannot contain consecutive periods'))); - - it('should reject names that start with a dash', () => - awsPlugin - .validateS3BucketName('-invalid.name') - .then(() => { - throw new Error('Should not get here'); - }) - .catch(err => expect(err.message).to.contain('start with a letter or number'))); - - it('should reject names that start with a period', () => - awsPlugin - .validateS3BucketName('.invalid.name') - .then(() => { - throw new Error('Should not get here'); - }) - .catch(err => expect(err.message).to.contain('start with a letter or number'))); - - it('should reject names that end with a dash', () => - awsPlugin - .validateS3BucketName('invalid.name-') - .then(() => { - throw new Error('Should not get here'); - }) - .catch(err => expect(err.message).to.contain('end with a letter or number'))); - - it('should reject names that end with a period', () => - awsPlugin - .validateS3BucketName('invalid.name.') - .then(() => { - throw new Error('Should not get here'); - }) - .catch(err => expect(err.message).to.contain('end with a letter or number'))); - - it('should reject names that contain uppercase letters', () => - awsPlugin - .validateS3BucketName('otherwise.Valid.name') - .then(() => { - throw new Error('Should not get here'); - }) - .catch(err => expect(err.message).to.contain('cannot contain uppercase letters'))); - - it('should accept valid names', () => - awsPlugin - .validateS3BucketName('1.this.is.valid.2') - .then(() => awsPlugin.validateS3BucketName('another.valid.name')) - .then(() => awsPlugin.validateS3BucketName('1-2-3')) - .then(() => awsPlugin.validateS3BucketName('123')) - .then(() => awsPlugin.validateS3BucketName('should.be.allowed-to-mix'))); - }); -}); diff --git a/lib/plugins/aws/package/lib/generateCoreTemplate.js b/lib/plugins/aws/package/lib/generateCoreTemplate.js index 4dbb90a8f8e..1346cabd3f4 100644 --- a/lib/plugins/aws/package/lib/generateCoreTemplate.js +++ b/lib/plugins/aws/package/lib/generateCoreTemplate.js @@ -4,12 +4,8 @@ const BbPromise = require('bluebird'); const path = require('path'); const _ = require('lodash'); -const validateS3BucketName = require('../../lib/validateS3BucketName'); - module.exports = { generateCoreTemplate() { - Object.assign(this, validateS3BucketName); - this.serverless.service.provider.compiledCloudFormationTemplate = this.serverless.utils.readFileSync( path.join( this.serverless.config.serverlessPath, @@ -47,7 +43,7 @@ module.exports = { } // enable S3 block public access for deployment bucket - if (deploymentBucketObject.blockPublicAccess === true) { + if (deploymentBucketObject.blockPublicAccess) { Object.assign( this.serverless.service.provider.compiledCloudFormationTemplate.Resources[ deploymentBucketLogicalId @@ -76,23 +72,21 @@ module.exports = { } if (bucketName) { - return BbPromise.bind(this) - .then(() => this.validateS3BucketName(bucketName)) - .then(() => { - if (isS3TransferAccelerationEnabled) { - const warningMessage = - 'Warning: S3 Transfer Acceleration will not be enabled on deploymentBucket.'; - this.serverless.cli.log(warningMessage); - } - this.bucketName = bucketName; - this.serverless.service.package.deploymentBucket = bucketName; - this.serverless.service.provider.compiledCloudFormationTemplate.Outputs.ServerlessDeploymentBucketName.Value = bucketName; - - delete this.serverless.service.provider.compiledCloudFormationTemplate.Resources - .ServerlessDeploymentBucket; - delete this.serverless.service.provider.compiledCloudFormationTemplate.Resources - .ServerlessDeploymentBucketPolicy; - }); + return BbPromise.bind(this).then(() => { + if (isS3TransferAccelerationEnabled) { + const warningMessage = + 'Warning: S3 Transfer Acceleration will not be enabled on deploymentBucket.'; + this.serverless.cli.log(warningMessage); + } + this.bucketName = bucketName; + this.serverless.service.package.deploymentBucket = bucketName; + this.serverless.service.provider.compiledCloudFormationTemplate.Outputs.ServerlessDeploymentBucketName.Value = bucketName; + + delete this.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ServerlessDeploymentBucket; + delete this.serverless.service.provider.compiledCloudFormationTemplate.Resources + .ServerlessDeploymentBucketPolicy; + }); } if (isS3TransferAccelerationEnabled && isS3TransferAccelerationSupported) { diff --git a/lib/plugins/aws/package/lib/mergeIamTemplates.js b/lib/plugins/aws/package/lib/mergeIamTemplates.js index 7ed53ab2ede..f3c32071d5c 100644 --- a/lib/plugins/aws/package/lib/mergeIamTemplates.js +++ b/lib/plugins/aws/package/lib/mergeIamTemplates.js @@ -6,12 +6,6 @@ const path = require('path'); module.exports = { mergeIamTemplates() { - this.validateStatements(this.serverless.service.provider.iamRoleStatements); - this.validateManagedPolicies(this.serverless.service.provider.iamManagedPolicies); - return this.merge(); - }, - - merge() { // resolve early if no functions are provided if (!this.serverless.service.getAllFunctions().length) { return BbPromise.resolve(); @@ -188,52 +182,4 @@ module.exports = { } resource.ManagedPolicyArns = resource.ManagedPolicyArns.concat(managedPolicies); }, - - validateStatements(statements) { - // Verify that iamRoleStatements (if present) is an array of { Effect: ..., - // Action: ..., Resource: ... } objects. - if (!statements) { - return; - } - let violationsFound; - if (!Array.isArray(statements)) { - violationsFound = 'it is not an array'; - } else { - const descriptions = statements.map((statement, i) => { - const missing = [ - ['Effect'], - ['Action', 'NotAction'], - ['Resource', 'NotResource'], - ].filter(props => props.every(prop => !statement[prop])); - return missing.length === 0 - ? null - : `statement ${i} is missing the following properties: ${missing - .map(m => m.join(' / ')) - .join(', ')}`; - }); - const flawed = descriptions.filter(curr => curr); - if (flawed.length) { - violationsFound = flawed.join('; '); - } - } - - if (violationsFound) { - const errorMessage = [ - 'iamRoleStatements should be an array of objects,', - ' where each object has Effect, Action / NotAction, Resource / NotResource fields.', - ` Specifically, ${violationsFound}`, - ].join(''); - throw new this.serverless.classes.Error(errorMessage); - } - }, - - validateManagedPolicies(iamManagedPolicies) { - // Verify that iamManagedPolicies (if present) is an array - if (!iamManagedPolicies) { - return; - } - if (!Array.isArray(iamManagedPolicies)) { - throw new this.serverless.classes.Error('iamManagedPolicies should be an array of arns'); - } - }, }; diff --git a/lib/plugins/aws/package/lib/mergeIamTemplates.test.js b/lib/plugins/aws/package/lib/mergeIamTemplates.test.js index 98effaf516b..18c63185675 100644 --- a/lib/plugins/aws/package/lib/mergeIamTemplates.test.js +++ b/lib/plugins/aws/package/lib/mergeIamTemplates.test.js @@ -489,47 +489,6 @@ describe('#mergeIamTemplates()', () => { }); }); - it('should throw error if custom IAM policy statements is not an array', () => { - awsPackage.serverless.service.provider.iamRoleStatements = { - policy: 'some_value', - statments: [ - { - Effect: 'Allow', - Action: ['something:SomethingElse'], - Resource: 'some:aws:arn:xxx:*:*', - }, - ], - }; - - expect(() => awsPackage.mergeIamTemplates()).to.throw('not an array'); - }); - - it('should throw error if a custom IAM policy statement does not have an Effect field', () => { - awsPackage.serverless.service.provider.iamRoleStatements = [ - { - Action: ['something:SomethingElse'], - Resource: '*', - }, - ]; - - expect(() => awsPackage.mergeIamTemplates()).to.throw( - 'missing the following properties: Effect' - ); - }); - - it('should throw error if a custom IAM policy statement does not have an Action field', () => { - awsPackage.serverless.service.provider.iamRoleStatements = [ - { - Effect: 'Allow', - Resource: '*', - }, - ]; - - expect(() => awsPackage.mergeIamTemplates()).to.throw( - 'missing the following properties: Action / NotAction' - ); - }); - it('should not throw error if a custom IAM policy statement has a NotAction field', () => { awsPackage.serverless.service.provider.iamRoleStatements = [ { @@ -548,19 +507,6 @@ describe('#mergeIamTemplates()', () => { }); }); - it('should throw error if a custom IAM policy statement does not have a Resource field', () => { - awsPackage.serverless.service.provider.iamRoleStatements = [ - { - Action: ['something:SomethingElse'], - Effect: 'Allow', - }, - ]; - - expect(() => awsPackage.mergeIamTemplates()).to.throw( - 'missing the following properties: Resource / NotResource' - ); - }); - it('should not throw error if a custom IAM policy statement has a NotResource field', () => { awsPackage.serverless.service.provider.iamRoleStatements = [ { @@ -579,62 +525,22 @@ describe('#mergeIamTemplates()', () => { }); }); - it('should throw an error describing all problematics custom IAM policy statements', () => { - awsPackage.serverless.service.provider.iamRoleStatements = [ - { - Action: ['something:SomethingElse'], - Effect: 'Allow', - }, - { - Action: ['something:SomethingElse'], - Resource: '*', - Effect: 'Allow', - }, - { - Resource: '*', - }, - ]; - - expect(() => awsPackage.mergeIamTemplates()).to.throw( - /statement 0 is missing.*Resource \/ NotResource; statement 2 is missing.*Effect, Action \/ NotAction/ - ); - }); - - it('should throw error if managed policies is not an array', () => { - awsPackage.serverless.service.provider.iamManagedPolicies = 'a string'; - expect(() => awsPackage.mergeIamTemplates()).to.throw( - 'iamManagedPolicies should be an array of arns' - ); - }); - - it('should add RetentionInDays to a CloudWatch LogGroup resource if logRetentionInDays is given', () => - Promise.all( - [5, '5'].map(logRetentionInDays => { - awsPackage.serverless.service.provider.logRetentionInDays = logRetentionInDays; - const normalizedName = awsPackage.provider.naming.getLogGroupLogicalId(functionName); - return awsPackage.mergeIamTemplates().then(() => { - expect( - awsPackage.serverless.service.provider.compiledCloudFormationTemplate.Resources[ - normalizedName - ] - ).to.deep.equal({ - Type: 'AWS::Logs::LogGroup', - Properties: { - LogGroupName: awsPackage.provider.naming.getLogGroupName(resolvedFunctionName), - RetentionInDays: 5, - }, - }); - }); - }) - )); - - it('should throw error if RetentionInDays is 0 or not an integer', () => { - awsPackage.serverless.service.provider.logRetentionInDays = 'string'; - expect(() => awsPackage.mergeIamTemplates()).to.throw('should be an integer'); - awsPackage.serverless.service.provider.logRetentionInDays = []; - expect(() => awsPackage.mergeIamTemplates()).to.throw('should be an integer'); - awsPackage.serverless.service.provider.logRetentionInDays = {}; - expect(() => awsPackage.mergeIamTemplates()).to.throw('should be an integer'); + it('should add RetentionInDays to a CloudWatch LogGroup resource if logRetentionInDays is given', () => { + awsPackage.serverless.service.provider.logRetentionInDays = 5; + const normalizedName = awsPackage.provider.naming.getLogGroupLogicalId(functionName); + return awsPackage.mergeIamTemplates().then(() => { + expect( + awsPackage.serverless.service.provider.compiledCloudFormationTemplate.Resources[ + normalizedName + ] + ).to.deep.equal({ + Type: 'AWS::Logs::LogGroup', + Properties: { + LogGroupName: awsPackage.provider.naming.getLogGroupName(resolvedFunctionName), + RetentionInDays: 5, + }, + }); + }); }); it('should add a CloudWatch LogGroup resource if all functions use custom roles', () => { diff --git a/lib/plugins/aws/provider/awsProvider.js b/lib/plugins/aws/provider/awsProvider.js index 73148fe2e15..2eb481ea271 100644 --- a/lib/plugins/aws/provider/awsProvider.js +++ b/lib/plugins/aws/provider/awsProvider.js @@ -282,6 +282,67 @@ class AwsProvider { required: ['Fn::Sub'], additionalProperties: false, }, + awsIamPolicyAction: { + anyOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' } }], + }, + awsIamPolicyPrincipal: { + anyOf: [ + { const: '*' }, + { + type: 'object', + properties: { + AWS: { + anyOf: [ + { const: '*' }, + { $ref: '#/definitions/awsArn' }, + { type: 'array', items: { $ref: '#/definitions/awsArn' } }, + ], + }, + Federated: { + anyOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' } }], + }, + Service: { + anyOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' } }], + }, + CanonicalUser: { + anyOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' } }], + }, + }, + additionalProperties: false, + }, + ], + }, + awsIamPolicyResource: { + anyOf: [ + { const: '*' }, + { $ref: '#/definitions/awsArn' }, + { type: 'array', items: { $ref: '#/definitions/awsArn' } }, + ], + }, + // Definition of Statement taken from https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_grammar.html#policies-grammar-bnf + awsIamPolicyStatements: { + type: 'array', + items: { + type: 'object', + properties: { + Sid: { type: 'string' }, + Effect: { enum: ['Allow', 'Deny'] }, + Action: { $ref: '#/definitions/awsIamPolicyAction' }, + NotAction: { $ref: '#/definitions/awsIamPolicyAction' }, + Principal: { $ref: '#/definitions/awsIamPolicyPrincipal' }, + NotPrincipal: { $ref: '#/definitions/awsIamPolicyPrincipal' }, + Resource: { $ref: '#/definitions/awsIamPolicyResource' }, + NotResource: { $ref: '#/definitions/awsIamPolicyResource' }, + Condition: { type: 'object' }, + }, + additionalProperties: false, + allOf: [ + { required: ['Effect'] }, + { oneOf: [{ required: ['Action'] }, { required: ['NotAction'] }] }, + { oneOf: [{ required: ['Resource'] }, { required: ['NotResource'] }] }, + ], + }, + }, awsLambdaEnvironment: { type: 'object', patternProperties: { @@ -339,6 +400,10 @@ class AwsProvider { }, }, }, + awsLogGroupName: { + type: 'string', + pattern: '^[/#A-Za-z0-9-_.]+$', + }, awsResourceCondition: { type: 'string' }, awsResourceDependsOn: { oneOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' } }], @@ -364,9 +429,13 @@ class AwsProvider { }, additionalProperties: false, }, - awsLogGroupName: { + awsS3BucketName: { type: 'string', - pattern: '^[/#A-Za-z0-9-_.]+$', + // pattern sourced from https://stackoverflow.com/questions/50480924/regex-for-s3-bucket-name + pattern: + '(?!^(\\d{1,3}\\.){3}\\d{1,3}$)(^(([a-z0-9]|[a-z0-9][a-z0-9-]*[a-z0-9])\\.)*([a-z0-9]|[a-z0-9][a-z0-9-]*[a-z0-9])$)', + minLength: 3, + maxLength: 63, }, }, provider: { @@ -385,6 +454,28 @@ class AwsProvider { }, additionalProperties: false, }, + cfnRole: { $ref: '#/definitions/awsArn' }, + deploymentBucket: { + anyOf: [ + { $ref: '#/definitions/awsS3BucketName' }, + { + type: 'object', + properties: { + name: { $ref: '#/definitions/awsS3BucketName' }, + maxPreviousDeploymentArtifacts: { type: 'integer', minimum: 0 }, + serverSideEncryption: { enum: ['AES256', 'aws:kms'] }, + sseCustomerAlgorithim: { type: 'string' }, + sseCustomerKey: { type: 'string' }, + sseCustomerKeyMD5: { type: 'string' }, + sseKMSKeyId: { type: 'string' }, + tags: { $ref: '#/definitions/awsResourceTags' }, + blockPublicAccess: { type: 'boolean' }, + }, + additionalProperties: false, + }, + ], + }, + deploymentPrefix: { type: 'string' }, environment: { $ref: '#/definitions/awsLambdaEnvironment' }, httpApi: { type: 'object', @@ -448,11 +539,17 @@ class AwsProvider { }, additionalProperties: false, }, + iamManagedPolicies: { type: 'array', items: { $ref: '#/definitions/awsArnString' } }, + iamRoleStatements: { $ref: '#/definitions/awsIamPolicyStatements' }, kmsKeyArn: { $ref: '#/definitions/awsKmsArn' }, layers: { $ref: '#/definitions/awsLambdaLayers' }, + logRetentionInDays: { + enum: [1, 3, 5, 7, 14, 30, 60, 90, 120, 150, 180, 365, 400, 545, 731, 1827, 3653], + }, logs: { type: 'object', properties: { + frameworkLambda: { type: 'boolean' }, httpApi: { oneOf: [ { type: 'boolean' }, @@ -498,14 +595,81 @@ class AwsProvider { }, }, memorySize: { $ref: '#/definitions/awsLambdaMemorySize' }, - resourcePolicy: { + notificationArns: { type: 'array', items: { $ref: '#/definitions/awsArnString' } }, + profile: { type: 'string' }, + region: { + enum: [ + 'us-east-1', + 'us-east-2', + 'us-gov-east-1', + 'us-gov-west-1', + 'us-west-1', + 'us-west-2', + 'af-south-1', + 'ap-east-1', + 'ap-northeast-1', + 'ap-northeast-2', + 'ap-northeast-3', + 'ap-south-1', + 'ap-southeast-1', + 'ap-southeast-2', + 'ca-central-1', + 'cn-north-1', + 'cn-northwest-1', + 'eu-central-1', + 'eu-north-1', + 'eu-south-1', + 'eu-west-1', + 'eu-west-2', + 'eu-west-3', + 'me-south-1', + 'sa-east-1', + ], + }, + resourcePolicy: { $ref: '#/definitions/awsIamPolicyStatements' }, + role: { $ref: '#/definitions/awsLambdaRole' }, + rolePermissionsBoundary: { $ref: '#/definitions/awsArnString' }, + rollbackConfiguration: { + type: 'object', + properties: { + RollbackTriggers: { + type: 'array', + items: { + type: 'object', + properties: { + Arn: { $ref: '#/definitions/awsArnString' }, + Type: { const: 'AWS::CloudWatch::Alarm' }, + }, + additionalProperties: false, + required: ['Arn', 'Type'], + }, + }, + MonitoringTimeInMinutes: { type: 'integer', minimum: 0 }, + }, + additionalProperties: false, + }, + runtime: { $ref: '#/definitions/awsLambdaRuntime' }, + stage: { type: 'string' }, + stackName: { + type: 'string', + pattern: '^[a-zA-Z][a-zA-Z0-9-]*$', + maxLength: 128, + }, + stackParameters: { type: 'array', items: { type: 'object', + properties: { + ParameterKey: { type: 'string' }, + ParameterValue: { type: 'string' }, + UsePreviousValue: { type: 'boolean' }, + ResolvedValue: { type: 'string' }, + }, + additionalProperties: false, }, }, - role: { $ref: '#/definitions/awsLambdaRole' }, - runtime: { $ref: '#/definitions/awsLambdaRuntime' }, + stackPolicy: { $ref: '#/definitions/awsIamPolicyStatements' }, + stackTags: { $ref: '#/definitions/awsResourceTags' }, tags: { $ref: '#/definitions/awsResourceTags' }, timeout: { $ref: '#/definitions/awsLambdaTimeout' }, tracing: { @@ -1095,17 +1259,7 @@ class AwsProvider { } getLogRetentionInDays() { - if (!this.serverless.service.provider.logRetentionInDays) { - return null; - } - const rawRetentionInDays = this.serverless.service.provider.logRetentionInDays; - const retentionInDays = parseInt(rawRetentionInDays, 10); - if (retentionInDays > 0) { - return retentionInDays; - } - - const errorMessage = `logRetentionInDays should be an integer over 0 but ${rawRetentionInDays}`; - throw new this.serverless.classes.Error(errorMessage); + return this.serverless.service.provider.logRetentionInDays; } getStageSourceValue() {