From 327511c43fda754017c6059de0be93986ee3c16b Mon Sep 17 00:00:00 2001 From: Tanner Barlow Date: Fri, 16 Aug 2019 07:53:04 -0700 Subject: [PATCH] perf: Short-circuit deployment if ARM template hasn't changed (#239) This checks the previously deployed template (including parameters/values) to see if the generated template has changed since the last deployment. If it has not changed, it will not deploy the ARM template. Since many cases will be code changes, this will drastically increase the speed of the average deployment. Resolves [AB#789] --- package-lock.json | 58 ++++++---------------------- package.json | 1 + src/services/armService.test.ts | 55 +++++++++++++++++++++++++- src/services/armService.ts | 43 ++++++++++++++++++++- src/services/baseService.ts | 8 ++-- src/services/loginService.test.ts | 2 +- src/services/resourceService.test.ts | 1 + src/services/resourceService.ts | 35 ++++++++++++++++- src/test/mockFactory.ts | 18 +++++---- 9 files changed, 159 insertions(+), 62 deletions(-) diff --git a/package-lock.json b/package-lock.json index 63f220ec..3969a80a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1794,7 +1794,6 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=", - "optional": true, "requires": { "kind-of": "^3.0.2", "longest": "^1.0.1", @@ -1967,7 +1966,6 @@ "version": "4.10.1", "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==", - "optional": true, "requires": { "bn.js": "^4.0.0", "inherits": "^2.0.1", @@ -2437,8 +2435,7 @@ "bn.js": { "version": "4.11.8", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", - "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==", - "optional": true + "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==" }, "body-parser": { "version": "1.19.0", @@ -2543,8 +2540,7 @@ "brorand": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", - "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=", - "optional": true + "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=" }, "browser-process-hrtime": { "version": "0.1.3", @@ -2573,7 +2569,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", - "optional": true, "requires": { "buffer-xor": "^1.0.3", "cipher-base": "^1.0.0", @@ -2610,7 +2605,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=", - "optional": true, "requires": { "bn.js": "^4.1.0", "randombytes": "^2.0.1" @@ -2707,8 +2701,7 @@ "buffer-xor": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", - "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=", - "optional": true + "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=" }, "builtin-modules": { "version": "1.1.1", @@ -2890,7 +2883,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", - "optional": true, "requires": { "inherits": "^2.0.1", "safe-buffer": "^5.0.1" @@ -3268,7 +3260,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", - "optional": true, "requires": { "cipher-base": "^1.0.1", "inherits": "^2.0.1", @@ -3281,7 +3272,6 @@ "version": "1.1.7", "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", - "optional": true, "requires": { "cipher-base": "^1.0.3", "create-hash": "^1.1.0", @@ -3521,8 +3511,7 @@ "deep-equal": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", - "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=", - "dev": true + "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=" }, "deep-extend": { "version": "0.6.0", @@ -3740,7 +3729,6 @@ "version": "6.4.1", "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.4.1.tgz", "integrity": "sha512-BsXLz5sqX8OHcsh7CqBMztyXARmGQ3LWPtGjJi6DiJHq5C/qvi9P3OqgswKSDftbu8+IoI/QDTAm2fFnQ9SZSQ==", - "optional": true, "requires": { "bn.js": "^4.4.0", "brorand": "^1.0.1", @@ -3802,7 +3790,6 @@ "version": "0.1.7", "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz", "integrity": "sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==", - "optional": true, "requires": { "prr": "~1.0.1" } @@ -4182,7 +4169,6 @@ "version": "0.3.5", "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", "integrity": "sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=", - "optional": true, "requires": { "d": "1", "es5-ext": "~0.10.14" @@ -4197,7 +4183,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", - "optional": true, "requires": { "md5.js": "^1.3.4", "safe-buffer": "^5.1.1" @@ -5476,7 +5461,6 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz", "integrity": "sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=", - "optional": true, "requires": { "inherits": "^2.0.1", "safe-buffer": "^5.0.1" @@ -5486,7 +5470,6 @@ "version": "1.1.7", "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", - "optional": true, "requires": { "inherits": "^2.0.3", "minimalistic-assert": "^1.0.1" @@ -5496,7 +5479,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", - "optional": true, "requires": { "hash.js": "^1.0.3", "minimalistic-assert": "^1.0.0", @@ -5815,8 +5797,7 @@ "is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "optional": true + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" }, "is-fullwidth-code-point": { "version": "1.0.0", @@ -7456,8 +7437,7 @@ "longest": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", - "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=", - "optional": true + "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=" }, "loose-envify": { "version": "1.4.0", @@ -7548,7 +7528,6 @@ "version": "1.3.5", "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", - "optional": true, "requires": { "hash-base": "^3.0.0", "inherits": "^2.0.1", @@ -7574,7 +7553,6 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", "integrity": "sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=", - "optional": true, "requires": { "errno": "^0.1.3", "readable-stream": "^2.0.1" @@ -7665,14 +7643,12 @@ "minimalistic-assert": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", - "optional": true + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" }, "minimalistic-crypto-utils": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", - "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=", - "optional": true + "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=" }, "minimatch": { "version": "3.0.4", @@ -8279,7 +8255,6 @@ "version": "5.1.4", "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.4.tgz", "integrity": "sha512-Qs5duJcuvNExRfFZ99HDD3z4mAi3r9Wl/FOjEOijlxwCZs7E7mW2vjTpgQ4J8LpTF8x5v+1Vn5UQFejmWT11aw==", - "optional": true, "requires": { "asn1.js": "^4.0.0", "browserify-aes": "^1.0.0", @@ -8388,7 +8363,6 @@ "version": "3.0.17", "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.17.tgz", "integrity": "sha512-U/il5MsrZp7mGg3mSQfn742na2T+1/vHDCG5/iTI3X9MKUuYUZVLQhyRsg06mCgDBTd57TxzgZt7P+fYfjRLtA==", - "optional": true, "requires": { "create-hash": "^1.1.2", "create-hmac": "^1.1.4", @@ -8591,8 +8565,7 @@ "prr": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", - "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=", - "optional": true + "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=" }, "pseudomap": { "version": "1.0.2", @@ -8659,7 +8632,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "optional": true, "requires": { "safe-buffer": "^5.1.0" } @@ -9072,7 +9044,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", - "optional": true, "requires": { "hash-base": "^3.0.0", "inherits": "^2.0.1" @@ -9462,7 +9433,6 @@ "version": "2.4.11", "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", - "optional": true, "requires": { "inherits": "^2.0.1", "safe-buffer": "^5.0.1" @@ -9658,8 +9628,7 @@ "source-list-map": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", - "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==", - "optional": true + "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==" }, "source-map": { "version": "0.7.3", @@ -10216,8 +10185,7 @@ "tapable": { "version": "0.2.9", "resolved": "https://registry.npmjs.org/tapable/-/tapable-0.2.9.tgz", - "integrity": "sha512-2wsvQ+4GwBvLPLWsNfLCDYGsW6xb7aeC6utq2Qh0PFwgEy7K7dsma9Jsmb2zSQj7GvYAyUGSntLtsv++GmgL1A==", - "optional": true + "integrity": "sha512-2wsvQ+4GwBvLPLWsNfLCDYGsW6xb7aeC6utq2Qh0PFwgEy7K7dsma9Jsmb2zSQj7GvYAyUGSntLtsv++GmgL1A==" }, "tar-stream": { "version": "1.6.2", @@ -11067,7 +11035,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.3.0.tgz", "integrity": "sha512-OiVgSrbGu7NEnEvQJJgdSFPl2qWKkWq5lHMhgiToIiN9w34EBnjYzSYs+VbL5KoYiLNtFFa7BZIKxRED3I32pA==", - "optional": true, "requires": { "source-list-map": "^2.0.0", "source-map": "~0.6.1" @@ -11076,8 +11043,7 @@ "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "optional": true + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" } } }, diff --git a/package.json b/package.json index 1396e588..b7cb6f82 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "@azure/ms-rest-nodeauth": "^1.0.1", "@azure/storage-blob": "^10.3.0", "axios": "^0.18.0", + "deep-equal": "^1.0.1", "js-yaml": "^3.13.1", "jsonpath": "^1.0.1", "lodash": "^4.16.6", diff --git a/src/services/armService.test.ts b/src/services/armService.test.ts index 825599e0..71dedfe0 100644 --- a/src/services/armService.test.ts +++ b/src/services/armService.test.ts @@ -1,12 +1,13 @@ import Serverless from "serverless"; import { MockFactory } from "../test/mockFactory"; import { ArmService } from "./armService"; -import { ArmResourceTemplate, ArmTemplateType } from "../models/armTemplates"; +import { ArmResourceTemplate, ArmTemplateType, ArmDeployment } from "../models/armTemplates"; import { ArmTemplateConfig, ServerlessAzureOptions } from "../models/serverless"; import mockFs from "mock-fs"; import jsonpath from "jsonpath"; import { Deployments } from "@azure/arm-resources"; import { Deployment } from "@azure/arm-resources/esm/models"; +import { ResourceService } from "./resourceService"; describe("Arm Service", () => { let sls: Serverless @@ -29,6 +30,13 @@ describe("Arm Service", () => { }; service = createService(); + ResourceService.prototype.getDeployments = jest.fn(() => MockFactory.createTestDeployments()) as any; + ResourceService.prototype.getDeploymentTemplate = jest.fn(() => { + return { + template: MockFactory.createTestArmTemplate() + } + }) as any; + }) afterEach(() => { @@ -151,6 +159,51 @@ describe("Arm Service", () => { Deployments.prototype.createOrUpdate = jest.fn(() => Promise.resolve(null)); }); + it("Does not deploy if previously deployed template is the same", async () => { + const deployment: ArmDeployment = { + parameters: MockFactory.createTestParameters(false), + template: MockFactory.createTestArmTemplate() + }; + await service.deployTemplate(deployment); + expect(Deployments.prototype.createOrUpdate).not.toBeCalled() + }); + + it("Calls deploy if parameters have changed from deployed template", async () => { + const deployment: ArmDeployment = { + parameters: MockFactory.createTestParameters(false), + template: MockFactory.createTestArmTemplate() + }; + deployment.parameters.param1 = "3" + await service.deployTemplate(deployment); + expect(Deployments.prototype.createOrUpdate).toBeCalled(); + }); + + it("Calls deploy if previously deployed template is different", async () => { + ResourceService.prototype.getDeploymentTemplate = jest.fn(() => { + return { + template: {} + } + }) as any; + const deployment: ArmDeployment = { + parameters: MockFactory.createTestParameters(false), + template: MockFactory.createTestArmTemplate() + }; + await service.deployTemplate(deployment); + expect(Deployments.prototype.createOrUpdate).toBeCalled() + }); + + it("Calls deploy if running first deployment", async () => { + ResourceService.prototype.getDeployments = jest.fn(() => { + return [] + }) as any; + const deployment: ArmDeployment = { + parameters: MockFactory.createTestParameters(false), + template: MockFactory.createTestArmTemplate() + }; + await service.deployTemplate(deployment); + expect(Deployments.prototype.createOrUpdate).toBeCalled() + }); + it("Appends environment variables into app settings of ARM template", async () => { const environmentConfig: any = { PARAM_1: "1", diff --git a/src/services/armService.ts b/src/services/armService.ts index d021b360..3abb531d 100644 --- a/src/services/armService.ts +++ b/src/services/armService.ts @@ -8,6 +8,8 @@ import { ArmDeployment, ArmResourceTemplateGenerator, ArmTemplateType } from ".. import { ArmTemplateConfig, ServerlessAzureConfig, ServerlessAzureOptions } from "../models/serverless"; import { Guard } from "../shared/guard"; import { BaseService } from "./baseService"; +import { ResourceService } from "./resourceService" +import deepEqual from "deep-equal"; export class ArmService extends BaseService { private resourceClient: ResourceManagementClient; @@ -110,17 +112,56 @@ export class ArmService extends BaseService { } }; + const resourceService = new ResourceService(this.serverless, this.options); + const latest = await resourceService.getLastDeploymentTemplate(); + + if (latest) { + const templateEqual = deepEqual(latest.template, deployment.template); + const mergedDefaultParameters = this.mergeDefaultParams(deploymentParameters, deployment.template.parameters); + const paramatersEqual = deepEqual(latest.parameters, mergedDefaultParameters); + + if (templateEqual && paramatersEqual) { + this.log("Generated template same as previous. Skipping ARM deployment"); + return; + } + } + // Deploy ARM template this.log("-> Deploying ARM template..."); this.log(`---> Resource Group: ${this.resourceGroup}`) this.log(`---> Deployment Name: ${this.deploymentName}`) - const result = await this.resourceClient.deployments.createOrUpdate(this.resourceGroup, this.deploymentName, armDeployment); + const result = await this.resourceClient.deployments.createOrUpdate( + this.resourceGroup, + this.deploymentName, + armDeployment + ); this.log("-> ARM deployment complete"); return result; } + /** + * Merge parameters and default parameters for comparison with previously deployed template + * @param parameters Parameters with specified values + * @param defaultParameters Parameters with `type` and `defaultValue` + */ + private mergeDefaultParams(parameters: any, defaultParameters: any) { + const mergedParams = {} + Object.keys(defaultParameters).forEach((key) => { + const defaultParam = defaultParameters[key]; + mergedParams[key] = { + type: defaultParam.type, + value: (key in parameters) + ? + parameters[key].value + : + defaultParameters[key].defaultValue + } + }); + return mergedParams; + } + /** * Applies sls yaml environment variables into the appSettings section of the function app configuration * @param deployment The ARM deployment diff --git a/src/services/baseService.ts b/src/services/baseService.ts index dc21b702..43c3808b 100644 --- a/src/services/baseService.ts +++ b/src/services/baseService.ts @@ -191,10 +191,6 @@ export abstract class BaseService { (this.serverless.cli.log as any)(message, entity, options); } - protected prettyPrint(object: any) { - this.log(JSON.stringify(object, null, 2)); - } - /** * Get function objects */ @@ -210,6 +206,10 @@ export abstract class BaseService { return Utils.get(this.options, key, defaultValue); } + protected prettyPrint(object: any) { + this.log(JSON.stringify(object, null, 2)); + } + private setDefaultValues(): void { // TODO: Right now the serverless core will always default to AWS default region if the // region has not been set in the serverless.yml or CLI options diff --git a/src/services/loginService.test.ts b/src/services/loginService.test.ts index 941b4fa3..cfbc7bd0 100644 --- a/src/services/loginService.test.ts +++ b/src/services/loginService.test.ts @@ -22,7 +22,7 @@ describe("Login Service", () => { const emptyObj = { subscriptions: [] }; Object.defineProperty(nodeauth, "interactiveLoginWithAuthResponse", - { value: jest.fn(_obj => emptyObj) } + { value: jest.fn(() => emptyObj) } ); await AzureLoginService.login(); diff --git a/src/services/resourceService.test.ts b/src/services/resourceService.test.ts index fe46f31c..dffaa4e9 100644 --- a/src/services/resourceService.test.ts +++ b/src/services/resourceService.test.ts @@ -89,6 +89,7 @@ describe("Resource Service", () => { const options = MockFactory.createTestServerlessOptions(); const service = new ResourceService(sls, options); const deps = await service.getDeployments(); + // Make sure deps are in correct order expect(deps).toEqual(deployments); }); diff --git a/src/services/resourceService.ts b/src/services/resourceService.ts index 1248e2d5..51885e63 100644 --- a/src/services/resourceService.ts +++ b/src/services/resourceService.ts @@ -3,6 +3,8 @@ import { ResourceManagementClient } from "@azure/arm-resources"; import { BaseService } from "./baseService"; import { Utils } from "../shared/utils"; import { AzureNamingService } from "./namingService"; +import { ArmDeployment } from "../models/armTemplates"; +import { DeploymentExtended } from "@azure/arm-resources/esm/models"; export class ResourceService extends BaseService { private resourceClient: ResourceManagementClient; @@ -14,11 +16,40 @@ export class ResourceService extends BaseService { } /** - * Get all deployments for resource group + * Get all deployments for resource group sorted by timestamp (most recent first) */ public async getDeployments() { this.log(`Listing deployments for resource group '${this.resourceGroup}':`); - return await this.resourceClient.deployments.listByResourceGroup(this.resourceGroup); + const deployments = await this.resourceClient.deployments.listByResourceGroup(this.resourceGroup); + return deployments.sort((a: DeploymentExtended, b: DeploymentExtended) => { + return (a.properties.timestamp > b.properties.timestamp) ? 1 : -1 + }); + } + + /** + * Get the most recent resource group deployment + */ + public async getLastDeployment() { + const deployments = await this.getDeployments(); + if (deployments && deployments.length) { + return deployments[0]; + } + } + + /** + * Get template from last resource group deployment + */ + public async getLastDeploymentTemplate(): Promise { + const deployment = await this.getLastDeployment(); + if (!deployment) { + return; + } + const { parameters } = deployment.properties; + const { template } = await this.getDeploymentTemplate(deployment.name); + return { + template, + parameters + } } /** diff --git a/src/test/mockFactory.ts b/src/test/mockFactory.ts index b44a7e67..97899407 100644 --- a/src/test/mockFactory.ts +++ b/src/test/mockFactory.ts @@ -169,25 +169,29 @@ export class MockFactory { const result = []; const originalTimestamp = +MockFactory.createTestTimestamp(); for (let i = 0; i < count; i++) { + const name = (includeTimestamp) ? `deployment${i + 1}-t${originalTimestamp + i}` : `deployment${i + 1}`; result.push( - MockFactory.createTestDeployment((includeTimestamp) ? `deployment${i + 1}-t${originalTimestamp + i}` : `deployment${i + 1}`) + MockFactory.createTestDeployment(name, i) ) } return result as DeploymentsListByResourceGroupResponse } - public static createTestParameters() { - return { + public static createTestParameters(wrap = true) { + return (wrap) ? { param1: { value: "1", type: "String" }, param2: { value: "2", type: "String" }, + } : { + param1: "1", + param2: "2", } } - public static createTestDeployment(name?: string): DeploymentExtended { + public static createTestDeployment(name?: string, second: number = 0): DeploymentExtended { return { name: name || `deployment1-t${MockFactory.createTestTimestamp()}`, properties: { - timestamp: new Date(), + timestamp: new Date(2019, 1, 1, 0, 0, second), parameters: MockFactory.createTestParameters(), } } @@ -430,7 +434,7 @@ export class MockFactory { handler: "handler.js", } } - + public static createTestBinding() { // Only supporting HTTP for now, could support others return MockFactory.createTestHttpBinding(); @@ -461,7 +465,7 @@ export class MockFactory { name: "item", eventhubname: "hello", consumerGroup: "$Default", - connection: "EventHubsConnection" + connection: "EventHubsConnection" } } public static createTestBindingsObject(name: string = "index.js") {