From 5f553f152b19f88b5a36a0d89431d821585049a1 Mon Sep 17 00:00:00 2001 From: Tanner Barlow Date: Mon, 12 Aug 2019 17:07:15 -0500 Subject: [PATCH] perf: Short-circuit deployment if ARM template hasn't changed --- 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") {