diff --git a/lib/plugins/aws/deploy/lib/check-for-changes.js b/lib/plugins/aws/deploy/lib/check-for-changes.js index 5f3cd5468..03feeb01a 100644 --- a/lib/plugins/aws/deploy/lib/check-for-changes.js +++ b/lib/plugins/aws/deploy/lib/check-for-changes.js @@ -152,11 +152,30 @@ module.exports = { // create a hash of the CloudFormation body const compiledCfTemplate = this.serverless.service.provider.compiledCloudFormationTemplate; - const normCfTemplate = normalizeFiles.normalizeCloudFormationTemplate(compiledCfTemplate); + const stateBasename = this.provider.naming.getServiceStateFileName(); + const stateObject = JSON.parse( + await fsp.readFile( + path.join(this.serverless.serviceDir, '.serverless', stateBasename), + 'utf-8' + ) + ); + const localHashesMap = new Map([ [ 'compiled-cloudformation-template.json', - crypto.createHash('sha256').update(JSON.stringify(normCfTemplate)).digest('base64'), + crypto + .createHash('sha256') + .update( + JSON.stringify(normalizeFiles.normalizeCloudFormationTemplate(compiledCfTemplate)) + ) + .digest('base64'), + ], + [ + stateBasename, + crypto + .createHash('sha256') + .update(JSON.stringify(normalizeFiles.normalizeState(stateObject))) + .digest('base64'), ], ]); diff --git a/lib/plugins/aws/deploy/lib/upload-artifacts.js b/lib/plugins/aws/deploy/lib/upload-artifacts.js index d1c00e122..a5128c238 100644 --- a/lib/plugins/aws/deploy/lib/upload-artifacts.js +++ b/lib/plugins/aws/deploy/lib/upload-artifacts.js @@ -83,7 +83,11 @@ module.exports = { 'utf-8' ); - const fileHash = crypto.createHash('sha256').update(content).digest('base64'); + const stateObject = JSON.parse(content); + const fileHash = crypto + .createHash('sha256') + .update(JSON.stringify(normalizeFiles.normalizeState(stateObject))) + .digest('base64'); let params = { Bucket: this.bucketName, diff --git a/lib/plugins/aws/lib/normalize-files.js b/lib/plugins/aws/lib/normalize-files.js index 06973fc1f..22e9d0381 100644 --- a/lib/plugins/aws/lib/normalize-files.js +++ b/lib/plugins/aws/lib/normalize-files.js @@ -44,4 +44,12 @@ module.exports = { return normalizedTemplate; }, + normalizeState(state) { + const result = deepSortObjectByKey(state); + delete result.service.initialServerlessConfig; + delete result.service.provider.coreCloudFormationTemplate; + delete result.service.provider.compiledCloudFormationTemplate; + delete result.package.artifactDirectoryName; + return result; + }, }; diff --git a/test/unit/lib/plugins/aws/deploy/index.test.js b/test/unit/lib/plugins/aws/deploy/index.test.js index b66274273..c0459ea44 100644 --- a/test/unit/lib/plugins/aws/deploy/index.test.js +++ b/test/unit/lib/plugins/aws/deploy/index.test.js @@ -408,6 +408,13 @@ describe('test/unit/lib/plugins/aws/deploy/index.test.js', () => { Size: 356, StorageClass: 'STANDARD', }, + { + Key: 'serverless/test-package-artifact/dev/1589988704359-2020-05-20T15:31:44.359Z/serverless-state.json', + LastModified: new Date(), + ETag: '"5102a4cf710cae6497dba9e61b85d0a4"', + Size: 356, + StorageClass: 'STANDARD', + }, { Key: 'serverless/test-package-artifact/dev/1589988704359-2020-05-20T15:31:44.359Z/my-own.zip', LastModified: new Date(), @@ -426,6 +433,15 @@ describe('test/unit/lib/plugins/aws/deploy/index.test.js', () => { .returns({ Metadata: { filesha256: 'qxp+iwSTMhcRUfHzka4AE4XAWawS8GnEyBh1WpGb7Vw=' }, }); + s3HeadObjectStub + .withArgs({ + Bucket: 's3-bucket-resource', + Key: 'serverless/test-package-artifact/dev/1589988704359-2020-05-20T15:31:44.359Z/serverless-state.json', + }) + .returns({ + Metadata: { filesha256: 'DCociWxGeu49Gi6Ej103YnbXACySaslxTzQn19R1Q5I=' }, + }); + s3HeadObjectStub .withArgs({ Bucket: 's3-bucket-resource', diff --git a/test/unit/lib/plugins/aws/deploy/lib/check-for-changes.test.js b/test/unit/lib/plugins/aws/deploy/lib/check-for-changes.test.js index d153e2f15..40e83a8dc 100644 --- a/test/unit/lib/plugins/aws/deploy/lib/check-for-changes.test.js +++ b/test/unit/lib/plugins/aws/deploy/lib/check-for-changes.test.js @@ -244,7 +244,9 @@ describe('checkForChanges', () => { .stub(normalizeFiles, 'normalizeCloudFormationTemplate') .returns(); globbySyncStub = sandbox.stub(globby, 'sync'); - readFileStub = sandbox.stub(fsp, 'readFile').returns(Promise.resolve('')); + readFileStub = sandbox + .stub(fsp, 'readFile') + .returns(Promise.resolve('{"service":{"provider":{}},"package":{}}')); }); afterEach(() => { @@ -279,7 +281,6 @@ describe('checkForChanges', () => { await awsDeploy.checkIfDeploymentIsNecessary(input); expect(normalizeCloudFormationTemplateStub).to.have.been.calledOnce; expect(globbySyncStub).to.have.been.calledOnce; - expect(readFileStub).to.have.been.calledOnce; expect(normalizeCloudFormationTemplateStub).to.have.been.calledWithExactly( awsDeploy.serverless.service.provider.compiledCloudFormationTemplate ); @@ -312,7 +313,6 @@ describe('checkForChanges', () => { return expect(awsDeploy.checkIfDeploymentIsNecessary(input)).to.be.fulfilled.then(() => { expect(normalizeCloudFormationTemplateStub).to.have.been.calledOnce; expect(globbySyncStub).to.have.been.calledOnce; - expect(readFileStub).to.have.been.calledOnce; expect(normalizeCloudFormationTemplateStub).to.have.been.calledWithExactly( awsDeploy.serverless.service.provider.compiledCloudFormationTemplate ); @@ -341,7 +341,6 @@ describe('checkForChanges', () => { return expect(awsDeploy.checkIfDeploymentIsNecessary(input)).to.be.fulfilled.then(() => { expect(normalizeCloudFormationTemplateStub).to.have.been.calledOnce; expect(globbySyncStub).to.have.been.calledOnce; - expect(readFileStub).to.have.been.calledOnce; expect(normalizeCloudFormationTemplateStub).to.have.been.calledWithExactly( awsDeploy.serverless.service.provider.compiledCloudFormationTemplate ); @@ -372,7 +371,6 @@ describe('checkForChanges', () => { return expect(awsDeploy.checkIfDeploymentIsNecessary(input)).to.be.fulfilled.then(() => { expect(normalizeCloudFormationTemplateStub).to.have.been.calledOnce; expect(globbySyncStub).to.have.been.calledOnce; - expect(readFileStub).to.have.been.calledTwice; expect(normalizeCloudFormationTemplateStub).to.have.been.calledWithExactly( awsDeploy.serverless.service.provider.compiledCloudFormationTemplate ); @@ -408,7 +406,6 @@ describe('checkForChanges', () => { return expect(awsDeploy.checkIfDeploymentIsNecessary(input, now)).to.be.fulfilled.then(() => { expect(normalizeCloudFormationTemplateStub).to.have.been.calledOnce; expect(globbySyncStub).to.have.been.calledOnce; - expect(readFileStub).to.have.been.calledOnce; expect(normalizeCloudFormationTemplateStub).to.have.been.calledWithExactly( awsDeploy.serverless.service.provider.compiledCloudFormationTemplate ); @@ -427,18 +424,19 @@ describe('checkForChanges', () => { it('should set a flag if the remote and local hashes are equal', () => { globbySyncStub.returns(['my-service.zip']); cryptoStub.createHash().update().digest.onCall(0).returns('hash-cf-template'); - cryptoStub.createHash().update().digest.onCall(1).returns('hash-zip-file-1'); + cryptoStub.createHash().update().digest.onCall(1).returns('hash-state'); + cryptoStub.createHash().update().digest.onCall(2).returns('hash-zip-file-1'); let fileCounter = 0; const input = [ { Metadata: { filesha256: 'hash-cf-template' }, Key: `file${++fileCounter}.zip` }, + { Metadata: { filesha256: 'hash-state' }, Key: `file${++fileCounter}.zip` }, { Metadata: { filesha256: 'hash-zip-file-1' }, Key: `file${++fileCounter}.zip` }, ]; return expect(awsDeploy.checkIfDeploymentIsNecessary(input)).to.be.fulfilled.then(() => { expect(normalizeCloudFormationTemplateStub).to.have.been.calledOnce; expect(globbySyncStub).to.have.been.calledOnce; - expect(readFileStub).to.have.been.calledOnce; expect(normalizeCloudFormationTemplateStub).to.have.been.calledWithExactly( awsDeploy.serverless.service.provider.compiledCloudFormationTemplate ); @@ -457,7 +455,8 @@ describe('checkForChanges', () => { it('should set a flag if the remote and local hashes are equal, and the edit times are ordered', () => { globbySyncStub.returns(['my-service.zip']); cryptoStub.createHash().update().digest.onCall(0).returns('hash-cf-template'); - cryptoStub.createHash().update().digest.onCall(1).returns('hash-zip-file-1'); + cryptoStub.createHash().update().digest.onCall(1).returns('hash-state'); + cryptoStub.createHash().update().digest.onCall(2).returns('hash-zip-file-1'); const longAgo = new Date(new Date().getTime() - 100000); const longerAgo = new Date(new Date().getTime() - 200000); @@ -469,6 +468,11 @@ describe('checkForChanges', () => { LastModified: longerAgo, Key: `file${++fileCounter}.zip`, }, + { + Metadata: { filesha256: 'hash-state' }, + LastModified: longerAgo, + Key: `file${++fileCounter}.zip`, + }, { Metadata: { filesha256: 'hash-zip-file-1' }, LastModified: longerAgo, @@ -480,7 +484,6 @@ describe('checkForChanges', () => { () => { expect(normalizeCloudFormationTemplateStub).to.have.been.calledOnce; expect(globbySyncStub).to.have.been.calledOnce; - expect(readFileStub).to.have.been.calledOnce; expect(normalizeCloudFormationTemplateStub).to.have.been.calledWithExactly( awsDeploy.serverless.service.provider.compiledCloudFormationTemplate ); @@ -500,13 +503,15 @@ describe('checkForChanges', () => { it('should set a flag if the remote and local hashes are duplicated and equal', () => { globbySyncStub.returns(['func1.zip', 'func2.zip']); cryptoStub.createHash().update().digest.onCall(0).returns('hash-cf-template'); + cryptoStub.createHash().update().digest.onCall(1).returns('hash-state'); // happens when package.individually is used - cryptoStub.createHash().update().digest.onCall(1).returns('hash-zip-file-1'); cryptoStub.createHash().update().digest.onCall(2).returns('hash-zip-file-1'); + cryptoStub.createHash().update().digest.onCall(3).returns('hash-zip-file-1'); let fileCounter = 0; const input = [ { Metadata: { filesha256: 'hash-cf-template' }, Key: `file${++fileCounter}.zip` }, + { Metadata: { filesha256: 'hash-state' }, Key: `file${++fileCounter}.zip` }, { Metadata: { filesha256: 'hash-zip-file-1' }, Key: `file${++fileCounter}.zip` }, { Metadata: { filesha256: 'hash-zip-file-1' }, Key: `file${++fileCounter}.zip` }, ]; @@ -514,7 +519,6 @@ describe('checkForChanges', () => { return expect(awsDeploy.checkIfDeploymentIsNecessary(input)).to.be.fulfilled.then(() => { expect(normalizeCloudFormationTemplateStub).to.have.been.calledOnce; expect(globbySyncStub).to.have.been.calledOnce; - expect(readFileStub).to.have.been.calledTwice; expect(normalizeCloudFormationTemplateStub).to.have.been.calledWithExactly( awsDeploy.serverless.service.provider.compiledCloudFormationTemplate ); @@ -540,18 +544,19 @@ describe('checkForChanges', () => { globbySyncStub.returns([]); cryptoStub.createHash().update().digest.onCall(0).returns('hash-cf-template'); - cryptoStub.createHash().update().digest.onCall(1).returns('local-my-own-hash'); + cryptoStub.createHash().update().digest.onCall(1).returns('hash-state'); + cryptoStub.createHash().update().digest.onCall(2).returns('local-my-own-hash'); let fileCounter = 0; const input = [ { Metadata: { filesha256: 'hash-cf-template' }, Key: `file${++fileCounter}.zip` }, + { Metadata: { filesha256: 'hash-state' }, Key: `file${++fileCounter}.zip` }, { Metadata: { filesha256: 'remote-my-own-hash' }, Key: `file${++fileCounter}.zip` }, ]; return expect(awsDeploy.checkIfDeploymentIsNecessary(input)).to.be.fulfilled.then(() => { expect(normalizeCloudFormationTemplateStub).to.have.been.calledOnce; expect(globbySyncStub).to.have.been.calledOnce; - expect(readFileStub).to.have.been.calledOnce; expect(normalizeCloudFormationTemplateStub).to.have.been.calledWithExactly( awsDeploy.serverless.service.provider.compiledCloudFormationTemplate ); @@ -1043,6 +1048,14 @@ describe('test/unit/lib/plugins/aws/deploy/lib/checkForChanges.test.js', () => { .returns({ Metadata: { filesha256: 'pZOdrt6qijT7ITsLQjPP9QwgMAfKA2RuUUSTW+l8wWs=' }, }); + headObjectStub + .withArgs({ + Bucket: 'deployment-bucket', + Key: 'serverless/test-package-artifact/dev/1589988704359-2020-05-20T15:31:44.359Z/serverless-state.json', + }) + .returns({ + Metadata: { filesha256: '+LsZPkrDKAtccClX0Uv88s71VPtsHAGeifgiRyW0/OQ=' }, + }); headObjectStub .withArgs({ @@ -1064,6 +1077,13 @@ describe('test/unit/lib/plugins/aws/deploy/lib/checkForChanges.test.js', () => { Size: 356, StorageClass: 'STANDARD', }, + { + Key: 'serverless/test-package-artifact/dev/1589988704359-2020-05-20T15:31:44.359Z/serverless-state.json', + LastModified: new Date(), + ETag: '"5102a4cf710cae6497dba9e61b85d0a4"', + Size: 356, + StorageClass: 'STANDARD', + }, { Key: 'serverless/test-package-artifact/dev/1589988704359-2020-05-20T15:31:44.359Z/my-own.zip', LastModified: new Date(),