Skip to content

Commit

Permalink
feat: Deploy pre-existing package with -p CLI option (#205)
Browse files Browse the repository at this point in the history
- [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]
  • Loading branch information
tbarlow12 authored Jul 19, 2019
1 parent 7ceb484 commit 2b97f75
Show file tree
Hide file tree
Showing 11 changed files with 159 additions and 31 deletions.
43 changes: 38 additions & 5 deletions src/plugins/deploy/azureDeployPlugin.test.ts
Original file line number Diff line number Diff line change
@@ -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();

Expand All @@ -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));
Expand All @@ -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));
Expand Down
19 changes: 13 additions & 6 deletions src/plugins/deploy/azureDeployPlugin.ts
Original file line number Diff line number Diff line change
@@ -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<AzureLoginOptions> {
public hooks: { [eventName: string]: Promise<any> };
Expand All @@ -28,13 +29,17 @@ export class AzureDeployPlugin extends AzureBasePlugin<AzureLoginOptions> {
}
},
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",
}
}
}
Expand Down Expand Up @@ -67,12 +72,14 @@ export class AzureDeployPlugin extends AzureBasePlugin<AzureLoginOptions> {

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);
}
}
2 changes: 1 addition & 1 deletion src/plugins/login/azureLoginPlugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => {
Expand Down
2 changes: 1 addition & 1 deletion src/plugins/login/azureLoginPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export class AzureLoginPlugin extends AzureBasePlugin<AzureLoginOptions> {
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),
Expand Down
28 changes: 28 additions & 0 deletions src/plugins/package/azurePackagePlugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
});
})
});
13 changes: 12 additions & 1 deletion src/plugins/package/azurePackagePlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,21 @@ export class AzurePackagePlugin extends AzureBasePlugin {
}

private async setupProviderConfiguration(): Promise<void> {
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;

return Promise.resolve();
}

private async webpack(): Promise<void> {
if (this.getOption("package")) {
this.log("No need to perform webpack. Using pre-existing package");
return Promise.resolve();
}
if (!this.bindingsCreated) {
await this.setupProviderConfiguration();
}
Expand All @@ -40,7 +48,10 @@ export class AzurePackagePlugin extends AzureBasePlugin {
* Cleans up generated folders & files after packaging is complete
*/
private async finalize(): Promise<void> {
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();
}
}

8 changes: 6 additions & 2 deletions src/services/baseService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -231,4 +235,4 @@ export abstract class BaseService {
}
return timestamp;
}
}
}
48 changes: 46 additions & 2 deletions src/services/functionAppService.test.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -55,6 +56,9 @@ describe("Function App Service", () => {

mockFs({
"app.zip": "contents",
".serverless": {
"serviceName.zip": "contents",
}
}, { createCwd: true, createTmp: true });
});

Expand Down Expand Up @@ -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"),
Expand All @@ -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")
});
});
22 changes: 11 additions & 11 deletions src/services/functionAppService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/services/packageService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,4 +92,4 @@ export class PackageService {

return Promise.resolve();
}
}
}
3 changes: 2 additions & 1 deletion src/test/mockFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,14 +65,15 @@ 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,
noDeploy: null,
region: null,
stage: null,
watch: null,
...options
};
}

Expand Down

0 comments on commit 2b97f75

Please sign in to comment.