Repository: serverless/serverless-azure-functions
Pull request #218 (2019-07-29 22:01:00)

fix: Inject current environment variables to spawn process
Not able to repro `ENOENT` error yet, but this PR should help with debugging.
- [x] Inject current environment variables to spawned process (most importantly, `PATH` so that an executable can be found)
- [x] Add additional log statements on errors for debugging purposes

See [this stack overflow answer](https://stackoverflow.com/a/27688805) for more details.

coveralls: 
[![Coverage Status](https://coveralls.io/builds/24863114/badge)](https://coveralls.io/builds/24863114)

Coverage increased (+0.01%) to 84.22% when pulling **150706152e9557c18ee6f753544ff9afe6f4fa4d on tabarlow/fix-spawn** into **1df392c52dc269f16561793e76663aa2771934a8 on dev**.


Patch:
diff --git a/src/armTemplates/compositeArmTemplate.ts b/src/armTemplates/compositeArmTemplate.ts
index 73d834b..299adbd 100644
--- a/src/armTemplates/compositeArmTemplate.ts
+++ b/src/armTemplates/compositeArmTemplate.ts
@@ -1,10 +1,7 @@
-import {
-  ArmResourceTemplateGenerator,
-  ArmResourceTemplate
-} from "../models/armTemplates";
-import { Guard } from "../shared/guard";
+import { ArmResourceTemplate, ArmResourceTemplateGenerator } from "../models/armTemplates";
 import { ServerlessAzureConfig } from "../models/serverless";
-import { Utils } from "../shared/utils";
+import { AzureNamingService } from "../services/namingService";
+import { Guard } from "../shared/guard";
 
 export class CompositeArmTemplate implements ArmResourceTemplateGenerator {
   public constructor(private childTemplates: ArmResourceTemplateGenerator[]) {
@@ -43,7 +40,7 @@ export class CompositeArmTemplate implements ArmResourceTemplateGenerator {
       parameters = {
         ...parameters,
         ...resource.getParameters(config),
-        location: Utils.getNormalizedRegionName(config.provider.region)
+        location: AzureNamingService.getNormalizedRegionName(config.provider.region)
       };
     });
 
diff --git a/src/armTemplates/resources/apim.ts b/src/armTemplates/resources/apim.ts
index eaca327..5003361 100644
--- a/src/armTemplates/resources/apim.ts
+++ b/src/armTemplates/resources/apim.ts
@@ -1,13 +1,15 @@
-import { ServerlessAzureConfig } from "../../models/serverless";
-import { ArmResourceTemplateGenerator, ArmResourceTemplate } from "../../models/armTemplates";
 import { ApiManagementConfig } from "../../models/apiManagement";
-import { Utils } from "../../shared/utils";
+import { ArmResourceTemplate, ArmResourceTemplateGenerator } from "../../models/armTemplates";
+import { ServerlessAzureConfig } from "../../models/serverless";
+import { AzureNamingService } from "../../services/namingService";
 
 export class ApimResource implements ArmResourceTemplateGenerator {
   public static getResourceName(config: ServerlessAzureConfig) {
-    return config.provider.apim && config.provider.apim.name
-      ? config.provider.apim.name
-      : `${config.provider.prefix}-${Utils.createShortAzureRegionName(config.provider.region)}-${Utils.createShortStageName(config.provider.stage)}-apim`;
+    return AzureNamingService.getResourceName(
+      config,
+      config.provider.apim,
+      "apim"
+    );
   }
 
   public getTemplate(): ArmResourceTemplate {
@@ -77,4 +79,4 @@ export class ApimResource implements ArmResourceTemplateGenerator {
       apimPublisherName: apimConfig.publisherName,
     };
   }
-}
\ No newline at end of file
+}
diff --git a/src/armTemplates/resources/appInsights.ts b/src/armTemplates/resources/appInsights.ts
index 8667d92..9426a0b 100644
--- a/src/armTemplates/resources/appInsights.ts
+++ b/src/armTemplates/resources/appInsights.ts
@@ -1,12 +1,14 @@
-import { ArmResourceTemplateGenerator, ArmResourceTemplate } from "../../models/armTemplates";
+import { ArmResourceTemplate, ArmResourceTemplateGenerator } from "../../models/armTemplates";
 import { ServerlessAzureConfig } from "../../models/serverless";
-import { Utils } from "../../shared/utils";
+import { AzureNamingService } from "../../services/namingService";
 
 export class AppInsightsResource implements ArmResourceTemplateGenerator {
   public static getResourceName(config: ServerlessAzureConfig) {
-    return config.provider.appInsights && config.provider.appInsights.name
-      ? config.provider.appInsights.name
-      : `${config.provider.prefix}-${Utils.createShortAzureRegionName(config.provider.region)}-${Utils.createShortStageName(config.provider.stage)}-appinsights`;
+    return AzureNamingService.getResourceName(
+      config,
+      config.provider.appInsights,
+      "appinsights"
+    );
   }
 
   public getTemplate(): ArmResourceTemplate {
@@ -45,4 +47,4 @@ export class AppInsightsResource implements ArmResourceTemplateGenerator {
       appInsightsName: AppInsightsResource.getResourceName(config),
     };
   }
-}
\ No newline at end of file
+}
diff --git a/src/armTemplates/resources/appServicePlan.ts b/src/armTemplates/resources/appServicePlan.ts
index 9581996..4485cb0 100644
--- a/src/armTemplates/resources/appServicePlan.ts
+++ b/src/armTemplates/resources/appServicePlan.ts
@@ -1,12 +1,14 @@
-import { ArmResourceTemplateGenerator, ArmResourceTemplate } from "../../models/armTemplates";
-import { ServerlessAzureConfig, ResourceConfig } from "../../models/serverless";
-import { Utils } from "../../shared/utils";
+import { ArmResourceTemplate, ArmResourceTemplateGenerator } from "../../models/armTemplates";
+import { ResourceConfig, ServerlessAzureConfig } from "../../models/serverless";
+import { AzureNamingService } from "../../services/namingService";
 
 export class AppServicePlanResource implements ArmResourceTemplateGenerator {
   public static getResourceName(config: ServerlessAzureConfig) {
-    return config.provider.appServicePlan && config.provider.appServicePlan.name
-      ? config.provider.appServicePlan.name
-      : `${config.provider.prefix}-${Utils.createShortAzureRegionName(config.provider.region)}-${Utils.createShortStageName(config.provider.stage)}-asp`;
+    return AzureNamingService.getResourceName(
+      config,
+      config.provider.appInsights,
+      "asp"
+    );
   }
 
   public getTemplate(): ArmResourceTemplate {
@@ -66,4 +68,4 @@ export class AppServicePlanResource implements ArmResourceTemplateGenerator {
       appServicePlanSkuTier: resourceConfig.sku.tier,
     }
   }
-}
\ No newline at end of file
+}
diff --git a/src/armTemplates/resources/functionApp.ts b/src/armTemplates/resources/functionApp.ts
index 48c7f5c..1af37a3 100644
--- a/src/armTemplates/resources/functionApp.ts
+++ b/src/armTemplates/resources/functionApp.ts
@@ -1,14 +1,15 @@
-import { ArmResourceTemplateGenerator, ArmResourceTemplate } from "../../models/armTemplates";
-import { ServerlessAzureConfig, FunctionAppConfig } from "../../models/serverless";
-import { Utils } from "../../shared/utils";
+import { ArmResourceTemplate, ArmResourceTemplateGenerator } from "../../models/armTemplates";
+import { FunctionAppConfig, ServerlessAzureConfig } from "../../models/serverless";
+import { AzureNamingService } from "../../services/namingService";
 
 export class FunctionAppResource implements ArmResourceTemplateGenerator {
   public static getResourceName(config: ServerlessAzureConfig) {
     const safeServiceName = config.service.replace(/\s/g, "-");
-
-    return config.provider.functionApp && config.provider.functionApp.name
-      ? config.provider.functionApp.name
-      : `${config.provider.prefix}-${Utils.createShortAzureRegionName(config.provider.region)}-${Utils.createShortStageName(config.provider.stage)}-${safeServiceName}`;
+    return AzureNamingService.getResourceName(
+      config,
+      config.provider.appInsights,
+      safeServiceName
+    );
   }
 
   public getTemplate(): ArmResourceTemplate {
diff --git a/src/armTemplates/resources/hostingEnvironment.ts b/src/armTemplates/resources/hostingEnvironment.ts
index 233aff0..7c8332b 100644
--- a/src/armTemplates/resources/hostingEnvironment.ts
+++ b/src/armTemplates/resources/hostingEnvironment.ts
@@ -1,12 +1,14 @@
-import { ArmResourceTemplateGenerator, ArmResourceTemplate } from "../../models/armTemplates";
+import { ArmResourceTemplate, ArmResourceTemplateGenerator } from "../../models/armTemplates";
 import { ServerlessAzureConfig } from "../../models/serverless";
-import { Utils } from "../../shared/utils";
+import { AzureNamingService } from "../../services/namingService";
 
 export class HostingEnvironmentResource implements ArmResourceTemplateGenerator {
   public static getResourceName(config: ServerlessAzureConfig) {
-    return config.provider.hostingEnvironment && config.provider.hostingEnvironment.name
-      ? config.provider.hostingEnvironment.name
-      : `${config.provider.prefix}-${Utils.createShortAzureRegionName(config.provider.region)}-${Utils.createShortStageName(config.provider.stage)}-ase`;
+    return AzureNamingService.getResourceName(
+      config,
+      config.provider.hostingEnvironment,
+      "ase"
+    );
   }
 
   public getTemplate(): ArmResourceTemplate {
diff --git a/src/armTemplates/resources/storageAccount.ts b/src/armTemplates/resources/storageAccount.ts
index 0b6c9d8..69dc7cd 100644
--- a/src/armTemplates/resources/storageAccount.ts
+++ b/src/armTemplates/resources/storageAccount.ts
@@ -1,13 +1,15 @@
-import { ArmResourceTemplateGenerator, ArmResourceTemplate } from "../../models/armTemplates";
-import { ServerlessAzureConfig, ResourceConfig } from "../../models/serverless";
-import { Utils } from "../../shared/utils";
-import md5 from "md5";
+import { ArmResourceTemplate, ArmResourceTemplateGenerator } from "../../models/armTemplates";
+import { ResourceConfig, ServerlessAzureConfig } from "../../models/serverless";
+import { AzureNamingService } from "../../services/namingService";
+import configConstants from "../../config";
 
 export class StorageAccountResource implements ArmResourceTemplateGenerator {
   public static getResourceName(config: ServerlessAzureConfig) {
-    return config.provider.storageAccount && config.provider.storageAccount.name
-      ? config.provider.storageAccount.name
-      : StorageAccountResource.getDefaultStorageAccountName(config)
+    return AzureNamingService.getSafeResourceName(
+      config,
+      configConstants.naming.maxLength.storageAccount,
+      config.provider.storageAccount
+    );
   }
 
   public getTemplate(): ArmResourceTemplate {
@@ -64,34 +66,4 @@ export class StorageAccountResource implements ArmResourceTemplateGenerator {
       storageAccoutSkuTier: resourceConfig.sku.tier,
     };
   }
-
-  /**
-   * Gets a default storage account name.
-   * Storage account names can have at most 24 characters and can have only alpha-numerics
-   * @param config Serverless Azure Config
-   */
-  private static getDefaultStorageAccountName(config: ServerlessAzureConfig): string {
-    const maxAccountNameLength = 24;
-    const nameHash = md5(config.service);
-    const replacer = /\W+/g;
-
-    let safePrefix = config.provider.prefix.replace(replacer, "");
-    const safeRegion = Utils.createShortAzureRegionName(config.provider.region);
-    let safeStage = Utils.createShortStageName(config.provider.stage);
-    let safeNameHash = nameHash.substr(0, 6);
-
-    const remaining = maxAccountNameLength - (safePrefix.length + safeRegion.length + safeStage.length + safeNameHash.length);
-
-    // Dynamically adjust the substring based on space needed
-    if (remaining < 0) {
-      const partLength = Math.floor(Math.abs(remaining) / 3);
-      safePrefix = safePrefix.substr(0, partLength);
-      safeStage = safeStage.substr(0, partLength);
-      safeNameHash = safeNameHash.substr(0, partLength);
-    }
-
-    return [safePrefix, safeRegion, safeStage, safeNameHash]
-      .join("")
-      .toLocaleLowerCase();
-  }
-}
\ No newline at end of file
+}
diff --git a/src/armTemplates/resources/virtualNetwork.ts b/src/armTemplates/resources/virtualNetwork.ts
index a943893..454c500 100644
--- a/src/armTemplates/resources/virtualNetwork.ts
+++ b/src/armTemplates/resources/virtualNetwork.ts
@@ -1,12 +1,14 @@
-import { ArmResourceTemplateGenerator, ArmResourceTemplate } from "../../models/armTemplates";
+import { ArmResourceTemplate, ArmResourceTemplateGenerator } from "../../models/armTemplates";
 import { ServerlessAzureConfig } from "../../models/serverless";
-import { Utils } from "../../shared/utils";
+import { AzureNamingService } from "../../services/namingService";
 
 export class VirtualNetworkResource implements ArmResourceTemplateGenerator {
   public static getResourceName(config: ServerlessAzureConfig) {
-    return config.provider.virtualNetwork && config.provider.virtualNetwork.name
-      ? config.provider.virtualNetwork.name
-      : `${config.provider.prefix}-${Utils.createShortAzureRegionName(config.provider.region)}-${Utils.createShortStageName(config.provider.stage)}-vnet`;
+    return AzureNamingService.getResourceName(
+      config,
+      config.provider.hostingEnvironment,
+      "vnet"
+    );
   }
 
   public getTemplate(): ArmResourceTemplate {
diff --git a/src/config.ts b/src/config.ts
index 36414ee..ec67058 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -5,6 +5,16 @@ export const configConstants = {
     rollback: true,
     runFromBlobUrl: false,
   },
+  naming: {
+    maxLength: {
+      storageAccount: 24,
+      deploymentName: 64,
+    },
+    suffix: {
+      deployment: "DEPLOYMENT",
+      artifact: "ARTIFACT",
+    }
+  },
   functionAppApiPath: "/api/",
   functionAppDomain: ".azurewebsites.net",
   functionsAdminApiPath: "/admin/functions/",
diff --git a/src/services/apimService.ts b/src/services/apimService.ts
index 9257b13..f66a722 100644
--- a/src/services/apimService.ts
+++ b/src/services/apimService.ts
@@ -216,7 +216,9 @@ export class ApimService extends BaseService {
         responses: options.operation.responses || [],
       };
 
-      const operationUrl = `${service.gatewayUrl}/${api.path}${operationConfig.urlTemplate}`;
+      // Ensure a single path seperator in the operation path
+      const operationPath = `/${api.path}/${operationConfig.urlTemplate}`.replace(/\/+/g, "/");
+      const operationUrl = `${service.gatewayUrl}${operationPath}`;
       this.log(`--> Deploying API operation ${options.function}: ${operationConfig.method.toUpperCase()} ${operationUrl}`);
 
       const operation = await client.apiOperation.createOrUpdate(
diff --git a/src/services/baseService.ts b/src/services/baseService.ts
index c5715f4..cbf5acb 100644
--- a/src/services/baseService.ts
+++ b/src/services/baseService.ts
@@ -14,6 +14,7 @@ import {
 } from "../models/serverless";
 import { Guard } from "../shared/guard";
 import { Utils } from "../shared/utils";
+import { AzureNamingService } from "./namingService";
 
 export abstract class BaseService {
   protected baseUrl: string;
@@ -42,9 +43,7 @@ export abstract class BaseService {
     this.resourceGroup = this.getResourceGroupName();
     this.deploymentConfig = this.getDeploymentConfig();
     this.deploymentName = this.getDeploymentName();
-    this.storageAccountName = StorageAccountResource.getResourceName(
-      serverless.service as any
-    );
+    this.storageAccountName = StorageAccountResource.getResourceName(this.config);
 
     if (!this.credentials && authenticate) {
       throw new Error(
@@ -75,12 +74,9 @@ export abstract class BaseService {
    * Name of current resource group
    */
   public getResourceGroupName(): string {
-    const regionName = Utils.createShortAzureRegionName(this.getRegion());
-    const stageName = Utils.createShortStageName(this.getStage());
-
     return this.options.resourceGroup
       || this.config.provider.resourceGroup
-      || `${this.getPrefix()}-${regionName}-${stageName}-${this.serviceName}-rg`;
+      || AzureNamingService.getResourceName(this.config, null, `${this.serviceName}-rg`);
   }
 
   /**
@@ -96,11 +92,20 @@ export abstract class BaseService {
   }
 
   /**
-   * Name of current ARM deployment
+   * Name of current ARM deployment.
+   *
+   * Naming convention:
+   *
+   * {safeName (see naming service)}--{serviceName}(if rollback enabled: -t{timestamp})
+   *
+   * The string is guaranteed to be less than 64 characters, since that is the limit
+   * imposed by Azure deployment names. If a trim is needed, the service name will be trimmed
    */
   public getDeploymentName(): string {
-    const name = this.config.provider.deploymentName || `${this.resourceGroup}-deployment`;
-    return this.rollbackConfiguredName(name);
+    return AzureNamingService.getDeploymentName(
+      this.config,
+      (this.deploymentConfig.rollback) ? `t${this.getTimestamp()}` : null
+    )
   }
 
   /**
@@ -115,9 +120,10 @@ export abstract class BaseService {
    * Takes name of deployment and replaces `rg-deployment` or `deployment` with `artifact`
    */
   protected getArtifactName(deploymentName: string): string {
+    const { deployment, artifact } = configConstants.naming.suffix;
     return `${deploymentName
-      .replace("rg-deployment", "artifact")
-      .replace("deployment", "artifact")}.zip`;
+      .replace(`rg-${deployment}`, artifact)
+      .replace(deployment, artifact)}`
   }
 
   /**
@@ -218,16 +224,6 @@ export abstract class BaseService {
     }
   }
 
-  /**
-   * Add `-t{timestamp}` if rollback is enabled
-   * @param name Original name
-   */
-  private rollbackConfiguredName(name: string) {
-    return this.deploymentConfig.rollback
-      ? `${name}-t${this.getTimestamp()}`
-      : name;
-  }
-
   /**
    * Get timestamp from `packageTimestamp` serverless variable
    * If not set, create timestamp, set variable and return timestamp
diff --git a/src/services/functionAppService.ts b/src/services/functionAppService.ts
index b0eb5cd..a94f575 100644
--- a/src/services/functionAppService.ts
+++ b/src/services/functionAppService.ts
@@ -10,8 +10,11 @@ import { Guard } from "../shared/guard";
 import { ArmService } from "./armService";
 import { AzureBlobStorageService } from "./azureBlobStorageService";
 import { BaseService } from "./baseService";
+import { Utils } from "../shared/utils";
 
 export class FunctionAppService extends BaseService {
+  private static readonly retryCount: number = 10;
+  private static readonly retryInterval: number = 5000;
   private webClient: WebSiteManagementClient;
   private blobService: AzureBlobStorageService;
 
@@ -90,27 +93,53 @@ export class FunctionAppService extends BaseService {
     Guard.null(functionApp);
 
     const getTokenUrl = `${this.baseUrl}${functionApp.id}/functions?api-version=2016-08-01`;
-    const response = await this.sendApiRequest("GET", getTokenUrl);
+    try {
+      const response = await Utils.runWithRetry(async () => {
+        const listFunctionsResponse = await this.sendApiRequest("GET", getTokenUrl);
 
-    if (response.status !== 200) {
-      return [];
-    }
+        if (listFunctionsResponse.status !== 200 || listFunctionsResponse.data.value.length === 0) {
+          this.log("-> Function App not ready. Retrying...");
+          throw new Error(listFunctionsResponse.data);
+        }
+
+        return listFunctionsResponse;
+      }, FunctionAppService.retryCount, FunctionAppService.retryInterval);
 
-    return response.data.value.map((functionConfig) => functionConfig.properties);
+      return response.data.value.map((functionConfig) => functionConfig.properties);
+    }
+    catch (e) {
+      this.log("-> Unable to retrieve function app list");
+      throw e;
+    }
   }
 
+  /**
+   * Gets the configuration of the specified function within the function app
+   * @param functionApp The parent function app
+   * @param functionName The name of hte function
+   */
   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);
 
-    if (response.status !== 200) {
+    try {
+      const response = await Utils.runWithRetry(async () => {
+        const getFunctionResponse = await this.sendApiRequest("GET", getFunctionUrl);
+
+        if (getFunctionResponse.status !== 200) {
+          this.log("-> Function app not ready. Retrying...")
+          throw new Error(response.data);
+        }
+
+        return getFunctionResponse;
+      }, FunctionAppService.retryCount, FunctionAppService.retryInterval);
+
+      return response.data.properties;
+    } catch (e) {
       return null;
     }
-
-    return response.data.properties;
   }
 
   public async uploadFunctions(functionApp: Site): Promise<any> {
@@ -122,6 +151,23 @@ export class FunctionAppService extends BaseService {
     const uploadFunctionApp = this.uploadZippedArfifactToFunctionApp(functionApp, functionZipFile);
     const uploadBlobStorage = this.uploadZippedArtifactToBlobStorage(functionZipFile);
     await Promise.all([uploadFunctionApp, uploadBlobStorage]);
+
+
+    this.log("Deployed serverless functions:")
+    const serverlessFunctions = this.serverless.service.getAllFunctions();
+    const deployedFunctions = await this.listFunctions(functionApp);
+
+    // List functions that are part of the serverless yaml config
+    deployedFunctions.forEach((functionConfig) => {
+      if (serverlessFunctions.includes(functionConfig.name)) {
+        const httpConfig = this.getFunctionHttpTriggerConfig(functionApp, functionConfig);
+
+        if (httpConfig) {
+          const method = httpConfig.methods[0].toUpperCase();
+          this.log(`-> ${functionConfig.name}: ${method} ${httpConfig.url}`);
+        }
+      }
+    });
   }
 
   /**
@@ -166,23 +212,7 @@ export class FunctionAppService extends BaseService {
     };
 
     await this.sendFile(requestOptions, functionZipFile);
-
     this.log("-> Function package uploaded successfully");
-    const serverlessFunctions = this.serverless.service.getAllFunctions();
-    const deployedFunctions = await this.listFunctions(functionApp);
-
-    this.log("Deployed serverless functions:")
-    deployedFunctions.forEach((functionConfig) => {
-      // List functions that are part of the serverless yaml config
-      if (serverlessFunctions.includes(functionConfig.name)) {
-        const httpConfig = this.getFunctionHttpTriggerConfig(functionApp, functionConfig);
-
-        if (httpConfig) {
-          const method = httpConfig.methods[0].toUpperCase();
-          this.log(`-> ${functionConfig.name}: ${method} ${httpConfig.url}`);
-        }
-      }
-    });
   }
 
   /**
diff --git a/src/services/namingService.ts b/src/services/namingService.ts
new file mode 100644
index 0000000..10c3755
--- /dev/null
+++ b/src/services/namingService.ts
@@ -0,0 +1,160 @@
+import { ServerlessAzureConfig, ResourceConfig } from "../models/serverless"
+import { Guard } from "../shared/guard"
+import md5 from "md5";
+import configConstants from "../config";
+
+export class AzureNamingService {
+
+  /**
+   * Get a resource name for an Azure Service. Naming convention:
+   *
+   * {prefix}-{shortRegionName}-{shortStageName}(optionally: -{suffix})
+   *
+   * @param config Serverless Azure Config for service (serverless.service)
+   * @param resourceConfig
+   * @param suffix
+   */
+  public static getResourceName(config: ServerlessAzureConfig, resourceConfig?: ResourceConfig, suffix?: string) {
+    if (resourceConfig && resourceConfig.name) {
+      return resourceConfig.name;
+    }
+    const { prefix, region, stage } = config.provider
+    let name = [
+      prefix,
+      this.createShortAzureRegionName(region),
+      this.createShortStageName(stage),
+    ].join("-");
+    if (suffix) {
+      name += `-${suffix}`;
+    }
+    return name.toLocaleLowerCase();
+  }
+
+  /**
+   * Get a name for an Azure resource that is shorter than a max length and has no forbidden characters
+   * Naming convention:
+   *
+   * {safePrefix}{safeRegion}{safeStage}{safeServiceNameHash}
+   *
+   * @param config Serverless Azure Config for service (serverless.service)
+   * @param maxLength Maximum length of name for resource
+   * @param resourceConfig Configuration for resource from serverless configuration
+   * @param forbidden Regex for characters to remove from name. Defaults to non-alpha-numerics
+   * @param replaceWith String to replace forbidden characters. Defaults to empty string
+   */
+  public static getSafeResourceName(config: ServerlessAzureConfig, maxLength: number, resourceConfig?: ResourceConfig, forbidden = /\W+/g, replaceWith = "") {
+    if (resourceConfig && resourceConfig.name) {
+      const { name } = resourceConfig;
+      if (name.length > maxLength) {
+        throw new Error(`Name '${name}' invalid. Should be shorter than ${maxLength} characters`);
+      }
+      return name.replace(forbidden, replaceWith);
+    }
+
+    const { prefix, region, stage } = config.provider;
+
+    const nameHash = md5(config.service);
+
+    let safePrefix = prefix.replace(forbidden, replaceWith);
+    const safeRegion = this.createShortAzureRegionName(region);
+    let safeStage = this.createShortStageName(stage);
+    let safeNameHash = nameHash.substr(0, 6);
+
+    const remaining = maxLength - (safePrefix.length + safeRegion.length + safeStage.length + safeNameHash.length);
+
+    // Dynamically adjust the substring based on space needed
+    if (remaining < 0) {
+      const partLength = Math.floor(Math.abs(remaining) / 3);
+      safePrefix = safePrefix.substr(0, partLength);
+      safeStage = safeStage.substr(0, partLength);
+      safeNameHash = safeNameHash.substr(0, partLength);
+    }
+
+    return [safePrefix, safeRegion, safeStage, safeNameHash]
+      .join("")
+      .toLocaleLowerCase();
+  }
+
+  public static getDeploymentName(config: ServerlessAzureConfig, timestamp?: string) {
+    let maxLength = configConstants.naming.maxLength.deploymentName;
+    const suffix = configConstants.naming.suffix.deployment;
+
+    const { deploymentName } = config.provider
+
+    if (timestamp) {
+      maxLength -= timestamp.length + suffix.length;;
+
+      const name = (deploymentName) ? deploymentName.substr(0, maxLength)
+        : [ AzureNamingService.getSafeResourceName(config, maxLength), suffix ].join("-");
+      return [ name, timestamp ].join("-");
+    }
+
+    return deploymentName.substr(0, maxLength);
+  }
+
+  /**
+   * Creates a short name to be used for state name abbreviation
+   * @param stageName The stage name
+   */
+  public static createShortStageName(stageName: string) {
+    Guard.empty(stageName);
+
+    const stageMap = {
+      "dogfood": "df",
+      "production": "prod",
+      "development": "dev",
+      "testing": "test"
+    };
+
+    return this.createShortName(stageName, stageMap);
+  }
+
+  /**
+   * Gets the normalized region name from long name (ex. West US 2 -> westus2)
+   * @param regionName The region name
+   */
+  public static getNormalizedRegionName(regionName: string) {
+    Guard.empty(regionName);
+    return regionName.replace(/\W/g, "").toLowerCase();
+  }
+
+  /**
+   * Creates a short name for an azure region
+   * @param regionName The azure region name
+   */
+  public static createShortAzureRegionName(regionName: string) {
+    Guard.empty(regionName);
+
+    const locationMap = {
+      "north": "n",
+      "south": "s",
+      "east": "e",
+      "west": "w",
+      "central": "c",
+    };
+
+    return this.createShortName(regionName, locationMap);
+  }
+
+  /**
+   * Creates a short name from a long name based on a well-known string map
+   * @param longName The long name to replace
+   * @param wellKnownMap A well known map of long terms to short abbreviations
+   */
+  private static createShortName(longName: string, wellKnownMap: { [key: string]: string }) {
+    Guard.empty(longName);
+    Guard.null(wellKnownMap);
+
+    const pattern = `(${Object.keys(wellKnownMap).join("|")})`;
+    const regex = new RegExp(pattern, "g");
+
+    return longName
+      .replace(/\W+/g, "")
+      .toLowerCase()
+      .split(regex)
+      .map((part) => {
+        return wellKnownMap[part] || part.substr(0, 3);
+      })
+      .join("");
+  }
+}
diff --git a/src/services/offlineService.ts b/src/services/offlineService.ts
index 40ac855..996a1cc 100644
--- a/src/services/offlineService.ts
+++ b/src/services/offlineService.ts
@@ -1,4 +1,4 @@
-import { spawn } from "child_process";
+import { spawn, SpawnOptions } from "child_process";
 import fs from "fs";
 import Serverless from "serverless";
 import configConstants from "../config";
@@ -29,7 +29,7 @@ export class OfflineService extends BaseService {
     await this.packageService.createBindings();
     const filenames = Object.keys(this.localFiles);
     for (const filename of filenames) {
-      if (!fs.existsSync(filename)){
+      if (!fs.existsSync(filename)) {
         fs.writeFileSync(
           filename,
           this.localFiles[filename]
@@ -44,7 +44,7 @@ export class OfflineService extends BaseService {
     await this.packageService.cleanUp();
     const filenames = Object.keys(this.localFiles);
     for (const filename of filenames) {
-      if (fs.existsSync(filename)){
+      if (fs.existsSync(filename)) {
         this.log(`Removing file '${filename}'`);
         fs.unlinkSync(filename)
       }
@@ -62,59 +62,30 @@ export class OfflineService extends BaseService {
   /**
    * Spawn a Node child process with predefined environment variables
    * @param command CLI Command - NO ARGS
-   * @param args Array of arguments for CLI command
-   * @param env Additional environment variables to be set in addition to
-   * predefined variables in `serverless.yml`
+   * @param spawnArgs Array of arguments for CLI command
    */
-  private spawn(command: string, args?: string[], env?: any): Promise<void> {
-    env = {
+  private spawn(command: string, spawnArgs?: string[]): Promise<void> {
+    if (process.platform === "win32") {
+      command += ".cmd";
+    }
+
+    const env = {
+      // Inherit environment from current process, most importantly, the PATH
+      ...process.env,
+      // Environment variables from serverless config are king
       ...this.serverless.service.provider["environment"],
-      ...env
     }
-    this.log(`Spawning process '${command} ${args.join(" ")}'`);
+    this.log(`Spawning process '${command} ${spawnArgs.join(" ")}'`);
     return new Promise((resolve, reject) => {
-      const childProcess = spawn(command, args, {env});
-
-      childProcess.stdout.on("data", (data) => {
-        this.log(data, {
-          color: configConstants.funcConsoleColor,
-        }, command);
-      });
-
-      childProcess.stderr.on("data", (data) => {
-        this.log(data, {
-          color: "red",
-        }, command);
-      })
-
-      childProcess.on("message", (message) => {
-        this.log(message, {
-          color: configConstants.funcConsoleColor,
-        }, command);
-      });
-
-      childProcess.on("error", (err) => {
-        this.log(`${err}`, {
-          color: "red"
-        }, command);
-        reject(err);
-      });
+      const spawnOptions: SpawnOptions = { env, stdio: "inherit" };
+      const childProcess = spawn(command, spawnArgs, spawnOptions);
 
       childProcess.on("exit", (code) => {
-        this.log(`Exited with code: ${code}`, {
-          color: (code === 0) ? "green" : "red",
-        }, command);
-      });
-
-      childProcess.on("close", (code) => {
-        this.log(`Closed with code: ${code}`, {
-          color: (code === 0) ? "green" : "red",
-        }, command);
-        resolve();
-      });
-
-      childProcess.on("disconnect", () => {
-        this.log("Process disconnected");
+        if (code === 0) {
+          resolve();
+        } else {
+          reject();
+        }
       });
     });
   }
diff --git a/src/services/resourceService.ts b/src/services/resourceService.ts
index 2d75181..1248e2d 100644
--- a/src/services/resourceService.ts
+++ b/src/services/resourceService.ts
@@ -2,6 +2,7 @@ import Serverless from "serverless";
 import { ResourceManagementClient } from "@azure/arm-resources";
 import { BaseService } from "./baseService";
 import { Utils } from "../shared/utils";
+import { AzureNamingService } from "./namingService";
 
 export class ResourceService extends BaseService {
   private resourceClient: ResourceManagementClient;
@@ -57,7 +58,7 @@ export class ResourceService extends BaseService {
     this.log(`Creating resource group: ${this.resourceGroup}`);
 
     return await this.resourceClient.resourceGroups.createOrUpdate(this.resourceGroup, {
-      location: Utils.getNormalizedRegionName(this.getRegion()),
+      location: AzureNamingService.getNormalizedRegionName(this.getRegion()),
     });
   }
 
diff --git a/src/shared/utils.ts b/src/shared/utils.ts
index 7490d1c..3d091fc 100644
--- a/src/shared/utils.ts
+++ b/src/shared/utils.ts
@@ -1,9 +1,8 @@
 import { relative } from "path";
 import Serverless from "serverless";
+import { ServerlessAzureFunctionConfig } from "../models/serverless";
 import { BindingUtils } from "./bindings";
 import { constants } from "./constants";
-import { Guard } from "./guard";
-import { ServerlessAzureFunctionConfig } from "../models/serverless";
 
 export interface FunctionMetadata {
   entryPoint: any;
@@ -132,72 +131,6 @@ export class Utils {
     return result;
   }
 
-  /**
-   * Creates a short name to be used for state name abbreviation
-   * @param stageName The stage name
-   */
-  public static createShortStageName(stageName: string) {
-    Guard.empty(stageName);
-
-    const stageMap = {
-      "dogfood": "df",
-      "production": "prod",
-      "development": "dev",
-      "testing": "test"
-    };
-
-    return this.createShortName(stageName, stageMap);
-  }
-
-  /**
-   * Gets the normalized region name from long name (ex. West US 2 -> westus2)
-   * @param regionName The region name
-   */
-  public static getNormalizedRegionName(regionName: string) {
-    Guard.empty(regionName);
-    return regionName.replace(/\W/g, "").toLowerCase();
-  }
-
-  /**
-   * Creates a short name for an azure region
-   * @param regionName The azure region name
-   */
-  public static createShortAzureRegionName(regionName: string) {
-    Guard.empty(regionName);
-
-    const locationMap = {
-      "north": "n",
-      "south": "s",
-      "east": "e",
-      "west": "w",
-      "central": "c",
-    };
-
-    return this.createShortName(regionName, locationMap);
-  }
-
-  /**
-   * Creates a short name from a long name based on a well-known string map
-   * @param longName The long name to replace
-   * @param wellKnownMap A well known map of long terms to short abbreviations
-   */
-  private static createShortName(longName: string, wellKnownMap: { [key: string]: string }) {
-    Guard.empty(longName);
-    Guard.null(wellKnownMap);
-
-    const pattern = `(${Object.keys(wellKnownMap).join("|")})`;
-    const regex = new RegExp(pattern, "g");
-
-    return longName
-      .replace(/\W+/g, "")
-      .toLowerCase()
-      .split(regex)
-      .map((part) => {
-        return wellKnownMap[part] || part.substr(0, 3);
-      })
-      .join("");
-  }
-
   public static get(object: any, key: string, defaultValue?: any) {
     if (key in object) {
       return object[key];
@@ -227,4 +160,39 @@ export class Utils {
       return settings && settings.direction === "out";
     });
   }
+
+  /**
+   * Runs an operation with auto retry policy
+   * @param operation The operation to run
+   * @param maxRetries The max number or retreis
+   * @param retryWaitInterval The time to wait between retries
+   */
+  public static async runWithRetry<T>(operation: (retry?: number) => Promise<T>, maxRetries: number = 3, retryWaitInterval: number = 1000) {
+    let retry = 0;
+    let error = null;
+
+    while (retry < maxRetries) {
+      try {
+        retry++;
+        return await operation(retry);
+      }
+      catch (e) {
+        error = e;
+      }
+
+      await Utils.wait(retryWaitInterval);
+    }
+
+    return Promise.reject(error);
+  }
+
+  /**
+   * Waits for the specified amount of time.
+   * @param time The amount of time to wait (default = 1000ms)
+   */
+  public static wait(time: number = 1000) {
+    return new Promise((resolve) => {
+      setTimeout(resolve, time);
+    });
+  }
 }


Test patch:
diff --git a/src/armTemplates/resources/storageAccount.test.ts b/src/armTemplates/resources/storageAccount.test.ts
index 6507027..b398cda 100644
--- a/src/armTemplates/resources/storageAccount.test.ts
+++ b/src/armTemplates/resources/storageAccount.test.ts
@@ -1,7 +1,7 @@
 import md5 from "md5";
-import { StorageAccountResource } from "./storageAccount";
 import { ServerlessAzureConfig } from "../../models/serverless";
-import { Utils } from "../../shared/utils";
+import { AzureNamingService } from "../../services/namingService";
+import { StorageAccountResource } from "./storageAccount";
 
 describe("Storage Account Resource", () => {
   const config: ServerlessAzureConfig = {
@@ -142,7 +142,7 @@ describe("Storage Account Resource", () => {
   function assertValidStorageAccountName(config: ServerlessAzureConfig, value: string) {
     expect(value.length).toBeLessThanOrEqual(24);
     expect(value.match(/[a-z0-9]/g).length).toEqual(value.length);
-    expect(value).toContain(Utils.createShortAzureRegionName(config.provider.region));
+    expect(value).toContain(AzureNamingService.createShortAzureRegionName(config.provider.region));
     expect(value).toContain(createSafeString(config.provider.prefix));
     expect(value).toContain(createSafeString(config.provider.stage));
     expect(value).toContain(md5(config.service).substr(0, 3));
@@ -151,4 +151,4 @@ describe("Storage Account Resource", () => {
   function createSafeString(value: string) {
     return value.replace(/\W+/g, "").toLocaleLowerCase().substr(0, 3);
   };
-});
\ No newline at end of file
+});
diff --git a/src/services/apimService.test.ts b/src/services/apimService.test.ts
index 50131f2..4711370 100644
--- a/src/services/apimService.test.ts
+++ b/src/services/apimService.test.ts
@@ -19,7 +19,7 @@ import {
   OperationContract,
   ApiPolicyCreateOrUpdateResponse,
 } from "@azure/arm-apimanagement/esm/models";
-import { Utils } from "../shared/utils";
+import { AzureNamingService } from "./namingService";
 
 describe("APIM Service", () => {
   const apimConfig = MockFactory.createTestApimConfig();
@@ -62,7 +62,7 @@ describe("APIM Service", () => {
     (serverless.service.provider as any).apim = apimConfigName;
 
     const service = new ApimService(serverless);
-    const expectedRegionName = Utils.createShortAzureRegionName(service.getRegion());
+    const expectedRegionName = AzureNamingService.createShortAzureRegionName(service.getRegion());
     expect(apimConfigName.name.includes(expectedRegionName)).toBeTruthy();
   });
 
diff --git a/src/services/baseService.test.ts b/src/services/baseService.test.ts
index d001cb8..fb7fbc7 100644
--- a/src/services/baseService.test.ts
+++ b/src/services/baseService.test.ts
@@ -2,9 +2,9 @@ import fs from "fs";
 import mockFs from "mock-fs";
 import Serverless from "serverless";
 import { ServerlessAzureOptions } from "../models/serverless";
-import { Utils } from "../shared/utils";
 import { MockFactory } from "../test/mockFactory";
 import { BaseService } from "./baseService";
+import { AzureNamingService } from "./namingService";
 
 jest.mock("axios", () => jest.fn());
 import axios from "axios";
@@ -42,7 +42,7 @@ describe("Base Service", () => {
   let sls: Serverless;
 
   const slsConfig = {
-    service: "My custom service",
+    service: "my custom service",
     provider: {
       resourceGroup: "My-Resource-Group",
       deploymentName: "My-Deployment",
@@ -121,8 +121,8 @@ describe("Base Service", () => {
     sls.service.provider["resourceGroup"] = null;
     const mockService = new MockService(sls);
     const actualResourceGroupName = mockService.getResourceGroupName();
-    const expectedRegion = Utils.createShortAzureRegionName(mockService.getRegion());
-    const expectedStage = Utils.createShortStageName(mockService.getStage());
+    const expectedRegion = AzureNamingService.createShortAzureRegionName(mockService.getRegion());
+    const expectedStage = AzureNamingService.createShortStageName(mockService.getStage());
     const expectedResourceGroupName = `sls-${expectedRegion}-${expectedStage}-${sls.service["service"]}-rg`;
 
     expect(actualResourceGroupName).toEqual(expectedResourceGroupName);
diff --git a/src/services/namingService.test.ts b/src/services/namingService.test.ts
new file mode 100644
index 0000000..33749c4
--- /dev/null
+++ b/src/services/namingService.test.ts
@@ -0,0 +1,89 @@
+import { AzureNamingService } from "./namingService"
+
+describe("Naming Service", () => {
+
+  it("Creates a short name for an azure region", () => {
+    const expected = "ausse";
+    const actual = AzureNamingService.createShortAzureRegionName("australiasoutheast");
+
+    expect(actual).toEqual(expected);
+  });
+
+  it("Creates a short stage name from a well known name", () => {
+    const expected = "prod";
+    const actual = AzureNamingService.createShortStageName("production");
+
+    expect(actual).toEqual(expected);
+  });
+
+  it("Creates a short stage name from a unknown name", () => {
+    const value = "user acceptance";
+    const actual = AzureNamingService.createShortStageName(value);
+
+    expect(actual).toEqual(value.substr(0, 3));
+  });
+
+  it("Creates a short stage name from multiple values", () => {
+    const actual = AzureNamingService.createShortStageName("production dogfood");
+    expect(actual).toEqual("proddf");
+  });
+
+
+  it("Creates unique short names for all azure regions", () => {
+    const regions = [
+      "eastasia",
+      "southeastasia",
+      "centralus",
+      "eastus",
+      "eastus2",
+      "westus",
+      "northcentralus",
+      "southcentralus",
+      "northeurope",
+      "westeurope",
+      "japanwest",
+      "japaneast",
+      "brazilsouth",
+      "australiaeast",
+      "australiasoutheast",
+      "southindia",
+      "centralindia",
+      "westindia",
+      "canadacentral",
+      "canadaeast",
+      "uksouth",
+      "ukwest",
+      "westcentralus",
+      "westus2",
+      "koreacentral",
+      "koreasouth",
+      "francecentral",
+      "francesouth",
+      "australiacentral",
+      "australiacentral2",
+      "uaecentral",
+      "uaenorth",
+      "southafricanorth",
+      "southafricawest"
+    ];
+
+    const results = {};
+    regions.forEach((region) => {
+      const result = AzureNamingService.createShortAzureRegionName(region);
+      results[result] = region;
+    });
+
+    expect(Object.keys(results)).toHaveLength(regions.length);
+  });
+
+  it("gets a normalized region name from full region name", () => {
+    const result = AzureNamingService.getNormalizedRegionName("West US 2");
+    expect(result).toEqual("westus2");
+  });
+
+  it("Performs noop if region name is already normalized", () => {
+    const expected = "westus2";
+    const actual = AzureNamingService.getNormalizedRegionName(expected);
+    expect(actual).toEqual(expected);
+  });
+});
diff --git a/src/services/offlineService.test.ts b/src/services/offlineService.test.ts
index c8fc4f4..df392ba 100644
--- a/src/services/offlineService.test.ts
+++ b/src/services/offlineService.test.ts
@@ -7,10 +7,7 @@ import { MockFactory } from "../test/mockFactory";
 import { OfflineService } from "./offlineService";
 
 describe("Offline Service", () => {
-
-  const mySpawn = mockSpawn();
-  require("child_process").spawn = mySpawn;
-  mySpawn.setDefault(mySpawn.simple(0, "Exit code"));
+  let mySpawn;
 
   function createService(sls?: Serverless): OfflineService {
     return new OfflineService(
@@ -21,12 +18,16 @@ describe("Offline Service", () => {
 
   beforeEach(() => {
     // Mocking the file system so that files are not created in project directory
-    mockFs({})
+    mockFs({});
+
+    mySpawn = mockSpawn();
+    require("child_process").spawn = mySpawn;
+    mySpawn.setDefault(mySpawn.simple(0, "Exit code"));
   });
 
   afterEach(() => {
     mockFs.restore();
-  })
+  });
 
   it("builds required files for offline execution", async () => {
     const sls = MockFactory.createTestServerless();
@@ -113,7 +114,12 @@ describe("Offline Service", () => {
     rmdirSpy.mockRestore();
   });
 
-  it("instructs users how to run locally", async () => {
+  it("calls func host start on Mac OS", async () => {
+    Object.defineProperty(process, "platform", {
+      value: "darwin",
+      writable: true,
+    });
+
     const sls = MockFactory.createTestServerless();
     const service = createService(sls);
     await service.start();
@@ -123,4 +129,20 @@ describe("Offline Service", () => {
     expect(call.command).toEqual("func");
     expect(call.args).toEqual(["host", "start"]);
   });
+
+  it("calls func host start on windows", async () => {
+    Object.defineProperty(process, "platform", {
+      value: "win32",
+      writable: true,
+    });
+
+    const sls = MockFactory.createTestServerless();
+    const service = createService(sls);
+    await service.start();
+    const calls = mySpawn.calls;
+    expect(calls).toHaveLength(1);
+    const call = calls[0];
+    expect(call.command).toEqual("func.cmd");
+    expect(call.args).toEqual(["host", "start"]);
+  });
 });
diff --git a/src/services/resourceService.test.ts b/src/services/resourceService.test.ts
index c78df52..fe46f31 100644
--- a/src/services/resourceService.test.ts
+++ b/src/services/resourceService.test.ts
@@ -1,10 +1,10 @@
+import { ResourceManagementClient } from "@azure/arm-resources";
 import { DeploymentsListByResourceGroupResponse } from "@azure/arm-resources/esm/models";
-import { Utils } from "../shared/utils";
 import { MockFactory } from "../test/mockFactory";
+import { AzureNamingService } from "./namingService";
 import { ResourceService } from "./resourceService";
 
 jest.mock("@azure/arm-resources")
-import { ResourceManagementClient } from "@azure/arm-resources";
 
 describe("Resource Service", () => {
   let deployments: DeploymentsListByResourceGroupResponse;
@@ -41,7 +41,7 @@ describe("Resource Service", () => {
     const sls = MockFactory.createTestServerless();
     const resourceGroup = "myResourceGroup"
     const location = "West Us";
-    const expectedLocation = Utils.getNormalizedRegionName(location);
+    const expectedLocation = AzureNamingService.getNormalizedRegionName(location);
     sls.service.provider["resourceGroup"] = resourceGroup
     sls.service.provider.region = location;
     sls.variables["azureCredentials"] = "fake credentials"
diff --git a/src/services/rollbackService.test.ts b/src/services/rollbackService.test.ts
index ffdf0e9..7d183ae 100644
--- a/src/services/rollbackService.test.ts
+++ b/src/services/rollbackService.test.ts
@@ -17,6 +17,7 @@ import { FunctionAppService } from "./functionAppService";
 
 jest.mock("./armService");
 import { ArmService } from "./armService";
+import configConstants from "../config";
 
 describe("Rollback Service", () => {
 
@@ -28,7 +29,8 @@ describe("Rollback Service", () => {
   const appStub = "appStub";
   const sasURL = "sasURL";
   const containerName = "deployment-artifacts";
-  const artifactName = MockFactory.createTestDeployment().name.replace("deployment", "artifact") + ".zip";
+  const artifactName = MockFactory.createTestDeployment().name.replace(
+    configConstants.naming.suffix.deployment, configConstants.naming.suffix.artifact);
   const artifactPath = `.serverless${path.sep}${artifactName}`
   const armDeployment: ArmDeployment = { template, parameters };
   const deploymentString = "deployments";
diff --git a/src/shared/utils.test.ts b/src/shared/utils.test.ts
index 4a121df..517c60b 100644
--- a/src/shared/utils.test.ts
+++ b/src/shared/utils.test.ts
@@ -8,7 +8,7 @@ describe("utils", () => {
 
   beforeEach(() => {
     const slsConfig = {
-      service: "My test service",
+      service: "my test service",
       provider: "azure",
       functions: MockFactory.createTestSlsFunctionConfig(),
     };
@@ -79,90 +79,6 @@ describe("utils", () => {
     ).toEqual("abfgklpquvab");
   });
 
-  it("Creates a short name for an azure region", () => {
-    const expected = "ausse";
-    const actual = Utils.createShortAzureRegionName("australiasoutheast");
-
-    expect(actual).toEqual(expected);
-  });
-
-  it("Creates a short stage name from a well known name", () => {
-    const expected = "prod";
-    const actual = Utils.createShortStageName("production");
-
-    expect(actual).toEqual(expected);
-  });
-
-  it("Creates a short stage name from a unknown name", () => {
-    const value = "user acceptance";
-    const actual = Utils.createShortStageName(value);
-
-    expect(actual).toEqual(value.substr(0, 3));
-  });
-
-  it("Creates a short stage name from multiple values", () => {
-    const actual = Utils.createShortStageName("production dogfood");
-    expect(actual).toEqual("proddf");
-  });
-
-  it("Creates unique short names for all azure regions", () => {
-    const regions = [
-      "eastasia",
-      "southeastasia",
-      "centralus",
-      "eastus",
-      "eastus2",
-      "westus",
-      "northcentralus",
-      "southcentralus",
-      "northeurope",
-      "westeurope",
-      "japanwest",
-      "japaneast",
-      "brazilsouth",
-      "australiaeast",
-      "australiasoutheast",
-      "southindia",
-      "centralindia",
-      "westindia",
-      "canadacentral",
-      "canadaeast",
-      "uksouth",
-      "ukwest",
-      "westcentralus",
-      "westus2",
-      "koreacentral",
-      "koreasouth",
-      "francecentral",
-      "francesouth",
-      "australiacentral",
-      "australiacentral2",
-      "uaecentral",
-      "uaenorth",
-      "southafricanorth",
-      "southafricawest"
-    ];
-
-    const results = {};
-    regions.forEach((region) => {
-      const result = Utils.createShortAzureRegionName(region);
-      results[result] = region;
-    });
-
-    expect(Object.keys(results)).toHaveLength(regions.length);
-  });
-
-  it("gets a normalized region name from full region name", () => {
-    const result = Utils.getNormalizedRegionName("West US 2");
-    expect(result).toEqual("westus2");
-  });
-
-  it("Performs noop if region name is already normalized", () => {
-    const expected = "westus2";
-    const actual = Utils.getNormalizedRegionName(expected);
-    expect(actual).toEqual(expected);
-  });
-
   it("should get a timestamp from a name", () => {
     expect(Utils.getTimestampFromName("myDeployment-t12345")).toEqual("12345");
     expect(Utils.getTimestampFromName("myDeployment-t678987645")).toEqual("678987645");
@@ -189,4 +105,87 @@ describe("utils", () => {
       }
     );
   });
+
+  describe("runWithRetry", () => {
+    it("returns values after 1st run", async () => {
+      const expected = "success";
+      let lastRetry = 0;
+
+      const result = await Utils.runWithRetry((retry) => {
+        lastRetry = retry;
+        return Promise.resolve(expected);
+      });
+
+      expect(lastRetry).toEqual(1);
+      expect(result).toEqual(expected);
+    });
+
+    it("returns values after successfully retry (reject promise)", async () => {
+      const expected = "success";
+      let lastRetry = 0;
+
+      const result = await Utils.runWithRetry((retry) => {
+        lastRetry = retry;
+        if (retry === 1) {
+          return Promise.reject("rejected");
+        }
+
+        return Promise.resolve(expected);
+      });
+
+      expect(lastRetry).toEqual(2);
+      expect(result).toEqual(expected);
+    });
+
+    it("returns values after successfully retry (throw error)", async () => {
+      const expected = "success";
+      let lastRetry = 0;
+
+      const result = await Utils.runWithRetry((retry) => {
+        lastRetry = retry;
+        if (retry === 1) {
+          throw new Error("Ooops!")
+        }
+
+        return Promise.resolve(expected);
+      });
+
+      expect(lastRetry).toEqual(2);
+      expect(result).toEqual(expected);
+    });
+    it("throws error after reties", async () => {
+      const maxRetries = 5;
+      let lastRetry = 0;
+
+      const test = async () => {
+        await Utils.runWithRetry((retry) => {
+          lastRetry = retry;
+          return Promise.reject("rejected");
+        }, maxRetries, 100);
+      };
+
+      await expect(test()).rejects.toEqual("rejected");
+      expect(lastRetry).toEqual(maxRetries);
+    });
+  });
+
+  describe("wait", () => {
+    const setTimeoutMock = jest.fn((resolve) => resolve());
+
+    beforeEach(() => {
+      global.setTimeout = setTimeoutMock;
+    });
+
+    it("waits 1000 by default", async () => {
+      await Utils.wait();
+
+      expect(setTimeoutMock).toBeCalledWith(expect.any(Function), 1000);
+    });
+
+    it("waits specified time", async () => {
+      await Utils.wait(2000);
+
+      expect(setTimeoutMock).toBeCalledWith(expect.any(Function), 2000);
+    });
+  });
 });