Repository: serverless/serverless-azure-functions
Pull request #175 (2019-06-20 17:48:01)

util: add script to generate service principal to ease testing
so user don't have to login every time they run `sls deploy`


Patch:
diff --git a/.gitignore b/.gitignore
index 300f95f..ee4336e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,4 +2,4 @@ node_modules/
 /lib/
 *.dll
 /coverage/
-.env
\ No newline at end of file
+.env*
\ No newline at end of file
diff --git a/.travis.yml b/.travis.yml
index 5d82365..536eeeb 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -16,7 +16,8 @@ branches:
     - /^v.*$/ # Ensure to build release tags of format v1.0.0-1
 
 stages:
-  - test
+  - name: test
+    if: tag IS NOT present
   - name: publish
     if: tag IS present # https://docs.travis-ci.com/user/conditions-v1
 
diff --git a/docs/DEPLOY.md b/docs/DEPLOY.md
new file mode 100644
index 0000000..45fbd25
--- /dev/null
+++ b/docs/DEPLOY.md
@@ -0,0 +1,58 @@
+# Overview
+
+Deploy usage guide and design decision.
+
+## Multi-stage, multi-region deploy
+
+By default, all functions are deploy to a stage (`dev`) and a region.  The
+autogenerated resource group name will reflect this information.  The name 
+follow the Microsoft recommended [naming convention](https://docs.microsoft.com/en-us/azure/architecture/best-practices/naming-conventions#general).
+
+### Using exitsing resource group
+
+`sls deploy -s dev -r westus2 -g myResourceGroup`
+
+- Use resource group `myResourceGroup`
+- Create/Update app service plan, app service
+- Zipdeploy code
+
+### No resource group specified
+
+`sls deploy -s dev -r westus2`
+
+- Create if not exist resource group `<service>-dev-westus2-rg`
+- Create/Update app service plan, app service
+- Zipdeploy code
+
+### Design decision on resource group support
+
+#### Specifying resourceGroup in serverless.yml
+
+- it is not clear if a resourceGroup has already been associated with a given stage and region.  
+
+For example, we have the following default config
+
+```yaml
+provider:
+  stage: dev
+  region: westus
+  resourceGroup: myResourceGroup
+```
+
+then user try to deploy
+`sls deploy -s prod -r westus`
+
+1. Do we use the environment variables / creds associated with `dev` or `prod`?
+1. Someone new to the codecase see both the `serverless.yaml` and the command in a CD script, how can they tell which resourceGroup goes where.
+
+#### Specifying resourceGroup in command line
+
+`sls deploy -s prod -r westus -g prodResourceGroup`
+
+1. With this format, it's more clear which stage+region combo is associated with a resourceGroup.  
+1. User can still make mistake, however, and use the `wrong` resource group for a specific stage.
+
+#### Don't support user defined resource group
+
+1. always using the right resource group
+1. restrictive for user who have already defined their resources
diff --git a/package-lock.json b/package-lock.json
index 3e81f79..98cfeca 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1753,6 +1753,7 @@
       "version": "0.1.4",
       "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz",
       "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=",
+      "optional": true,
       "requires": {
         "kind-of": "^3.0.2",
         "longest": "^1.0.1",
@@ -1925,6 +1926,7 @@
       "version": "4.10.1",
       "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz",
       "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==",
+      "optional": true,
       "requires": {
         "bn.js": "^4.0.0",
         "inherits": "^2.0.1",
@@ -2394,7 +2396,8 @@
     "bn.js": {
       "version": "4.11.8",
       "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz",
-      "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA=="
+      "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==",
+      "optional": true
     },
     "body-parser": {
       "version": "1.19.0",
@@ -2499,7 +2502,8 @@
     "brorand": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz",
-      "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8="
+      "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=",
+      "optional": true
     },
     "browser-process-hrtime": {
       "version": "0.1.3",
@@ -2528,6 +2532,7 @@
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz",
       "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==",
+      "optional": true,
       "requires": {
         "buffer-xor": "^1.0.3",
         "cipher-base": "^1.0.0",
@@ -2564,6 +2569,7 @@
       "version": "4.0.1",
       "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz",
       "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=",
+      "optional": true,
       "requires": {
         "bn.js": "^4.1.0",
         "randombytes": "^2.0.1"
@@ -2660,7 +2666,8 @@
     "buffer-xor": {
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz",
-      "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk="
+      "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=",
+      "optional": true
     },
     "builtin-modules": {
       "version": "1.1.1",
@@ -2842,6 +2849,7 @@
       "version": "1.0.4",
       "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz",
       "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==",
+      "optional": true,
       "requires": {
         "inherits": "^2.0.1",
         "safe-buffer": "^5.0.1"
@@ -3219,6 +3227,7 @@
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz",
       "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==",
+      "optional": true,
       "requires": {
         "cipher-base": "^1.0.1",
         "inherits": "^2.0.1",
@@ -3231,6 +3240,7 @@
       "version": "1.1.7",
       "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz",
       "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==",
+      "optional": true,
       "requires": {
         "cipher-base": "^1.0.3",
         "create-hash": "^1.1.0",
@@ -3689,6 +3699,7 @@
       "version": "6.4.1",
       "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.4.1.tgz",
       "integrity": "sha512-BsXLz5sqX8OHcsh7CqBMztyXARmGQ3LWPtGjJi6DiJHq5C/qvi9P3OqgswKSDftbu8+IoI/QDTAm2fFnQ9SZSQ==",
+      "optional": true,
       "requires": {
         "bn.js": "^4.4.0",
         "brorand": "^1.0.1",
@@ -3750,6 +3761,7 @@
       "version": "0.1.7",
       "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz",
       "integrity": "sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==",
+      "optional": true,
       "requires": {
         "prr": "~1.0.1"
       }
@@ -4129,6 +4141,7 @@
       "version": "0.3.5",
       "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz",
       "integrity": "sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=",
+      "optional": true,
       "requires": {
         "d": "1",
         "es5-ext": "~0.10.14"
@@ -4144,6 +4157,7 @@
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz",
       "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==",
+      "optional": true,
       "requires": {
         "md5.js": "^1.3.4",
         "safe-buffer": "^5.1.1"
@@ -4673,7 +4687,8 @@
         },
         "ansi-regex": {
           "version": "2.1.1",
-          "bundled": true
+          "bundled": true,
+          "optional": true
         },
         "aproba": {
           "version": "1.2.0",
@@ -5038,7 +5053,8 @@
         },
         "safe-buffer": {
           "version": "5.1.2",
-          "bundled": true
+          "bundled": true,
+          "optional": true
         },
         "safer-buffer": {
           "version": "2.1.2",
@@ -5086,6 +5102,7 @@
         "strip-ansi": {
           "version": "3.0.1",
           "bundled": true,
+          "optional": true,
           "requires": {
             "ansi-regex": "^2.0.0"
           }
@@ -5124,11 +5141,13 @@
         },
         "wrappy": {
           "version": "1.0.2",
-          "bundled": true
+          "bundled": true,
+          "optional": true
         },
         "yallist": {
           "version": "3.0.3",
-          "bundled": true
+          "bundled": true,
+          "optional": true
         }
       }
     },
@@ -5436,6 +5455,7 @@
       "version": "3.0.4",
       "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz",
       "integrity": "sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=",
+      "optional": true,
       "requires": {
         "inherits": "^2.0.1",
         "safe-buffer": "^5.0.1"
@@ -5445,6 +5465,7 @@
       "version": "1.1.7",
       "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz",
       "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==",
+      "optional": true,
       "requires": {
         "inherits": "^2.0.3",
         "minimalistic-assert": "^1.0.1"
@@ -5454,6 +5475,7 @@
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz",
       "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=",
+      "optional": true,
       "requires": {
         "hash.js": "^1.0.3",
         "minimalistic-assert": "^1.0.0",
@@ -5772,7 +5794,8 @@
     "is-extglob": {
       "version": "2.1.1",
       "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
-      "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI="
+      "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
+      "optional": true
     },
     "is-fullwidth-code-point": {
       "version": "1.0.0",
@@ -7412,7 +7435,8 @@
     "longest": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz",
-      "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc="
+      "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=",
+      "optional": true
     },
     "loose-envify": {
       "version": "1.4.0",
@@ -7503,6 +7527,7 @@
       "version": "1.3.5",
       "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz",
       "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==",
+      "optional": true,
       "requires": {
         "hash-base": "^3.0.0",
         "inherits": "^2.0.1",
@@ -7528,6 +7553,7 @@
       "version": "0.4.1",
       "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz",
       "integrity": "sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=",
+      "optional": true,
       "requires": {
         "errno": "^0.1.3",
         "readable-stream": "^2.0.1"
@@ -7618,12 +7644,14 @@
     "minimalistic-assert": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
-      "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A=="
+      "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==",
+      "optional": true
     },
     "minimalistic-crypto-utils": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz",
-      "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo="
+      "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=",
+      "optional": true
     },
     "minimatch": {
       "version": "3.0.4",
@@ -8221,6 +8249,7 @@
       "version": "5.1.4",
       "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.4.tgz",
       "integrity": "sha512-Qs5duJcuvNExRfFZ99HDD3z4mAi3r9Wl/FOjEOijlxwCZs7E7mW2vjTpgQ4J8LpTF8x5v+1Vn5UQFejmWT11aw==",
+      "optional": true,
       "requires": {
         "asn1.js": "^4.0.0",
         "browserify-aes": "^1.0.0",
@@ -8329,6 +8358,7 @@
       "version": "3.0.17",
       "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.17.tgz",
       "integrity": "sha512-U/il5MsrZp7mGg3mSQfn742na2T+1/vHDCG5/iTI3X9MKUuYUZVLQhyRsg06mCgDBTd57TxzgZt7P+fYfjRLtA==",
+      "optional": true,
       "requires": {
         "create-hash": "^1.1.2",
         "create-hmac": "^1.1.4",
@@ -8531,7 +8561,8 @@
     "prr": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz",
-      "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY="
+      "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=",
+      "optional": true
     },
     "pseudomap": {
       "version": "1.0.2",
@@ -8598,6 +8629,7 @@
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
       "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
+      "optional": true,
       "requires": {
         "safe-buffer": "^5.1.0"
       }
@@ -9010,6 +9042,7 @@
       "version": "2.0.2",
       "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz",
       "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==",
+      "optional": true,
       "requires": {
         "hash-base": "^3.0.0",
         "inherits": "^2.0.1"
@@ -9399,6 +9432,7 @@
       "version": "2.4.11",
       "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz",
       "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==",
+      "optional": true,
       "requires": {
         "inherits": "^2.0.1",
         "safe-buffer": "^5.0.1"
@@ -9594,7 +9628,8 @@
     "source-list-map": {
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz",
-      "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw=="
+      "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==",
+      "optional": true
     },
     "source-map": {
       "version": "0.7.3",
@@ -10151,7 +10186,8 @@
     "tapable": {
       "version": "0.2.9",
       "resolved": "https://registry.npmjs.org/tapable/-/tapable-0.2.9.tgz",
-      "integrity": "sha512-2wsvQ+4GwBvLPLWsNfLCDYGsW6xb7aeC6utq2Qh0PFwgEy7K7dsma9Jsmb2zSQj7GvYAyUGSntLtsv++GmgL1A=="
+      "integrity": "sha512-2wsvQ+4GwBvLPLWsNfLCDYGsW6xb7aeC6utq2Qh0PFwgEy7K7dsma9Jsmb2zSQj7GvYAyUGSntLtsv++GmgL1A==",
+      "optional": true
     },
     "tar-stream": {
       "version": "1.6.2",
@@ -11001,6 +11037,7 @@
       "version": "1.3.0",
       "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.3.0.tgz",
       "integrity": "sha512-OiVgSrbGu7NEnEvQJJgdSFPl2qWKkWq5lHMhgiToIiN9w34EBnjYzSYs+VbL5KoYiLNtFFa7BZIKxRED3I32pA==",
+      "optional": true,
       "requires": {
         "source-list-map": "^2.0.0",
         "source-map": "~0.6.1"
@@ -11009,7 +11046,8 @@
         "source-map": {
           "version": "0.6.1",
           "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
-          "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
+          "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+          "optional": true
         }
       }
     },
diff --git a/scripts/generate-service-principal.sh b/scripts/generate-service-principal.sh
new file mode 100755
index 0000000..7df6adf
--- /dev/null
+++ b/scripts/generate-service-principal.sh
@@ -0,0 +1,38 @@
+#!/bin/bash
+
+###
+# This script generate a service principal using your currently set subscription
+#   then write out the credentials of your new sp in a file 
+#   that you can export as environment variables
+###
+set -e
+
+if ! [[ -x "$(command -v jq)" ]]; then
+  echo "Please install [jq] before continuing -> https://stedolan.github.io/jq/download/.  Aborting."
+  exit 1
+fi
+
+SP_NAME=$1
+
+if [[ -z $SP_NAME ]]; then
+  SP_NAME="FunctionsTestingPrincipal"
+  echo "You didn't pass in a name for the service principal.  We'll use the default: ${SP_NAME}"
+  echo
+fi
+
+SUBSCRIPTION=$(az account show | jq .name)
+echo "You're using subscription: ${SUBSCRIPTION}"
+echo "--> Creating service principal with name: ${SP_NAME}"
+SP_RESPONSE=$(az ad sp create-for-rbac --name "http://${SP_NAME}")
+
+FILE=".env.servicePrincipal"
+
+echo "--> Writing creds to '${FILE}'"
+cat <<EOF >${FILE}
+export azureSubId=$(az account show | jq .id)
+export azureServicePrincipalTenantId=$(echo "${SP_RESPONSE}" | jq .tenant)
+export azureServicePrincipalClientId=$(echo "${SP_RESPONSE}" | jq .name)
+export azureServicePrincipalPassword=$(echo "${SP_RESPONSE}" | jq .password)
+EOF
+
+echo "Run \"source ${FILE}\" to set your Azure account credentials "
diff --git a/scripts/tagBranchForRelease.sh b/scripts/tagBranchForRelease.sh
index 6afa93e..84ae760 100755
--- a/scripts/tagBranchForRelease.sh
+++ b/scripts/tagBranchForRelease.sh
@@ -12,5 +12,6 @@ fi
 echo "getting latest"
 git pull
 PACKAGE_VERSION=$(node -p -e "require('./package.json').version")
-git tag $PACKAGE_VERSION
-git push origin $PACKAGE_VERSION
+TAG="v${PACKAGE_VERSION}"
+git tag $TAG
+git push origin $TAG
diff --git a/src/plugins/deploy/azureDeployPlugin.ts b/src/plugins/deploy/azureDeployPlugin.ts
index 48f0967..76c88eb 100644
--- a/src/plugins/deploy/azureDeployPlugin.ts
+++ b/src/plugins/deploy/azureDeployPlugin.ts
@@ -4,11 +4,46 @@ import { FunctionAppService } from "../../services/functionAppService";
 
 export class AzureDeployPlugin {
   public hooks: { [eventName: string]: Promise<any> };
+  public commands: any;
 
   public constructor(private serverless: Serverless, private options: Serverless.Options) {
     this.hooks = {
-      "deploy:deploy": this.deploy.bind(this)
+      "deploy:deploy": this.deploy.bind(this),
+      "deploy:list:list": this.list.bind(this),
     };
+
+    this.commands = {
+      deploy: {
+        commands: {
+          list: {
+            usage: "List deployments",
+            lifecycleEvents: [
+              "list"
+            ]
+          }
+        }
+      }
+    }
+  }
+
+  private async list() {
+    this.serverless.cli.log("Listing deployments");
+    const resourceService = new ResourceService(this.serverless, this.options);
+    const deployments = await resourceService.getDeployments();
+    if (!deployments || deployments.length === 0) {
+      this.serverless.cli.log(`No deployments found for resource group '${resourceService.getResourceGroup()}'`);
+      return;
+    }
+    let stringDeployments = "\n\nDeployments";
+
+    for (const dep of deployments) {
+      stringDeployments += "\n-----------\n"
+      stringDeployments += `Name: ${dep.name}\n`
+      stringDeployments += `Timestamp: ${dep.properties.timestamp.getTime()}\n`;
+      stringDeployments += `Datetime: ${dep.properties.timestamp.toISOString()}\n`
+    }
+    stringDeployments += "-----------\n"
+    this.serverless.cli.log(stringDeployments);
   }
 
   private async deploy() {
diff --git a/src/plugins/login/loginPlugin.ts b/src/plugins/login/loginPlugin.ts
index d64635e..083831f 100644
--- a/src/plugins/login/loginPlugin.ts
+++ b/src/plugins/login/loginPlugin.ts
@@ -10,7 +10,8 @@ export class AzureLoginPlugin {
     this.provider = (this.serverless.getProvider("azure") as any) as AzureProvider;
 
     this.hooks = {
-      "before:package:initialize": this.login.bind(this)
+      "before:package:initialize": this.login.bind(this),
+      "before:deploy:list:list": this.login.bind(this),
     };
   }
 
diff --git a/src/services/baseService.ts b/src/services/baseService.ts
index d95bd16..4a81638 100644
--- a/src/services/baseService.ts
+++ b/src/services/baseService.ts
@@ -12,20 +12,30 @@ export abstract class BaseService {
   protected resourceGroup: string;
   protected deploymentName: string;
 
-  protected constructor(protected serverless: Serverless, protected options?: Serverless.Options, authenticate = true) {
+  protected constructor(
+    protected serverless: Serverless,
+    protected options: Serverless.Options = { stage: null, region: null },
+    authenticate: boolean = true,
+  ) {
     Guard.null(serverless);
 
     this.baseUrl = "https://management.azure.com";
     this.serviceName = serverless.service["service"];
     this.credentials = serverless.variables["azureCredentials"];
     this.subscriptionId = serverless.variables["subscriptionId"];
-    this.resourceGroup = serverless.service.provider["resourceGroup"] || `${this.serviceName}-rg`;
+    this.resourceGroup = this.getResourceGroupName();
     this.deploymentName = serverless.service.provider["deploymentName"] || `${this.resourceGroup}-deployment`;
 
+    this.setDefaultValues();
+
     if (!this.credentials && authenticate) {
       throw new Error(`Azure Credentials has not been set in ${this.constructor.name}`);
     }
   }
+
+  public getResourceGroup(): string {
+    return this.resourceGroup;
+  }
   
   /**
    * Sends an API request using axios HTTP library
@@ -81,4 +91,33 @@ export abstract class BaseService {
   protected slsConfigFile(): string {
     return ("config" in this.options) ? this.options["config"] : "serverless.yml";
   }
+
+  private setDefaultValues(): void {
+    // TODO: Right now the serverless core will always default to AWS default region if the
+    // region has not been set in the serverless.yml or CLI options
+    const awsDefault = "us-east-1"
+    const providerRegion = this.serverless.service.provider.region;
+
+    if (!providerRegion || providerRegion === awsDefault) { // no region specified in serverless.yml
+      this.serverless.service.provider.region = "westus";
+    }
+
+    if (!this.serverless.service.provider.stage) {
+      this.serverless.service.provider.stage = "dev";
+    }
+  }
+
+  protected getRegion(): string {
+    return this.options.region || this.serverless.service.provider.region;
+  }
+
+  protected getStage(): string {
+    return this.options.stage || this.serverless.service.provider.stage;
+  }
+
+  protected getResourceGroupName(): string {
+    return this.options["resourceGroup"]
+      || this.serverless.service.provider["resourceGroup"]
+      || `${this.serviceName}-${this.getRegion()}-${this.getStage()}-rg`;
+  }
 }
diff --git a/src/services/functionAppService.ts b/src/services/functionAppService.ts
index 5ea7c7d..4237f37 100644
--- a/src/services/functionAppService.ts
+++ b/src/services/functionAppService.ts
@@ -9,6 +9,7 @@ import Serverless from "serverless";
 import { BaseService } from "./baseService";
 import { FunctionAppHttpTriggerConfig } from "../models/functionApp";
 import { Site, FunctionEnvelope } from "@azure/arm-appservice/esm/models";
+import { Guard } from "../shared/guard";
 
 export class FunctionAppService extends BaseService {
   private resourceClient: ResourceManagementClient;
@@ -46,6 +47,9 @@ export class FunctionAppService extends BaseService {
   }
 
   public async deleteFunction(functionApp: Site, functionName: string) {
+    Guard.null(functionApp);
+    Guard.empty(functionName);
+
     this.serverless.cli.log(`-> Deleting function: ${functionName}`);
     const deleteFunctionUrl = `${this.baseUrl}${functionApp.id}/functions/${functionName}?api-version=2016-08-01`;
 
@@ -53,6 +57,8 @@ export class FunctionAppService extends BaseService {
   }
 
   public async syncTriggers(functionApp: Site) {
+    Guard.null(functionApp);
+
     this.serverless.cli.log("Syncing function triggers");
 
     const syncTriggersUrl = `${this.baseUrl}${functionApp.id}/syncfunctiontriggers?api-version=2016-08-01`;
@@ -60,6 +66,8 @@ export class FunctionAppService extends BaseService {
   }
 
   public async cleanUp(functionApp: Site) {
+    Guard.null(functionApp);
+
     this.serverless.cli.log("Cleaning up existing functions");
     const deleteTasks = [];
 
@@ -76,6 +84,8 @@ export class FunctionAppService extends BaseService {
   }
 
   public async listFunctions(functionApp: Site): Promise<FunctionEnvelope[]> {
+    Guard.null(functionApp);
+
     const getTokenUrl = `${this.baseUrl}${functionApp.id}/functions?api-version=2016-08-01`;
     const response = await this.sendApiRequest("GET", getTokenUrl);
 
@@ -87,6 +97,9 @@ export class FunctionAppService extends BaseService {
   }
 
   public async getFunction(functionApp: Site, functionName: string): Promise<FunctionEnvelope> {
+    Guard.null(functionApp);
+    Guard.empty(functionName);
+
     const getFunctionUrl = `${this.baseUrl}${functionApp.id}/functions/${functionName}?api-version=2016-08-01`;
     const response = await this.sendApiRequest("GET", getFunctionUrl);
 
@@ -98,6 +111,9 @@ export class FunctionAppService extends BaseService {
   }
 
   public async uploadFunctions(functionApp: Site): Promise<any> {
+    Guard.null(functionApp);
+
+    this.log("Deploying serverless functions...");
     await this.zipDeploy(functionApp);
   }
 
@@ -175,7 +191,7 @@ export class FunctionAppService extends BaseService {
 
   private async zipDeploy(functionApp) {
     const functionAppName = functionApp.name;
-    const scmDomain = functionApp.enabledHostNames[0];
+    const scmDomain = this.getScmDomain(functionApp);
 
     this.serverless.cli.log(`Deploying zip file to function app: ${functionAppName}`);
 
@@ -246,7 +262,7 @@ export class FunctionAppService extends BaseService {
   private async runKuduCommand(functionApp: Site, command: string) {
     this.serverless.cli.log(`-> Running Kudu command ${command}...`);
 
-    const scmDomain = functionApp.enabledHostNames[0];
+    const scmDomain = this.getScmDomain(functionApp);
     const requestUrl = `https://${scmDomain}/api/command`;
 
     // TODO: There is a case where the body will contain an error, but it's
@@ -275,4 +291,15 @@ export class FunctionAppService extends BaseService {
 
     return response.data.replace(/"/g, "");
   }
+
+  /**
+   * Retrieves the SCM domain from the list of enabled domains within the app
+   * Note: The SCM domain exposes additional API calls from the standard REST APIs.
+   * @param functionApp The function app / web site
+   */
+  private getScmDomain(functionApp: Site) {
+    return functionApp.enabledHostNames.find((hostName: string) => {
+      return hostName.endsWith("scm.azurewebsites.net");
+    });
+  }
 }
diff --git a/src/services/resourceService.ts b/src/services/resourceService.ts
index 7bd4653..e36dd90 100644
--- a/src/services/resourceService.ts
+++ b/src/services/resourceService.ts
@@ -11,14 +11,17 @@ export class ResourceService extends BaseService {
     this.resourceClient = new ResourceManagementClient(this.credentials, this.subscriptionId);
   }
 
+  public async getDeployments() {
+    this.log(`Listing deployments for resource group '${this.resourceGroup}':`);
+    return await this.resourceClient.deployments.listByResourceGroup(this.resourceGroup);
+  }
+
   public async deployResourceGroup() {
     this.log(`Creating resource group: ${this.resourceGroup}`);
 
-    const groupParameters = {
-      location: this.serverless.service.provider["location"]
-    };
-
-    return await this.resourceClient.resourceGroups.createOrUpdate(this.resourceGroup, groupParameters);
+    return await this.resourceClient.resourceGroups.createOrUpdate(this.resourceGroup, {
+      location: this.getRegion(),
+    });
   }
 
   public async deleteDeployment() {
diff --git a/src/shared/utils.ts b/src/shared/utils.ts
index 9b8654a..f8102a9 100644
--- a/src/shared/utils.ts
+++ b/src/shared/utils.ts
@@ -113,6 +113,10 @@ export class Utils {
     return metaData;
   }
 
+  public static prettyPrint(json) {
+    return JSON.stringify(json, null, 2);
+  }
+
   public static interpolateFile(sls: Serverless, path: string, params: Map<string, string>) {
     const template = sls.utils.readFileSync(path);
     const names = params.keys();


Test patch:
diff --git a/src/plugins/deploy/azureDeployPlugin.test.ts b/src/plugins/deploy/azureDeployPlugin.test.ts
index 2818890..3c3db2f 100644
--- a/src/plugins/deploy/azureDeployPlugin.test.ts
+++ b/src/plugins/deploy/azureDeployPlugin.test.ts
@@ -10,6 +10,11 @@ import { ResourceService } from "../../services/resourceService";
 import { Site } from "@azure/arm-appservice/esm/models";
 
 describe("Deploy plugin", () => {
+
+  afterEach(() => {
+    jest.resetAllMocks();
+  })
+
   it("calls deploy hook", async () => {
     const deployResourceGroup = jest.fn();
     const functionAppStub: Site = MockFactory.createTestSite();
@@ -30,4 +35,33 @@ describe("Deploy plugin", () => {
     expect(deploy).toBeCalled();
     expect(uploadFunctions).toBeCalledWith(functionAppStub);
   });
+
+  it("lists deployments", async () => {
+    const deployments = MockFactory.createTestDeployments();
+    ResourceService.prototype.getDeployments = jest.fn(() => Promise.resolve(deployments));
+    const sls = MockFactory.createTestServerless();
+    const options = MockFactory.createTestServerlessOptions();
+    const plugin = new AzureDeployPlugin(sls, options);
+    await invokeHook(plugin, "deploy:list:list");
+    let expectedLogStatement = "\n\nDeployments";
+    for (const dep of deployments) {
+      expectedLogStatement += "\n-----------\n"
+      expectedLogStatement += `Name: ${dep.name}\n`
+      expectedLogStatement += `Timestamp: ${dep.properties.timestamp.getTime()}\n`;
+      expectedLogStatement += `Datetime: ${dep.properties.timestamp.toISOString()}\n`
+    }
+    expectedLogStatement += "-----------\n"
+    expect(sls.cli.log).lastCalledWith(expectedLogStatement);
+  });
+
+  it("logs empty deployment list", async () => {
+    const sls = MockFactory.createTestServerless();
+    const resourceGroup = "rg1";
+    ResourceService.prototype.getDeployments = jest.fn(() => Promise.resolve([])) as any;
+    ResourceService.prototype.getResourceGroup = jest.fn(() => resourceGroup);
+    const options = MockFactory.createTestServerlessOptions();
+    const plugin = new AzureDeployPlugin(sls, options);
+    await invokeHook(plugin, "deploy:list:list");
+    expect(sls.cli.log).lastCalledWith(`No deployments found for resource group '${resourceGroup}'`);
+  });
 });
diff --git a/src/services/apimService.test.ts b/src/services/apimService.test.ts
index 896c32a..1b14fe4 100644
--- a/src/services/apimService.test.ts
+++ b/src/services/apimService.test.ts
@@ -43,7 +43,7 @@ describe("APIM Service", () => {
       provider: {
         name: "azure",
         resourceGroup: "test-sls-rg",
-        location: "West US",
+        region: "West US",
         apim: apimConfig,
       },
     };
@@ -91,7 +91,7 @@ describe("APIM Service", () => {
       const expectedResponse = interpolateJson(apimGetService200, {
         resourceGroup: {
           name: serverless.service.provider["resourceGroup"],
-          location: serverless.service.provider["location"],
+          location: serverless.service.provider.region,
         },
         resource: {
           name: apimConfig.name,
@@ -106,7 +106,7 @@ describe("APIM Service", () => {
       expect(resource).not.toBeNull();
       expect(resource).toMatchObject({
         name: apimConfig.name,
-        location: serverless.service.provider["location"],
+        location: serverless.service.provider.region,
       });
     });
   });
@@ -133,7 +133,7 @@ describe("APIM Service", () => {
       const expectedResponse = interpolateJson(apimGetApi200, {
         resourceGroup: {
           name: serverless.service.provider["resourceGroup"],
-          location: serverless.service.provider["location"],
+          location: serverless.service.provider.region,
         },
         service: {
           name: apimConfig.name,
diff --git a/src/services/baseService.test.ts b/src/services/baseService.test.ts
index a7e0225..ac3f65a 100644
--- a/src/services/baseService.test.ts
+++ b/src/services/baseService.test.ts
@@ -8,7 +8,7 @@ import request from "request";
 import fs from "fs";
 import { BaseService } from "./baseService";
 
-class TestService extends BaseService {
+class MockService extends BaseService {
   public constructor(serverless: Serverless, options?: Serverless.Options) {
     super(serverless, options);
   }
@@ -21,6 +21,18 @@ class TestService extends BaseService {
     return this.sendFile(requestOptions, filePath);
   }
 
+  public getSlsResourceGroupName() {
+    return this.getResourceGroupName();
+  }
+
+  public getSlsRegion() {
+    return this.getRegion();
+  }
+
+  public getSlsStage() {
+    return this.getStage();
+  }
+
   public getProperties() {
     return {
       baseUrl: this.baseUrl,
@@ -34,7 +46,7 @@ class TestService extends BaseService {
 }
 
 describe("Base Service", () => {
-  let service: TestService;
+  let service: MockService;
   let sls: Serverless;
 
   const slsConfig = {
@@ -49,12 +61,17 @@ describe("Base Service", () => {
     mockFs.restore();
   });
 
-  beforeEach(() => {
+  function createTestService(options?: Serverless.Options) {
     sls = MockFactory.createTestServerless();
     sls.variables["azureCredentials"] = MockFactory.createTestAzureCredentials();
     sls.variables["subscriptionId"] = "ABC123";
     Object.assign(sls.service, slsConfig);
-    service = new TestService(sls);
+
+    return new MockService(sls, options);
+  }
+
+  beforeEach(() => {
+    service = createTestService();
   });
 
   it("Initializes common service properties", () => {
@@ -67,9 +84,45 @@ describe("Base Service", () => {
     expect(props.deploymentName).toEqual(slsConfig.provider.deploymentName);
   });
 
+  it("Sets default region and stage values if not defined", () => {
+    const testService = new MockService(sls);
+
+    expect(testService).not.toBeNull();
+    expect(sls.service.provider.region).toEqual("westus");
+    expect(sls.service.provider.stage).toEqual("dev");
+  });
+
+  it("returns region and stage based on CLI options", () => {
+    const cliOptions = {
+      stage: "prod",
+      region: "eastus2"
+    };
+    const testService = new MockService(sls, cliOptions);
+
+    expect(testService.getSlsRegion()).toEqual(cliOptions.region);
+    expect(testService.getSlsStage()).toEqual(cliOptions.stage);
+  });
+
+  it("Generates resource group name from sls yaml config", () => {
+    const testService = new MockService(sls);
+    const resourceGroupName = testService.getSlsResourceGroupName();
+
+    expect(resourceGroupName).toEqual(sls.service.provider["resourceGroup"]);
+  });
+
+  it("Generates resource group from convention when NOT defined in sls yaml", () => {
+    sls.service.provider["resourceGroup"] = null;
+    const testService = new MockService(sls);
+    const resourceGroupName = testService.getSlsResourceGroupName();
+    const region = testService.getSlsRegion();
+    const stage = testService.getSlsStage();
+
+    expect(resourceGroupName).toEqual(`${sls.service["service"]}-${region}-${stage}-rg`);
+  });
+
   it("Fails if credentials have not been set in serverless config", () => {
     sls.variables["azureCredentials"] = null;
-    expect(() => new TestService(sls)).toThrow()
+    expect(() => new MockService(sls)).toThrow()
   });
 
   it("Makes HTTP request via axios", async () => {
diff --git a/src/services/functionAppService.test.ts b/src/services/functionAppService.test.ts
index d87f2c9..fce61cb 100644
--- a/src/services/functionAppService.test.ts
+++ b/src/services/functionAppService.test.ts
@@ -2,7 +2,6 @@ import axios from "axios";
 import MockAdapter from "axios-mock-adapter";
 import mockFs from "mock-fs";
 import Serverless from "serverless";
-import { constants } from "../config";
 import { MockFactory } from "../test/mockFactory";
 import { FunctionAppService } from "./functionAppService";
 
@@ -11,7 +10,7 @@ import { WebSiteManagementClient } from "@azure/arm-appservice";
 jest.mock("@azure/arm-resources")
 
 describe("Function App Service", () => {
-  
+
   const app = MockFactory.createTestSite();
   const slsService = MockFactory.createTestService();
   const variables = MockFactory.createTestVariables();
@@ -29,17 +28,18 @@ describe("Function App Service", () => {
   const authKeyUrl = `${baseUrl}${app.id}/functions/admin/token?api-version=2016-08-01`;
   const syncTriggersUrl = `${baseUrl}${app.id}/syncfunctiontriggers?api-version=2016-08-01`;
   const listFunctionsUrl = `${baseUrl}${app.id}/functions?api-version=2016-08-01`;
-  const uploadUrl = `https://${app.enabledHostNames[0]}${constants.scmZipDeployApiPath}/`
+  const scmDomain = app.enabledHostNames.find((hostname) => hostname.endsWith("scm.azurewebsites.net"));
+  const uploadUrl = `https://${scmDomain}/api/zipdeploy/`;
 
-  beforeAll(() => {   
+  beforeAll(() => {
 
     // TODO: How to spy on defaul exported function?
     const axiosMock = new MockAdapter(axios);
-    
+
     // Master Key
     axiosMock.onGet(masterKeyUrl).reply(200, { value: masterKey });
     // Auth Key
-    axiosMock.onGet(authKeyUrl).reply(200, authKey);  
+    axiosMock.onGet(authKeyUrl).reply(200, authKey);
     // Sync Triggers
     axiosMock.onPost(syncTriggersUrl).reply(200, syncTriggersMessage);
     // List Functions
@@ -52,7 +52,7 @@ describe("Function App Service", () => {
 
     mockFs({
       "app.zip": "contents",
-    }, {createCwd: true, createTmp: true});
+    }, { createCwd: true, createTmp: true });
   });
 
   beforeEach(() => {
@@ -80,8 +80,8 @@ describe("Function App Service", () => {
       }),
       options || MockFactory.createTestServerlessOptions()
     )
-  } 
-  
+  }
+
   it("get returns function app", async () => {
     const service = createService();
     const result = await service.get();
@@ -93,7 +93,7 @@ describe("Function App Service", () => {
   it("get returns null if error occurred", async () => {
     const service = createService();
     WebSiteManagementClient.prototype.webApps = {
-      get: jest.fn(() => { return { error: { code: "ResourceNotFound"}}}),
+      get: jest.fn(() => { return { error: { code: "ResourceNotFound" } } }),
       deleteFunction: jest.fn(),
     } as any;
     const result = await service.get();
@@ -106,14 +106,14 @@ describe("Function App Service", () => {
     const service = createService();
     const masterKey = await service.getMasterKey();
     expect(masterKey).toEqual(masterKey);
-    
+
   });
 
   it("deletes function", async () => {
     const service = createService();
     const response = await service.deleteFunction(app, Object.keys(functions)[0]);
     expect(response.data).toEqual(deleteFunctionMessage);
-    
+
   });
 
   it("syncs triggers", async () => {
diff --git a/src/services/resourceService.test.ts b/src/services/resourceService.test.ts
index 3ce6df3..22ac5ad 100644
--- a/src/services/resourceService.test.ts
+++ b/src/services/resourceService.test.ts
@@ -6,6 +6,7 @@ jest.mock("@azure/arm-resources")
 import { ResourceManagementClient } from "@azure/arm-resources";
 
 describe("Resource Service", () => {
+  const deployments = MockFactory.createTestDeployments();
 
   beforeAll(() => {
     ResourceManagementClient.prototype.resourceGroups = {
@@ -14,7 +15,8 @@ describe("Resource Service", () => {
     } as any;
 
     ResourceManagementClient.prototype.deployments = {
-      deleteMethod: jest.fn()
+      deleteMethod: jest.fn(),
+      listByResourceGroup: jest.fn(() => Promise.resolve(deployments)),
     } as any;
   });
 
@@ -36,7 +38,7 @@ describe("Resource Service", () => {
     const resourceGroup = "myResourceGroup"
     const location = "West Us";
     sls.service.provider["resourceGroup"] = resourceGroup
-    sls.service.provider["location"] = location;
+    sls.service.provider.region = location;
     sls.variables["azureCredentials"] = "fake credentials"
     const options = MockFactory.createTestServerlessOptions();
     const service = new ResourceService(sls, options);
@@ -70,4 +72,15 @@ describe("Resource Service", () => {
     expect(ResourceManagementClient.prototype.resourceGroups.deleteMethod)
       .toBeCalledWith(resourceGroup);
   });
+
+  it("lists deployments", async () => {
+    const sls = MockFactory.createTestServerless();
+    const resourceGroup = "myResourceGroup";
+    sls.service.provider["resourceGroup"] = resourceGroup
+    sls.variables["azureCredentials"] = "fake credentials"
+    const options = MockFactory.createTestServerlessOptions();
+    const service = new ResourceService(sls, options);
+    const deps = await service.getDeployments();
+    expect(deps).toEqual(deployments);
+  });
 });
\ No newline at end of file
diff --git a/src/test/mockFactory.ts b/src/test/mockFactory.ts
index 7efaaa3..2a7ede2 100644
--- a/src/test/mockFactory.ts
+++ b/src/test/mockFactory.ts
@@ -13,6 +13,7 @@ import { ServerlessAzureConfig } from "../models/serverless";
 import { AzureServiceProvider, ServicePrincipalEnvVariables } from "../models/azureProvider"
 import { Logger } from "../models/generic";
 import { ApiCorsPolicy } from "../models/apiManagement";
+import { DeploymentsListByResourceGroupResponse } from "@azure/arm-resources/esm/models";
 
 function getAttribute(object: any, prop: string, defaultValue: any): any {
   if (object && object[prop]) {
@@ -140,6 +141,19 @@ export class MockFactory {
     return credentials;
   }
 
+  public static createTestDeployments(count: number = 5): DeploymentsListByResourceGroupResponse {
+    const result = [];
+    for (let i = 0; i < count; i++) {
+      result.push({
+        name: `deployment${i+1}`,
+        properties: {
+          timestamp: new Date(),
+        }
+      })
+    }
+    return result as DeploymentsListByResourceGroupResponse
+  }
+
   public static createTestAxiosResponse<T>(
     config: AxiosRequestConfig,
     responseJson: T,
@@ -184,7 +198,7 @@ export class MockFactory {
     const data = {
       provider: {
         name: "azure",
-        location: "West US 2"
+        region: "West US 2"
       },
       plugins: [
         "serverless-azure-functions"
@@ -278,9 +292,10 @@ export class MockFactory {
       id: "appId",
       name: name,
       location: "West US",
-      defaultHostName: "myHostName",
+      defaultHostName: "myHostName.azurewebsites.net",
       enabledHostNames: [
-        "myHostName"
+        "myHostName.azurewebsites.net",
+        "myHostName.scm.azurewebsites.net",
       ]
     };
   }