diff --git a/src/plugins/deploy/azureDeployPlugin.test.ts b/src/plugins/deploy/azureDeployPlugin.test.ts index f54ed117..52b98fb1 100644 --- a/src/plugins/deploy/azureDeployPlugin.test.ts +++ b/src/plugins/deploy/azureDeployPlugin.test.ts @@ -1,23 +1,31 @@ +import { Site } from "@azure/arm-appservice/esm/models"; +import Serverless from "serverless"; +import { ServerlessAzureOptions } from "../../models/serverless"; import { MockFactory } from "../../test/mockFactory"; import { invokeHook } from "../../test/utils"; import { AzureDeployPlugin } from "./azureDeployPlugin"; +import mockFs from "mock-fs" jest.mock("../../services/functionAppService"); import { FunctionAppService } from "../../services/functionAppService"; jest.mock("../../services/resourceService"); import { ResourceService } from "../../services/resourceService"; -import { Site } from "@azure/arm-appservice/esm/models"; -import { ServerlessAzureOptions } from "../../models/serverless"; -import Serverless from "serverless"; describe("Deploy plugin", () => { let sls: Serverless; let options: ServerlessAzureOptions; let plugin: AzureDeployPlugin; + beforeAll(() => { + mockFs({ + "serviceName.zip": "contents", + }, { createCwd: true, createTmp: true }); + }); + beforeEach(() => { - jest.resetAllMocks(); + FunctionAppService.prototype.getFunctionZipFile = jest.fn(() => "serviceName.zip"); + sls = MockFactory.createTestServerless(); options = MockFactory.createTestServerlessOptions(); @@ -26,9 +34,13 @@ describe("Deploy plugin", () => { afterEach(() => { jest.resetAllMocks(); + }); + + afterAll(() => { + mockFs.restore(); }) - it("calls deploy hook", async () => { + it("calls deploy", async () => { const deployResourceGroup = jest.fn(); const functionAppStub: Site = MockFactory.createTestSite(); const deploy = jest.fn(() => Promise.resolve(functionAppStub)); @@ -45,6 +57,27 @@ describe("Deploy plugin", () => { expect(uploadFunctions).toBeCalledWith(functionAppStub); }); + it("does not call deploy if zip does not exist", async () => { + const deployResourceGroup = jest.fn(); + const functionAppStub: Site = MockFactory.createTestSite(); + const deploy = jest.fn(() => Promise.resolve(functionAppStub)); + const uploadFunctions = jest.fn(); + + const zipFile = "fake.zip"; + + FunctionAppService.prototype.getFunctionZipFile = (() => zipFile); + ResourceService.prototype.deployResourceGroup = deployResourceGroup; + FunctionAppService.prototype.deploy = deploy; + FunctionAppService.prototype.uploadFunctions = uploadFunctions; + + await invokeHook(plugin, "deploy:deploy"); + + expect(deployResourceGroup).not.toBeCalled(); + expect(deploy).not.toBeCalled(); + expect(uploadFunctions).not.toBeCalled(); + expect(sls.cli.log).lastCalledWith(`Function app zip file '${zipFile}' does not exist`); + }); + it("lists deployments with timestamps", async () => { const deployments = MockFactory.createTestDeployments(5, true); ResourceService.prototype.getDeployments = jest.fn(() => Promise.resolve(deployments)); diff --git a/src/plugins/deploy/azureDeployPlugin.ts b/src/plugins/deploy/azureDeployPlugin.ts index 753f8b21..70f48227 100644 --- a/src/plugins/deploy/azureDeployPlugin.ts +++ b/src/plugins/deploy/azureDeployPlugin.ts @@ -1,9 +1,10 @@ +import fs from "fs"; import Serverless from "serverless"; import { FunctionAppService } from "../../services/functionAppService"; +import { AzureLoginOptions } from "../../services/loginService"; import { ResourceService } from "../../services/resourceService"; import { Utils } from "../../shared/utils"; import { AzureBasePlugin } from "../azureBasePlugin"; -import { AzureLoginOptions } from "../../services/loginService"; export class AzureDeployPlugin extends AzureBasePlugin { public hooks: { [eventName: string]: Promise }; @@ -28,13 +29,17 @@ export class AzureDeployPlugin extends AzureBasePlugin { } }, options: { - "resourceGroup": { + resourceGroup: { usage: "Resource group for the service", shortcut: "g", }, subscriptionId: { usage: "Sets the Azure subscription ID", shortcut: "i", + }, + package: { + usage: "Package to deploy", + shortcut: "p", } } } @@ -67,12 +72,14 @@ export class AzureDeployPlugin extends AzureBasePlugin { private async deploy() { const resourceService = new ResourceService(this.serverless, this.options); - - await resourceService.deployResourceGroup(); - const functionAppService = new FunctionAppService(this.serverless, this.options); + const zipFile = functionAppService.getFunctionZipFile(); + if (!fs.existsSync(zipFile)) { + this.log(`Function app zip file '${zipFile}' does not exist`); + return Promise.resolve(); + } + await resourceService.deployResourceGroup(); const functionApp = await functionAppService.deploy(); - await functionAppService.uploadFunctions(functionApp); } } diff --git a/src/plugins/login/azureLoginPlugin.test.ts b/src/plugins/login/azureLoginPlugin.test.ts index 846dfba6..18383181 100644 --- a/src/plugins/login/azureLoginPlugin.test.ts +++ b/src/plugins/login/azureLoginPlugin.test.ts @@ -32,7 +32,7 @@ describe("Login Plugin", () => { async function invokeLoginHook(hasCreds = false, serverless?: Serverless, options?: Serverless.Options) { const plugin = createPlugin(hasCreds, serverless, options); - await invokeHook(plugin, "before:package:initialize"); + await invokeHook(plugin, "before:deploy:deploy"); } beforeEach(() => { diff --git a/src/plugins/login/azureLoginPlugin.ts b/src/plugins/login/azureLoginPlugin.ts index fa0b246f..77311ae0 100644 --- a/src/plugins/login/azureLoginPlugin.ts +++ b/src/plugins/login/azureLoginPlugin.ts @@ -13,7 +13,7 @@ export class AzureLoginPlugin extends AzureBasePlugin { this.provider = (this.serverless.getProvider("azure") as any) as AzureProvider; this.hooks = { - "before:package:initialize": this.login.bind(this), + "before:deploy:deploy": this.login.bind(this), "before:deploy:list:list": this.login.bind(this), "before:invoke:invoke": this.login.bind(this), "before:rollback:rollback": this.login.bind(this), diff --git a/src/plugins/package/azurePackagePlugin.test.ts b/src/plugins/package/azurePackagePlugin.test.ts index f40c307d..d3d38676 100644 --- a/src/plugins/package/azurePackagePlugin.test.ts +++ b/src/plugins/package/azurePackagePlugin.test.ts @@ -39,5 +39,33 @@ describe("Azure Package Plugin", () => { it("cleans up package after package:finalize", async () => { await invokeHook(plugin, "after:package:finalize"); expect(PackageService.prototype.cleanUp).toBeCalled(); + }); + + describe("Package specified in options", () => { + + beforeEach(() => { + plugin = new AzurePackagePlugin(sls, MockFactory.createTestServerlessOptions({ + package: "fake.zip", + })); + }); + + it("does not call create bindings if package specified in options", async () => { + await invokeHook(plugin, "before:package:setupProviderConfiguration"); + expect(PackageService.prototype.createBindings).not.toBeCalled(); + expect(sls.cli.log).lastCalledWith("No need to create bindings. Using pre-existing package"); + }); + + it("does not call webpack if package specified in options", async () => { + await invokeHook(plugin, "before:webpack:package:packageModules"); + expect(PackageService.prototype.createBindings).not.toBeCalled(); + expect(PackageService.prototype.prepareWebpack).not.toBeCalled(); + expect(sls.cli.log).lastCalledWith("No need to perform webpack. Using pre-existing package"); + }); + + it("does not call finalize if package specified in options", async () => { + await invokeHook(plugin, "after:package:finalize"); + expect(PackageService.prototype.cleanUp).not.toBeCalled(); + expect(sls.cli.log).lastCalledWith("No need to clean up generated folders & files. Using pre-existing package"); + }); }) }); diff --git a/src/plugins/package/azurePackagePlugin.ts b/src/plugins/package/azurePackagePlugin.ts index abfb7b92..9fe86c34 100644 --- a/src/plugins/package/azurePackagePlugin.ts +++ b/src/plugins/package/azurePackagePlugin.ts @@ -22,6 +22,10 @@ export class AzurePackagePlugin extends AzureBasePlugin { } private async setupProviderConfiguration(): Promise { + if (this.getOption("package")) { + this.log("No need to create bindings. Using pre-existing package"); + return Promise.resolve(); + } await this.packageService.createBindings(); this.bindingsCreated = true; @@ -29,6 +33,10 @@ export class AzurePackagePlugin extends AzureBasePlugin { } private async webpack(): Promise { + if (this.getOption("package")) { + this.log("No need to perform webpack. Using pre-existing package"); + return Promise.resolve(); + } if (!this.bindingsCreated) { await this.setupProviderConfiguration(); } @@ -40,7 +48,10 @@ export class AzurePackagePlugin extends AzureBasePlugin { * Cleans up generated folders & files after packaging is complete */ private async finalize(): Promise { + if (this.getOption("package")) { + this.log("No need to clean up generated folders & files. Using pre-existing package"); + return Promise.resolve(); + } await this.packageService.cleanUp(); } } - diff --git a/src/services/baseService.ts b/src/services/baseService.ts index f7f1e795..58d0d50f 100644 --- a/src/services/baseService.ts +++ b/src/services/baseService.ts @@ -121,7 +121,7 @@ export abstract class BaseService { protected getAccessToken(): string { return (this.credentials.tokenCache as any)._entries[0].accessToken; } - + /** * Sends an API request using axios HTTP library * @param method The HTTP method @@ -189,6 +189,10 @@ export abstract class BaseService { return "config" in this.options ? this.options["config"] : "serverless.yml"; } + protected getOption(key: string, defaultValue?: any) { + return Utils.get(this.options, key, defaultValue); + } + 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 @@ -231,4 +235,4 @@ export abstract class BaseService { } return timestamp; } -} \ No newline at end of file +} diff --git a/src/services/functionAppService.test.ts b/src/services/functionAppService.test.ts index e81cef81..9de1cf7a 100644 --- a/src/services/functionAppService.test.ts +++ b/src/services/functionAppService.test.ts @@ -1,6 +1,7 @@ import axios from "axios"; import MockAdapter from "axios-mock-adapter"; import mockFs from "mock-fs"; +import path from "path"; import Serverless from "serverless"; import { MockFactory } from "../test/mockFactory"; import { FunctionAppService } from "./functionAppService"; @@ -55,6 +56,9 @@ describe("Function App Service", () => { mockFs({ "app.zip": "contents", + ".serverless": { + "serviceName.zip": "contents", + } }, { createCwd: true, createTmp: true }); }); @@ -222,6 +226,37 @@ describe("Function App Service", () => { expect(uploadCall[2]).toMatch(/.*-t([0-9]+)/) }); + it("uploads functions to function app and blob storage with default naming convention", async () => { + const scmDomain = app.enabledHostNames.find((hostname) => hostname.endsWith("scm.azurewebsites.net")); + const expectedUploadUrl = `https://${scmDomain}/api/zipdeploy/`; + + const sls = MockFactory.createTestServerless(); + delete sls.service["artifact"] + const service = createService(sls); + await service.uploadFunctions(app); + + const defaultArtifact = path.join(".serverless", `${sls.service.getServiceName()}.zip`); + + expect((FunctionAppService.prototype as any).sendFile).toBeCalledWith({ + method: "POST", + uri: expectedUploadUrl, + json: true, + headers: { + Authorization: `Bearer ${variables["azureCredentials"].tokenCache._entries[0].accessToken}`, + Accept: "*/*", + ContentType: "application/octet-stream", + } + }, defaultArtifact); + const expectedArtifactName = service.getDeploymentName().replace("rg-deployment", "artifact"); + expect((AzureBlobStorageService.prototype as any).uploadFile).toBeCalledWith( + defaultArtifact, + configConstants.deploymentConfig.container, + `${expectedArtifactName}.zip`, + ) + const uploadCall = ((AzureBlobStorageService.prototype as any).uploadFile).mock.calls[0]; + expect(uploadCall[2]).toMatch(/.*-t([0-9]+)/) + }); + it("uploads functions with custom SCM domain (aka App service environments)", async () => { const customApp = { ...MockFactory.createTestSite("CustomAppWithinASE"), @@ -248,10 +283,19 @@ describe("Function App Service", () => { }, slsService["artifact"]) }); - it("throws an error with no zip file", async () => { + it("uses default name when no artifact provided", async () => { const sls = MockFactory.createTestServerless(); delete sls.service["artifact"]; const service = createService(sls); - await expect(service.uploadFunctions(app)).rejects.not.toBeNull() + expect(service.getFunctionZipFile()).toEqual(path.join(".serverless", `${sls.service.getServiceName()}.zip`)) + }); + + it("uses package param from options if provided", async () => { + const sls = MockFactory.createTestServerless(); + const options = MockFactory.createTestServerlessOptions({ + package: "fake.zip", + }); + const service = createService(sls, options); + expect(service.getFunctionZipFile()).toEqual("fake.zip") }); }); diff --git a/src/services/functionAppService.ts b/src/services/functionAppService.ts index 7f8f8201..b0eb5cd5 100644 --- a/src/services/functionAppService.ts +++ b/src/services/functionAppService.ts @@ -185,6 +185,17 @@ export class FunctionAppService extends BaseService { }); } + /** + * Gets local path of packaged function app + */ + public getFunctionZipFile(): string { + let functionZipFile = this.getOption("package") || this.serverless.service["artifact"]; + if (!functionZipFile) { + functionZipFile = path.join(this.serverless.config.servicePath, ".serverless", `${this.serverless.service.getServiceName()}.zip`); + } + return functionZipFile; + } + /** * Uploads artifact file to blob storage container */ @@ -198,17 +209,6 @@ export class FunctionAppService extends BaseService { ); } - /** - * Gets local path of packaged function app - */ - private getFunctionZipFile(): string { - let functionZipFile = this.serverless.service["artifact"]; - if (!functionZipFile) { - functionZipFile = path.join(this.serverless.config.servicePath, ".serverless", `${this.serverless.service.getServiceName()}.zip`); - } - return functionZipFile; - } - /** * Get rollback-configured artifact name. Contains `-t{timestamp}` * if rollback is configured diff --git a/src/services/packageService.ts b/src/services/packageService.ts index 89127a42..86a8d2d7 100644 --- a/src/services/packageService.ts +++ b/src/services/packageService.ts @@ -92,4 +92,4 @@ export class PackageService { return Promise.resolve(); } -} \ No newline at end of file +} diff --git a/src/test/mockFactory.ts b/src/test/mockFactory.ts index 485b689a..e453e20b 100644 --- a/src/test/mockFactory.ts +++ b/src/test/mockFactory.ts @@ -65,7 +65,7 @@ export class MockFactory { sls.service = MockFactory.createTestService(sls.service["functions"]); } - public static createTestServerlessOptions(): Serverless.Options { + public static createTestServerlessOptions(options?: any): Serverless.Options { return { extraServicePath: null, function: null, @@ -73,6 +73,7 @@ export class MockFactory { region: null, stage: null, watch: null, + ...options }; }