From 474bada2f68c71d752f00483f90dbf87f83d16b9 Mon Sep 17 00:00:00 2001 From: Corey Walker Date: Tue, 4 Sep 2018 23:02:08 -0400 Subject: [PATCH 1/4] Added ability to add deployment resource tags --- docs/providers/aws/guide/serverless.yml.md | 5 ++ docs/providers/aws/guide/services.md | 5 ++ lib/plugins/aws/deploy/lib/uploadArtifacts.js | 17 +++++ .../aws/deploy/lib/uploadArtifacts.test.js | 75 +++++++++++++++++++ 4 files changed, 102 insertions(+) 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()', () => { From 1de43e477b94e4c3b9b2a70f9def0478e410dcb2 Mon Sep 17 00:00:00 2001 From: Corey Walker Date: Fri, 15 Feb 2019 16:18:08 -0500 Subject: [PATCH 2/4] Changed format to use same tagging format as other properties --- docs/providers/aws/guide/serverless.yml.md | 6 ++---- docs/providers/aws/guide/services.md | 6 ++---- lib/plugins/aws/deploy/lib/uploadArtifacts.js | 3 ++- lib/plugins/aws/deploy/lib/uploadArtifacts.test.js | 12 ++++-------- 4 files changed, 10 insertions(+), 17 deletions(-) diff --git a/docs/providers/aws/guide/serverless.yml.md b/docs/providers/aws/guide/serverless.yml.md index 39c9f8357..0436cafc0 100644 --- a/docs/providers/aws/guide/serverless.yml.md +++ b/docs/providers/aws/guide/serverless.yml.md @@ -38,10 +38,8 @@ provider: 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" + key1: value1 + key2: 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 932cbe49a..a8ba49f81 100644 --- a/docs/providers/aws/guide/services.md +++ b/docs/providers/aws/guide/services.md @@ -108,10 +108,8 @@ provider: 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" + key1: value1 + key2: 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 5d3d8a5fa..5c503e07d 100644 --- a/lib/plugins/aws/deploy/lib/uploadArtifacts.js +++ b/lib/plugins/aws/deploy/lib/uploadArtifacts.js @@ -140,7 +140,8 @@ function createTags(putParams, deploymentBucketObject) { if (deploymentBucketObject.tags) { for (const tag of deploymentBucketObject.tags) { - taggingString += `${encodeURIComponent(tag.Key)}=${encodeURIComponent(tag.Value)}&`; + taggingString += `${encodeURIComponent(Object.keys(tag)[0])}=`; + taggingString += `${encodeURIComponent(tag[Object.keys(tag)[0]])}&`; } taggingString = taggingString.substring(0, taggingString.length - 1); params.Tagging = taggingString; diff --git a/lib/plugins/aws/deploy/lib/uploadArtifacts.test.js b/lib/plugins/aws/deploy/lib/uploadArtifacts.test.js index 7f7c15440..8330a989c 100644 --- a/lib/plugins/aws/deploy/lib/uploadArtifacts.test.js +++ b/lib/plugins/aws/deploy/lib/uploadArtifacts.test.js @@ -149,12 +149,10 @@ describe('uploadArtifacts', () => { awsDeploy.serverless.service.provider.deploymentBucketObject = { tags: [ { - Key: 'key1', - Value: 'value1', + key1: 'value1', }, { - Key: 'key2', - Value: 'value with a space', + key2: 'value with a space', }, ], }; @@ -268,12 +266,10 @@ describe('uploadArtifacts', () => { awsDeploy.serverless.service.provider.deploymentBucketObject = { tags: [ { - Key: 'key1', - Value: 'value1', + key1: 'value1', }, { - Key: 'key2', - Value: 'value with a space', + key2: 'value with a space', }, ], }; From 627600fca78e2171e0c0c561d29c6150b2b858f1 Mon Sep 17 00:00:00 2001 From: Philipp Muens Date: Thu, 21 Feb 2019 13:13:12 +0100 Subject: [PATCH 3/4] Fix tests --- lib/plugins/aws/deploy/lib/uploadArtifacts.js | 13 ++++++---- .../aws/deploy/lib/uploadArtifacts.test.js | 24 +++++++------------ 2 files changed, 16 insertions(+), 21 deletions(-) diff --git a/lib/plugins/aws/deploy/lib/uploadArtifacts.js b/lib/plugins/aws/deploy/lib/uploadArtifacts.js index 6eb317836..850a441e7 100644 --- a/lib/plugins/aws/deploy/lib/uploadArtifacts.js +++ b/lib/plugins/aws/deploy/lib/uploadArtifacts.js @@ -152,11 +152,14 @@ function createTags(putParams, deploymentBucketObject) { const params = putParams; let taggingString = ''; - if (deploymentBucketObject.tags) { - for (const tag of deploymentBucketObject.tags) { - taggingString += `${encodeURIComponent(Object.keys(tag)[0])}=`; - taggingString += `${encodeURIComponent(tag[Object.keys(tag)[0]])}&`; - } + const tags = deploymentBucketObject.tags; + + if (tags && Object.keys(tags).length) { + Object.keys(tags).forEach((key) => { + const value = tags[key]; + taggingString += `${encodeURIComponent(key)}=`; + taggingString += `${encodeURIComponent(value)}&`; + }); taggingString = taggingString.substring(0, taggingString.length - 1); params.Tagging = taggingString; } diff --git a/lib/plugins/aws/deploy/lib/uploadArtifacts.test.js b/lib/plugins/aws/deploy/lib/uploadArtifacts.test.js index ea3c59c88..6e9083240 100644 --- a/lib/plugins/aws/deploy/lib/uploadArtifacts.test.js +++ b/lib/plugins/aws/deploy/lib/uploadArtifacts.test.js @@ -148,14 +148,10 @@ describe('uploadArtifacts', () => { 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: [ - { - key1: 'value1', - }, - { - key2: 'value with a space', - }, - ], + tags: { + key1: 'value1', + key2: 'value with a space', + }, }; return awsDeploy.uploadCloudFormationFile().then(() => { @@ -265,14 +261,10 @@ describe('uploadArtifacts', () => { const artifactFilePath = path.join(tmpDirPath, 'artifact.zip'); serverless.utils.writeFileSync(artifactFilePath, 'artifact.zip file content'); awsDeploy.serverless.service.provider.deploymentBucketObject = { - tags: [ - { - key1: 'value1', - }, - { - key2: 'value with a space', - }, - ], + tags: { + key1: 'value1', + key2: 'value with a space', + }, }; return awsDeploy.uploadZipFile(artifactFilePath).then(() => { From e8ccc7afe670b0a48c9d80f4cf5504fbbd4c402e Mon Sep 17 00:00:00 2001 From: Philipp Muens Date: Mon, 25 Feb 2019 14:37:26 +0100 Subject: [PATCH 4/4] Replace tagging of uploaded artifacts with resource tagging of bucket --- lib/plugins/aws/deploy/lib/uploadArtifacts.js | 21 ------- .../aws/deploy/lib/uploadArtifacts.test.js | 63 ------------------- .../aws/package/lib/generateCoreTemplate.js | 18 ++++++ .../package/lib/generateCoreTemplate.test.js | 35 +++++++++++ 4 files changed, 53 insertions(+), 84 deletions(-) diff --git a/lib/plugins/aws/deploy/lib/uploadArtifacts.js b/lib/plugins/aws/deploy/lib/uploadArtifacts.js index 850a441e7..e921fce18 100644 --- a/lib/plugins/aws/deploy/lib/uploadArtifacts.js +++ b/lib/plugins/aws/deploy/lib/uploadArtifacts.js @@ -44,7 +44,6 @@ module.exports = { const deploymentBucketObject = this.serverless.service.provider.deploymentBucketObject; if (deploymentBucketObject) { params = setServersideEncryptionOptions(params, deploymentBucketObject); - params = createTags(params, deploymentBucketObject); } return this.provider.request('S3', @@ -72,7 +71,6 @@ module.exports = { const deploymentBucketObject = this.serverless.service.provider.deploymentBucketObject; if (deploymentBucketObject) { params = setServersideEncryptionOptions(params, deploymentBucketObject); - params = createTags(params, deploymentBucketObject); } return this.provider.request('S3', @@ -147,22 +145,3 @@ function setServersideEncryptionOptions(putParams, deploymentBucketOptions) { return params; } - -function createTags(putParams, deploymentBucketObject) { - const params = putParams; - let taggingString = ''; - - const tags = deploymentBucketObject.tags; - - if (tags && Object.keys(tags).length) { - Object.keys(tags).forEach((key) => { - const value = tags[key]; - taggingString += `${encodeURIComponent(key)}=`; - taggingString += `${encodeURIComponent(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 6e9083240..532a8bac2 100644 --- a/lib/plugins/aws/deploy/lib/uploadArtifacts.test.js +++ b/lib/plugins/aws/deploy/lib/uploadArtifacts.test.js @@ -144,36 +144,6 @@ 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: { - key1: 'value1', - key2: '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()', () => { @@ -253,39 +223,6 @@ 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: { - key1: 'value1', - key2: '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('#uploadFunctionsAndLayers()', () => { diff --git a/lib/plugins/aws/package/lib/generateCoreTemplate.js b/lib/plugins/aws/package/lib/generateCoreTemplate.js index 47be04c97..3f99b2060 100644 --- a/lib/plugins/aws/package/lib/generateCoreTemplate.js +++ b/lib/plugins/aws/package/lib/generateCoreTemplate.js @@ -24,6 +24,24 @@ module.exports = { ); const bucketName = this.serverless.service.provider.deploymentBucket; + + // resource tags support for deployment bucket + const deploymentBucketObject = this.serverless.service.provider.deploymentBucketObject; + if (!_.isEmpty(deploymentBucketObject) && !_.isEmpty(deploymentBucketObject.tags)) { + const tags = deploymentBucketObject.tags; + const deploymentBucketLogicalId = this.provider.naming.getDeploymentBucketLogicalId(); + + const bucketTags = _.map(_.keys(tags), (key) => ({ + Key: key, + Value: tags[key], + })); + + Object.assign(this.serverless.service.provider.compiledCloudFormationTemplate + .Resources[deploymentBucketLogicalId].Properties, { + Tags: bucketTags, + }); + } + const isS3TransferAccelerationSupported = this.provider.isS3TransferAccelerationSupported(); const isS3TransferAccelerationEnabled = this.provider.isS3TransferAccelerationEnabled(); const isS3TransferAccelerationDisabled = this.provider.isS3TransferAccelerationDisabled(); diff --git a/lib/plugins/aws/package/lib/generateCoreTemplate.test.js b/lib/plugins/aws/package/lib/generateCoreTemplate.test.js index ba46b9647..177693e10 100644 --- a/lib/plugins/aws/package/lib/generateCoreTemplate.test.js +++ b/lib/plugins/aws/package/lib/generateCoreTemplate.test.js @@ -72,6 +72,41 @@ describe('#generateCoreTemplate()', () => { }); }); + it('should add resource tags to the bucket if present', () => { + const deploymentBucketObject = { + tags: { + FOO: 'bar', + BAZ: 'qux', + }, + }; + + awsPlugin.serverless.service.provider.deploymentBucketObject = deploymentBucketObject; + + return expect(awsPlugin.generateCoreTemplate()).to.be.fulfilled.then(() => { + expect( + awsPlugin.serverless.service.provider.compiledCloudFormationTemplate + .Resources.ServerlessDeploymentBucket + ).to.be.deep.equal({ + Type: 'AWS::S3::Bucket', + Properties: { + BucketEncryption: { + ServerSideEncryptionConfiguration: [ + { + ServerSideEncryptionByDefault: { + SSEAlgorithm: 'AES256', + }, + }, + ], + }, + Tags: [ + { Key: 'FOO', Value: 'bar' }, + { Key: 'BAZ', Value: 'qux' }, + ], + }, + }); + }); + }); + it('should use a custom bucket if specified, even with S3 transfer acceleration', () => { const bucketName = 'com.serverless.deploys';