diff --git a/src/models/serverless.ts b/src/models/serverless.ts index 727d8ffe..c15adf1b 100644 --- a/src/models/serverless.ts +++ b/src/models/serverless.ts @@ -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[]; @@ -73,6 +90,7 @@ export interface ServerlessCommand { export interface ServerlessCommandMap { [command: string]: ServerlessCommand; } + export interface ServerlessAzureOptions extends Serverless.Options { resourceGroup?: string; } diff --git a/src/plugins/package/azurePackagePlugin.ts b/src/plugins/package/azurePackagePlugin.ts index 9fe86c34..1d3ab579 100644 --- a/src/plugins/package/azurePackagePlugin.ts +++ b/src/plugins/package/azurePackagePlugin.ts @@ -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 { diff --git a/src/services/baseService.ts b/src/services/baseService.ts index 58d0d50f..7b21371b 100644 --- a/src/services/baseService.ts +++ b/src/services/baseService.ts @@ -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"; @@ -181,7 +181,7 @@ export abstract class BaseService { /** * Get function objects */ - protected slsFunctions() { + protected slsFunctions(): { [functionName: string]: ServerlessAzureFunctionConfig } { return this.serverless.service["functions"]; } diff --git a/src/services/offlineService.ts b/src/services/offlineService.ts index 5661c83d..35f62e37 100644 --- a/src/services/offlineService.ts +++ b/src/services/offlineService.ts @@ -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() { @@ -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") } -} \ No newline at end of file +} diff --git a/src/services/packageService.test.ts b/src/services/packageService.test.ts index 4e067555..be57599e 100644 --- a/src/services/packageService.test.ts +++ b/src/services/packageService.test.ts @@ -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(() => { @@ -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({}); @@ -91,19 +111,26 @@ 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"), + ] + }, }, }; @@ -111,7 +138,7 @@ describe("Package Service", () => { const expectedFilePath = path.join(expectedFolderPath, "function.json"); mockFs({ - "helloWorld": { + "hello": { "index.js": "contents", }, }); @@ -162,4 +189,4 @@ describe("Package Service", () => { mkDirSpy.mockRestore(); copyFileSpy.mockRestore(); }); -}); \ No newline at end of file +}); diff --git a/src/services/packageService.ts b/src/services/packageService.ts index 86a8d2d7..e81ca475 100644 --- a/src/services/packageService.ts +++ b/src/services/packageService.ts @@ -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); } /** @@ -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(); } diff --git a/src/shared/utils.test.ts b/src/shared/utils.test.ts index 3e6feda9..4a121dfb 100644 --- a/src/shared/utils.test.ts +++ b/src/shared/utils.test.ts @@ -162,6 +162,7 @@ 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"); @@ -169,5 +170,23 @@ describe("utils", () => { 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"), + } + ); + }); }); diff --git a/src/shared/utils.ts b/src/shared/utils.ts index 0fa71d8c..7490d1cd 100644 --- a/src/shared/utils.ts +++ b/src/shared/utils.ts @@ -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; @@ -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"; + }); + } } diff --git a/src/test/mockFactory.ts b/src/test/mockFactory.ts index 2d48e2f6..ed5f074c 100644 --- a/src/test/mockFactory.ts +++ b/src/test/mockFactory.ts @@ -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]) { @@ -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 {