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

feat: Deploy pre-existing package with -p CLI option #205

Merged
merged 2 commits into from
Jul 19, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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({
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I would extract the toBeCalledWith arguments out just so it's easier to read

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(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: extract AzureBlobStorageService.prototype as any

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