Skip to content

Commit

Permalink
fix(utils): generatephysicalname handles short names (awslabs#196)
Browse files Browse the repository at this point in the history
Resources for awslabs#169 need physical names shorter than 32 characters with
only lowercase letters, numbers, and hyphens. This change preserves
existing behavior as much as possible.

When `maxLength` is greater than `prefixLength` + `stackIdGuidLength`
+ 3, the original algorithm is used. Otherwise, a new algorithm is
used that requires an `IConstruct` for the stack name and node unique
id.

An optional boolean parameter `lower` was added. When true, `allParts`
ias also made lower case.

It throws errors if the resulting name is longer than `maxLength` or
if `resource` is needed by not provided.

Fixes awslabs#195
  • Loading branch information
jstrunk authored and Patrick committed Jan 22, 2024
1 parent 0e618b5 commit 647a45f
Show file tree
Hide file tree
Showing 2 changed files with 142 additions and 7 deletions.
41 changes: 34 additions & 7 deletions src/common/helpers/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@
* OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions
* and limitations under the License.
*/
import * as crypto from 'crypto';
import * as cdk from 'aws-cdk-lib';
import { IConstruct } from 'constructs';
/**
* The version of this package
*/
Expand All @@ -19,17 +21,23 @@ export const version = require('../../../package.json').version;
/**
* @internal This is an internal core function and should not be called directly by Solutions Constructs clients.
*
* @summary Creates a physical resource name in the style of the CDK (string+hash) - this value incorporates Stack ID,
* so it will remain static in multiple updates of a single stack, but will be different in a separate stack instance
* @summary Creates a physical resource name in the style of the CDK (string+hash) - this value incorporates Stack ID or
* the Stack Name, so it will remain static in multiple updates of a single stack, but will be different in a separate
* stack instance.
* @param {string} prefix - the prefix to use for the name
* @param {string[]} parts - the various string components of the name (eg - stackName, solutions construct ID, L2 construct ID)
* @param {number} maxLength - the longest string that can be returned
* @param {boolean} lower - whether to return the name in lowercase or mixed case
* @param {IConstruct} resource - the resource that is calling this function (used to extract the stack Name and Node ID)
* @returns {string} - a string with concatenated parts (truncated if necessary) + a hash of the full concatenated parts
*
*/
export function generatePhysicalName(
prefix: string,
parts: string[],
maxLength: number,
lower?: boolean,
resource?: IConstruct,
): string {
// The result will consist of:
// -The prefix - unaltered
Expand All @@ -39,10 +47,26 @@ export function generatePhysicalName(

const stackIdGuidLength = 36;
const prefixLength = prefix.length;
const maxPartsLength = maxLength - prefixLength - 1 - stackIdGuidLength; // 1 is the hyphen
let maxPartsLength = maxLength - prefixLength - 1 - stackIdGuidLength; // 1 is the hyphen

// Extract the Stack ID Guid
const uniqueStackIdPart = cdk.Fn.select(2, cdk.Fn.split('/', `${cdk.Aws.STACK_ID}`));
let uniqueStackIdPart = '';
let uniqueStackIdPartLength = stackIdGuidLength;
if (maxPartsLength > 2) {
// Extract the Stack ID Guid
uniqueStackIdPart = cdk.Fn.select(2, cdk.Fn.split('/', `${cdk.Aws.STACK_ID}`));
} else if (resource) {
const stack = cdk.Stack.of(resource);

const hashLength = 8;
const sha256 = crypto.createHash('sha256')
.update(stack.stackName)
.update(cdk.Names.nodeUniqueId(resource.node));
uniqueStackIdPart = sha256.digest('hex').slice(0, hashLength);
uniqueStackIdPartLength = hashLength;
maxPartsLength = maxLength - prefixLength - 1 - hashLength; // 1 is the hyphen
} else {
throw new Error('The resource parameter is required for short names.');
}

let allParts: string = '';

Expand All @@ -55,6 +79,9 @@ export function generatePhysicalName(
allParts = allParts.substring(0, subStringLength) + allParts.substring(allParts.length - subStringLength);
}

const finalName = prefix.toLowerCase() + allParts + '-' + uniqueStackIdPart;
return finalName;
if (prefix.length + allParts.length + uniqueStackIdPartLength + 1 /* hyphen */ > maxLength) {
throw new Error(`The generated name is longer than the maximum length of ${maxLength}`);
}

return prefix.toLowerCase() + (lower ? allParts.toLowerCase() : allParts) + '-' + uniqueStackIdPart;
}
108 changes: 108 additions & 0 deletions test/common/helpers/utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/**
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance
* with the License. A copy of the License is located at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES
* OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions
* and limitations under the License.
*/
import * as cdk from 'aws-cdk-lib';
import { generatePhysicalName } from '../../../src/common/helpers/utils';


describe('generatePhysicalName', () => {
let testResourceA: TestResource;
let testResourceB: TestResource;
let testStack: cdk.Stack;

afterAll(() => {
console.log('Test completed');
});

beforeAll(() => {
const app = new cdk.App();
testStack = new cdk.Stack(app, 'TestStack', { env: { account: '012345678912', region: 'bermuda-triangle-1' } });

testResourceA = new TestResource(testStack, 'A');
testResourceB = new TestResource(testResourceA, 'B');
});

test('short physical name', () => {
const nameA = generatePhysicalName(
'testPrefix',
['Foo', 'Bar', 'Baz'],
32,
true,
testResourceA,
);
const nameB = generatePhysicalName(
'testPrefix',
['Foo', 'Bar', 'Baz'],
32,
true,
testResourceB,
);
expect(nameA).toEqual('testprefixfoobarbaz-900740fe');
expect(nameB).toEqual('testprefixfoobarbaz-99f0d580');
expect(nameA).not.toEqual(nameB);
expect(nameA.length).toBeLessThanOrEqual(32);
expect(nameB.length).toBeLessThanOrEqual(32);
});

test('long physical name', () => {
const maxLogGroupNameLength = 255;
const logGroupPrefix = '/aws/vendedlogs/states/constructs/';
const maxGeneratedNameLength = maxLogGroupNameLength - logGroupPrefix.length;
const nameParts: string[] = [
testStack.stackName, // Name of the stack
'StateMachineLogRag', // Literal string for log group name portion
];
const logGroupName = generatePhysicalName(logGroupPrefix, nameParts, maxGeneratedNameLength);
expect(logGroupName).toMatch(new RegExp('^/aws/vendedlogs/states/constructs/TestStackStateMachineLogRag-\\$\{Token\[TOKEN\.[0-9]+\]\}$'));
expect(logGroupName.length).toBeLessThanOrEqual(maxLogGroupNameLength);
});

test('lowercase long physical name', () => {
const maxLogGroupNameLength = 255;
const logGroupPrefix = '/aws/vendedlogs/states/constructs/';
const maxGeneratedNameLength = maxLogGroupNameLength - logGroupPrefix.length;
const nameParts: string[] = [
testStack.stackName, // Name of the stack
'StateMachineLogRag', // Literal string for log group name portion
];
const logGroupName = generatePhysicalName(logGroupPrefix, nameParts, maxGeneratedNameLength, true);
expect(logGroupName).toMatch(new RegExp('^/aws/vendedlogs/states/constructs/teststackstatemachinelograg-\\$\{Token\[TOKEN\.[0-9]+\]\}$'));
expect(logGroupName.length).toBeLessThanOrEqual(maxLogGroupNameLength);
});

test('short name with no resource', () => {
expect(() => {
generatePhysicalName(
'test',
['Foo', 'Bar', 'Baz'],
32,
true,
);
},
).toThrow('The resource parameter is required for short names.');
});

test('name too long', () => {
expect(() => {
generatePhysicalName(
'/aws/vendedlogs/states/constructs/',
['Foo', 'Bar', 'Baz'],
32,
true,
testResourceA,
);
}).toThrow('The generated name is longer than the maximum length of');
});
});


class TestResource extends cdk.Resource {}

0 comments on commit 647a45f

Please sign in to comment.