diff --git a/src/armTemplates/compositeArmTemplate.ts b/src/armTemplates/compositeArmTemplate.ts index 73d834b9..299adbdc 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 eaca3277..5003361c 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 8667d923..9426a0b6 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 95819962..4485cb06 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 48c7f5cb..1af37a3e 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 233aff04..7c8332bd 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.test.ts b/src/armTemplates/resources/storageAccount.test.ts index 6507027b..b398cda6 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/armTemplates/resources/storageAccount.ts b/src/armTemplates/resources/storageAccount.ts index 0b6c9d8c..69dc7cd6 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 a9438935..454c5003 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 36414eef..ec67058e 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.test.ts b/src/services/apimService.test.ts index 50131f2d..4711370e 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 d001cb8e..fb7fbc7d 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/baseService.ts b/src/services/baseService.ts index c5715f45..cbf5acbe 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/namingService.test.ts b/src/services/namingService.test.ts new file mode 100644 index 00000000..33749c45 --- /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/namingService.ts b/src/services/namingService.ts new file mode 100644 index 00000000..10c37550 --- /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/resourceService.test.ts b/src/services/resourceService.test.ts index c78df520..fe46f31c 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/resourceService.ts b/src/services/resourceService.ts index 2d751814..1248e2d5 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/services/rollbackService.test.ts b/src/services/rollbackService.test.ts index ffdf0e9a..7d183ae3 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 4a121dfb..668d6e8a 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"); diff --git a/src/shared/utils.ts b/src/shared/utils.ts index 7490d1cd..bf3717ff 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];