Skip to content
This repository was archived by the owner on Dec 9, 2024. It is now read-only.
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
4 changes: 3 additions & 1 deletion src/services/apimService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,9 @@ export class ApimService extends BaseService {
responses: options.operation.responses || [],
};

const operationUrl = `${service.gatewayUrl}/${api.path}${operationConfig.urlTemplate}`;
// Ensure a single path seperator in the operation path
const operationPath = `/${api.path}/${operationConfig.urlTemplate}`.replace(/\/+/g, "/");
const operationUrl = `${service.gatewayUrl}${operationPath}`;
this.log(`--> Deploying API operation ${options.function}: ${operationConfig.method.toUpperCase()} ${operationUrl}`);

const operation = await client.apiOperation.createOrUpdate(
Expand Down
80 changes: 55 additions & 25 deletions src/services/functionAppService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,11 @@ import { Guard } from "../shared/guard";
import { ArmService } from "./armService";
import { AzureBlobStorageService } from "./azureBlobStorageService";
import { BaseService } from "./baseService";
import { Utils } from "../shared/utils";

export class FunctionAppService extends BaseService {
private static readonly retryCount: number = 10;
private static readonly retryInterval: number = 5000;
private webClient: WebSiteManagementClient;
private blobService: AzureBlobStorageService;

Expand Down Expand Up @@ -90,27 +93,53 @@ export class FunctionAppService extends BaseService {
Guard.null(functionApp);

const getTokenUrl = `${this.baseUrl}${functionApp.id}/functions?api-version=2016-08-01`;
const response = await this.sendApiRequest("GET", getTokenUrl);
try {
const response = await Utils.runWithRetry(async () => {
const listFunctionsResponse = await this.sendApiRequest("GET", getTokenUrl);

if (response.status !== 200) {
return [];
}
if (listFunctionsResponse.status !== 200 || listFunctionsResponse.data.value.length === 0) {
this.log("-> Function App not ready. Retrying...");
throw new Error(listFunctionsResponse.data);
}

return listFunctionsResponse;
}, FunctionAppService.retryCount, FunctionAppService.retryInterval);

return response.data.value.map((functionConfig) => functionConfig.properties);
return response.data.value.map((functionConfig) => functionConfig.properties);
}
catch (e) {
this.log("-> Unable to retrieve function app list");
throw e;
}
}

/**
* Gets the configuration of the specified function within the function app
* @param functionApp The parent function app
* @param functionName The name of hte function
*/
public async getFunction(functionApp: Site, functionName: string): Promise<FunctionEnvelope> {
Guard.null(functionApp);
Guard.empty(functionName);

const getFunctionUrl = `${this.baseUrl}${functionApp.id}/functions/${functionName}?api-version=2016-08-01`;
const response = await this.sendApiRequest("GET", getFunctionUrl);

if (response.status !== 200) {
try {
const response = await Utils.runWithRetry(async () => {
const getFunctionResponse = await this.sendApiRequest("GET", getFunctionUrl);

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

return getFunctionResponse;
}, FunctionAppService.retryCount, FunctionAppService.retryInterval);

return response.data.properties;
} catch (e) {
return null;
}

return response.data.properties;
}

public async uploadFunctions(functionApp: Site): Promise<any> {
Expand All @@ -122,6 +151,23 @@ export class FunctionAppService extends BaseService {
const uploadFunctionApp = this.uploadZippedArfifactToFunctionApp(functionApp, functionZipFile);
const uploadBlobStorage = this.uploadZippedArtifactToBlobStorage(functionZipFile);
await Promise.all([uploadFunctionApp, uploadBlobStorage]);


this.log("Deployed serverless functions:")
const serverlessFunctions = this.serverless.service.getAllFunctions();
const deployedFunctions = await this.listFunctions(functionApp);

// List functions that are part of the serverless yaml config
Copy link
Contributor

Choose a reason for hiding this comment

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

will there ever be a use case where there are deployed functions that are not in the yamL?

deployedFunctions.forEach((functionConfig) => {
if (serverlessFunctions.includes(functionConfig.name)) {
const httpConfig = this.getFunctionHttpTriggerConfig(functionApp, functionConfig);

if (httpConfig) {
const method = httpConfig.methods[0].toUpperCase();
this.log(`-> ${functionConfig.name}: ${method} ${httpConfig.url}`);
}
}
});
}

/**
Expand Down Expand Up @@ -166,23 +212,7 @@ export class FunctionAppService extends BaseService {
};

await this.sendFile(requestOptions, functionZipFile);

this.log("-> Function package uploaded successfully");
const serverlessFunctions = this.serverless.service.getAllFunctions();
const deployedFunctions = await this.listFunctions(functionApp);

this.log("Deployed serverless functions:")
deployedFunctions.forEach((functionConfig) => {
// List functions that are part of the serverless yaml config
if (serverlessFunctions.includes(functionConfig.name)) {
const httpConfig = this.getFunctionHttpTriggerConfig(functionApp, functionConfig);

if (httpConfig) {
const method = httpConfig.methods[0].toUpperCase();
this.log(`-> ${functionConfig.name}: ${method} ${httpConfig.url}`);
}
}
});
}

/**
Expand Down
83 changes: 83 additions & 0 deletions src/shared/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,4 +105,87 @@ describe("utils", () => {
}
);
});

describe("runWithRetry", () => {
it("returns values after 1st run", async () => {
const expected = "success";
let lastRetry = 0;

const result = await Utils.runWithRetry((retry) => {
lastRetry = retry;
return Promise.resolve(expected);
});

expect(lastRetry).toEqual(1);
expect(result).toEqual(expected);
});

it("returns values after successfully retry (reject promise)", async () => {
const expected = "success";
let lastRetry = 0;

const result = await Utils.runWithRetry((retry) => {
lastRetry = retry;
if (retry === 1) {
return Promise.reject("rejected");
}

return Promise.resolve(expected);
});

expect(lastRetry).toEqual(2);
expect(result).toEqual(expected);
});

it("returns values after successfully retry (throw error)", async () => {
const expected = "success";
let lastRetry = 0;

const result = await Utils.runWithRetry((retry) => {
lastRetry = retry;
if (retry === 1) {
throw new Error("Ooops!")
}

return Promise.resolve(expected);
});

expect(lastRetry).toEqual(2);
expect(result).toEqual(expected);
});
it("throws error after reties", async () => {
const maxRetries = 5;
let lastRetry = 0;

const test = async () => {
await Utils.runWithRetry((retry) => {
lastRetry = retry;
return Promise.reject("rejected");
}, maxRetries, 100);
};

await expect(test()).rejects.toEqual("rejected");
expect(lastRetry).toEqual(maxRetries);
});
});

describe("wait", () => {
const setTimeoutMock = jest.fn((resolve) => resolve());

beforeEach(() => {
global.setTimeout = setTimeoutMock;
});

it("waits 1000 by default", async () => {
await Utils.wait();

expect(setTimeoutMock).toBeCalledWith(expect.any(Function), 1000);
});

it("waits specified time", async () => {
await Utils.wait(2000);

expect(setTimeoutMock).toBeCalledWith(expect.any(Function), 2000);
});
});
});
35 changes: 35 additions & 0 deletions src/shared/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,4 +160,39 @@ export class Utils {
return settings && settings.direction === "out";
});
}

/**
* Runs an operation with auto retry policy
* @param operation The operation to run
* @param maxRetries The max number or retreis
* @param retryWaitInterval The time to wait between retries
*/
public static async runWithRetry<T>(operation: (retry?: number) => Promise<T>, maxRetries: number = 3, retryWaitInterval: number = 1000) {
let retry = 0;
let error = null;

while (retry < maxRetries) {
try {
retry++;
return await operation(retry);
}
catch (e) {
error = e;
}

await Utils.wait(retryWaitInterval);
}

return Promise.reject(error);
}

/**
* Waits for the specified amount of time.
* @param time The amount of time to wait (default = 1000ms)
*/
public static wait(time: number = 1000) {
return new Promise((resolve) => {
setTimeout(resolve, time);
});
}
}