Skip to content
This repository was archived by the owner on Dec 9, 2024. It is now read-only.

Commit 74fdbe6

Browse files
authored
fix: Update storage account naming convention (#190)
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
1 parent 691a4af commit 74fdbe6

21 files changed

+459
-88
lines changed

package-lock.json

Lines changed: 20 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
"js-yaml": "^3.13.1",
4747
"jsonpath": "^1.0.1",
4848
"lodash": "^4.16.6",
49+
"md5": "^2.2.1",
4950
"open": "^6.3.0",
5051
"request": "^2.81.0",
5152
"rimraf": "^2.6.3",
Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1-
import { ArmResourceTemplateGenerator, ArmResourceTemplate } from "../models/armTemplates";
1+
import {
2+
ArmResourceTemplateGenerator,
3+
ArmResourceTemplate
4+
} from "../models/armTemplates";
25
import { Guard } from "../shared/guard";
36
import { ServerlessAzureConfig } from "../models/serverless";
7+
import { Utils } from "../shared/utils";
48

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

1014
public getTemplate(): ArmResourceTemplate {
1115
const template: ArmResourceTemplate = {
12-
"$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
13-
"contentVersion": "1.0.0.0",
14-
"parameters": {},
15-
"resources": [],
16+
$schema:
17+
"https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
18+
contentVersion: "1.0.0.0",
19+
parameters: {},
20+
resources: []
1621
};
1722

18-
this.childTemplates.forEach((resource) => {
23+
this.childTemplates.forEach(resource => {
1924
const resourceTemplate = resource.getTemplate();
2025
template.parameters = {
2126
...template.parameters,
22-
...resourceTemplate.parameters,
27+
...resourceTemplate.parameters
2328
};
2429

2530
template.resources = [
2631
...template.resources,
27-
...resourceTemplate.resources,
32+
...resourceTemplate.resources
2833
];
2934
});
3035

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

37-
this.childTemplates.forEach((resource) => {
42+
this.childTemplates.forEach(resource => {
3843
parameters = {
3944
...parameters,
4045
...resource.getParameters(config),
41-
location: config.provider.region,
42-
}
46+
location: Utils.getNormalizedRegionName(config.provider.region)
47+
};
4348
});
4449

4550
return parameters;
4651
}
47-
}
52+
}

src/armTemplates/resources/apim.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import { ServerlessAzureConfig } from "../../models/serverless";
22
import { ArmResourceTemplateGenerator, ArmResourceTemplate } from "../../models/armTemplates";
33
import { ApiManagementConfig } from "../../models/apiManagement";
4+
import { Utils } from "../../shared/utils";
45

56
export class ApimResource implements ArmResourceTemplateGenerator {
67
public static getResourceName(config: ServerlessAzureConfig) {
78
return config.provider.apim && config.provider.apim.name
89
? config.provider.apim.name
9-
: `${config.provider.prefix}-${config.provider.region}-${config.provider.stage}-apim`;
10+
: `${config.provider.prefix}-${Utils.createShortAzureRegionName(config.provider.region)}-${Utils.createShortStageName(config.provider.stage)}-apim`;
1011
}
1112

1213
public getTemplate(): ArmResourceTemplate {

src/armTemplates/resources/appInsights.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import { ArmResourceTemplateGenerator, ArmResourceTemplate } from "../../models/armTemplates";
22
import { ServerlessAzureConfig } from "../../models/serverless";
3+
import { Utils } from "../../shared/utils";
34

45
export class AppInsightsResource implements ArmResourceTemplateGenerator {
56
public static getResourceName(config: ServerlessAzureConfig) {
67
return config.provider.appInsights && config.provider.appInsights.name
78
? config.provider.appInsights.name
8-
: `${config.provider.prefix}-${config.provider.region}-${config.provider.stage}-appinsights`;
9+
: `${config.provider.prefix}-${Utils.createShortAzureRegionName(config.provider.region)}-${Utils.createShortStageName(config.provider.stage)}-appinsights`;
910
}
1011

1112
public getTemplate(): ArmResourceTemplate {

src/armTemplates/resources/appServicePlan.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import { ArmResourceTemplateGenerator, ArmResourceTemplate } from "../../models/armTemplates";
22
import { ServerlessAzureConfig, ResourceConfig } from "../../models/serverless";
3+
import { Utils } from "../../shared/utils";
34

45
export class AppServicePlanResource implements ArmResourceTemplateGenerator {
56
public static getResourceName(config: ServerlessAzureConfig) {
67
return config.provider.appServicePlan && config.provider.appServicePlan.name
78
? config.provider.appServicePlan.name
8-
: `${config.provider.prefix}-${config.provider.region}-${config.provider.stage}-asp`;
9+
: `${config.provider.prefix}-${Utils.createShortAzureRegionName(config.provider.region)}-${Utils.createShortStageName(config.provider.stage)}-asp`;
910
}
1011

1112
public getTemplate(): ArmResourceTemplate {

src/armTemplates/resources/functionApp.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import { ArmResourceTemplateGenerator, ArmResourceTemplate } from "../../models/armTemplates";
22
import { ServerlessAzureConfig, FunctionAppConfig } from "../../models/serverless";
3+
import { Utils } from "../../shared/utils";
34

45
export class FunctionAppResource implements ArmResourceTemplateGenerator {
56
public static getResourceName(config: ServerlessAzureConfig) {
7+
const safeServiceName = config.service.replace(/\s/g, "-");
8+
69
return config.provider.functionApp && config.provider.functionApp.name
710
? config.provider.functionApp.name
8-
: `${config.provider.prefix}-${config.provider.region}-${config.provider.stage}-${config.service}`;
11+
: `${config.provider.prefix}-${Utils.createShortAzureRegionName(config.provider.region)}-${Utils.createShortStageName(config.provider.stage)}-${safeServiceName}`;
912
}
1013

1114
public getTemplate(): ArmResourceTemplate {

src/armTemplates/resources/hostingEnvironment.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import { ArmResourceTemplateGenerator, ArmResourceTemplate } from "../../models/armTemplates";
22
import { ServerlessAzureConfig } from "../../models/serverless";
3+
import { Utils } from "../../shared/utils";
34

45
export class HostingEnvironmentResource implements ArmResourceTemplateGenerator {
56
public static getResourceName(config: ServerlessAzureConfig) {
67
return config.provider.hostingEnvironment && config.provider.hostingEnvironment.name
78
? config.provider.hostingEnvironment.name
8-
: `${config.provider.prefix}-${config.provider.region}-${config.provider.stage}-ase`;
9+
: `${config.provider.prefix}-${Utils.createShortAzureRegionName(config.provider.region)}-${Utils.createShortStageName(config.provider.stage)}-ase`;
910
}
1011

1112
public getTemplate(): ArmResourceTemplate {
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
import md5 from "md5";
2+
import { StorageAccountResource } from "./storageAccount";
3+
import { ServerlessAzureConfig } from "../../models/serverless";
4+
import { Utils } from "../../shared/utils";
5+
6+
describe("Storage Account Resource", () => {
7+
const config: ServerlessAzureConfig = {
8+
functions: [],
9+
plugins: [],
10+
provider: {
11+
prefix: "sls",
12+
name: "azure",
13+
region: "westus",
14+
stage: "dev",
15+
},
16+
service: "test-api"
17+
}
18+
19+
it("Generates safe storage account name with short parts", () => {
20+
const testConfig: ServerlessAzureConfig = {
21+
...config,
22+
service: "test-api",
23+
};
24+
25+
const result = StorageAccountResource.getResourceName(testConfig);
26+
assertValidStorageAccountName(testConfig, result);
27+
expect(result.startsWith("slswusdev")).toBe(true);
28+
});
29+
30+
it("Generates safe storage account names with long parts", () => {
31+
const testConfig: ServerlessAzureConfig = {
32+
...config,
33+
provider: {
34+
...config.provider,
35+
prefix: "my-long-test-prefix-name",
36+
region: "Australia Southeast",
37+
stage: "development"
38+
},
39+
service: "my-long-test-api",
40+
};
41+
42+
const result = StorageAccountResource.getResourceName(testConfig);
43+
assertValidStorageAccountName(testConfig, result);
44+
expect(result.startsWith("mylaussedev")).toBe(true);
45+
});
46+
47+
it("Generating a storage account name is idempotent", () => {
48+
const result1 = StorageAccountResource.getResourceName(config);
49+
const result2 = StorageAccountResource.getResourceName(config);
50+
51+
expect(result1).toEqual(result2);
52+
});
53+
54+
it("Generates distinct account names based on region", () => {
55+
const regions = [
56+
"eastasia",
57+
"southeastasia",
58+
"centralus",
59+
"eastus",
60+
"eastus2",
61+
"westus",
62+
"northcentralus",
63+
"southcentralus",
64+
"northeurope",
65+
"westeurope",
66+
"japanwest",
67+
"japaneast",
68+
"brazilsouth",
69+
"australiaeast",
70+
"australiasoutheast",
71+
"southindia",
72+
"centralindia",
73+
"westindia",
74+
"canadacentral",
75+
"canadaeast",
76+
"uksouth",
77+
"ukwest",
78+
"westcentralus",
79+
"westus2",
80+
"koreacentral",
81+
"koreasouth",
82+
"francecentral",
83+
"francesouth",
84+
"australiacentral",
85+
"australiacentral2",
86+
"uaecentral",
87+
"uaenorth",
88+
"southafricanorth",
89+
"southafricawest"
90+
];
91+
92+
const regionConfigs = regions.map((region) => {
93+
return {
94+
...config,
95+
provider: {
96+
...config.provider,
97+
region: region,
98+
}
99+
};
100+
});
101+
102+
const results = {};
103+
regionConfigs.forEach((config) => {
104+
const result = StorageAccountResource.getResourceName(config);
105+
assertValidStorageAccountName(config, result);
106+
results[result] = config;
107+
});
108+
109+
expect(Object.keys(results)).toHaveLength(regionConfigs.length);
110+
});
111+
112+
it("Generates distinct account names based on stage", () => {
113+
const stages = [
114+
"dev",
115+
"test",
116+
"qa",
117+
"uat",
118+
"prod",
119+
"preprod",
120+
];
121+
122+
const stageConfigs = stages.map((region) => {
123+
return {
124+
...config,
125+
provider: {
126+
...config.provider,
127+
region: region,
128+
}
129+
};
130+
});
131+
132+
const results = {};
133+
stageConfigs.forEach((config) => {
134+
const result = StorageAccountResource.getResourceName(config);
135+
assertValidStorageAccountName(config, result);
136+
results[result] = config;
137+
});
138+
139+
expect(Object.keys(results)).toHaveLength(stageConfigs.length);
140+
});
141+
142+
function assertValidStorageAccountName(config: ServerlessAzureConfig, value: string) {
143+
expect(value.length).toBeLessThanOrEqual(24);
144+
expect(value.match(/[a-z0-9]/g).length).toEqual(value.length);
145+
expect(value).toContain(Utils.createShortAzureRegionName(config.provider.region));
146+
expect(value).toContain(createSafeString(config.provider.prefix));
147+
expect(value).toContain(createSafeString(config.provider.stage));
148+
expect(value).toContain(md5(config.service).substr(0, 3));
149+
}
150+
151+
function createSafeString(value: string) {
152+
return value.replace(/\W+/g, "").toLocaleLowerCase().substr(0, 3);
153+
};
154+
});

src/armTemplates/resources/storageAccount.ts

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { ArmResourceTemplateGenerator, ArmResourceTemplate } from "../../models/armTemplates";
22
import { ServerlessAzureConfig, ResourceConfig } from "../../models/serverless";
33
import { Utils } from "../../shared/utils";
4+
import md5 from "md5";
45

56
export class StorageAccountResource implements ArmResourceTemplateGenerator {
67
public static getResourceName(config: ServerlessAzureConfig) {
@@ -67,19 +68,30 @@ export class StorageAccountResource implements ArmResourceTemplateGenerator {
6768
/**
6869
* Gets a default storage account name.
6970
* Storage account names can have at most 24 characters and can have only alpha-numerics
70-
* Default naming convention:
71-
*
72-
* "(first 3 of prefix)(first 3 of region)(first 3 of stage)(first 12 of service)sa"
73-
* (Maximum of 23 characters)
7471
* @param config Serverless Azure Config
7572
*/
7673
private static getDefaultStorageAccountName(config: ServerlessAzureConfig): string {
77-
const prefix = Utils.appendSubstrings(
78-
3,
79-
config.provider.prefix,
80-
config.provider.region,
81-
config.provider.stage,
82-
);
83-
return `${prefix}${config.service.substr(0, 12)}sa`.replace("-", "").toLocaleLowerCase();
74+
const maxAccountNameLength = 24;
75+
const nameHash = md5(config.service);
76+
const replacer = /\W+/g;
77+
78+
let safePrefix = config.provider.prefix.replace(replacer, "");
79+
const safeRegion = Utils.createShortAzureRegionName(config.provider.region);
80+
let safeStage = Utils.createShortStageName(config.provider.stage);
81+
let safeNameHash = nameHash.substr(0, 6);
82+
83+
const remaining = maxAccountNameLength - (safePrefix.length + safeRegion.length + safeStage.length + safeNameHash.length);
84+
85+
// Dynamically adjust the substring based on space needed
86+
if (remaining < 0) {
87+
const partLength = Math.floor(Math.abs(remaining) / 3);
88+
safePrefix = safePrefix.substr(0, partLength);
89+
safeStage = safeStage.substr(0, partLength);
90+
safeNameHash = safeNameHash.substr(0, partLength);
91+
}
92+
93+
return [safePrefix, safeRegion, safeStage, safeNameHash]
94+
.join("")
95+
.toLocaleLowerCase();
8496
}
8597
}

0 commit comments

Comments
 (0)