Skip to content

Commit

Permalink
fix: Update storage account naming convention (#190)
Browse files Browse the repository at this point in the history
Ensures storage account names do not clash based on combination of prefix, region, stage & service name

- Ensures generated name is <= 24 chars
-  Ensures region names don't conflict
-  Generates short region part from full value
-  Generates short stage part from full value
-  Includes partial hash of service name in full name

Resolves AB#420
  • Loading branch information
wbreza authored Jul 5, 2019
1 parent 691a4af commit 74fdbe6
Show file tree
Hide file tree
Showing 21 changed files with 459 additions and 88 deletions.
20 changes: 20 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"js-yaml": "^3.13.1",
"jsonpath": "^1.0.1",
"lodash": "^4.16.6",
"md5": "^2.2.1",
"open": "^6.3.0",
"request": "^2.81.0",
"rimraf": "^2.6.3",
Expand Down
29 changes: 17 additions & 12 deletions src/armTemplates/compositeArmTemplate.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { ArmResourceTemplateGenerator, ArmResourceTemplate } from "../models/armTemplates";
import {
ArmResourceTemplateGenerator,
ArmResourceTemplate
} from "../models/armTemplates";
import { Guard } from "../shared/guard";
import { ServerlessAzureConfig } from "../models/serverless";
import { Utils } from "../shared/utils";

export class CompositeArmTemplate implements ArmResourceTemplateGenerator {
public constructor(private childTemplates: ArmResourceTemplateGenerator[]) {
Expand All @@ -9,22 +13,23 @@ export class CompositeArmTemplate implements ArmResourceTemplateGenerator {

public getTemplate(): ArmResourceTemplate {
const template: ArmResourceTemplate = {
"$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {},
"resources": [],
$schema:
"https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
contentVersion: "1.0.0.0",
parameters: {},
resources: []
};

this.childTemplates.forEach((resource) => {
this.childTemplates.forEach(resource => {
const resourceTemplate = resource.getTemplate();
template.parameters = {
...template.parameters,
...resourceTemplate.parameters,
...resourceTemplate.parameters
};

template.resources = [
...template.resources,
...resourceTemplate.resources,
...resourceTemplate.resources
];
});

Expand All @@ -34,14 +39,14 @@ export class CompositeArmTemplate implements ArmResourceTemplateGenerator {
public getParameters(config: ServerlessAzureConfig) {
let parameters = {};

this.childTemplates.forEach((resource) => {
this.childTemplates.forEach(resource => {
parameters = {
...parameters,
...resource.getParameters(config),
location: config.provider.region,
}
location: Utils.getNormalizedRegionName(config.provider.region)
};
});

return parameters;
}
}
}
3 changes: 2 additions & 1 deletion src/armTemplates/resources/apim.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { ServerlessAzureConfig } from "../../models/serverless";
import { ArmResourceTemplateGenerator, ArmResourceTemplate } from "../../models/armTemplates";
import { ApiManagementConfig } from "../../models/apiManagement";
import { Utils } from "../../shared/utils";

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}-${config.provider.region}-${config.provider.stage}-apim`;
: `${config.provider.prefix}-${Utils.createShortAzureRegionName(config.provider.region)}-${Utils.createShortStageName(config.provider.stage)}-apim`;
}

public getTemplate(): ArmResourceTemplate {
Expand Down
3 changes: 2 additions & 1 deletion src/armTemplates/resources/appInsights.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { ArmResourceTemplateGenerator, ArmResourceTemplate } 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}-${config.provider.region}-${config.provider.stage}-appinsights`;
: `${config.provider.prefix}-${Utils.createShortAzureRegionName(config.provider.region)}-${Utils.createShortStageName(config.provider.stage)}-appinsights`;
}

public getTemplate(): ArmResourceTemplate {
Expand Down
3 changes: 2 additions & 1 deletion src/armTemplates/resources/appServicePlan.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { ArmResourceTemplateGenerator, ArmResourceTemplate } from "../../models/armTemplates";
import { ServerlessAzureConfig, ResourceConfig } from "../../models/serverless";
import { Utils } from "../../shared/utils";

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}-${config.provider.region}-${config.provider.stage}-asp`;
: `${config.provider.prefix}-${Utils.createShortAzureRegionName(config.provider.region)}-${Utils.createShortStageName(config.provider.stage)}-asp`;
}

public getTemplate(): ArmResourceTemplate {
Expand Down
5 changes: 4 additions & 1 deletion src/armTemplates/resources/functionApp.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { ArmResourceTemplateGenerator, ArmResourceTemplate } from "../../models/armTemplates";
import { ServerlessAzureConfig, FunctionAppConfig } from "../../models/serverless";
import { Utils } from "../../shared/utils";

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}-${config.provider.region}-${config.provider.stage}-${config.service}`;
: `${config.provider.prefix}-${Utils.createShortAzureRegionName(config.provider.region)}-${Utils.createShortStageName(config.provider.stage)}-${safeServiceName}`;
}

public getTemplate(): ArmResourceTemplate {
Expand Down
3 changes: 2 additions & 1 deletion src/armTemplates/resources/hostingEnvironment.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { ArmResourceTemplateGenerator, ArmResourceTemplate } 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}-${config.provider.region}-${config.provider.stage}-ase`;
: `${config.provider.prefix}-${Utils.createShortAzureRegionName(config.provider.region)}-${Utils.createShortStageName(config.provider.stage)}-ase`;
}

public getTemplate(): ArmResourceTemplate {
Expand Down
154 changes: 154 additions & 0 deletions src/armTemplates/resources/storageAccount.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
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);
};
});
34 changes: 23 additions & 11 deletions src/armTemplates/resources/storageAccount.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ArmResourceTemplateGenerator, ArmResourceTemplate } from "../../models/armTemplates";
import { ServerlessAzureConfig, ResourceConfig } from "../../models/serverless";
import { Utils } from "../../shared/utils";
import md5 from "md5";

export class StorageAccountResource implements ArmResourceTemplateGenerator {
public static getResourceName(config: ServerlessAzureConfig) {
Expand Down Expand Up @@ -67,19 +68,30 @@ export class StorageAccountResource implements ArmResourceTemplateGenerator {
/**
* Gets a default storage account name.
* Storage account names can have at most 24 characters and can have only alpha-numerics
* Default naming convention:
*
* "(first 3 of prefix)(first 3 of region)(first 3 of stage)(first 12 of service)sa"
* (Maximum of 23 characters)
* @param config Serverless Azure Config
*/
private static getDefaultStorageAccountName(config: ServerlessAzureConfig): string {
const prefix = Utils.appendSubstrings(
3,
config.provider.prefix,
config.provider.region,
config.provider.stage,
);
return `${prefix}${config.service.substr(0, 12)}sa`.replace("-", "").toLocaleLowerCase();
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();
}
}
Loading

0 comments on commit 74fdbe6

Please sign in to comment.