Skip to content

Commit

Permalink
feat: Path from x-azure-settings is included in function.json (#212)
Browse files Browse the repository at this point in the history
In serverless.yml, you can now specify an API route within `x-azure-settings`, which will be added to the `function.json` file.

Resolves [AB#552]
  • Loading branch information
tbarlow12 committed Sep 13, 2019
1 parent 1716eec commit 55620ee
Show file tree
Hide file tree
Showing 9 changed files with 131 additions and 23 deletions.
18 changes: 18 additions & 0 deletions src/models/serverless.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,23 @@ export interface ServerlessAzureConfig {
functions: any;
}

export interface ServerlessAzureFunctionConfig {
handler: string;
events: ServerlessAzureFunctionBindingConfig[];
}

export interface ServerlessAzureFunctionBindingConfig {
http?: boolean;
"x-azure-settings": ServerlessExtraAzureSettingsConfig;
}

export interface ServerlessExtraAzureSettingsConfig {
direction?: string;
route?: string;
name?: string;
authLevel?: string;
}

export interface ServerlessCommand {
usage: string;
lifecycleEvents: string[];
Expand All @@ -73,6 +90,7 @@ export interface ServerlessCommand {
export interface ServerlessCommandMap {
[command: string]: ServerlessCommand;
}

export interface ServerlessAzureOptions extends Serverless.Options {
resourceGroup?: string;
}
2 changes: 1 addition & 1 deletion src/plugins/package/azurePackagePlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export class AzurePackagePlugin extends AzureBasePlugin {
"after:package:finalize": this.finalize.bind(this),
};

this.packageService = new PackageService(this.serverless);
this.packageService = new PackageService(this.serverless, this.options);
}

private async setupProviderConfiguration(): Promise<void> {
Expand Down
4 changes: 2 additions & 2 deletions src/services/baseService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import axios from "axios";
import fs from "fs";
import request from "request";
import Serverless from "serverless";
import { ServerlessAzureOptions } from "../models/serverless";
import { ServerlessAzureOptions, ServerlessAzureFunctionConfig } from "../models/serverless";
import { StorageAccountResource } from "../armTemplates/resources/storageAccount";
import { configConstants } from "../config";
import { DeploymentConfig, ServerlessAzureConfig } from "../models/serverless";
Expand Down Expand Up @@ -181,7 +181,7 @@ export abstract class BaseService {
/**
* Get function objects
*/
protected slsFunctions() {
protected slsFunctions(): { [functionName: string]: ServerlessAzureFunctionConfig } {
return this.serverless.service["functions"];
}

Expand Down
6 changes: 3 additions & 3 deletions src/services/offlineService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ export class OfflineService extends BaseService {
}
}),
}

public constructor(serverless: Serverless, options: Serverless.Options) {
super(serverless, options, false);
this.packageService = new PackageService(serverless);
this.packageService = new PackageService(serverless, options);
}

public async build() {
Expand Down Expand Up @@ -55,4 +55,4 @@ export class OfflineService extends BaseService {
this.log("Make sure you have Azure Functions Core Tools installed");
this.log("If not installed run 'npm i azure-functions-core-tools -g")
}
}
}
49 changes: 38 additions & 11 deletions src/services/packageService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,15 @@ import { FunctionMetadata } from "../shared/utils";
describe("Package Service", () => {
let sls: Serverless;
let packageService: PackageService;
const functionRoute = "myRoute";

beforeEach(() => {
sls = MockFactory.createTestServerless();
sls.config.servicePath = process.cwd();

packageService = new PackageService(sls);
sls.service["functions"] = {
hello: MockFactory.createTestAzureFunctionConfig(functionRoute),
}
packageService = new PackageService(sls, MockFactory.createTestServerlessOptions());
});

afterEach(() => {
Expand Down Expand Up @@ -71,17 +74,34 @@ describe("Package Service", () => {
});

it("createBinding writes function.json files into function folder", async () => {
const functionName = "helloWorld";
const functionName = "hello";
const functionMetadata: FunctionMetadata = {
entryPoint: "handler",
handlerPath: "src/handlers/hello.js",
handlerPath: "src/handlers/hello",
params: {
functionsJson: {},
functionsJson: {
bindings: [
MockFactory.createTestHttpBinding("out"),
MockFactory.createTestHttpBinding("in"),
]
}
},
};

const expectedFolderPath = path.join(sls.config.servicePath, functionName);
const expectedFilePath = path.join(expectedFolderPath, "function.json");
const expectedFunctionJson = {
entryPoint: "handler",
scriptFile: "src/handlers/hello",
bindings: [
MockFactory.createTestHttpBinding("out"),
{
...MockFactory.createTestHttpBinding("in"),
route: functionRoute
}
]

}

mockFs({});

Expand All @@ -91,27 +111,34 @@ describe("Package Service", () => {
await packageService.createBinding(functionName, functionMetadata);

expect(mkdirSpy).toBeCalledWith(expectedFolderPath);
expect(writeFileSpy).toBeCalledWith(expectedFilePath, expect.any(String));
const call = writeFileSpy.mock.calls[0];
expect(call[0]).toEqual(expectedFilePath);
expect(JSON.parse(call[1])).toEqual(expectedFunctionJson);

mkdirSpy.mockRestore();
writeFileSpy.mockRestore();
});

it("createBinding does not need to create directory if function folder already exists", async () => {
const functionName = "helloWorld";
const functionName = "hello";
const functionMetadata: FunctionMetadata = {
entryPoint: "handler",
handlerPath: "src/handlers/hello.js",
handlerPath: "src/handlers/hello",
params: {
functionsJson: {},
functionsJson: {
bindings: [
MockFactory.createTestHttpBinding("out"),
MockFactory.createTestHttpBinding("in"),
]
},
},
};

const expectedFolderPath = path.join(sls.config.servicePath, functionName);
const expectedFilePath = path.join(expectedFolderPath, "function.json");

mockFs({
"helloWorld": {
"hello": {
"index.js": "contents",
},
});
Expand Down Expand Up @@ -162,4 +189,4 @@ describe("Package Service", () => {
mkDirSpy.mockRestore();
copyFileSpy.mockRestore();
});
});
});
18 changes: 15 additions & 3 deletions src/services/packageService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ import fs from "fs";
import path from "path";
import Serverless from "serverless";
import { FunctionMetadata, Utils } from "../shared/utils";
import { BaseService } from "./baseService";

/**
* Adds service packing support
*/
export class PackageService {
public constructor(private serverless: Serverless) {
export class PackageService extends BaseService {
public constructor(serverless: Serverless, options: Serverless.Options) {
super(serverless, options, false);
}

/**
Expand Down Expand Up @@ -82,13 +84,23 @@ export class PackageService {
const functionJSON = functionMetadata.params.functionsJson;
functionJSON.entryPoint = functionMetadata.entryPoint;
functionJSON.scriptFile = functionMetadata.handlerPath;
const functionObject = this.slsFunctions()[functionName];
const bindingAzureSettings = Utils.getIncomingBindingConfig(functionObject)["x-azure-settings"];
if (bindingAzureSettings.route) {
// Find incoming binding within functionJSON and set the route
const index = (functionJSON.bindings as any[])
.findIndex((binding) => (!binding.direction || binding.direction === "in"));
functionJSON.bindings[index].route = bindingAzureSettings.route;
}

const functionDirPath = path.join(this.serverless.config.servicePath, functionName);
if (!fs.existsSync(functionDirPath)) {
fs.mkdirSync(functionDirPath);
}

fs.writeFileSync(path.join(functionDirPath, "function.json"), JSON.stringify(functionJSON, null, 2));
const functionJsonString = JSON.stringify(functionJSON, null, 2);

fs.writeFileSync(path.join(functionDirPath, "function.json"), functionJsonString);

return Promise.resolve();
}
Expand Down
21 changes: 20 additions & 1 deletion src/shared/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,12 +162,31 @@ describe("utils", () => {
const actual = Utils.getNormalizedRegionName(expected);
expect(actual).toEqual(expected);
});

it("should get a timestamp from a name", () => {
expect(Utils.getTimestampFromName("myDeployment-t12345")).toEqual("12345");
expect(Utils.getTimestampFromName("myDeployment-t678987645")).toEqual("678987645");
expect(Utils.getTimestampFromName("-t12345")).toEqual("12345");

expect(Utils.getTimestampFromName("myDeployment-t")).toEqual(null);
expect(Utils.getTimestampFromName("")).toEqual(null);
})
});

it("should get incoming binding", () => {
expect(Utils.getIncomingBindingConfig(MockFactory.createTestAzureFunctionConfig())).toEqual(
{
http: true,
"x-azure-settings": MockFactory.createTestHttpBinding("in"),
}
);
});

it("should get outgoing binding", () => {
expect(Utils.getOutgoingBinding(MockFactory.createTestAzureFunctionConfig())).toEqual(
{
http: true,
"x-azure-settings": MockFactory.createTestHttpBinding("out"),
}
);
});
});
15 changes: 15 additions & 0 deletions src/shared/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import Serverless from "serverless";
import { BindingUtils } from "./bindings";
import { constants } from "./constants";
import { Guard } from "./guard";
import { ServerlessAzureFunctionConfig } from "../models/serverless";

export interface FunctionMetadata {
entryPoint: any;
Expand Down Expand Up @@ -212,4 +213,18 @@ export class Utils {
}
return match[1];
}

public static getIncomingBindingConfig(functionConfig: ServerlessAzureFunctionConfig) {
return functionConfig.events.find((event) => {
const settings = event["x-azure-settings"]
return settings && (!settings.direction || settings.direction === "in");
});
}

public static getOutgoingBinding(functionConfig: ServerlessAzureFunctionConfig) {
return functionConfig.events.find((event) => {
const settings = event["x-azure-settings"]
return settings && settings.direction === "out";
});
}
}
21 changes: 19 additions & 2 deletions src/test/mockFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { ApiCorsPolicy, ApiManagementConfig } from "../models/apiManagement";
import { ArmDeployment, ArmResourceTemplate } from "../models/armTemplates";
import { ServicePrincipalEnvVariables } from "../models/azureProvider";
import { Logger } from "../models/generic";
import { ServerlessAzureConfig, ServerlessAzureProvider } from "../models/serverless";
import { ServerlessAzureConfig, ServerlessAzureProvider, ServerlessAzureFunctionConfig } from "../models/serverless";

function getAttribute(object: any, prop: string, defaultValue: any): any {
if (object && object[prop]) {
Expand Down Expand Up @@ -394,18 +394,35 @@ export class MockFactory {
return bindings;
}

public static createTestAzureFunctionConfig(route?: string): ServerlessAzureFunctionConfig {
return {
events: [
{
http: true,
"x-azure-settings": MockFactory.createTestHttpBinding("in", route),
},
{
http: true,
"x-azure-settings": MockFactory.createTestHttpBinding("out"),
}
],
handler: "handler.js",
}
}

public static createTestBinding() {
// Only supporting HTTP for now, could support others
return MockFactory.createTestHttpBinding();
}

public static createTestHttpBinding(direction: string = "in") {
public static createTestHttpBinding(direction: string = "in", route?: string) {
if (direction === "in") {
return {
authLevel: "anonymous",
type: "httpTrigger",
direction,
name: "req",
route,
}
} else {
return {
Expand Down

0 comments on commit 55620ee

Please sign in to comment.