From a529b7138ef68d3a528a65e75451f1b8f722a1f1 Mon Sep 17 00:00:00 2001 From: Tanner Barlow Date: Mon, 29 Jul 2019 11:09:55 -0400 Subject: [PATCH] feat: Add Azure Naming Service --- src/armTemplates/compositeArmTemplate.ts | 12 +- src/armTemplates/resources/apim.ts | 18 +- src/armTemplates/resources/appInsights.ts | 16 +- src/armTemplates/resources/appServicePlan.ts | 18 +- src/armTemplates/resources/functionApp.ts | 16 +- .../resources/hostingEnvironment.ts | 14 +- .../resources/storageAccount.test.ts | 154 ---------- src/armTemplates/resources/storageAccount.ts | 49 +-- src/armTemplates/resources/virtualNetwork.ts | 14 +- src/models/armTemplates.ts | 14 +- src/services/apimService.test.ts | 9 +- src/services/apimService.ts | 20 +- src/services/armService.ts | 6 +- src/services/azureBlobStorageService.test.ts | 22 +- src/services/azureNamingService.test.ts | 279 ++++++++++++++++++ src/services/azureNamingService.ts | 228 ++++++++++++++ src/services/baseService.test.ts | 59 ---- src/services/baseService.ts | 120 +------- src/services/functionAppService.test.ts | 38 +-- src/services/functionAppService.ts | 18 +- src/services/resourceService.ts | 4 +- src/services/rollbackService.ts | 2 +- 22 files changed, 646 insertions(+), 484 deletions(-) delete mode 100644 src/armTemplates/resources/storageAccount.test.ts create mode 100644 src/services/azureNamingService.test.ts create mode 100644 src/services/azureNamingService.ts diff --git a/src/armTemplates/compositeArmTemplate.ts b/src/armTemplates/compositeArmTemplate.ts index 73d834b9..3ea16d53 100644 --- a/src/armTemplates/compositeArmTemplate.ts +++ b/src/armTemplates/compositeArmTemplate.ts @@ -1,12 +1,18 @@ import { ArmResourceTemplateGenerator, - ArmResourceTemplate + ArmResourceTemplate, + ArmResourceType } from "../models/armTemplates"; import { Guard } from "../shared/guard"; import { ServerlessAzureConfig } from "../models/serverless"; import { Utils } from "../shared/utils"; export class CompositeArmTemplate implements ArmResourceTemplateGenerator { + + public getArmResourceType(): ArmResourceType { + return ArmResourceType.Composite; + } + public constructor(private childTemplates: ArmResourceTemplateGenerator[]) { Guard.null(childTemplates); } @@ -36,13 +42,13 @@ export class CompositeArmTemplate implements ArmResourceTemplateGenerator { return template; } - public getParameters(config: ServerlessAzureConfig) { + public getParameters(config: ServerlessAzureConfig, namer: (resource: ArmResourceType) => string) { let parameters = {}; this.childTemplates.forEach(resource => { parameters = { ...parameters, - ...resource.getParameters(config), + ...resource.getParameters(config, namer), location: Utils.getNormalizedRegionName(config.provider.region) }; }); diff --git a/src/armTemplates/resources/apim.ts b/src/armTemplates/resources/apim.ts index eaca3277..102b5218 100644 --- a/src/armTemplates/resources/apim.ts +++ b/src/armTemplates/resources/apim.ts @@ -1,13 +1,11 @@ -import { ServerlessAzureConfig } from "../../models/serverless"; -import { ArmResourceTemplateGenerator, ArmResourceTemplate } from "../../models/armTemplates"; import { ApiManagementConfig } from "../../models/apiManagement"; -import { Utils } from "../../shared/utils"; +import { ArmResourceTemplate, ArmResourceTemplateGenerator, ArmResourceType } from "../../models/armTemplates"; +import { ServerlessAzureConfig } from "../../models/serverless"; 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`; + + public getArmResourceType(): ArmResourceType { + return ArmResourceType.Apim; } public getTemplate(): ArmResourceTemplate { @@ -63,18 +61,18 @@ export class ApimResource implements ArmResourceTemplateGenerator { }; } - public getParameters(config: ServerlessAzureConfig) { + public getParameters(config: ServerlessAzureConfig, namer: (resource: ArmResourceType) => string) { const apimConfig: ApiManagementConfig = { sku: {}, ...config.provider.apim, }; return { - apiManagementName: ApimResource.getResourceName(config), + apiManagementName: namer(this.getArmResourceType()), apimSkuName: apimConfig.sku.name, apimSkuCapacity: apimConfig.sku.capacity, apimPublisherEmail: apimConfig.publisherEmail, 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..38b58428 100644 --- a/src/armTemplates/resources/appInsights.ts +++ b/src/armTemplates/resources/appInsights.ts @@ -1,12 +1,10 @@ -import { ArmResourceTemplateGenerator, ArmResourceTemplate } from "../../models/armTemplates"; +import { ArmResourceTemplate, ArmResourceTemplateGenerator, ArmResourceType } from "../../models/armTemplates"; import { ServerlessAzureConfig } from "../../models/serverless"; -import { Utils } from "../../shared/utils"; 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`; + + public getArmResourceType(): ArmResourceType { + return ArmResourceType.AppInsights; } public getTemplate(): ArmResourceTemplate { @@ -40,9 +38,9 @@ export class AppInsightsResource implements ArmResourceTemplateGenerator { } } - public getParameters(config: ServerlessAzureConfig): any { + public getParameters(config: ServerlessAzureConfig, namer: (resource: ArmResourceType) => string): any { return { - appInsightsName: AppInsightsResource.getResourceName(config), + appInsightsName: namer(this.getArmResourceType()), }; } -} \ No newline at end of file +} diff --git a/src/armTemplates/resources/appServicePlan.ts b/src/armTemplates/resources/appServicePlan.ts index 95819962..da40be6d 100644 --- a/src/armTemplates/resources/appServicePlan.ts +++ b/src/armTemplates/resources/appServicePlan.ts @@ -1,12 +1,10 @@ -import { ArmResourceTemplateGenerator, ArmResourceTemplate } from "../../models/armTemplates"; -import { ServerlessAzureConfig, ResourceConfig } from "../../models/serverless"; -import { Utils } from "../../shared/utils"; +import { ArmResourceTemplate, ArmResourceTemplateGenerator, ArmResourceType } from "../../models/armTemplates"; +import { ResourceConfig, ServerlessAzureConfig } from "../../models/serverless"; 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`; + + public getArmResourceType(): ArmResourceType { + return ArmResourceType.AppServicePlan; } public getTemplate(): ArmResourceTemplate { @@ -54,16 +52,16 @@ export class AppServicePlanResource implements ArmResourceTemplateGenerator { }; } - public getParameters(config: ServerlessAzureConfig): any { + public getParameters(config: ServerlessAzureConfig, namer: (resource: ArmResourceType) => string): any { const resourceConfig: ResourceConfig = { sku: {}, ...config.provider.storageAccount, }; return { - appServicePlanName: AppServicePlanResource.getResourceName(config), + appServicePlanName: namer(this.getArmResourceType()), appServicePlanSkuName: resourceConfig.sku.name, 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..5f8e679a 100644 --- a/src/armTemplates/resources/functionApp.ts +++ b/src/armTemplates/resources/functionApp.ts @@ -1,14 +1,10 @@ -import { ArmResourceTemplateGenerator, ArmResourceTemplate } from "../../models/armTemplates"; -import { ServerlessAzureConfig, FunctionAppConfig } from "../../models/serverless"; -import { Utils } from "../../shared/utils"; +import { ArmResourceTemplate, ArmResourceTemplateGenerator, ArmResourceType } from "../../models/armTemplates"; +import { FunctionAppConfig, ServerlessAzureConfig } from "../../models/serverless"; 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}`; + public getArmResourceType(): ArmResourceType { + return ArmResourceType.FunctionApp; } public getTemplate(): ArmResourceTemplate { @@ -107,13 +103,13 @@ export class FunctionAppResource implements ArmResourceTemplateGenerator { }; } - public getParameters(config: ServerlessAzureConfig): any { + public getParameters(config: ServerlessAzureConfig, namer: (resource: ArmResourceType) => string): any { const resourceConfig: FunctionAppConfig = { ...config.provider.functionApp, }; return { - functionAppName: FunctionAppResource.getResourceName(config), + functionAppName: namer(this.getArmResourceType()), functionAppNodeVersion: resourceConfig.nodeVersion, functionAppWorkerRuntime: resourceConfig.workerRuntime, functionAppExtensionVersion: resourceConfig.extensionVersion, diff --git a/src/armTemplates/resources/hostingEnvironment.ts b/src/armTemplates/resources/hostingEnvironment.ts index 233aff04..c4feb2e8 100644 --- a/src/armTemplates/resources/hostingEnvironment.ts +++ b/src/armTemplates/resources/hostingEnvironment.ts @@ -1,12 +1,10 @@ -import { ArmResourceTemplateGenerator, ArmResourceTemplate } from "../../models/armTemplates"; +import { ArmResourceTemplate, ArmResourceTemplateGenerator, ArmResourceType } from "../../models/armTemplates"; import { ServerlessAzureConfig } from "../../models/serverless"; -import { Utils } from "../../shared/utils"; 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`; + + public getArmResourceType(): ArmResourceType { + return ArmResourceType.HostingEnvironment; } public getTemplate(): ArmResourceTemplate { @@ -62,9 +60,9 @@ export class HostingEnvironmentResource implements ArmResourceTemplateGenerator }; } - public getParameters(config: ServerlessAzureConfig): any { + public getParameters(config: ServerlessAzureConfig, namer: (resource: ArmResourceType) => string): any { return { - hostingEnvironmentName: HostingEnvironmentResource.getResourceName(config) + hostingEnvironmentName: namer(this.getArmResourceType()), } } } diff --git a/src/armTemplates/resources/storageAccount.test.ts b/src/armTemplates/resources/storageAccount.test.ts deleted file mode 100644 index 6507027b..00000000 --- a/src/armTemplates/resources/storageAccount.test.ts +++ /dev/null @@ -1,154 +0,0 @@ -import md5 from "md5"; -import { StorageAccountResource } from "./storageAccount"; -import { ServerlessAzureConfig } from "../../models/serverless"; -import { Utils } from "../../shared/utils"; - -describe("Storage Account Resource", () => { - const config: ServerlessAzureConfig = { - functions: [], - plugins: [], - provider: { - prefix: "sls", - name: "azure", - region: "westus", - stage: "dev", - }, - service: "test-api" - } - - it("Generates safe storage account name with short parts", () => { - const testConfig: ServerlessAzureConfig = { - ...config, - service: "test-api", - }; - - const result = StorageAccountResource.getResourceName(testConfig); - assertValidStorageAccountName(testConfig, result); - expect(result.startsWith("slswusdev")).toBe(true); - }); - - it("Generates safe storage account names with long parts", () => { - const testConfig: ServerlessAzureConfig = { - ...config, - provider: { - ...config.provider, - prefix: "my-long-test-prefix-name", - region: "Australia Southeast", - stage: "development" - }, - service: "my-long-test-api", - }; - - const result = StorageAccountResource.getResourceName(testConfig); - assertValidStorageAccountName(testConfig, result); - expect(result.startsWith("mylaussedev")).toBe(true); - }); - - it("Generating a storage account name is idempotent", () => { - const result1 = StorageAccountResource.getResourceName(config); - const result2 = StorageAccountResource.getResourceName(config); - - expect(result1).toEqual(result2); - }); - - it("Generates distinct account names based on region", () => { - 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 regionConfigs = regions.map((region) => { - return { - ...config, - provider: { - ...config.provider, - region: region, - } - }; - }); - - const results = {}; - regionConfigs.forEach((config) => { - const result = StorageAccountResource.getResourceName(config); - assertValidStorageAccountName(config, result); - results[result] = config; - }); - - expect(Object.keys(results)).toHaveLength(regionConfigs.length); - }); - - it("Generates distinct account names based on stage", () => { - const stages = [ - "dev", - "test", - "qa", - "uat", - "prod", - "preprod", - ]; - - const stageConfigs = stages.map((region) => { - return { - ...config, - provider: { - ...config.provider, - region: region, - } - }; - }); - - const results = {}; - stageConfigs.forEach((config) => { - const result = StorageAccountResource.getResourceName(config); - assertValidStorageAccountName(config, result); - results[result] = config; - }); - - expect(Object.keys(results)).toHaveLength(stageConfigs.length); - }); - - 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(createSafeString(config.provider.prefix)); - expect(value).toContain(createSafeString(config.provider.stage)); - expect(value).toContain(md5(config.service).substr(0, 3)); - } - - 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..28581d58 100644 --- a/src/armTemplates/resources/storageAccount.ts +++ b/src/armTemplates/resources/storageAccount.ts @@ -1,13 +1,10 @@ -import { ArmResourceTemplateGenerator, ArmResourceTemplate } from "../../models/armTemplates"; -import { ServerlessAzureConfig, ResourceConfig } from "../../models/serverless"; -import { Utils } from "../../shared/utils"; -import md5 from "md5"; +import { ArmResourceTemplate, ArmResourceTemplateGenerator, ArmResourceType } from "../../models/armTemplates"; +import { ResourceConfig, ServerlessAzureConfig } from "../../models/serverless"; 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) + + public getArmResourceType(): ArmResourceType { + return ArmResourceType.StorageAccount; } public getTemplate(): ArmResourceTemplate { @@ -52,46 +49,16 @@ export class StorageAccountResource implements ArmResourceTemplateGenerator { } } - public getParameters(config: ServerlessAzureConfig): any { + public getParameters(config: ServerlessAzureConfig, namer: (resource: ArmResourceType) => string): any { const resourceConfig: ResourceConfig = { sku: {}, ...config.provider.storageAccount, }; return { - storageAccountName: StorageAccountResource.getResourceName(config), + storageAccountName: namer(this.getArmResourceType()), storageAccountSkuName: resourceConfig.sku.name, 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..26bfa1bc 100644 --- a/src/armTemplates/resources/virtualNetwork.ts +++ b/src/armTemplates/resources/virtualNetwork.ts @@ -1,12 +1,10 @@ -import { ArmResourceTemplateGenerator, ArmResourceTemplate } from "../../models/armTemplates"; +import { ArmResourceTemplate, ArmResourceTemplateGenerator, ArmResourceType } from "../../models/armTemplates"; import { ServerlessAzureConfig } from "../../models/serverless"; -import { Utils } from "../../shared/utils"; 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`; + + public getArmResourceType(): ArmResourceType { + return ArmResourceType.VirtualNetwork; } public getTemplate(): ArmResourceTemplate { @@ -80,9 +78,9 @@ export class VirtualNetworkResource implements ArmResourceTemplateGenerator { }; } - public getParameters(config: ServerlessAzureConfig): any { + public getParameters(config: ServerlessAzureConfig, namer: (resource: ArmResourceType) => string): any { return { - virtualNetworkName: VirtualNetworkResource.getResourceName(config), + virtualNetworkName: namer(this.getArmResourceType()), } } } diff --git a/src/models/armTemplates.ts b/src/models/armTemplates.ts index a35d050a..fda28efa 100644 --- a/src/models/armTemplates.ts +++ b/src/models/armTemplates.ts @@ -1,11 +1,23 @@ import { ServerlessAzureConfig } from "./serverless"; +export enum ArmResourceType { + Apim, + AppInsights, + AppServicePlan, + FunctionApp, + HostingEnvironment, + StorageAccount, + VirtualNetwork, + Composite +} + /** * ARM Resource Template Generator */ export interface ArmResourceTemplateGenerator { + getArmResourceType(): ArmResourceType; getTemplate(): ArmResourceTemplate; - getParameters(config: ServerlessAzureConfig): any; + getParameters(config: ServerlessAzureConfig, namer: (resource: ArmResourceType) => string): any; } /** diff --git a/src/services/apimService.test.ts b/src/services/apimService.test.ts index 50131f2d..c633ce50 100644 --- a/src/services/apimService.test.ts +++ b/src/services/apimService.test.ts @@ -20,10 +20,12 @@ import { ApiPolicyCreateOrUpdateResponse, } from "@azure/arm-apimanagement/esm/models"; import { Utils } from "../shared/utils"; +import { AzureNamingService } from "./azureNamingService"; describe("APIM Service", () => { const apimConfig = MockFactory.createTestApimConfig(); let serverless: Serverless; + let namingService: AzureNamingService; beforeEach(() => { const slsConfig: any = { @@ -41,12 +43,15 @@ describe("APIM Service", () => { service: slsConfig }); + namingService = new AzureNamingService(serverless, MockFactory.createTestServerlessOptions()); + serverless.variables = { ...serverless.variables, azureCredentials: MockFactory.createTestAzureCredentials(), subscriptionId: "ABC123", }; }); + it("is defined", () => { expect(ApimService).toBeDefined(); }); @@ -61,8 +66,8 @@ describe("APIM Service", () => { const apimConfigName = MockFactory.createTestApimConfig(true); (serverless.service.provider as any).apim = apimConfigName; - const service = new ApimService(serverless); - const expectedRegionName = Utils.createShortAzureRegionName(service.getRegion()); + new ApimService(serverless); + const expectedRegionName = Utils.createShortAzureRegionName(namingService.getRegion()); expect(apimConfigName.name.includes(expectedRegionName)).toBeTruthy(); }); diff --git a/src/services/apimService.ts b/src/services/apimService.ts index 9257b132..3d698cf6 100644 --- a/src/services/apimService.ts +++ b/src/services/apimService.ts @@ -1,16 +1,14 @@ -import Serverless from "serverless"; -import xml from "xml"; import { ApiManagementClient } from "@azure/arm-apimanagement"; -import { FunctionAppService } from "./functionAppService"; -import { BaseService } from "./baseService"; -import { ApiManagementConfig, ApiOperationOptions, ApiCorsPolicy } from "../models/apiManagement"; -import { - ApiContract, BackendContract, OperationContract, - PropertyContract, ApiManagementServiceResource, -} from "@azure/arm-apimanagement/esm/models"; +import { ApiContract, ApiManagementServiceResource, BackendContract, + OperationContract, PropertyContract } from "@azure/arm-apimanagement/esm/models"; import { Site } from "@azure/arm-appservice/esm/models"; +import Serverless from "serverless"; +import xml from "xml"; +import { ApiCorsPolicy, ApiManagementConfig, ApiOperationOptions } from "../models/apiManagement"; +import { ArmResourceType } from "../models/armTemplates"; import { Guard } from "../shared/guard"; -import { ApimResource } from "../armTemplates/resources/apim"; +import { BaseService } from "./baseService"; +import { FunctionAppService } from "./functionAppService"; /** * APIM Service handles deployment and integration with Azure API Management @@ -29,7 +27,7 @@ export class ApimService extends BaseService { } if (!this.apimConfig.name) { - this.apimConfig.name = ApimResource.getResourceName(this.config); + this.apimConfig.name = this.namingService.getResourceName(ArmResourceType.Apim); } if (!this.apimConfig.backend) { diff --git a/src/services/armService.ts b/src/services/armService.ts index d021b360..79c8145f 100644 --- a/src/services/armService.ts +++ b/src/services/armService.ts @@ -4,7 +4,7 @@ import fs from "fs"; import jsonpath from "jsonpath"; import path from "path"; import Serverless from "serverless"; -import { ArmDeployment, ArmResourceTemplateGenerator, ArmTemplateType } from "../models/armTemplates"; +import { ArmDeployment, ArmResourceTemplateGenerator, ArmTemplateType, ArmResourceType } from "../models/armTemplates"; import { ArmTemplateConfig, ServerlessAzureConfig, ServerlessAzureOptions } from "../models/serverless"; import { Guard } from "../shared/guard"; import { BaseService } from "./baseService"; @@ -39,11 +39,11 @@ export class ArmService extends BaseService { const azureConfig: ServerlessAzureConfig = this.serverless.service as any; const mergedTemplate = template.getTemplate(); - let parameters = template.getParameters(azureConfig); + let parameters = template.getParameters(azureConfig, (resource: ArmResourceType) => this.namingService.getResourceName(resource)); if (this.config.provider.apim) { const apimTemplate = apimResource.getTemplate(); - const apimParameters = apimResource.getParameters(azureConfig); + const apimParameters = apimResource.getParameters(azureConfig, (resource: ArmResourceType) => this.namingService.getResourceName(resource)); mergedTemplate.parameters = { ...mergedTemplate.parameters, diff --git a/src/services/azureBlobStorageService.test.ts b/src/services/azureBlobStorageService.test.ts index 6eb6e62e..fcdd356b 100644 --- a/src/services/azureBlobStorageService.test.ts +++ b/src/services/azureBlobStorageService.test.ts @@ -1,20 +1,23 @@ import mockFs from "mock-fs"; +import { ArmResourceType } from "../models/armTemplates"; import { MockFactory } from "../test/mockFactory"; import { AzureBlobStorageService, AzureStorageAuthType } from "./azureBlobStorageService"; +import { AzureNamingService } from "./azureNamingService"; jest.mock("@azure/storage-blob"); -jest.genMockFromModule("@azure/storage-blob") -import { Aborter, BlockBlobURL, ContainerURL, ServiceURL, uploadFileToBlockBlob, TokenCredential, SharedKeyCredential, downloadBlobToBuffer, generateBlobSASQueryParameters, BlobSASPermissions } from "@azure/storage-blob"; +jest.genMockFromModule("@azure/storage-blob"); +import { Aborter, BlobSASPermissions, BlockBlobURL, + ContainerURL, downloadBlobToBuffer, generateBlobSASQueryParameters, + ServiceURL, SharedKeyCredential, TokenCredential, uploadFileToBlockBlob } from "@azure/storage-blob"; -jest.mock("@azure/arm-storage") +jest.mock("@azure/arm-storage"); jest.genMockFromModule("@azure/arm-storage"); -import { StorageAccounts, StorageManagementClientContext } from "@azure/arm-storage" +import { StorageAccounts, StorageManagementClientContext } from "@azure/arm-storage"; jest.mock("./loginService"); -import { AzureLoginService } from "./loginService" -import { StorageAccountResource } from "../armTemplates/resources/storageAccount"; +import { AzureLoginService } from "./loginService"; -jest.mock("fs") +jest.mock("fs"); import fs from "fs"; describe("Azure Blob Storage Service", () => { @@ -25,7 +28,8 @@ describe("Azure Blob Storage Service", () => { const containers = MockFactory.createTestAzureContainers(); const sls = MockFactory.createTestServerless(); - const accountName = StorageAccountResource.getResourceName(sls.service as any); + const namingService = new AzureNamingService(sls, MockFactory.createTestServerlessOptions()); + const accountName = namingService.getResourceName(ArmResourceType.StorageAccount); const options = MockFactory.createTestServerlessOptions(); const blobContent = "testContent"; const blockBlobUrl = MockFactory.createTestBlockBlobUrl(containerName, filePath, blobContent); @@ -142,7 +146,7 @@ describe("Azure Blob Storage Service", () => { expect(ContainerURL.fromServiceURL).not.toBeCalled(); expect(ContainerURL.prototype.create).not.toBeCalled }) - + it("should delete a container", async () => { const containerToDelete = "delete container"; ContainerURL.fromServiceURL = jest.fn(() => new ContainerURL(null, null)); diff --git a/src/services/azureNamingService.test.ts b/src/services/azureNamingService.test.ts new file mode 100644 index 00000000..535be59b --- /dev/null +++ b/src/services/azureNamingService.test.ts @@ -0,0 +1,279 @@ +import { ServerlessAzureOptions, ServerlessAzureConfig } from "../models/serverless" +import { AzureNamingService } from "./azureNamingService" +import { MockFactory } from "../test/mockFactory" +import { ArmResourceType } from "../models/armTemplates"; +import md5 from "md5"; +import { Utils } from "../shared/utils" + +describe("Azure Naming Service", () => { + + let service: AzureNamingService; + + const azureConfig: ServerlessAzureConfig = { + functions: [], + plugins: [], + provider: { + prefix: "sls", + name: "azure", + region: "westus", + stage: "dev", + }, + service: "test-api" + } + + function getConfig(config?): ServerlessAzureConfig { + return { + ...azureConfig, + ...config + } + } + + function getService(config?: ServerlessAzureConfig, options?: ServerlessAzureOptions) { + const sls = MockFactory.createTestServerless(); + options = options || MockFactory.createTestServerlessOptions(); + sls.service = getConfig(config) as any; + return new AzureNamingService(sls, options); + } + + beforeEach(() => { + service = getService(); + }); + + it("Sets default region and stage values if not defined", () => { + expect(service.getRegion()).toEqual("westus"); + expect(service.getStage()).toEqual("dev"); + }); + + it("returns region and stage based on CLI options", () => { + const cliOptions = { + stage: "prod", + region: "eastus2", + }; + service = getService(azureConfig, cliOptions); + + expect(service.getRegion()).toEqual(cliOptions.region); + expect(service.getStage()).toEqual(cliOptions.stage); + }); + + it("uses the resource group name specified in CLI", () => { + const resourceGroupName = "cliResourceGroupName" + const cliOptions = { + stage: "prod", + region: "eastus2", + resourceGroup: resourceGroupName + }; + + const service = getService(null, cliOptions); + const actualResourceGroupName = service.getResourceGroupName(); + expect(actualResourceGroupName).toEqual(resourceGroupName); + }); + + it("uses the resource group name from the sls yaml config", () => { + const resourceGroupName = "myResourceGroup"; + service = getService({ + ...azureConfig, + provider: { + ...azureConfig.provider, + resourceGroup: resourceGroupName, + } + }) + const actualResourceGroupName = service.getResourceGroupName(); + + expect(actualResourceGroupName).toEqual(resourceGroupName); + }); + + it("Generates resource group from convention when NOT defined in sls yaml", () => { + const serviceName = "myService"; + service = getService({ + ...azureConfig, + provider: { + ...azureConfig.provider, + resourceGroup: null + }, + service: serviceName + }); + + const actualResourceGroupName = service.getResourceGroupName(); + const expectedRegion = Utils.createShortAzureRegionName(service.getRegion()); + const expectedStage = Utils.createShortStageName(service.getStage()); + const expectedResourceGroupName = `sls-${expectedRegion}-${expectedStage}-${serviceName}-rg`; + + expect(actualResourceGroupName).toEqual(expectedResourceGroupName); + }); + + it("set default prefix when one is not defined in yaml config", () => { + service = getService({ + ...azureConfig, + provider: { + name: "azure", + region: "westus", + stage: "dev", + } + }); + expect(service.getPrefix()).toEqual("sls"); + }); + + it("use the prefix defined in sls yaml config", () => { + service = getService({ + ...azureConfig, + provider: { + name: "azure", + region: "westus", + stage: "dev", + prefix: "hello" + } + }); + expect(service.getPrefix()).toEqual("hello"); + }); + + describe("Resource names", () => { + + 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(createSafeString(config.provider.prefix)); + expect(value).toContain(createSafeString(config.provider.stage)); + expect(value).toContain(md5(config.service).substr(0, 3)); + } + + function createSafeString(value: string) { + return value.replace(/\W+/g, "").toLocaleLowerCase().substr(0, 3); + }; + + it("Generates safe storage account name with short parts", () => { + const result = service.getResourceName(ArmResourceType.StorageAccount); + assertValidStorageAccountName(azureConfig, result); + expect(result.startsWith("slswusdev")).toBe(true); + }); + + it("Generates safe storage account names with long parts", () => { + const testConfig: ServerlessAzureConfig = getConfig({ + provider: { + ...azureConfig.provider, + prefix: "my-long-test-prefix-name", + region: "Australia Southeast", + stage: "development" + }, + service: "my-long-test-api", + }); + + service = getService(testConfig); + + const result = service.getResourceName(ArmResourceType.StorageAccount) + assertValidStorageAccountName(testConfig, result); + expect(result.startsWith("mylaussedev")).toBe(true); + }); + + it("Generating a storage account name is idempotent", () => { + const service1 = getService({ + service: "myService" + } as any) + const result1 = service1.getResourceName(ArmResourceType.StorageAccount); + + const service2 = getService({ + service: "myService" + } as any); + const result2 = service2.getResourceName(ArmResourceType.StorageAccount); + + expect(result1).toEqual(result2); + + const service3 = getService({ + service: "myOtherService" + } as any); + const result3 = service3.getResourceName(ArmResourceType.StorageAccount); + + expect(result3).not.toEqual(result1); + }); + + it("Generates distinct account names based on region", () => { + 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 regionConfigs = regions.map((region) => { + return { + ...azureConfig, + provider: { + ...azureConfig.provider, + region: region, + } + }; + }); + + const results = {}; + regionConfigs.forEach((config) => { + service = getService(config); + const result = service.getResourceName(ArmResourceType.StorageAccount) + assertValidStorageAccountName(config, result); + results[result] = config; + }); + + expect(Object.keys(results)).toHaveLength(regionConfigs.length); + }); + + it("Generates distinct account names based on stage", () => { + const stages = [ + "dev", + "test", + "qa", + "uat", + "prod", + "preprod", + ]; + + const stageConfigs = stages.map((region) => { + return { + ...azureConfig, + provider: { + ...azureConfig.provider, + region: region, + } + }; + }); + + const results = {}; + stageConfigs.forEach((config) => { + service = getService(config); + const result = service.getResourceName(ArmResourceType.StorageAccount) + assertValidStorageAccountName(config, result); + results[result] = config; + }); + + expect(Object.keys(results)).toHaveLength(stageConfigs.length); + }); + }); +}); diff --git a/src/services/azureNamingService.ts b/src/services/azureNamingService.ts new file mode 100644 index 00000000..4fd25b34 --- /dev/null +++ b/src/services/azureNamingService.ts @@ -0,0 +1,228 @@ +import md5 from "md5"; +import Serverless from "serverless"; +import configConstants from "../config"; +import { ArmResourceType } from "../models/armTemplates"; +import { DeploymentConfig, ServerlessAzureConfig, ServerlessAzureOptions } from "../models/serverless"; +import { Utils } from "../shared/utils"; + +export class AzureNamingService { + + private config: ServerlessAzureConfig; + + public constructor( + private serverless: Serverless, + private options: ServerlessAzureOptions = { stage: null, region: null } + ) { + this.setDefaultValues(); + this.config = serverless.service as any; + } + + /** + * Name of Function App Service + */ + public getServiceName(): string { + return this.config.service; + } + + /** + * Name of Azure Region for deployment + */ + public getRegion(): string { + return this.options.region || this.config.provider.region; + } + + /** + * Name of current deployment stage + */ + public getStage(): string { + return this.options.stage || this.config.provider.stage; + } + + /** + * Name of prefix for service + */ + public getPrefix(): string { + return this.config.provider.prefix; + } + + /** + * 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.getServiceName()}-rg`; + } + + /** + * Name of current ARM deployment + */ + public getDeploymentName(): string { + const name = this.config.provider.deploymentName || `${this.getResourceGroupName()}-deployment`; + return this.rollbackConfiguredName(name); + } + + /** + * Name of artifact uploaded to blob storage + */ + public getArtifactName(deploymentName: string): string { + return `${deploymentName + .replace("rg-deployment", "artifact") + .replace("deployment", "artifact")}.zip`; + } + + /** + * Get name of Azure resource + * @param resource ARM Resource to name + */ + public getResourceName(resource: ArmResourceType): string { + switch (resource) { + case ArmResourceType.Apim: + return this.getApimName(); + case ArmResourceType.AppInsights: + return this.getAppInsightsName(); + case ArmResourceType.AppServicePlan: + return this.getAppServicePlanName(); + case ArmResourceType.FunctionApp: + return this.getFunctionAppName(); + case ArmResourceType.HostingEnvironment: + return this.getHostingEnvironmentName(); + case ArmResourceType.StorageAccount: + return this.getStorageAccountName(); + case ArmResourceType.VirtualNetwork: + return this.getVirtualNetworkName(); + } + } + + private getConfiguredName(resource: { name?: string }, suffix: string) { + return resource && resource.name + ? resource.name + : this.config.provider.prefix + + "-" + + Utils.createShortAzureRegionName(this.config.provider.region) + + "-" + + Utils.createShortStageName(this.config.provider.stage) + + "-" + + suffix; + } + + private getApimName(): string { + return this.getConfiguredName(this.config.provider.apim, "apim"); + } + + private getAppInsightsName(): string { + return this.getConfiguredName(this.config.provider.appInsights, "appinsights"); + } + + private getAppServicePlanName(): string { + return this.getConfiguredName(this.config.provider.appServicePlan, "asp"); + } + + private getFunctionAppName(): string { + const safeServiceName = this.config.service.replace(/\s/g, "-"); + return this.getConfiguredName(this.config.provider.functionApp, safeServiceName); + } + + private getHostingEnvironmentName(): string { + return this.getConfiguredName(this.config.provider.hostingEnvironment, "ase"); + } + + private getStorageAccountName(): string { + return this.config.provider.storageAccount && this.config.provider.storageAccount.name + ? this.config.provider.storageAccount.name + : this.getDefaultStorageAccountName() + } + + private getVirtualNetworkName(): string { + return this.getConfiguredName(this.config.provider.virtualNetwork, "vnet"); + } + + /** + * Gets a default storage account name. + * Storage account names can have at most 24 characters and can have only alpha-numerics + * @param this.config Serverless Azure this.config + */ + private getDefaultStorageAccountName(): string { + const maxAccountNameLength = 24; + const nameHash = md5(this.config.service); + const replacer = /\W+/g; + + let safePrefix = this.config.provider.prefix.replace(replacer, ""); + const safeRegion = Utils.createShortAzureRegionName(this.config.provider.region); + let safeStage = Utils.createShortStageName(this.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(); + } + + /** + * Add `-t{timestamp}` if rollback is enabled + * @param name Original name + */ + private rollbackConfiguredName(name: string) { + return this.getDeploymentConfig().rollback + ? `${name}-t${this.getTimestamp()}` + : name; + } + + /** + * Get timestamp from `packageTimestamp` serverless variable + * If not set, create timestamp, set variable and return timestamp + */ + private getTimestamp(): number { + let timestamp = +this.serverless.variables["packageTimestamp"]; + if (!timestamp) { + timestamp = Date.now(); + this.serverless.variables["packageTimestamp"] = timestamp; + } + return timestamp; + } + + /** + * Deployment this.config from `serverless.yml` or default. + * Defaults can be found in the `config.ts` file + */ + private getDeploymentConfig(): DeploymentConfig { + const providedConfig = this.serverless["deploy"] as DeploymentConfig; + return { + ...configConstants.deploymentConfig, + ...providedConfig, + } + } + + + 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"; + } + + if (!this.serverless.service.provider["prefix"]) { + this.serverless.service.provider["prefix"] = "sls"; + } + } +} diff --git a/src/services/baseService.test.ts b/src/services/baseService.test.ts index d001cb8e..ae88f090 100644 --- a/src/services/baseService.test.ts +++ b/src/services/baseService.test.ts @@ -2,7 +2,6 @@ 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"; @@ -85,64 +84,6 @@ describe("Base Service", () => { expect(sls.service.provider.stage).toEqual("dev"); }); - it("returns region and stage based on CLI options", () => { - const cliOptions = { - stage: "prod", - region: "eastus2", - }; - const mockService = new MockService(sls, cliOptions); - - expect(mockService.getRegion()).toEqual(cliOptions.region); - expect(mockService.getStage()).toEqual(cliOptions.stage); - }); - - it("use the resource group name specified in CLI", () => { - const resourceGroupName = "cliResourceGroupName" - const cliOptions = { - stage: "prod", - region: "eastus2", - resourceGroup: resourceGroupName - }; - - const mockService = new MockService(sls, cliOptions); - const actualResourceGroupName = mockService.getResourceGroupName(); - - expect(actualResourceGroupName).toEqual(resourceGroupName); - }); - - it("use the resource group name from sls yaml config", () => { - const mockService = new MockService(sls); - const actualResourceGroupName = mockService.getResourceGroupName(); - - expect(actualResourceGroupName).toEqual(sls.service.provider["resourceGroup"]); - }); - - it("Generates resource group from convention when NOT defined in sls yaml", () => { - 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 expectedResourceGroupName = `sls-${expectedRegion}-${expectedStage}-${sls.service["service"]}-rg`; - - expect(actualResourceGroupName).toEqual(expectedResourceGroupName); - }); - - it("set default prefix when one is not defined in yaml config", () => { - const mockService = new MockService(sls); - const actualPrefix = mockService.getPrefix(); - expect(actualPrefix).toEqual("sls"); - }); - - it("use the prefix defined in sls yaml config", () => { - const expectedPrefix = "testPrefix" - sls.service.provider["prefix"] = expectedPrefix; - const mockService = new MockService(sls); - const actualPrefix = mockService.getPrefix(); - - expect(actualPrefix).toEqual(expectedPrefix); - }); - it("Fails if credentials have not been set in serverless config", () => { sls.variables["azureCredentials"] = null; expect(() => new MockService(sls)).toThrow() diff --git a/src/services/baseService.ts b/src/services/baseService.ts index c5715f45..e5029136 100644 --- a/src/services/baseService.ts +++ b/src/services/baseService.ts @@ -3,8 +3,8 @@ import axios from "axios"; import fs from "fs"; import request from "request"; import Serverless from "serverless"; -import { StorageAccountResource } from "../armTemplates/resources/storageAccount"; import { configConstants } from "../config"; +import { ArmResourceType } from "../models/armTemplates"; import { DeploymentConfig, ServerlessAzureConfig, @@ -14,9 +14,11 @@ import { } from "../models/serverless"; import { Guard } from "../shared/guard"; import { Utils } from "../shared/utils"; +import { AzureNamingService } from "./azureNamingService"; export abstract class BaseService { protected baseUrl: string; + protected namingService: AzureNamingService; protected serviceName: string; protected credentials: TokenCredentialsBase; protected subscriptionId: string; @@ -32,19 +34,16 @@ export abstract class BaseService { authenticate: boolean = true ) { Guard.null(serverless); - this.setDefaultValues(); - this.baseUrl = "https://management.azure.com"; - this.serviceName = this.getServiceName(); + this.namingService = new AzureNamingService(this.serverless, this.options); + this.serviceName = this.namingService.getServiceName(); this.config = serverless.service as any; this.credentials = serverless.variables["azureCredentials"]; this.subscriptionId = serverless.variables["subscriptionId"]; - this.resourceGroup = this.getResourceGroupName(); + this.resourceGroup = this.namingService.getResourceGroupName(); this.deploymentConfig = this.getDeploymentConfig(); - this.deploymentName = this.getDeploymentName(); - this.storageAccountName = StorageAccountResource.getResourceName( - serverless.service as any - ); + this.deploymentName = this.namingService.getDeploymentName(); + this.storageAccountName = this.namingService.getResourceName(ArmResourceType.StorageAccount); if (!this.credentials && authenticate) { throw new Error( @@ -53,41 +52,11 @@ export abstract class BaseService { } } - /** - * Name of Azure Region for deployment - */ - public getRegion(): string { - return this.options.region || this.config.provider.region; - } - - /** - * Name of current deployment stage - */ - public getStage(): string { - return this.options.stage || this.config.provider.stage; - } - - public getPrefix(): string { - return this.config.provider.prefix; - } - - /** - * 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`; - } - /** * Deployment config from `serverless.yml` or default. * Defaults can be found in the `config.ts` file */ - public getDeploymentConfig(): DeploymentConfig { + protected getDeploymentConfig(): DeploymentConfig { const providedConfig = this.serverless["deploy"] as DeploymentConfig; return { ...configConstants.deploymentConfig, @@ -95,31 +64,6 @@ export abstract class BaseService { } } - /** - * Name of current ARM deployment - */ - public getDeploymentName(): string { - const name = this.config.provider.deploymentName || `${this.resourceGroup}-deployment`; - return this.rollbackConfiguredName(name); - } - - /** - * Name of Function App Service - */ - public getServiceName(): string { - return this.serverless.service["service"]; - } - - /** - * Get rollback-configured artifact name. Contains `-t{timestamp}` - * Takes name of deployment and replaces `rg-deployment` or `deployment` with `artifact` - */ - protected getArtifactName(deploymentName: string): string { - return `${deploymentName - .replace("rg-deployment", "artifact") - .replace("deployment", "artifact")}.zip`; - } - /** * Get the access token from credentials token cache */ @@ -190,6 +134,9 @@ export abstract class BaseService { return this.serverless.service["functions"]; } + /** + * Returns string contents of serverless configuration file + */ protected slsConfigFile(): string { return "config" in this.options ? this.options["config"] : "serverless.yml"; } @@ -197,47 +144,4 @@ export abstract class BaseService { protected getOption(key: string, defaultValue?: any) { return Utils.get(this.options, key, defaultValue); } - - 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"; - } - - if (!this.serverless.service.provider["prefix"]) { - this.serverless.service.provider["prefix"] = "sls"; - } - } - - /** - * 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 - */ - private getTimestamp(): number { - let timestamp = +this.serverless.variables["packageTimestamp"]; - if (!timestamp) { - timestamp = Date.now(); - this.serverless.variables["packageTimestamp"] = timestamp; - } - return timestamp; - } } diff --git a/src/services/functionAppService.test.ts b/src/services/functionAppService.test.ts index 9de1cf7a..8f1b9442 100644 --- a/src/services/functionAppService.test.ts +++ b/src/services/functionAppService.test.ts @@ -3,19 +3,19 @@ import MockAdapter from "axios-mock-adapter"; import mockFs from "mock-fs"; import path from "path"; import Serverless from "serverless"; +import configConstants from "../config"; +import { ArmDeployment, ArmTemplateType } from "../models/armTemplates"; import { MockFactory } from "../test/mockFactory"; -import { FunctionAppService } from "./functionAppService"; import { ArmService } from "./armService"; -import { FunctionAppResource } from "../armTemplates/resources/functionApp"; +import { FunctionAppService } from "./functionAppService"; -jest.mock("@azure/arm-appservice") +jest.mock("@azure/arm-appservice"); import { WebSiteManagementClient } from "@azure/arm-appservice"; -import { ArmDeployment, ArmTemplateType } from "../models/armTemplates"; + jest.mock("@azure/arm-resources"); jest.mock("./azureBlobStorageService"); -import { AzureBlobStorageService } from "./azureBlobStorageService" -import configConstants from "../config"; +import { AzureBlobStorageService } from "./azureBlobStorageService"; describe("Function App Service", () => { @@ -92,7 +92,7 @@ describe("Function App Service", () => { const service = createService(); const result = await service.get(); expect(WebSiteManagementClient.prototype.webApps.get) - .toBeCalledWith(provider.resourceGroup, FunctionAppResource.getResourceName(slsService as any)); + .toBeCalledWith(provider.resourceGroup, "sls-eus2-dev-serviceName"); expect(result).toEqual(app) }); @@ -104,7 +104,7 @@ describe("Function App Service", () => { } as any; const result = await service.get(); expect(WebSiteManagementClient.prototype.webApps.get) - .toBeCalledWith(provider.resourceGroup, FunctionAppResource.getResourceName(slsService as any)); + .toBeCalledWith(provider.resourceGroup, "sls-eus2-dev-serviceName"); expect(result).toBeNull(); }); @@ -216,14 +216,11 @@ describe("Function App Service", () => { ContentType: "application/octet-stream", } }, slsService["artifact"]); - const expectedArtifactName = service.getDeploymentName().replace("rg-deployment", "artifact"); - expect((AzureBlobStorageService.prototype as any).uploadFile).toBeCalledWith( - slsService["artifact"], - configConstants.deploymentConfig.container, - `${expectedArtifactName}.zip`, - ) + const uploadCall = ((AzureBlobStorageService.prototype as any).uploadFile).mock.calls[0]; - expect(uploadCall[2]).toMatch(/.*-t([0-9]+)/) + expect(uploadCall[0]).toEqual(slsService["artifact"]); + expect(uploadCall[1]).toEqual(configConstants.deploymentConfig.container) + expect(uploadCall[2]).toMatch(/myDeploymentName-t([0-9]+)/) }); it("uploads functions to function app and blob storage with default naming convention", async () => { @@ -247,14 +244,11 @@ describe("Function App Service", () => { ContentType: "application/octet-stream", } }, defaultArtifact); - const expectedArtifactName = service.getDeploymentName().replace("rg-deployment", "artifact"); - expect((AzureBlobStorageService.prototype as any).uploadFile).toBeCalledWith( - defaultArtifact, - configConstants.deploymentConfig.container, - `${expectedArtifactName}.zip`, - ) + const uploadCall = ((AzureBlobStorageService.prototype as any).uploadFile).mock.calls[0]; - expect(uploadCall[2]).toMatch(/.*-t([0-9]+)/) + expect(uploadCall[0]).toEqual(defaultArtifact); + expect(uploadCall[1]).toEqual(configConstants.deploymentConfig.container); + expect(uploadCall[2]).toMatch(/myDeploymentName-t([0-9]+)/) }); it("uploads functions with custom SCM domain (aka App service environments)", async () => { diff --git a/src/services/functionAppService.ts b/src/services/functionAppService.ts index b0eb5cd5..c8f8ca7e 100644 --- a/src/services/functionAppService.ts +++ b/src/services/functionAppService.ts @@ -3,8 +3,7 @@ import { FunctionEnvelope, Site } from "@azure/arm-appservice/esm/models"; import fs from "fs"; import path from "path"; import Serverless from "serverless"; -import { FunctionAppResource } from "../armTemplates/resources/functionApp"; -import { ArmDeployment } from "../models/armTemplates"; +import { ArmDeployment, ArmResourceType } from "../models/armTemplates"; import { FunctionAppHttpTriggerConfig } from "../models/functionApp"; import { Guard } from "../shared/guard"; import { ArmService } from "./armService"; @@ -23,10 +22,11 @@ export class FunctionAppService extends BaseService { } public async get(): Promise { - const response: any = await this.webClient.webApps.get(this.resourceGroup, FunctionAppResource.getResourceName(this.config)); + const resourceName = this.namingService.getResourceName(ArmResourceType.FunctionApp); + const response: any = await this.webClient.webApps.get(this.resourceGroup, resourceName); if (response.error && (response.error.code === "ResourceNotFound" || response.error.code === "ResourceGroupNotFound")) { this.serverless.cli.log(this.resourceGroup); - this.serverless.cli.log(FunctionAppResource.getResourceName(this.config)); + this.serverless.cli.log(resourceName); this.serverless.cli.log(JSON.stringify(response)); return null; } @@ -205,18 +205,10 @@ export class FunctionAppService extends BaseService { await this.blobService.uploadFile( functionZipFile, this.deploymentConfig.container, - this.getArtifactName(this.deploymentName), + this.namingService.getArtifactName(this.deploymentName), ); } - /** - * Get rollback-configured artifact name. Contains `-t{timestamp}` - * if rollback is configured - */ - public getArtifactName(deploymentName: string): string { - return `${deploymentName.replace("rg-deployment", "artifact")}.zip`; - } - public getFunctionHttpTriggerConfig(functionApp: Site, functionConfig: FunctionEnvelope): FunctionAppHttpTriggerConfig { const httpTrigger = functionConfig.config.bindings.find((binding) => { return binding.type === "httpTrigger"; diff --git a/src/services/resourceService.ts b/src/services/resourceService.ts index 2d751814..64ddd7cd 100644 --- a/src/services/resourceService.ts +++ b/src/services/resourceService.ts @@ -26,7 +26,7 @@ export class ResourceService extends BaseService { public async listDeployments(): Promise { const deployments = await this.getDeployments() if (!deployments || deployments.length === 0) { - this.log(`No deployments found for resource group '${this.getResourceGroupName()}'`); + this.log(`No deployments found for resource group '${this.namingService.getResourceGroupName()}'`); return; } let stringDeployments = "\n\nDeployments"; @@ -57,7 +57,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: Utils.getNormalizedRegionName(this.namingService.getRegion()), }); } diff --git a/src/services/rollbackService.ts b/src/services/rollbackService.ts index a8576938..28a3256d 100644 --- a/src/services/rollbackService.ts +++ b/src/services/rollbackService.ts @@ -40,7 +40,7 @@ export class RollbackService extends BaseService { return; } // Name of artifact in blob storage - const artifactName = this.getArtifactName(deployment.name); + const artifactName = this.namingService.getArtifactName(deployment.name); // Redeploy resource group (includes SAS token URL if running from blob URL) await this.redeployDeployment(deployment, artifactName); }