Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,22 @@
*/
package software.amazon.cloudformation.resource;

import com.google.common.base.Splitter;
import com.google.common.collect.Iterables;
import java.util.Arrays;
import java.util.Random;
import java.util.regex.Pattern;
import org.apache.commons.lang3.RandomStringUtils;

public class IdentifierUtils {

private static final int GENERATED_PHYSICALID_MAXLEN = 40;
private static final int GUID_LENGTH = 12;
private static final int MIN_PHYSICAL_RESOURCE_ID_LENGTH = 15;
private static final int MIN_PREFERRED_LENGTH = 17;
private static final Splitter STACKID_SPLITTER = Splitter.on('/');
private static final Pattern STACK_ARN_PATTERN = Pattern.compile("^[a-z0-9-:]*stack/[-a-z0-9A-Z/]*");
private static final Pattern STACK_NAME_PATTERN = Pattern.compile("^[-a-z0-9A-Z]*");

private IdentifierUtils() {
}
Expand Down Expand Up @@ -56,7 +65,7 @@ public static String generateResourceIdentifier(final String logicalResourceId,
generateResourceIdentifier(final String logicalResourceId, final String clientRequestToken, final int maxLength) {
int maxLogicalIdLength = maxLength - (GUID_LENGTH + 1);

int endIndex = logicalResourceId.length() > maxLogicalIdLength ? maxLogicalIdLength : logicalResourceId.length();
int endIndex = Math.min(logicalResourceId.length(), maxLogicalIdLength);

StringBuilder sb = new StringBuilder();
if (endIndex > 0) {
Expand All @@ -66,4 +75,92 @@ public static String generateResourceIdentifier(final String logicalResourceId,
return sb.append(RandomStringUtils.random(GUID_LENGTH, 0, 0, true, true, null, new Random(clientRequestToken.hashCode())))
.toString();
}

public static String generateResourceIdentifier(final String stackId,
final String logicalResourceId,
final String clientRequestToken,
final int maxLength) {

if (maxLength < MIN_PHYSICAL_RESOURCE_ID_LENGTH) {
throw new IllegalArgumentException("Cannot generate resource IDs shorter than " + MIN_PHYSICAL_RESOURCE_ID_LENGTH
+ " characters.");
}

String stackName = stackId;

if (isStackArn(stackId)) {
stackName = STACKID_SPLITTER.splitToList(stackId).get(1);
}

if (!isValidStackName(stackName)) {
throw new IllegalArgumentException(String.format("%s is not a valid Stack name", stackName));
}

// some services don't allow leading dashes. Since stack name is first, clean
// off any + no consecutive dashes

final String cleanStackName = stackName.replaceFirst("^-+", "").replaceAll("-{2,}", "-");

final boolean separate = maxLength > MIN_PREFERRED_LENGTH;
// 13 char length is reserved for the hashed value and one
// for each dash separator (if needed). the rest if the characters
// will get allocated evenly between the stack and resource names

final int freeCharacters = maxLength - 13 - (separate ? 1 : 0);
final int[] requestedLengths = new int[2];

requestedLengths[0] = cleanStackName.length();
requestedLengths[1] = logicalResourceId.length();

final int[] availableLengths = fairSplit(freeCharacters, requestedLengths);
final int charsForStackName = availableLengths[0];
final int charsForResrcName = availableLengths[1];

final StringBuilder prefix = new StringBuilder();

prefix.append(cleanStackName, 0, charsForStackName);
if (separate) {
prefix.append("-");
}
prefix.append(logicalResourceId, 0, charsForResrcName);

return IdentifierUtils.generateResourceIdentifier(prefix.toString(), clientRequestToken, maxLength);
}

private static boolean isStackArn(String stackId) {
return STACK_ARN_PATTERN.matcher(stackId).matches() && Iterables.size(STACKID_SPLITTER.split(stackId)) == 3;
}

private static boolean isValidStackName(String stackName) {
return STACK_NAME_PATTERN.matcher(stackName).matches();
}

private static int[] fairSplit(final int cap, final int[] buckets) {
int remaining = cap;

int[] allocated = new int[buckets.length];
Arrays.fill(allocated, 0);

while (remaining > 0) {
// at least one capacity unit
int maxAllocation = remaining < buckets.length ? 1 : remaining / buckets.length;

int bucketSatisfied = 0; // reset on each cap

for (int i = -1; ++i < buckets.length;) {
if (allocated[i] < buckets[i]) {
final int incrementalAllocation = Math.min(maxAllocation, buckets[i] - allocated[i]);
allocated[i] += incrementalAllocation;
remaining -= incrementalAllocation;
} else {
bucketSatisfied++;
}

if (remaining <= 0 || bucketSatisfied == buckets.length) {
return allocated;
}
}
}
return allocated;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,92 @@ public void generateResourceIdentifier_withShortLength_prefixTruncated() {
// string that is left to fix the max length.
assertThat(result).startsWith("my-re-");
}

@Test
public void generateResourceIdentifier_withStackNameStackId() {
String result = IdentifierUtils.generateResourceIdentifier("my-stack-name", "my-resource", "123456", 18);
assertThat(result.length()).isLessThanOrEqualTo(18);

// to ensure randomness in the identity, the result will always be a random
// string PREFIXED by the size of
// string that is left to fix the max length.
assertThat(result).startsWith("my-my-");
}

@Test
public void generateResourceIdentifier_withStackName() {
String result = IdentifierUtils.generateResourceIdentifier("my-stack-name", "my-resource", "123456", 50);
assertThat(result.length()).isLessThanOrEqualTo(49);

// to ensure randomness in the identity, the result will always be a random
// string PREFIXED by the size of
// string that is left to fix the max length.
assertThat(result).startsWith("my-stack-name-my-resource-");
}

@Test
public void generateResourceIdentifier_withStackNameLessThanPreferredLen() {
String result = IdentifierUtils.generateResourceIdentifier("my-stack-name", "my-resource", "123456", 16);
assertThat(result.length()).isLessThanOrEqualTo(16);

// to ensure randomness in the identity, the result will always be a random
// string PREFIXED by the size of
// string that is left to fix the max length.
assertThat(result).startsWith("mym-");
}

@Test
public void generateResourceIdentifier_withStackNameBothFitMaxLen() {
String result = IdentifierUtils.generateResourceIdentifier(
"arn:aws:cloudformation:us-east-1:123456789012:stack/my-stack-name/084c0bd1-082b-11eb-afdc-0a2fadfa68a5",
"my-resource", "123456", 255);
assertThat(result.length()).isLessThanOrEqualTo(44);
assertThat(result).isEqualTo("my-stack-name-my-resource-hDoP0dahAFjd");
}

@Test
public void generateResourceIdentifier_withLongStackNameAndShotLogicalId() {
String result = IdentifierUtils.generateResourceIdentifier(
"arn:aws:cloudformation:us-east-1:123456789012:stack/my-very-very-very-very-very-very-long-custom-stack-name/084c0bd1-082b-11eb-afdc-0a2fadfa68a5",
"abc", "123456", 36);
assertThat(result.length()).isLessThanOrEqualTo(36);
assertThat(result).isEqualTo("my-very-very-very-v-abc-hDoP0dahAFjd");
}

@Test
public void generateResourceIdentifier_withShortStackNameAndLongLogicalId() {
String result = IdentifierUtils.generateResourceIdentifier("abc",
"my-very-very-very-very-very-very-long-custom-logical-id", "123456", 36);
assertThat(result.length()).isLessThanOrEqualTo(36);
assertThat(result).isEqualTo("abc-my-very-very-very-v-hDoP0dahAFjd");
}

@Test
public void generateResourceIdentifier_withLongStackNameAndLongLogicalId() {
String result = IdentifierUtils.generateResourceIdentifier(
"arn:aws:cloudformation:us-east-1:123456789012:stack/my-very-very-very-very-very-very-long-custom-stack-name/084c0bd1-082b-11eb-afdc-0a2fadfa68a5",
"my-very-very-very-very-very-very-long-custom-logical-id", "123456", 36);
assertThat(result.length()).isEqualTo(36);
assertThat(result).isEqualTo("my-very-ver-my-very-ver-hDoP0dahAFjd");
}

@Test
public void generateResourceIdentifier_withStackInValidInput() {
try {
IdentifierUtils.generateResourceIdentifier("stack/my-stack-name", "my-resource", "123456", 255);
} catch (IllegalArgumentException e) {
assertThat(e.getMessage()).isEqualTo("stack/my-stack-name is not a valid Stack name");
}
}

@Test
public void generateResourceIdentifier_withStackValidStackId() {
try {
IdentifierUtils.generateResourceIdentifier(
"arn:aws:cloudformation:us-east-1:123456789012:stack/my-stack-name/084c0bd1-082b-11eb-afdc-0a2fadfa68a5",
"my-resource", "123456", 14);
} catch (IllegalArgumentException e) {
assertThat(e.getMessage()).isEqualTo("Cannot generate resource IDs shorter than 15 characters.");
}
}
}