Repository: serverless/serverless-azure-functions
Pull request #203 (2019-07-12 20:31:25)

feat: Added feature to specify Azure subcription ID in CLI before deployment
Added feature to specify subscription ID in the CLI which overrides any existing subscriptions ID and deploys the functions to the subscription specified by the user.

```bash
sls deploy -i <subscriptionID>
```

AB#360


Patch:
diff --git a/README.md b/README.md
index f545fbb..c6f018b 100644
--- a/README.md
+++ b/README.md
@@ -1,10 +1,10 @@
-# Azure Functions Serverless Plugin 
+# Azure Functions Serverless Plugin
 
 This plugin enables Azure Functions support within the Serverless Framework.
 
 [![Coverage Status](https://coveralls.io/repos/github/serverless/serverless-azure-functions/badge.svg)](https://coveralls.io/github/serverless/serverless-azure-functions)
 
-## Getting started
+## Quickstart
 
 ### Pre-requisites
 
@@ -12,50 +12,35 @@ This plugin enables Azure Functions support within the Serverless Framework.
 2. Serverless CLI `v1.9.0+`. You can run `npm i -g serverless` if you don't already have it.
 3. An Azure account. If you don't already have one, you can sign up for a [free trial](https://azure.microsoft.com/en-us/free/) that includes $200 of free credit.
 
-### Create a new Azure service
-
-1. Create a new service using the standard Node.js template, specifying a unique name for your app: `serverless create -t azure-nodejs -p <appName>`
-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:
+### Create a new Azure Function App
 
 ```bash
-sls func add -n {functionName}
+# Create Azure Function App from template
+$ sls create -t azure-nodejs -p <appName>
+# Move into project directory
+$ cd <appName>
+# Install dependencies (including this plugin)
+$ npm install
 ```
 
-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
-
 ### Running Function App Locally (`offline` plugin)
 
 In order to run a Azure Function App locally, the `azure-functions-core-tools` package needs to be installed from NPM. Since it is only used for local development, we did not include it in the `devDependencies` of `package.json`. To install globally, run:
 
 ```bash
-npm i azure-functions-core-tools -g
+$ npm i azure-functions-core-tools -g
 ```
 
 Then, at the root of your project directory, run:
 
 ```bash
 # Builds necessary function bindings files
-sls offline
+$ sls offline
 # Starts the function app
-npm start
+$ npm start
 ```
 
-The build process will generate a directory for each of your functions, which will contain a file titled `function.json`. This will contain a relative reference to your handler file & exported function from that file as long as they are referenced correctly in `serverless.yml`.
+The `offline` process will generate a directory for each of your functions, which will contain a file titled `function.json`. This will contain a relative reference to your handler file & exported function from that file as long as they are referenced correctly in `serverless.yml`.
 
 The `npm start` script just runs `func host start`, but we included the `npm` script for ease of use.
 
@@ -65,58 +50,146 @@ To clean up files generated from the build, you can simply run:
 sls offline cleanup
 ```
 
-### Deploy, test, and diagnose your Azure service
+### Deploy Your Function App
 
-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.
+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.
 
-    ```bash
-    serverless deploy
-    ```
+```bash
+$ sls deploy
+```
 
-    > Note: Once you've authenticated, a new Azure "service principal" will be created, and used for subsequent deployments. This prevents you from needing to manually login again. See [below](#advanced-authentication) if you'd prefer to use a custom service principal instead.
+For more advanced deployment scenarios, see our [deployment docs](docs/DEPLOY.md)
 
-2. Invoke a function, in order to test that it works:
+### Test Your Function App
 
-    ```bash
-    serverless invoke -f hello
-    ```
+Invoke your HTTP functions without ever leaving the CLI using:
 
-3. Stream the output logs for your function:
+```bash
+$ sls invoke -f <functionName>
+```
 
-    ```bash
-    serverless logs -f hello
-    ```
- 
-4. Make some code changes, `deploy` again, view logs, etc. and provide us feedback on how to make the experience even better!
+##### Invoke Options
 
-    > Note: If you're working on a single function, you can use the `serverless deploy function -f <function>` command instead of `serverless deploy`, which will simply deploy the specified function instead of the entire service.
+- `-f` or `--function` - Function to Invoke
+- `-d` or `--data` - Stringified JSON data to use as either query params or request body
+- `-p` or `--path` - Path to JSON file to use as either query params or request body
+- `-m` or `--method` - HTTP method for request
 
-If at any point, you no longer need your service, you can run the following command to remove the Azure Functions that were created, and ensure you don't incur any unexpected charges:
+##### Example
+
+After deploying template function app, run
 
 ```bash
-serverless remove
-``` 
+$ sls invoke -f hello '{"name": "Azure"}'
+```
 
-### Advanced Authentication
+### Roll Back Your Function App
+
+To roll back your function app to a previous deployment, simply select a timestamp of a previous deployment and use `rollback` command.
+
+```bash
+# List all deployments to know the timestamp for rollback
+$ sls deploy list
+Serverless:
+-----------
+Name: myFunctionApp-t1561479533
+Timestamp: 1561479533
+Datetime: 2019-06-25T16:18:53+00:00
+-----------
+Name: myFunctionApp-t1561479506
+Timestamp: 1561479506
+Datetime: 2019-06-25T16:18:26+00:00
+-----------
+Name: myFunctionApp-t1561479444
+Timestamp: 1561479444
+Datetime: 2019-06-25T16:17:24+00:00
+-----------
+
+# Rollback Function App to timestamp
+$ sls rollback -t 1561479506
+```
+
+This will update the app code and infrastructure to the selected previous deployment.
+
+For more details, check out our [rollback docs](docs/ROLLBACK.md).
+
+### Deleting Your Function App
 
-The getting started walkthrough illustrates the interactive login experience, which is recommended for most users. However, if you'd prefer to create an Azure ["service principal"](https://github.com/Azure/azure-sdk-for-node/blob/master/Documentation/Authentication.md#2-azure-cli) yourself, you can indicate that this plugin should use its credentials instead, by setting the following environment variables:
+If at any point you no longer need your service, you can run the following command to delete the resource group containing your Azure Function App and other depoloyed resources using:
 
-**Bash**
 ```bash
-export azureSubId='<subscriptionId>'
-export azureServicePrincipalTenantId='<tenantId>'
-export azureServicePrincipalClientId='<servicePrincipalName>'
-export azureServicePrincipalPassword='<password>'
+$ sls remove
 ```
 
-**Powershell**
-```powershell
-$env:azureSubId='<subscriptionId>'
-$env:azureServicePrincipalTenantId='<tenantId>'
-$env:azureServicePrincipalClientId='<servicePrincipalName>'
-$env:azureServicePrincipalPassword='<password>'
+### 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
+# -n or --name for name of new function
+$ sls func add -n {functionName}
 ```
 
+This will create a new handler file at the root of your project with the title `{functionName}.js`. 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
+# -n or --name for name of function to remove
+$ sls func remove -n {functionName}
+```
+
+This will remove the `{functionName}.js` handler 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
+
+### Advanced Authentication
+
+The getting started walkthrough illustrates the interactive login experience, which is recommended when getting started. However, for more robust use, a [service principal](https://docs.microsoft.com/en-us/azure/active-directory/develop/app-objects-and-service-principals) is recommended for authentication.
+
+##### Creating a Service Principal
+
+1. [Install the Azure CLI](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli?view=azure-cli-latest)
+2. Login via Azure CLI and set subscription
+    ```bash
+    # Login to Azure
+    $ az login
+    # Set Azure Subscription for which to create Service Principal
+    $ az account set -s <subscription-id>
+    ```
+3. Generate Service Principal for Azure Subscription
+    ```bash
+    # Create SP with unique name
+    $ az ad sp create-for-rbac --name <name>
+    ```
+    This will yield something like:
+    ```json
+    {
+      "appId": "<servicePrincipalId>",
+      "displayName": "<name>",
+      "name": "<name>",
+      "password": "<password>",
+      "tenant": "<tenantId>"
+    }
+    ```
+4. Set environment variables
+
+    **Bash**
+    ```bash
+    $ export azureSubId='<subscriptionId>'
+    $ export azureServicePrincipalTenantId='<tenantId>'
+    $ export azureServicePrincipalClientId='<servicePrincipalId>'
+    $ export azureServicePrincipalPassword='<password>'
+    ```
+
+    **Powershell**
+    ```powershell
+    $env:azureSubId='<subscriptionId>'
+    $env:azureServicePrincipalTenantId='<tenantId>'
+    $env:azureServicePrincipalClientId='<servicePrincipalName>'
+    $env:azureServicePrincipalPassword='<password>'
+    ```
+
 ### Contributing
 
 Please create issues in this repo for any problems or questions you find. Before sending a PR for any major changes, please create an issue to discuss.
@@ -128,27 +201,23 @@ We're still in the process of getting everying running 100%, but please refer to
 1. Clone this repository to your local machine
 2. Navigate to the cloned folder
 3. Run `npm install`
-4. Run `npm run-script build`
+4. Run `npm run build`
 5. Navigate to a folder where you created a new Serverless project, run `npm install`, and then run `npm link {path to serverless-azure-functions folder}`. Running `npm install` after the link command may override the link.
-6. The npm modules should now contain your local version of this plugin.  
-
-#### Signing commits
-
-All commits in your Pull Request will need to be signed. When looking at the commits in the pull request, you will see a green 'verified' icon next to your commit. Commit signature verification is discussed [here](https://help.github.com/en/articles/about-commit-signature-verification)
+6. The npm modules should now contain your local version of this plugin.
 
-Follow the directions [here](https://help.github.com/en/articles/signing-commits) to configure commit signing.
+#### Unit Tests
 
-If any of your commits are not signed, your pull request will not be able to be merged into the base branch. You can fix this by squashing any unsigned commits into signed commits using an interactive rebase, and force pushing your new commit history. More detail [here](https://git-scm.com/book/en/v2/Git-Tools-Rewriting-History)
+We use [Jest](https://jestjs.io/) for unit tests, and it is expected that every Pull Request containing code changes have accompanying unit tests.
 
-When using Windows you may also encounter an error when trying to sign a commit, stating that a security key could not be found. Ensure that you have set the path the gpg in the git config: `git config --global gpg.program "C:\Program Files (x86)\GnuPG\bin\gpg.exe"`
+Run unit tests using `npm test` or `npm run test:coverage` to get coverage results.
 
 #### Signing commits
 
-All commits in your Pull Request will need to be signed. When looking at the commits in the pull request, you will see a green 'verified' icon next to your commit. Commit signature verification is discussed [here](https://help.github.com/en/articles/about-commit-signature-verification)
+All commits in your Pull Request will need to be signed. When looking at the commits in the pull request, you will see a green 'verified' icon next to your commit. Commit signature verification is discussed [here](https://help.github.com/en/articles/about-commit-signature-verification).
 
 Follow the directions [here](https://help.github.com/en/articles/signing-commits) to configure commit signing.
 
-If any of your commits are not signed, your pull request will not be able to be merged into the base branch. You can fix this by squashing any unsigned commits into signed commits using an interactive rebase, and force pushing your new commit history. More detail [here](https://git-scm.com/book/en/v2/Git-Tools-Rewriting-History)
+If any of your commits are not signed, your pull request will not be able to be merged into the base branch. You can fix this by squashing any unsigned commits into signed commits using an interactive rebase, and force pushing your new commit history. More detail [here](https://git-scm.com/book/en/v2/Git-Tools-Rewriting-History).
 
 When using Windows you may also encounter an error when trying to sign a commit, stating that a security key could not be found. Ensure that you have set the path the gpg in the git config: `git config --global gpg.program "C:\Program Files (x86)\GnuPG\bin\gpg.exe"`
 
diff --git a/package-lock.json b/package-lock.json
index b52ea54..8f27d8f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
 {
   "name": "serverless-azure-functions",
-  "version": "1.0.0-5",
+  "version": "1.0.0-6",
   "lockfileVersion": 1,
   "requires": true,
   "dependencies": {
diff --git a/package.json b/package.json
index a6baa47..f8d38ce 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "serverless-azure-functions",
-  "version": "1.0.0-5",
+  "version": "1.0.0-6",
   "description": "Provider plugin for the Serverless Framework v1.x which adds support for Azure Functions.",
   "license": "MIT",
   "main": "./lib/index.js",
@@ -16,7 +16,8 @@
     "test:coverage": "npm run test -- --coverage",
     "compile": "tsc",
     "prebuild": "shx rm -rf lib/ && npm run test",
-    "build": "npm run compile"
+    "build": "npm run compile",
+    "generate:spn": "bash ./scripts/generate-service-principal.sh"
   },
   "repository": {
     "git": "https://github.com/serverless/serverless-azure-functions"
diff --git a/scripts/npmVersionBumpPrerelease.sh b/scripts/npmVersionBumpPrerelease.sh
index a3c30ec..e1951d2 100755
--- a/scripts/npmVersionBumpPrerelease.sh
+++ b/scripts/npmVersionBumpPrerelease.sh
@@ -10,12 +10,12 @@ git checkout -b npmRelease
 ## configure npm to sign commit
 npm config set sign-git-tag true
 
-version=$(npm version prerelease -m "Bumped to version %s")
+version=$(npm version prerelease -m "release: Bumped to version %s")
 
 echo "Bumped to version ${version}"
 
 # remove git tag, we don't want to tag pr
-git tag -d ${version} 
+git tag -d ${version}
 
 # push to remote
 git push origin npmRelease
diff --git a/src/plugins/azureBasePlugin.ts b/src/plugins/azureBasePlugin.ts
index e90b584..3aa7834 100644
--- a/src/plugins/azureBasePlugin.ts
+++ b/src/plugins/azureBasePlugin.ts
@@ -2,10 +2,10 @@ import { Guard } from "../shared/guard";
 import Serverless from "serverless";
 import { Utils } from "../shared/utils";
 
-export abstract class AzureBasePlugin {
+export abstract class AzureBasePlugin<TOptions=Serverless.Options> {
   public constructor(
     protected serverless: Serverless,
-    protected options: Serverless.Options,
+    protected options: TOptions,
   ) {
     Guard.null(serverless);
   }
diff --git a/src/plugins/deploy/azureDeployPlugin.ts b/src/plugins/deploy/azureDeployPlugin.ts
index 945b672..753f8b2 100644
--- a/src/plugins/deploy/azureDeployPlugin.ts
+++ b/src/plugins/deploy/azureDeployPlugin.ts
@@ -3,13 +3,13 @@ import { FunctionAppService } from "../../services/functionAppService";
 import { ResourceService } from "../../services/resourceService";
 import { Utils } from "../../shared/utils";
 import { AzureBasePlugin } from "../azureBasePlugin";
+import { AzureLoginOptions } from "../../services/loginService";
 
-
-export class AzureDeployPlugin extends AzureBasePlugin {
+export class AzureDeployPlugin extends AzureBasePlugin<AzureLoginOptions> {
   public hooks: { [eventName: string]: Promise<any> };
   public commands: any;
 
-  public constructor(serverless: Serverless, options: Serverless.Options) {
+  public constructor(serverless: Serverless, options: AzureLoginOptions) {
     super(serverless, options);
 
     this.hooks = {
@@ -31,6 +31,10 @@ export class AzureDeployPlugin extends AzureBasePlugin {
           "resourceGroup": {
             usage: "Resource group for the service",
             shortcut: "g",
+          },
+          subscriptionId: {
+            usage: "Sets the Azure subscription ID",
+            shortcut: "i",
           }
         }
       }
diff --git a/src/plugins/login/azureLoginPlugin.ts b/src/plugins/login/azureLoginPlugin.ts
index 188086f..fa0b246 100644
--- a/src/plugins/login/azureLoginPlugin.ts
+++ b/src/plugins/login/azureLoginPlugin.ts
@@ -2,12 +2,13 @@ import Serverless from "serverless";
 import AzureProvider from "../../provider/azureProvider";
 import { AzureLoginService } from "../../services/loginService";
 import { AzureBasePlugin } from "../azureBasePlugin";
+import { AzureLoginOptions } from "../../services/loginService";
 
-export class AzureLoginPlugin extends AzureBasePlugin {
+export class AzureLoginPlugin extends AzureBasePlugin<AzureLoginOptions> {
   private provider: AzureProvider;
   public hooks: { [eventName: string]: Promise<any> };
 
-  public constructor(serverless: Serverless, options: Serverless.Options) {
+  public constructor(serverless: Serverless, options: AzureLoginOptions) {
     super(serverless, options);
     this.provider = (this.serverless.getProvider("azure") as any) as AzureProvider;
 
@@ -32,7 +33,8 @@ export class AzureLoginPlugin extends AzureBasePlugin {
       this.serverless.variables["azureCredentials"] = authResult.credentials;
       // Use environment variable for sub ID or use the first subscription in the list (service principal can
       // have access to more than one subscription)
-      this.serverless.variables["subscriptionId"] = process.env.azureSubId || authResult.subscriptions[0].id;
+      this.serverless.variables["subscriptionId"] = this.options.subscriptionId || process.env.azureSubId || authResult.subscriptions[0].id;
+      this.serverless.cli.log(`Using subscription ID: ${this.serverless.variables["subscriptionId"]}`);
     }
     catch (e) {
       this.log("Error logging into azure");
diff --git a/src/services/loginService.ts b/src/services/loginService.ts
index 736671a..94d00b9 100644
--- a/src/services/loginService.ts
+++ b/src/services/loginService.ts
@@ -1,4 +1,5 @@
 import open from "open";
+import Serverless from "serverless";
 import {
   interactiveLoginWithAuthResponse,
   loginWithServicePrincipalSecretWithAuthResponse,
@@ -7,6 +8,10 @@ import {
   InteractiveLoginOptions,  
 } from "@azure/ms-rest-nodeauth";
 
+export interface AzureLoginOptions extends Serverless.Options {
+  subscriptionId?: string;
+}
+
 export class AzureLoginService {
 
   /**


Test patch:
diff --git a/src/plugins/login/azureLoginPlugin.test.ts b/src/plugins/login/azureLoginPlugin.test.ts
index aef2f2d..846dfba 100644
--- a/src/plugins/login/azureLoginPlugin.test.ts
+++ b/src/plugins/login/azureLoginPlugin.test.ts
@@ -10,13 +10,12 @@ describe("Login Plugin", () => {
   const envVariables = MockFactory.createTestServicePrincipalEnvVariables()
   const credentials = MockFactory.createTestVariables().azureCredentials;
 
-  function createPlugin(hasCreds = false, serverless?: Serverless): AzureLoginPlugin {
+  function createPlugin(hasCreds = false, serverless?: Serverless, options?: Serverless.Options): AzureLoginPlugin {
     const sls = serverless || MockFactory.createTestServerless();
     if (!hasCreds) {
       delete sls.variables["azureCredentials"];
     }
-    const options = MockFactory.createTestServerlessOptions();
-    return new AzureLoginPlugin(sls, options);
+    return new AzureLoginPlugin(sls, options || MockFactory.createTestServerlessOptions());
   }
 
   function createMockLoginFunction() {
@@ -31,8 +30,8 @@ describe("Login Plugin", () => {
     unsetEnvVariables(envVariables);
   }
 
-  async function invokeLoginHook(hasCreds = false, serverless?: Serverless) {
-    const plugin = createPlugin(hasCreds, serverless);
+  async function invokeLoginHook(hasCreds = false, serverless?: Serverless, options?: Serverless.Options) {
+    const plugin = createPlugin(hasCreds, serverless, options);
     await invokeHook(plugin, "before:package:initialize");
   }
 
@@ -92,4 +91,23 @@ describe("Login Plugin", () => {
     expect(sls.cli.log).lastCalledWith(`Error: ${errorMessage}`);
     expect(process.exit).toBeCalledWith(0);
   });
+
+  it("Uses the user specified subscription ID", async () => {
+    const sls = MockFactory.createTestServerless();
+    const opt = MockFactory.createTestServerlessOptions();
+    opt["subscriptionId"] = "test-subs-id";
+    await invokeLoginHook(false, sls, opt);
+    expect(AzureLoginService.interactiveLogin).toBeCalled()
+    expect(sls.variables["subscriptionId"]).toEqual("test-subs-id");
+    expect(sls.cli.log).toBeCalledWith("Using subscription ID: test-subs-id");
+  })
+
+  it("Uses the default subscription ID" , async () => {
+    const sls = MockFactory.createTestServerless();
+    const opt = MockFactory.createTestServerlessOptions();
+    await invokeLoginHook(false, sls, opt);
+    expect(AzureLoginService.interactiveLogin).toBeCalled()
+    expect(sls.variables["subscriptionId"]).toEqual("azureSubId");
+    expect(sls.cli.log).toBeCalledWith("Using subscription ID: azureSubId");
+  });
 });