diff --git a/docs/providers/aws/guide/serverless.yml.md b/docs/providers/aws/guide/serverless.yml.md index c48c0f6d4..39c9f8357 100644 --- a/docs/providers/aws/guide/serverless.yml.md +++ b/docs/providers/aws/guide/serverless.yml.md @@ -37,6 +37,11 @@ provider: deploymentBucket: name: com.serverless.${self:provider.region}.deploys # Deployment bucket name. Default is generated by the framework serverSideEncryption: AES256 # when using server-side encryption + tags: # Tags that will be added to each of the deployment resources + - Key: "key1" + Value: "value1" + - Key: "key2" + Value: "value2" role: arn:aws:iam::XXXXXX:role/role # Overwrite the default IAM role which is used for all functions cfnRole: arn:aws:iam::XXXXXX:role/role # ARN of an IAM role for CloudFormation service. If specified, CloudFormation uses the role's credentials versionFunctions: false # Optional function versioning diff --git a/docs/providers/aws/guide/services.md b/docs/providers/aws/guide/services.md index f477df5a0..932cbe49a 100644 --- a/docs/providers/aws/guide/services.md +++ b/docs/providers/aws/guide/services.md @@ -107,6 +107,11 @@ provider: deploymentBucket: name: com.serverless.${self:provider.region}.deploys # Overwrite the default deployment bucket serverSideEncryption: AES256 # when using server-side encryption + tags: # Tags that will be added to each of the deployment resources + - Key: "key1" + Value: "value1" + - Key: "key2" + Value: "value2" versionFunctions: false # Optional function versioning stackTags: # Optional CF stack tags key: value diff --git a/lib/plugins/aws/deploy/lib/uploadArtifacts.js b/lib/plugins/aws/deploy/lib/uploadArtifacts.js index ed9d2e100..5d3d8a5fa 100644 --- a/lib/plugins/aws/deploy/lib/uploadArtifacts.js +++ b/lib/plugins/aws/deploy/lib/uploadArtifacts.js @@ -44,6 +44,7 @@ module.exports = { const deploymentBucketObject = this.serverless.service.provider.deploymentBucketObject; if (deploymentBucketObject) { params = setServersideEncryptionOptions(params, deploymentBucketObject); + params = createTags(params, deploymentBucketObject); } return this.provider.request('S3', @@ -71,6 +72,7 @@ module.exports = { const deploymentBucketObject = this.serverless.service.provider.deploymentBucketObject; if (deploymentBucketObject) { params = setServersideEncryptionOptions(params, deploymentBucketObject); + params = createTags(params, deploymentBucketObject); } return this.provider.request('S3', @@ -131,3 +133,18 @@ function setServersideEncryptionOptions(putParams, deploymentBucketOptions) { return params; } + +function createTags(putParams, deploymentBucketObject) { + const params = putParams; + let taggingString = ''; + + if (deploymentBucketObject.tags) { + for (const tag of deploymentBucketObject.tags) { + taggingString += `${encodeURIComponent(tag.Key)}=${encodeURIComponent(tag.Value)}&`; + } + taggingString = taggingString.substring(0, taggingString.length - 1); + params.Tagging = taggingString; + } + + return params; +} diff --git a/lib/plugins/aws/deploy/lib/uploadArtifacts.test.js b/lib/plugins/aws/deploy/lib/uploadArtifacts.test.js index a54249fe6..7f7c15440 100644 --- a/lib/plugins/aws/deploy/lib/uploadArtifacts.test.js +++ b/lib/plugins/aws/deploy/lib/uploadArtifacts.test.js @@ -143,6 +143,42 @@ describe('uploadArtifacts', () => { expect(normalizeCloudFormationTemplateStub).to.have.been.calledWithExactly({ foo: 'bar' }); }); }); + + it('should upload the CloudFormation file to the S3 bucket and add tags to resource', () => { + cryptoStub.createHash().update().digest.onCall(0).returns('local-hash-cf-template'); + awsDeploy.serverless.service.provider.deploymentBucketObject = { + tags: [ + { + Key: 'key1', + Value: 'value1', + }, + { + Key: 'key2', + Value: 'value with a space', + }, + ], + }; + + return awsDeploy.uploadCloudFormationFile().then(() => { + expect(normalizeCloudFormationTemplateStub).to.have.been.calledOnce; + expect(uploadStub).to.have.been.calledOnce; + expect(uploadStub).to.have.been.calledWithExactly( + 'S3', + 'upload', + { + Bucket: awsDeploy.bucketName, + Key: `${awsDeploy.serverless.service.package + .artifactDirectoryName}/compiled-cloudformation-template.json`, + Body: JSON.stringify({ foo: 'bar' }), + ContentType: 'application/json', + Tagging: 'key1=value1&key2=value%20with%20a%20space', + Metadata: { + filesha256: 'local-hash-cf-template', + }, + }); + expect(normalizeCloudFormationTemplateStub).to.have.been.calledWithExactly({ foo: 'bar' }); + }); + }); }); describe('#uploadZipFile()', () => { @@ -222,6 +258,45 @@ describe('uploadArtifacts', () => { expect(readFileSyncStub).to.have.been.calledWithExactly(artifactFilePath); }); }); + + it('should upload the .zip file to the S3 bucket and add tags', () => { + cryptoStub.createHash().update().digest.onCall(0).returns('local-hash-zip-file'); + + const tmpDirPath = testUtils.getTmpDirPath(); + const artifactFilePath = path.join(tmpDirPath, 'artifact.zip'); + serverless.utils.writeFileSync(artifactFilePath, 'artifact.zip file content'); + awsDeploy.serverless.service.provider.deploymentBucketObject = { + tags: [ + { + Key: 'key1', + Value: 'value1', + }, + { + Key: 'key2', + Value: 'value with a space', + }, + ], + }; + + return awsDeploy.uploadZipFile(artifactFilePath).then(() => { + expect(uploadStub).to.have.been.calledOnce; + expect(readFileSyncStub).to.have.been.calledOnce; + expect(uploadStub).to.have.been.calledWithExactly( + 'S3', + 'upload', + { + Bucket: awsDeploy.bucketName, + Key: `${awsDeploy.serverless.service.package.artifactDirectoryName}/artifact.zip`, + Body: sinon.match.object.and(sinon.match.has('path', artifactFilePath)), + ContentType: 'application/zip', + Tagging: 'key1=value1&key2=value%20with%20a%20space', + Metadata: { + filesha256: 'local-hash-zip-file', + }, + }); + expect(readFileSyncStub).to.have.been.calledWithExactly(artifactFilePath); + }); + }); }); describe('#uploadFunctions()', () => {