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

Commit fd762b4

Browse files
committed
feat: Update function app setting to run from external package
1 parent 676960a commit fd762b4

File tree

5 files changed

+119
-74
lines changed

5 files changed

+119
-74
lines changed

src/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export const configConstants = {
2222
funcCoreTools: "func",
2323
funcCoreToolsArgs: ["host", "start"],
2424
funcConsoleColor: "blue",
25+
runFromPackageSetting: "WEBSITE_RUN_FROM_PACKAGE",
2526
jsonContentType: "application/json",
2627
logInvocationsApiPath: "/azurejobs/api/functions/definitions/",
2728
logOutputApiPath: "/azurejobs/api/log/output/",

src/plugins/login/azureLoginPlugin.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ describe("Login Plugin", () => {
5252
});
5353

5454
it("calls login if azure credentials are not set", async () => {
55+
unsetServicePrincipalEnvVariables();
5556
await invokeLoginHook();
5657
expect(AzureLoginService.interactiveLogin).toBeCalled();
5758
expect(AzureLoginService.servicePrincipalLogin).not.toBeCalled();
@@ -115,7 +116,7 @@ describe("Login Plugin", () => {
115116
expect(sls.variables["subscriptionId"]).toEqual("azureSubId");
116117
expect(sls.cli.log).toBeCalledWith("Using subscription ID: azureSubId");
117118
});
118-
119+
119120
it("Uses the subscription ID specified in serverless yaml", async () => {
120121
const sls = MockFactory.createTestServerless();
121122
const opt = MockFactory.createTestServerlessOptions();

src/services/baseService.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,10 @@ export abstract class BaseService {
190190
(this.serverless.cli.log as any)(message, entity, options);
191191
}
192192

193+
protected prettyPrint(object: any) {
194+
this.log(JSON.stringify(object, null, 2));
195+
}
196+
193197
/**
194198
* Get function objects
195199
*/

src/services/functionAppService.test.ts

Lines changed: 89 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ describe("Function App Service", () => {
3636
const syncTriggersUrl = `${baseUrl}${app.id}/syncfunctiontriggers?api-version=2016-08-01`;
3737
const listFunctionsUrl = `${baseUrl}${app.id}/functions?api-version=2016-08-01`;
3838

39+
const appSettings = {
40+
setting1: "value1",
41+
setting2: "value2",
42+
}
43+
3944
beforeAll(() => {
4045
const axiosMock = new MockAdapter(axios);
4146

@@ -65,6 +70,8 @@ describe("Function App Service", () => {
6570
WebSiteManagementClient.prototype.webApps = {
6671
get: jest.fn(() => app),
6772
deleteFunction: jest.fn(),
73+
listApplicationSettings: jest.fn(() => Promise.resolve({ properties: { ...appSettings } })),
74+
updateApplicationSettings: jest.fn(),
6875
} as any;
6976
(FunctionAppService.prototype as any).sendFile = jest.fn();
7077
});
@@ -155,11 +162,22 @@ describe("Function App Service", () => {
155162

156163
beforeEach(() => {
157164
FunctionAppService.prototype.get = jest.fn(() => Promise.resolve(expectedSite));
165+
(FunctionAppService.prototype as any).sendFile = jest.fn();
158166
ArmService.prototype.createDeploymentFromConfig = jest.fn(() => Promise.resolve(expectedDeployment));
159167
ArmService.prototype.createDeploymentFromType = jest.fn(() => Promise.resolve(expectedDeployment));
160168
ArmService.prototype.deployTemplate = jest.fn(() => Promise.resolve(null));
169+
WebSiteManagementClient.prototype.webApps = {
170+
get: jest.fn(() => app),
171+
deleteFunction: jest.fn(),
172+
listApplicationSettings: jest.fn(() => Promise.resolve({ properties: { ...appSettings } })),
173+
updateApplicationSettings: jest.fn(),
174+
} as any;
161175
});
162176

177+
afterEach(() => {
178+
jest.restoreAllMocks();
179+
})
180+
163181
it("deploys ARM templates with custom configuration", async () => {
164182
slsService.provider["armTemplate"] = {};
165183

@@ -184,63 +202,6 @@ describe("Function App Service", () => {
184202
expect(ArmService.prototype.deployTemplate).toBeCalledWith(expectedDeployment);
185203
});
186204

187-
it("deploys ARM template with SAS URL if running from blob URL", async() => {
188-
const sasUrl = "sasUrl";
189-
AzureBlobStorageService.prototype.generateBlobSasTokenUrl = jest.fn(() => Promise.resolve(sasUrl));
190-
191-
const newSlsService = MockFactory.createTestService();
192-
newSlsService.provider["armTemplate"] = null;
193-
newSlsService.provider["deployment"] = {
194-
runFromBlobUrl: true,
195-
}
196-
197-
const service = createService(MockFactory.createTestServerless({
198-
service: newSlsService,
199-
}));
200-
201-
const site = await service.deploy();
202-
203-
// Deploy should upload to blob FIRST and then set the SAS URL
204-
// as the WEBSITE_RUN_FROM_PACKAGE setting in the template
205-
const uploadFileCalls = (AzureBlobStorageService.prototype.uploadFile as any).mock.calls;
206-
expect(uploadFileCalls).toHaveLength(1);
207-
const call = uploadFileCalls[0];
208-
expect(call[0]).toEqual("app.zip");
209-
expect(call[1]).toEqual("deployment-artifacts");
210-
expect(call[2]).toMatch(/myDeploymentName-t([0-9])+.zip/);
211-
212-
expect(site).toEqual(expectedSite);
213-
expect(ArmService.prototype.createDeploymentFromConfig).not.toBeCalled();
214-
expect(ArmService.prototype.createDeploymentFromType).toBeCalledWith(ArmTemplateType.Consumption);
215-
// Should set parameter of arm template to include SAS URL
216-
expect(ArmService.prototype.deployTemplate).toBeCalledWith({
217-
...expectedDeployment,
218-
parameters: {
219-
...expectedDeployment.parameters,
220-
functionAppRunFromPackage: sasUrl,
221-
}
222-
});
223-
});
224-
225-
it("does not generate SAS URL if not configured", async() => {
226-
AzureBlobStorageService.prototype.generateBlobSasTokenUrl = jest.fn();
227-
228-
const newSlsService = MockFactory.createTestService();
229-
newSlsService.provider["armTemplate"] = null;
230-
newSlsService.provider["deployment"] = {
231-
runFromBlobUrl: false,
232-
}
233-
234-
const service = createService(MockFactory.createTestServerless({
235-
service: newSlsService,
236-
}));
237-
238-
await service.deploy();
239-
240-
expect(AzureBlobStorageService.prototype.generateBlobSasTokenUrl).not.toBeCalled();
241-
expect(ArmService.prototype.deployTemplate).toBeCalledWith(expectedDeployment);
242-
});
243-
244205
it("deploys ARM template from well-known configuration", async () => {
245206
slsService.provider["armTemplate"] = null;
246207
slsService.provider["type"] = "premium";
@@ -354,4 +315,75 @@ describe("Function App Service", () => {
354315
const service = createService(sls, options);
355316
expect(service.getFunctionZipFile()).toEqual("fake.zip")
356317
});
318+
319+
it("adds a new function app setting", async () => {
320+
const service = createService();
321+
const settingName = "TEST_SETTING";
322+
const settingValue = "TEST_VALUE"
323+
await service.updateFunctionAppSetting(app, settingName, settingValue);
324+
expect(WebSiteManagementClient.prototype.webApps.updateApplicationSettings).toBeCalledWith(
325+
"myResourceGroup",
326+
"Test",
327+
{
328+
...appSettings,
329+
TEST_SETTING: settingValue
330+
}
331+
)
332+
});
333+
334+
it("updates an existing function app setting", async () => {
335+
const service = createService();
336+
const settingName = "setting1";
337+
const settingValue = "TEST_VALUE"
338+
await service.updateFunctionAppSetting(app, settingName, settingValue);
339+
expect(WebSiteManagementClient.prototype.webApps.updateApplicationSettings).toBeCalledWith(
340+
"myResourceGroup",
341+
"Test",
342+
{
343+
setting1: settingValue,
344+
setting2: appSettings.setting2
345+
}
346+
);
347+
});
348+
349+
describe("Updating Function App Settings", () => {
350+
351+
const sasUrl = "sasUrl"
352+
353+
beforeEach(() => {
354+
FunctionAppService.prototype.updateFunctionAppSetting = jest.fn();
355+
AzureBlobStorageService.prototype.generateBlobSasTokenUrl = jest.fn(() => Promise.resolve(sasUrl));
356+
});
357+
358+
afterEach(() => {
359+
(FunctionAppService.prototype.updateFunctionAppSetting as any).mockRestore();
360+
});
361+
362+
it("updates WEBSITE_RUN_FROM_PACKAGE with SAS URL if configured to run from blob", async () => {
363+
const newSlsService = MockFactory.createTestService();
364+
newSlsService.provider["deployment"] = {
365+
runFromBlobUrl: true,
366+
}
367+
368+
const service = createService(MockFactory.createTestServerless({
369+
service: newSlsService,
370+
}));
371+
await service.uploadFunctions(app);
372+
expect(AzureBlobStorageService.prototype.generateBlobSasTokenUrl).toBeCalled();
373+
expect(FunctionAppService.prototype.updateFunctionAppSetting).toBeCalledWith(
374+
app,
375+
"WEBSITE_RUN_FROM_PACKAGE",
376+
sasUrl
377+
);
378+
});
379+
380+
it("does not generate SAS URL or update WEBSITE_RUN_FROM_PACKAGE if not configured to run from blob", async() => {
381+
const service = createService();
382+
await service.uploadFunctions(app);
383+
expect(AzureBlobStorageService.prototype.generateBlobSasTokenUrl).not.toBeCalled();
384+
expect(FunctionAppService.prototype.updateFunctionAppSetting).not.toBeCalled();
385+
});
386+
});
387+
388+
357389
});

src/services/functionAppService.ts

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,10 @@ import { Utils } from "../shared/utils";
1111
import { ArmService } from "./armService";
1212
import { AzureBlobStorageService } from "./azureBlobStorageService";
1313
import { BaseService } from "./baseService";
14+
import configConstants from "../config";
1415

1516
export class FunctionAppService extends BaseService {
16-
private static readonly retryCount: number = 10;
17+
private static readonly retryCount: number = 30;
1718
private static readonly retryInterval: number = 5000;
1819
private webClient: WebSiteManagementClient;
1920
private blobService: AzureBlobStorageService;
@@ -99,7 +100,7 @@ export class FunctionAppService extends BaseService {
99100

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

105106
return listFunctionsResponse;
@@ -130,7 +131,7 @@ export class FunctionAppService extends BaseService {
130131

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

136137
return getFunctionResponse;
@@ -149,15 +150,22 @@ export class FunctionAppService extends BaseService {
149150

150151
const functionZipFile = this.getFunctionZipFile();
151152
const uploadFunctionApp = this.uploadZippedArfifactToFunctionApp(functionApp, functionZipFile);
152-
// If `runFromBlobUrl` is configured, the artifact will have already been uploaded
153-
const uploadBlobStorage = (this.deploymentConfig.runFromBlobUrl)
154-
?
155-
Promise.resolve()
156-
:
157-
this.uploadZippedArtifactToBlobStorage(functionZipFile)
153+
const uploadBlobStorage = this.uploadZippedArtifactToBlobStorage(functionZipFile);
158154

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

157+
if (this.deploymentConfig.runFromBlobUrl) {
158+
this.log("Updating function app setting to run from external package...");
159+
const sasUrl = await this.blobService.generateBlobSasTokenUrl(
160+
this.deploymentConfig.container,
161+
this.artifactName
162+
)
163+
await this.updateFunctionAppSetting(
164+
functionApp,
165+
configConstants.runFromPackageSetting,
166+
sasUrl
167+
)
168+
}
161169

162170
this.log("Deployed serverless functions:")
163171
const serverlessFunctions = this.serverless.service.getAllFunctions();
@@ -189,13 +197,6 @@ export class FunctionAppService extends BaseService {
189197
? await armService.createDeploymentFromConfig(armTemplate)
190198
: await armService.createDeploymentFromType(type || "consumption");
191199

192-
if (this.deploymentConfig.runFromBlobUrl) {
193-
await this.uploadZippedArtifactToBlobStorage(this.getFunctionZipFile());
194-
deployment.parameters.functionAppRunFromPackage = await this.blobService.generateBlobSasTokenUrl(
195-
this.deploymentConfig.container,
196-
this.artifactName
197-
)
198-
}
199200
await armService.deployTemplate(deployment);
200201

201202
// Return function app
@@ -240,6 +241,12 @@ export class FunctionAppService extends BaseService {
240241
return functionZipFile;
241242
}
242243

244+
public async updateFunctionAppSetting(functionApp: Site, setting: string, value: string) {
245+
const { properties } = await this.webClient.webApps.listApplicationSettings(this.resourceGroup, functionApp.name);
246+
properties[setting] = value;
247+
await this.webClient.webApps.updateApplicationSettings(this.resourceGroup, functionApp.name, properties);
248+
}
249+
243250
/**
244251
* Uploads artifact file to blob storage container
245252
*/

0 commit comments

Comments
 (0)