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

Commit

Permalink
feat: Func plugin to add/remove functions within function app (#151)
Browse files Browse the repository at this point in the history
  • Loading branch information
tbarlow12 committed Sep 13, 2019
1 parent 919f491 commit 8e44411
Show file tree
Hide file tree
Showing 11 changed files with 444 additions and 10 deletions.
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,26 @@ This plugin enables Azure Functions support within the Serverless Framework.
2. CD into the generated app directory: `cd <appName>`
3. Install the app's NPM dependencies, which includes this plugin: `npm install`

### Creating or removing Azure Functions

To create a new Azure Function within your function app, run the following command from within your app's directory:

```bash
sls func add -n {functionName}
```

This will create a new `{functionName}` directory at the root of your application with `index.js` and `function.json` inside the directory. It will also update `serverless.yml` to contain the new function.

To remove an existing Azure Function from your function app, run the following command from within your app's directory:

```bash
sls func remove -n {functionName}
```

This will remove the `{functionName}` directory and remove the function from `serverless.yml`

*Note: Add & remove currently only support HTTP triggered functions. For other triggers, you will need to update `serverless.yml` manually

### Deploy, test, and diagnose your Azure service

1. Deploy your new service to Azure! The first time you do this, you will be asked to authenticate with your Azure account, so the `serverless` CLI can manage Functions on your behalf. Simply follow the provided instructions, and the deployment will continue as soon as the authentication process is completed.
Expand Down
10 changes: 2 additions & 8 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,12 @@
"@azure/arm-resources": "^1.0.1",
"@azure/ms-rest-nodeauth": "^1.0.1",
"axios": "^0.18.0",
"js-yaml": "^3.13.1",
"jsonpath": "^1.0.1",
"lodash": "^4.16.6",
"open": "^6.3.0",
"request": "^2.81.0"
"request": "^2.81.0",
"rimraf": "^2.6.3"
},
"devDependencies": {
"@types/jest": "^24.0.13",
Expand Down
3 changes: 3 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import { AzureDeployPlugin } from "./plugins/deploy/azureDeployPlugin";
import { AzureLoginPlugin } from "./plugins/login/loginPlugin";
import { AzureApimServicePlugin } from "./plugins/apim/apimServicePlugin";
import { AzureApimFunctionPlugin } from "./plugins/apim/apimFunctionPlugin";
import { AzureFuncPlugin } from "./plugins/func/azureFunc";


export class AzureIndex {
public constructor(private serverless: Serverless, private options) {
Expand All @@ -29,6 +31,7 @@ export class AzureIndex {
this.serverless.pluginManager.addPlugin(AzureDeployPlugin);
this.serverless.pluginManager.addPlugin(AzureApimServicePlugin);
this.serverless.pluginManager.addPlugin(AzureApimFunctionPlugin);
this.serverless.pluginManager.addPlugin(AzureFuncPlugin);
}
}

Expand Down
117 changes: 117 additions & 0 deletions src/plugins/func/azureFunc.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import fs from "fs";
import mockFs from "mock-fs";
import path from "path";
import { MockFactory } from "../../test/mockFactory";
import { invokeHook } from "../../test/utils";
import { AzureFuncPlugin } from "./azureFunc";
import rimraf from "rimraf";

describe("Azure Func Plugin", () => {

it("displays a help message", async () => {
const sls = MockFactory.createTestServerless();
const options = MockFactory.createTestServerlessOptions();
const plugin = new AzureFuncPlugin(sls, options);
await invokeHook(plugin, "func:func");
expect(sls.cli.log).toBeCalledWith("Use the func plugin to add or remove functions within Function App");
})

describe("Add command", () => {

beforeAll(() => {
mockFs({
"myExistingFunction": {
"index.js": "contents",
"function.json": "contents",
},
"serverless.yml": MockFactory.createTestServerlessYml(true),
}, {createCwd: true, createTmp: true})
});

afterAll(() => {
mockFs.restore();
});

it("returns with missing name", async () => {
const sls = MockFactory.createTestServerless();
const options = MockFactory.createTestServerlessOptions();
const plugin = new AzureFuncPlugin(sls, options);
await invokeHook(plugin, "func:add:add");
expect(sls.cli.log).toBeCalledWith("Need to provide a name of function to add")
});

it("returns with pre-existing function", async () => {
const sls = MockFactory.createTestServerless();
const options = MockFactory.createTestServerlessOptions();
options["name"] = "myExistingFunction";
const plugin = new AzureFuncPlugin(sls, options);
await invokeHook(plugin, "func:add:add");
expect(sls.cli.log).toBeCalledWith(`Function myExistingFunction already exists`);
});

it("creates function directory and updates serverless.yml", async () => {
const sls = MockFactory.createTestServerless();
const options = MockFactory.createTestServerlessOptions();
const functionName = "myFunction";
options["name"] = functionName;
const plugin = new AzureFuncPlugin(sls, options);
const mkdirSpy = jest.spyOn(fs, "mkdirSync");
await invokeHook(plugin, "func:add:add");
expect(mkdirSpy).toBeCalledWith(functionName);
const calls = (sls.utils.writeFileSync as any).mock.calls;
expect(calls[0][0]).toBe(path.join(functionName, "index.js"));
expect(calls[1][0]).toBe(path.join(functionName, "function.json"));
const expectedFunctionsYml = MockFactory.createTestFunctionsMetadata();
expectedFunctionsYml[functionName] = MockFactory.createTestFunctionMetadata();
expect(calls[2][0]).toBe("serverless.yml");
expect(calls[2][1]).toBe(MockFactory.createTestServerlessYml(true, expectedFunctionsYml));
});
});

describe("Remove command", () => {

beforeAll(() => {
mockFs({
"function1": {
"index.js": "contents",
"function.json": "contents",
},
}, {createCwd: true, createTmp: true});
});

afterAll(() => {
mockFs.restore();
});

it("returns with missing name", async () => {
const sls = MockFactory.createTestServerless();
const options = MockFactory.createTestServerlessOptions();
const plugin = new AzureFuncPlugin(sls, options);
await invokeHook(plugin, "func:remove:remove");
expect(sls.cli.log).toBeCalledWith("Need to provide a name of function to remove")
});

it("returns with non-existing function", async () => {
const sls = MockFactory.createTestServerless();
const options = MockFactory.createTestServerlessOptions();
options["name"] = "myNonExistingFunction";
const plugin = new AzureFuncPlugin(sls, options);
await invokeHook(plugin, "func:remove:remove");
expect(sls.cli.log).toBeCalledWith(`Function myNonExistingFunction does not exist`);
});

it("deletes directory and updates serverless.yml", async () => {
const sls = MockFactory.createTestServerless();
const options = MockFactory.createTestServerlessOptions();
const plugin = new AzureFuncPlugin(sls, options);
const functionName = "function1";
options["name"] = functionName;
const rimrafSpy = jest.spyOn(rimraf, "sync");
await invokeHook(plugin, "func:remove:remove");
expect(rimrafSpy).toBeCalledWith(functionName);
const expectedFunctionsYml = MockFactory.createTestFunctionsMetadata();
delete expectedFunctionsYml[functionName];
expect(sls.utils.writeFileSync).toBeCalledWith("serverless.yml", MockFactory.createTestServerlessYml(true, expectedFunctionsYml))
});
});
});
113 changes: 113 additions & 0 deletions src/plugins/func/azureFunc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import fs from "fs";
import path from "path";
import rimraf from "rimraf";
import Serverless from "serverless";
import { FuncPluginUtils } from "./funcUtils";

export class AzureFuncPlugin {
public hooks: { [eventName: string]: Promise<any> };
public commands: any;


public constructor(private serverless: Serverless, private options: Serverless.Options) {
this.hooks = {
"func:func": this.func.bind(this),
"func:add:add": this.add.bind(this),
"func:remove:remove": this.remove.bind(this)
};

this.commands = {
func: {
usage: "Add or remove functions",
lifecycleEvents: [
"func",
],
commands: {
add: {
usage: "Add azure function",
lifecycleEvents: [
"add",
],
options: {
name: {
usage: "Name of function to add",
shortcut: "n",
}
}
},
remove: {
usage: "Remove azure function",
lifecycleEvents: [
"remove",
],
options: {
name: {
usage: "Name of function to remove",
shortcut: "n",
}
}
}
}
}
}
}

private async func() {
this.serverless.cli.log("Use the func plugin to add or remove functions within Function App");
}

private async add() {
if (!("name" in this.options)) {
this.serverless.cli.log("Need to provide a name of function to add");
return;
}
const funcToAdd = this.options["name"]
const exists = fs.existsSync(funcToAdd);
if (exists) {
this.serverless.cli.log(`Function ${funcToAdd} already exists`);
return;
}
this.createFunctionDir(funcToAdd);
this.addToServerlessYml(funcToAdd);
}

private createFunctionDir(name: string) {
this.serverless.cli.log("Creating function dir");
try {
fs.mkdirSync(name);
} catch (e) {
this.serverless.cli.log(`Error making directory ${e}`);
}
this.serverless.utils.writeFileSync(path.join(name, "index.js"), FuncPluginUtils.getFunctionHandler(name));
this.serverless.utils.writeFileSync(path.join(name, "function.json"), FuncPluginUtils.getFunctionJsonString(name, this.options))
}

private addToServerlessYml(name: string) {
this.serverless.cli.log("Adding to serverless.yml");
const functionYml = FuncPluginUtils.getFunctionsYml(this.serverless);
functionYml[name] = FuncPluginUtils.getFunctionSlsObject(name, this.options);
FuncPluginUtils.updateFunctionsYml(this.serverless, functionYml);
}

private async remove() {
if (!("name" in this.options)) {
this.serverless.cli.log("Need to provide a name of function to remove");
return;
}
const funcToRemove = this.options["name"];
const exists = fs.existsSync(funcToRemove);
if (!exists) {
this.serverless.cli.log(`Function ${funcToRemove} does not exist`);
return;
}
this.serverless.cli.log(`Removing ${funcToRemove}`);
rimraf.sync(funcToRemove);
await this.removeFromServerlessYml(funcToRemove);
}

private async removeFromServerlessYml(name: string) {
const functionYml = FuncPluginUtils.getFunctionsYml(this.serverless);
delete functionYml[name];
FuncPluginUtils.updateFunctionsYml(this.serverless, functionYml)
}
}
19 changes: 19 additions & 0 deletions src/plugins/func/bindingTemplates/http.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"bindings": [
{
"authLevel": "anonymous",
"type": "httpTrigger",
"direction": "in",
"name": "req",
"methods": [
"get",
"post"
]
},
{
"type": "http",
"direction": "out",
"name": "res"
}
]
}
32 changes: 32 additions & 0 deletions src/plugins/func/funcUtils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { MockFactory } from "../../test/mockFactory";
import { FuncPluginUtils } from "./funcUtils";

describe("Func Utils", () => {

it("gets functions yml", () => {
const sls = MockFactory.createTestServerless();
const funcYaml = FuncPluginUtils.getFunctionsYml(sls);
expect(funcYaml).toEqual(MockFactory.createTestFunctionsMetadata());
});

it("updates functions yml", () => {
const updatedFunctions = MockFactory.createTestFunctionsMetadata(3);
const originalSls = MockFactory.createTestServerlessYml(false, 2);
const sls = MockFactory.createTestServerless();
FuncPluginUtils.updateFunctionsYml(sls, updatedFunctions, originalSls);
const calls = (sls.utils.writeFileSync as any).mock.calls[0]
expect(calls[0]).toBe("serverless.yml");
const expected = MockFactory.createTestServerlessYml(
true,
MockFactory.createTestFunctionsMetadata(3)
);
expect(calls[1]).toBe(expected);
});

it("adds new function name to function handler", () => {
const name = "This is my function name"
const handler = FuncPluginUtils.getFunctionHandler(name);
expect(handler)
.toContain(`body: "${name} " + (req.query.name || req.body.name)`);
});
});
Loading

0 comments on commit 8e44411

Please sign in to comment.