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

feat: Deploy function app code from blob storage URL #231

Merged
merged 2 commits into from
Aug 13, 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
23 changes: 13 additions & 10 deletions docs/DEPLOY.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,23 +69,26 @@ then user try to deploy
```yml
service: my-app
provider:
...
deployment:
# Rollback enabled, deploying to blob storage
# Default is true
# If false, deploys directly to function app
rollback: true
# Container in blob storage containing deployed packages
# Default is DEPLOYMENT_ARTIFACTS
container: MY_CONTAINER_NAME
# Sets the WEBSITE_RUN_FROM_PACKAGE setting of function app
# to the SAS url of the artifact sitting in blob storage
# Recommended when using linux, not recommended when using windows
# Default is false
runFromBlobUrl: true

plugins:
- serverless-azure-functions

package:
...

deploy:
# Rollback enabled, deploying to blob storage
# Default is true
# If false, deploys directly to function app
rollback: true
# Container in blob storage containing deployed packages
# Default is DEPLOYMENT_ARTIFACTS
container: MY_CONTAINER_NAME

functions:
...
```
Expand Down
1 change: 1 addition & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export const configConstants = {
funcCoreTools: "func",
funcCoreToolsArgs: ["host", "start"],
funcConsoleColor: "blue",
runFromPackageSetting: "WEBSITE_RUN_FROM_PACKAGE",
jsonContentType: "application/json",
logInvocationsApiPath: "/azurejobs/api/functions/definitions/",
logOutputApiPath: "/azurejobs/api/log/output/",
Expand Down
1 change: 1 addition & 0 deletions src/models/serverless.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export interface ServerlessAzureProvider {
environment?: {
[key: string]: any;
};
deployment?: DeploymentConfig;
deploymentName?: string;
resourceGroup?: string;
apim?: ApiManagementConfig;
Expand Down
3 changes: 2 additions & 1 deletion src/plugins/login/azureLoginPlugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ describe("Login Plugin", () => {
});

it("calls login if azure credentials are not set", async () => {
unsetServicePrincipalEnvVariables();
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This test was failing for some reason... Not sure how it got through the checks

Copy link
Contributor

Choose a reason for hiding this comment

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

huh, that's weird, I thought it had been added in the tests, good thing they work now then

await invokeLoginHook();
expect(AzureLoginService.interactiveLogin).toBeCalled();
expect(AzureLoginService.servicePrincipalLogin).not.toBeCalled();
Expand Down Expand Up @@ -115,7 +116,7 @@ describe("Login Plugin", () => {
expect(sls.variables["subscriptionId"]).toEqual("azureSubId");
expect(sls.cli.log).toBeCalledWith("Using subscription ID: azureSubId");
});

it("Uses the subscription ID specified in serverless yaml", async () => {
const sls = MockFactory.createTestServerless();
const opt = MockFactory.createTestServerlessOptions();
Expand Down
11 changes: 8 additions & 3 deletions src/services/baseService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export abstract class BaseService {
protected subscriptionId: string;
protected resourceGroup: string;
protected deploymentName: string;
protected artifactName: string;
protected deploymentConfig: DeploymentConfig;
protected storageAccountName: string;
protected config: ServerlessAzureConfig;
Expand All @@ -43,6 +44,7 @@ export abstract class BaseService {
this.resourceGroup = this.getResourceGroupName();
this.deploymentConfig = this.getDeploymentConfig();
this.deploymentName = this.getDeploymentName();
this.artifactName = this.getArtifactName(this.deploymentName);
this.storageAccountName = StorageAccountResource.getResourceName(this.config);

if (!this.credentials && authenticate) {
Expand Down Expand Up @@ -84,10 +86,9 @@ export abstract class BaseService {
* Defaults can be found in the `config.ts` file
*/
public getDeploymentConfig(): DeploymentConfig {
const providedConfig = this.serverless["deploy"] as DeploymentConfig;
return {
...configConstants.deploymentConfig,
...providedConfig,
...this.config.provider.deployment,
}
}

Expand Down Expand Up @@ -123,7 +124,7 @@ export abstract class BaseService {
const { deployment, artifact } = configConstants.naming.suffix;
return `${deploymentName
.replace(`rg-${deployment}`, artifact)
.replace(deployment, artifact)}`
.replace(deployment, artifact)}.zip`
}

/**
Expand Down Expand Up @@ -189,6 +190,10 @@ 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
*/
Expand Down
90 changes: 89 additions & 1 deletion src/services/functionAppService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ jest.mock("./azureBlobStorageService");
import { AzureBlobStorageService } from "./azureBlobStorageService"
import configConstants from "../config";


describe("Function App Service", () => {
const app = MockFactory.createTestSite();
const slsService = MockFactory.createTestService();
Expand All @@ -37,6 +36,11 @@ describe("Function App Service", () => {
const syncTriggersUrl = `${baseUrl}${app.id}/syncfunctiontriggers?api-version=2016-08-01`;
const listFunctionsUrl = `${baseUrl}${app.id}/functions?api-version=2016-08-01`;

const appSettings = {
setting1: "value1",
setting2: "value2",
}

beforeAll(() => {
const axiosMock = new MockAdapter(axios);

Expand Down Expand Up @@ -66,6 +70,8 @@ describe("Function App Service", () => {
WebSiteManagementClient.prototype.webApps = {
get: jest.fn(() => app),
deleteFunction: jest.fn(),
listApplicationSettings: jest.fn(() => Promise.resolve({ properties: { ...appSettings } })),
updateApplicationSettings: jest.fn(),
} as any;
(FunctionAppService.prototype as any).sendFile = jest.fn();
});
Expand Down Expand Up @@ -156,11 +162,22 @@ describe("Function App Service", () => {

beforeEach(() => {
FunctionAppService.prototype.get = jest.fn(() => Promise.resolve(expectedSite));
(FunctionAppService.prototype as any).sendFile = jest.fn();
ArmService.prototype.createDeploymentFromConfig = jest.fn(() => Promise.resolve(expectedDeployment));
ArmService.prototype.createDeploymentFromType = jest.fn(() => Promise.resolve(expectedDeployment));
ArmService.prototype.deployTemplate = jest.fn(() => Promise.resolve(null));
WebSiteManagementClient.prototype.webApps = {
get: jest.fn(() => app),
deleteFunction: jest.fn(),
listApplicationSettings: jest.fn(() => Promise.resolve({ properties: { ...appSettings } })),
updateApplicationSettings: jest.fn(),
} as any;
});

afterEach(() => {
jest.restoreAllMocks();
})

it("deploys ARM templates with custom configuration", async () => {
slsService.provider["armTemplate"] = {};

Expand Down Expand Up @@ -298,4 +315,75 @@ describe("Function App Service", () => {
const service = createService(sls, options);
expect(service.getFunctionZipFile()).toEqual("fake.zip")
});

it("adds a new function app setting", async () => {
const service = createService();
const settingName = "TEST_SETTING";
const settingValue = "TEST_VALUE"
await service.updateFunctionAppSetting(app, settingName, settingValue);
expect(WebSiteManagementClient.prototype.webApps.updateApplicationSettings).toBeCalledWith(
"myResourceGroup",
"Test",
{
...appSettings,
TEST_SETTING: settingValue
}
)
});

it("updates an existing function app setting", async () => {
const service = createService();
const settingName = "setting1";
const settingValue = "TEST_VALUE"
await service.updateFunctionAppSetting(app, settingName, settingValue);
expect(WebSiteManagementClient.prototype.webApps.updateApplicationSettings).toBeCalledWith(
"myResourceGroup",
"Test",
{
setting1: settingValue,
setting2: appSettings.setting2
}
);
});

describe("Updating Function App Settings", () => {

const sasUrl = "sasUrl"

beforeEach(() => {
FunctionAppService.prototype.updateFunctionAppSetting = jest.fn();
AzureBlobStorageService.prototype.generateBlobSasTokenUrl = jest.fn(() => Promise.resolve(sasUrl));
});

afterEach(() => {
(FunctionAppService.prototype.updateFunctionAppSetting as any).mockRestore();
});

it("updates WEBSITE_RUN_FROM_PACKAGE with SAS URL if configured to run from blob", async () => {
const newSlsService = MockFactory.createTestService();
newSlsService.provider["deployment"] = {
runFromBlobUrl: true,
}

const service = createService(MockFactory.createTestServerless({
service: newSlsService,
}));
await service.uploadFunctions(app);
expect(AzureBlobStorageService.prototype.generateBlobSasTokenUrl).toBeCalled();
expect(FunctionAppService.prototype.updateFunctionAppSetting).toBeCalledWith(
app,
"WEBSITE_RUN_FROM_PACKAGE",
sasUrl
);
});

it("does not generate SAS URL or update WEBSITE_RUN_FROM_PACKAGE if not configured to run from blob", async() => {
const service = createService();
await service.uploadFunctions(app);
expect(AzureBlobStorageService.prototype.generateBlobSasTokenUrl).not.toBeCalled();
expect(FunctionAppService.prototype.updateFunctionAppSetting).not.toBeCalled();
});
});


});
45 changes: 29 additions & 16 deletions src/services/functionAppService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,14 @@ import { FunctionAppResource } from "../armTemplates/resources/functionApp";
import { ArmDeployment } from "../models/armTemplates";
import { FunctionAppHttpTriggerConfig } from "../models/functionApp";
import { Guard } from "../shared/guard";
import { Utils } from "../shared/utils";
import { ArmService } from "./armService";
import { AzureBlobStorageService } from "./azureBlobStorageService";
import { BaseService } from "./baseService";
import { Utils } from "../shared/utils";
import configConstants from "../config";

export class FunctionAppService extends BaseService {
private static readonly retryCount: number = 10;
private static readonly retryCount: number = 30;
private static readonly retryInterval: number = 5000;
private webClient: WebSiteManagementClient;
private blobService: AzureBlobStorageService;
Expand Down Expand Up @@ -99,7 +100,7 @@ export class FunctionAppService extends BaseService {

if (listFunctionsResponse.status !== 200 || listFunctionsResponse.data.value.length === 0) {
this.log("-> Function App not ready. Retrying...");
throw new Error(listFunctionsResponse.data);
throw new Error(JSON.stringify(listFunctionsResponse.data, null, 2));
}

return listFunctionsResponse;
Expand Down Expand Up @@ -130,7 +131,7 @@ export class FunctionAppService extends BaseService {

if (getFunctionResponse.status !== 200) {
this.log("-> Function app not ready. Retrying...")
throw new Error(response.data);
throw new Error(JSON.stringify(response.data, null, 2));
}

return getFunctionResponse;
Expand All @@ -150,8 +151,21 @@ export class FunctionAppService extends BaseService {
const functionZipFile = this.getFunctionZipFile();
const uploadFunctionApp = this.uploadZippedArfifactToFunctionApp(functionApp, functionZipFile);
const uploadBlobStorage = this.uploadZippedArtifactToBlobStorage(functionZipFile);

await Promise.all([uploadFunctionApp, uploadBlobStorage]);

if (this.deploymentConfig.runFromBlobUrl) {
this.log("Updating function app setting to run from external package...");
const sasUrl = await this.blobService.generateBlobSasTokenUrl(
this.deploymentConfig.container,
this.artifactName
)
await this.updateFunctionAppSetting(
functionApp,
configConstants.runFromPackageSetting,
sasUrl
)
}

this.log("Deployed serverless functions:")
const serverlessFunctions = this.serverless.service.getAllFunctions();
Expand All @@ -178,9 +192,10 @@ export class FunctionAppService extends BaseService {
this.log(`Creating function app: ${this.serviceName}`);

const armService = new ArmService(this.serverless, this.options);
let deployment: ArmDeployment = this.config.provider.armTemplate
? await armService.createDeploymentFromConfig(this.config.provider.armTemplate)
: await armService.createDeploymentFromType(this.config.provider.type || "consumption");
const { armTemplate, type } = this.config.provider;
let deployment: ArmDeployment = armTemplate
? await armService.createDeploymentFromConfig(armTemplate)
: await armService.createDeploymentFromType(type || "consumption");

await armService.deployTemplate(deployment);

Expand Down Expand Up @@ -226,6 +241,12 @@ export class FunctionAppService extends BaseService {
return functionZipFile;
}

public async updateFunctionAppSetting(functionApp: Site, setting: string, value: string) {
const { properties } = await this.webClient.webApps.listApplicationSettings(this.resourceGroup, functionApp.name);
properties[setting] = value;
await this.webClient.webApps.updateApplicationSettings(this.resourceGroup, functionApp.name, properties);
}

/**
* Uploads artifact file to blob storage container
*/
Expand All @@ -235,18 +256,10 @@ export class FunctionAppService extends BaseService {
await this.blobService.uploadFile(
functionZipFile,
this.deploymentConfig.container,
this.getArtifactName(this.deploymentName),
this.artifactName,
);
}

/**
* Get rollback-configured artifact name. Contains `-t{timestamp}`
* if rollback is configured
*/
public getArtifactName(deploymentName: string): string {
return `${deploymentName.replace("rg-deployment", "artifact")}.zip`;
}

public getFunctionHttpTriggerConfig(functionApp: Site, functionConfig: FunctionEnvelope): FunctionAppHttpTriggerConfig {
const httpTrigger = functionConfig.config.bindings.find((binding) => {
return binding.type === "httpTrigger";
Expand Down
4 changes: 2 additions & 2 deletions src/services/rollbackService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ describe("Rollback Service", () => {
const sasURL = "sasURL";
const containerName = "deployment-artifacts";
const artifactName = MockFactory.createTestDeployment().name.replace(
configConstants.naming.suffix.deployment, configConstants.naming.suffix.artifact);
configConstants.naming.suffix.deployment, configConstants.naming.suffix.artifact) + ".zip";
const artifactPath = `.serverless${path.sep}${artifactName}`
const armDeployment: ArmDeployment = { template, parameters };
const deploymentString = "deployments";
Expand Down Expand Up @@ -115,7 +115,7 @@ describe("Rollback Service", () => {
const deploymentConfig: DeploymentConfig = {
runFromBlobUrl: true
}
sls["deploy"] = deploymentConfig;
sls.service.provider["deployment"] = deploymentConfig;
const service = createService(sls);
await service.rollback();
expect(AzureBlobStorageService.prototype.initialize).toBeCalled();
Expand Down