Skip to content
This repository was archived by the owner on Dec 9, 2024. It is now read-only.

Commit 2b97f75

Browse files
authored
feat: Deploy pre-existing package with -p CLI option (#205)
- [x] Allow `-p` or `--package` param to specify package to deploy - [x] Log in before deployment rather than packaging - [x] Don't run deployment if local package does not exist Resolves [AB#500]
1 parent 7ceb484 commit 2b97f75

11 files changed

+159
-31
lines changed

src/plugins/deploy/azureDeployPlugin.test.ts

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,31 @@
1+
import { Site } from "@azure/arm-appservice/esm/models";
2+
import Serverless from "serverless";
3+
import { ServerlessAzureOptions } from "../../models/serverless";
14
import { MockFactory } from "../../test/mockFactory";
25
import { invokeHook } from "../../test/utils";
36
import { AzureDeployPlugin } from "./azureDeployPlugin";
7+
import mockFs from "mock-fs"
48

59
jest.mock("../../services/functionAppService");
610
import { FunctionAppService } from "../../services/functionAppService";
711

812
jest.mock("../../services/resourceService");
913
import { ResourceService } from "../../services/resourceService";
10-
import { Site } from "@azure/arm-appservice/esm/models";
11-
import { ServerlessAzureOptions } from "../../models/serverless";
12-
import Serverless from "serverless";
1314

1415
describe("Deploy plugin", () => {
1516
let sls: Serverless;
1617
let options: ServerlessAzureOptions;
1718
let plugin: AzureDeployPlugin;
1819

20+
beforeAll(() => {
21+
mockFs({
22+
"serviceName.zip": "contents",
23+
}, { createCwd: true, createTmp: true });
24+
});
25+
1926
beforeEach(() => {
20-
jest.resetAllMocks();
27+
FunctionAppService.prototype.getFunctionZipFile = jest.fn(() => "serviceName.zip");
28+
2129
sls = MockFactory.createTestServerless();
2230
options = MockFactory.createTestServerlessOptions();
2331

@@ -26,9 +34,13 @@ describe("Deploy plugin", () => {
2634

2735
afterEach(() => {
2836
jest.resetAllMocks();
37+
});
38+
39+
afterAll(() => {
40+
mockFs.restore();
2941
})
3042

31-
it("calls deploy hook", async () => {
43+
it("calls deploy", async () => {
3244
const deployResourceGroup = jest.fn();
3345
const functionAppStub: Site = MockFactory.createTestSite();
3446
const deploy = jest.fn(() => Promise.resolve(functionAppStub));
@@ -45,6 +57,27 @@ describe("Deploy plugin", () => {
4557
expect(uploadFunctions).toBeCalledWith(functionAppStub);
4658
});
4759

60+
it("does not call deploy if zip does not exist", async () => {
61+
const deployResourceGroup = jest.fn();
62+
const functionAppStub: Site = MockFactory.createTestSite();
63+
const deploy = jest.fn(() => Promise.resolve(functionAppStub));
64+
const uploadFunctions = jest.fn();
65+
66+
const zipFile = "fake.zip";
67+
68+
FunctionAppService.prototype.getFunctionZipFile = (() => zipFile);
69+
ResourceService.prototype.deployResourceGroup = deployResourceGroup;
70+
FunctionAppService.prototype.deploy = deploy;
71+
FunctionAppService.prototype.uploadFunctions = uploadFunctions;
72+
73+
await invokeHook(plugin, "deploy:deploy");
74+
75+
expect(deployResourceGroup).not.toBeCalled();
76+
expect(deploy).not.toBeCalled();
77+
expect(uploadFunctions).not.toBeCalled();
78+
expect(sls.cli.log).lastCalledWith(`Function app zip file '${zipFile}' does not exist`);
79+
});
80+
4881
it("lists deployments with timestamps", async () => {
4982
const deployments = MockFactory.createTestDeployments(5, true);
5083
ResourceService.prototype.getDeployments = jest.fn(() => Promise.resolve(deployments));

src/plugins/deploy/azureDeployPlugin.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1+
import fs from "fs";
12
import Serverless from "serverless";
23
import { FunctionAppService } from "../../services/functionAppService";
4+
import { AzureLoginOptions } from "../../services/loginService";
35
import { ResourceService } from "../../services/resourceService";
46
import { Utils } from "../../shared/utils";
57
import { AzureBasePlugin } from "../azureBasePlugin";
6-
import { AzureLoginOptions } from "../../services/loginService";
78

89
export class AzureDeployPlugin extends AzureBasePlugin<AzureLoginOptions> {
910
public hooks: { [eventName: string]: Promise<any> };
@@ -28,13 +29,17 @@ export class AzureDeployPlugin extends AzureBasePlugin<AzureLoginOptions> {
2829
}
2930
},
3031
options: {
31-
"resourceGroup": {
32+
resourceGroup: {
3233
usage: "Resource group for the service",
3334
shortcut: "g",
3435
},
3536
subscriptionId: {
3637
usage: "Sets the Azure subscription ID",
3738
shortcut: "i",
39+
},
40+
package: {
41+
usage: "Package to deploy",
42+
shortcut: "p",
3843
}
3944
}
4045
}
@@ -67,12 +72,14 @@ export class AzureDeployPlugin extends AzureBasePlugin<AzureLoginOptions> {
6772

6873
private async deploy() {
6974
const resourceService = new ResourceService(this.serverless, this.options);
70-
71-
await resourceService.deployResourceGroup();
72-
7375
const functionAppService = new FunctionAppService(this.serverless, this.options);
76+
const zipFile = functionAppService.getFunctionZipFile();
77+
if (!fs.existsSync(zipFile)) {
78+
this.log(`Function app zip file '${zipFile}' does not exist`);
79+
return Promise.resolve();
80+
}
81+
await resourceService.deployResourceGroup();
7482
const functionApp = await functionAppService.deploy();
75-
7683
await functionAppService.uploadFunctions(functionApp);
7784
}
7885
}

src/plugins/login/azureLoginPlugin.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ describe("Login Plugin", () => {
3232

3333
async function invokeLoginHook(hasCreds = false, serverless?: Serverless, options?: Serverless.Options) {
3434
const plugin = createPlugin(hasCreds, serverless, options);
35-
await invokeHook(plugin, "before:package:initialize");
35+
await invokeHook(plugin, "before:deploy:deploy");
3636
}
3737

3838
beforeEach(() => {

src/plugins/login/azureLoginPlugin.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export class AzureLoginPlugin extends AzureBasePlugin<AzureLoginOptions> {
1313
this.provider = (this.serverless.getProvider("azure") as any) as AzureProvider;
1414

1515
this.hooks = {
16-
"before:package:initialize": this.login.bind(this),
16+
"before:deploy:deploy": this.login.bind(this),
1717
"before:deploy:list:list": this.login.bind(this),
1818
"before:invoke:invoke": this.login.bind(this),
1919
"before:rollback:rollback": this.login.bind(this),

src/plugins/package/azurePackagePlugin.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,5 +39,33 @@ describe("Azure Package Plugin", () => {
3939
it("cleans up package after package:finalize", async () => {
4040
await invokeHook(plugin, "after:package:finalize");
4141
expect(PackageService.prototype.cleanUp).toBeCalled();
42+
});
43+
44+
describe("Package specified in options", () => {
45+
46+
beforeEach(() => {
47+
plugin = new AzurePackagePlugin(sls, MockFactory.createTestServerlessOptions({
48+
package: "fake.zip",
49+
}));
50+
});
51+
52+
it("does not call create bindings if package specified in options", async () => {
53+
await invokeHook(plugin, "before:package:setupProviderConfiguration");
54+
expect(PackageService.prototype.createBindings).not.toBeCalled();
55+
expect(sls.cli.log).lastCalledWith("No need to create bindings. Using pre-existing package");
56+
});
57+
58+
it("does not call webpack if package specified in options", async () => {
59+
await invokeHook(plugin, "before:webpack:package:packageModules");
60+
expect(PackageService.prototype.createBindings).not.toBeCalled();
61+
expect(PackageService.prototype.prepareWebpack).not.toBeCalled();
62+
expect(sls.cli.log).lastCalledWith("No need to perform webpack. Using pre-existing package");
63+
});
64+
65+
it("does not call finalize if package specified in options", async () => {
66+
await invokeHook(plugin, "after:package:finalize");
67+
expect(PackageService.prototype.cleanUp).not.toBeCalled();
68+
expect(sls.cli.log).lastCalledWith("No need to clean up generated folders & files. Using pre-existing package");
69+
});
4270
})
4371
});

src/plugins/package/azurePackagePlugin.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,21 @@ export class AzurePackagePlugin extends AzureBasePlugin {
2222
}
2323

2424
private async setupProviderConfiguration(): Promise<void> {
25+
if (this.getOption("package")) {
26+
this.log("No need to create bindings. Using pre-existing package");
27+
return Promise.resolve();
28+
}
2529
await this.packageService.createBindings();
2630
this.bindingsCreated = true;
2731

2832
return Promise.resolve();
2933
}
3034

3135
private async webpack(): Promise<void> {
36+
if (this.getOption("package")) {
37+
this.log("No need to perform webpack. Using pre-existing package");
38+
return Promise.resolve();
39+
}
3240
if (!this.bindingsCreated) {
3341
await this.setupProviderConfiguration();
3442
}
@@ -40,7 +48,10 @@ export class AzurePackagePlugin extends AzureBasePlugin {
4048
* Cleans up generated folders & files after packaging is complete
4149
*/
4250
private async finalize(): Promise<void> {
51+
if (this.getOption("package")) {
52+
this.log("No need to clean up generated folders & files. Using pre-existing package");
53+
return Promise.resolve();
54+
}
4355
await this.packageService.cleanUp();
4456
}
4557
}
46-

src/services/baseService.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ export abstract class BaseService {
121121
protected getAccessToken(): string {
122122
return (this.credentials.tokenCache as any)._entries[0].accessToken;
123123
}
124-
124+
125125
/**
126126
* Sends an API request using axios HTTP library
127127
* @param method The HTTP method
@@ -189,6 +189,10 @@ export abstract class BaseService {
189189
return "config" in this.options ? this.options["config"] : "serverless.yml";
190190
}
191191

192+
protected getOption(key: string, defaultValue?: any) {
193+
return Utils.get(this.options, key, defaultValue);
194+
}
195+
192196
private setDefaultValues(): void {
193197
// TODO: Right now the serverless core will always default to AWS default region if the
194198
// region has not been set in the serverless.yml or CLI options
@@ -231,4 +235,4 @@ export abstract class BaseService {
231235
}
232236
return timestamp;
233237
}
234-
}
238+
}

src/services/functionAppService.test.ts

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import axios from "axios";
22
import MockAdapter from "axios-mock-adapter";
33
import mockFs from "mock-fs";
4+
import path from "path";
45
import Serverless from "serverless";
56
import { MockFactory } from "../test/mockFactory";
67
import { FunctionAppService } from "./functionAppService";
@@ -55,6 +56,9 @@ describe("Function App Service", () => {
5556

5657
mockFs({
5758
"app.zip": "contents",
59+
".serverless": {
60+
"serviceName.zip": "contents",
61+
}
5862
}, { createCwd: true, createTmp: true });
5963
});
6064

@@ -222,6 +226,37 @@ describe("Function App Service", () => {
222226
expect(uploadCall[2]).toMatch(/.*-t([0-9]+)/)
223227
});
224228

229+
it("uploads functions to function app and blob storage with default naming convention", async () => {
230+
const scmDomain = app.enabledHostNames.find((hostname) => hostname.endsWith("scm.azurewebsites.net"));
231+
const expectedUploadUrl = `https://${scmDomain}/api/zipdeploy/`;
232+
233+
const sls = MockFactory.createTestServerless();
234+
delete sls.service["artifact"]
235+
const service = createService(sls);
236+
await service.uploadFunctions(app);
237+
238+
const defaultArtifact = path.join(".serverless", `${sls.service.getServiceName()}.zip`);
239+
240+
expect((FunctionAppService.prototype as any).sendFile).toBeCalledWith({
241+
method: "POST",
242+
uri: expectedUploadUrl,
243+
json: true,
244+
headers: {
245+
Authorization: `Bearer ${variables["azureCredentials"].tokenCache._entries[0].accessToken}`,
246+
Accept: "*/*",
247+
ContentType: "application/octet-stream",
248+
}
249+
}, defaultArtifact);
250+
const expectedArtifactName = service.getDeploymentName().replace("rg-deployment", "artifact");
251+
expect((AzureBlobStorageService.prototype as any).uploadFile).toBeCalledWith(
252+
defaultArtifact,
253+
configConstants.deploymentConfig.container,
254+
`${expectedArtifactName}.zip`,
255+
)
256+
const uploadCall = ((AzureBlobStorageService.prototype as any).uploadFile).mock.calls[0];
257+
expect(uploadCall[2]).toMatch(/.*-t([0-9]+)/)
258+
});
259+
225260
it("uploads functions with custom SCM domain (aka App service environments)", async () => {
226261
const customApp = {
227262
...MockFactory.createTestSite("CustomAppWithinASE"),
@@ -248,10 +283,19 @@ describe("Function App Service", () => {
248283
}, slsService["artifact"])
249284
});
250285

251-
it("throws an error with no zip file", async () => {
286+
it("uses default name when no artifact provided", async () => {
252287
const sls = MockFactory.createTestServerless();
253288
delete sls.service["artifact"];
254289
const service = createService(sls);
255-
await expect(service.uploadFunctions(app)).rejects.not.toBeNull()
290+
expect(service.getFunctionZipFile()).toEqual(path.join(".serverless", `${sls.service.getServiceName()}.zip`))
291+
});
292+
293+
it("uses package param from options if provided", async () => {
294+
const sls = MockFactory.createTestServerless();
295+
const options = MockFactory.createTestServerlessOptions({
296+
package: "fake.zip",
297+
});
298+
const service = createService(sls, options);
299+
expect(service.getFunctionZipFile()).toEqual("fake.zip")
256300
});
257301
});

src/services/functionAppService.ts

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,17 @@ export class FunctionAppService extends BaseService {
185185
});
186186
}
187187

188+
/**
189+
* Gets local path of packaged function app
190+
*/
191+
public getFunctionZipFile(): string {
192+
let functionZipFile = this.getOption("package") || this.serverless.service["artifact"];
193+
if (!functionZipFile) {
194+
functionZipFile = path.join(this.serverless.config.servicePath, ".serverless", `${this.serverless.service.getServiceName()}.zip`);
195+
}
196+
return functionZipFile;
197+
}
198+
188199
/**
189200
* Uploads artifact file to blob storage container
190201
*/
@@ -198,17 +209,6 @@ export class FunctionAppService extends BaseService {
198209
);
199210
}
200211

201-
/**
202-
* Gets local path of packaged function app
203-
*/
204-
private getFunctionZipFile(): string {
205-
let functionZipFile = this.serverless.service["artifact"];
206-
if (!functionZipFile) {
207-
functionZipFile = path.join(this.serverless.config.servicePath, ".serverless", `${this.serverless.service.getServiceName()}.zip`);
208-
}
209-
return functionZipFile;
210-
}
211-
212212
/**
213213
* Get rollback-configured artifact name. Contains `-t{timestamp}`
214214
* if rollback is configured

src/services/packageService.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,4 +92,4 @@ export class PackageService {
9292

9393
return Promise.resolve();
9494
}
95-
}
95+
}

src/test/mockFactory.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,14 +65,15 @@ export class MockFactory {
6565
sls.service = MockFactory.createTestService(sls.service["functions"]);
6666
}
6767

68-
public static createTestServerlessOptions(): Serverless.Options {
68+
public static createTestServerlessOptions(options?: any): Serverless.Options {
6969
return {
7070
extraServicePath: null,
7171
function: null,
7272
noDeploy: null,
7373
region: null,
7474
stage: null,
7575
watch: null,
76+
...options
7677
};
7778
}
7879

0 commit comments

Comments
 (0)