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
Show all changes
27 commits
Select commit Hold shift + click to select a range
287c478
Added stubs for add and remove func plugins
tbarlow12 May 28, 2019
da5d06c
Removing from serverless yml and function directory
tbarlow12 May 28, 2019
544accf
Generating scaffolding code with add function
tbarlow12 May 28, 2019
7e22553
Mocking file system
tbarlow12 May 28, 2019
6882d73
Stub tests for func utils
tbarlow12 May 28, 2019
4b01f4c
Added tests for add and remove plugins
tbarlow12 May 29, 2019
ffebc30
Remove sample data
tbarlow12 May 29, 2019
6a74d86
Add docs
tbarlow12 May 29, 2019
552fce2
Remove junk files from test
tbarlow12 May 29, 2019
d32aef2
Update message in func plugin
tbarlow12 May 29, 2019
b518cd8
Removed jest functions and instead added spies
tbarlow12 May 29, 2019
acc4fa1
Update to double quotes
tbarlow12 May 29, 2019
c3a5c33
Remove unused import
tbarlow12 May 29, 2019
2bd21b2
Update with changes from dev
tbarlow12 May 29, 2019
c60fc50
Update func utils and tests to not use regex
tbarlow12 May 29, 2019
490e983
Fix bug in altering serverless yml
tbarlow12 May 29, 2019
ef2ee1b
Clearing all mocks after each test to avoid conflicts
tbarlow12 May 30, 2019
4f59c32
Added template file for generated javascript
tbarlow12 May 30, 2019
9f1787c
Create cwd
tbarlow12 May 30, 2019
4d497b8
Run tests in band, add mocked file to func plugin tests
tbarlow12 May 30, 2019
8b339eb
Changed test to not spy on writeFileSync
tbarlow12 May 30, 2019
dc5afbd
Using inline handler script and templates directory
tbarlow12 May 30, 2019
4245bb0
Update func util to use sls util read file
tbarlow12 May 30, 2019
e5bf4da
Remove text from tsconfig include
tbarlow12 May 30, 2019
136c97a
Respond to PR feedback
tbarlow12 May 30, 2019
85d8cca
Update string to not use template string
tbarlow12 May 31, 2019
49a0e2a
Include updates from dev branch
tbarlow12 May 31, 2019
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
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();
Copy link
Contributor

Choose a reason for hiding this comment

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

Q: Would it make sense to add a default name to the options and then remove/override as needed? Just thinking if we'll need it for other function tests if we add more functionality, like the option to include the extension bundles

Copy link
Contributor Author

Choose a reason for hiding this comment

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

For sure. Let's add that in another PR

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