Skip to content

Commit 953665c

Browse files
authored
added stack-id in resource name generation (#317)
1 parent e34f047 commit 953665c

File tree

2 files changed

+186
-1
lines changed

2 files changed

+186
-1
lines changed

src/main/java/software/amazon/cloudformation/resource/IdentifierUtils.java

+98-1
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,22 @@
1414
*/
1515
package software.amazon.cloudformation.resource;
1616

17+
import com.google.common.base.Splitter;
18+
import com.google.common.collect.Iterables;
19+
import java.util.Arrays;
1720
import java.util.Random;
21+
import java.util.regex.Pattern;
1822
import org.apache.commons.lang3.RandomStringUtils;
1923

2024
public class IdentifierUtils {
2125

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

2534
private IdentifierUtils() {
2635
}
@@ -56,7 +65,7 @@ public static String generateResourceIdentifier(final String logicalResourceId,
5665
generateResourceIdentifier(final String logicalResourceId, final String clientRequestToken, final int maxLength) {
5766
int maxLogicalIdLength = maxLength - (GUID_LENGTH + 1);
5867

59-
int endIndex = logicalResourceId.length() > maxLogicalIdLength ? maxLogicalIdLength : logicalResourceId.length();
68+
int endIndex = Math.min(logicalResourceId.length(), maxLogicalIdLength);
6069

6170
StringBuilder sb = new StringBuilder();
6271
if (endIndex > 0) {
@@ -66,4 +75,92 @@ public static String generateResourceIdentifier(final String logicalResourceId,
6675
return sb.append(RandomStringUtils.random(GUID_LENGTH, 0, 0, true, true, null, new Random(clientRequestToken.hashCode())))
6776
.toString();
6877
}
78+
79+
public static String generateResourceIdentifier(final String stackId,
80+
final String logicalResourceId,
81+
final String clientRequestToken,
82+
final int maxLength) {
83+
84+
if (maxLength < MIN_PHYSICAL_RESOURCE_ID_LENGTH) {
85+
throw new IllegalArgumentException("Cannot generate resource IDs shorter than " + MIN_PHYSICAL_RESOURCE_ID_LENGTH
86+
+ " characters.");
87+
}
88+
89+
String stackName = stackId;
90+
91+
if (isStackArn(stackId)) {
92+
stackName = STACKID_SPLITTER.splitToList(stackId).get(1);
93+
}
94+
95+
if (!isValidStackName(stackName)) {
96+
throw new IllegalArgumentException(String.format("%s is not a valid Stack name", stackName));
97+
}
98+
99+
// some services don't allow leading dashes. Since stack name is first, clean
100+
// off any + no consecutive dashes
101+
102+
final String cleanStackName = stackName.replaceFirst("^-+", "").replaceAll("-{2,}", "-");
103+
104+
final boolean separate = maxLength > MIN_PREFERRED_LENGTH;
105+
// 13 char length is reserved for the hashed value and one
106+
// for each dash separator (if needed). the rest if the characters
107+
// will get allocated evenly between the stack and resource names
108+
109+
final int freeCharacters = maxLength - 13 - (separate ? 1 : 0);
110+
final int[] requestedLengths = new int[2];
111+
112+
requestedLengths[0] = cleanStackName.length();
113+
requestedLengths[1] = logicalResourceId.length();
114+
115+
final int[] availableLengths = fairSplit(freeCharacters, requestedLengths);
116+
final int charsForStackName = availableLengths[0];
117+
final int charsForResrcName = availableLengths[1];
118+
119+
final StringBuilder prefix = new StringBuilder();
120+
121+
prefix.append(cleanStackName, 0, charsForStackName);
122+
if (separate) {
123+
prefix.append("-");
124+
}
125+
prefix.append(logicalResourceId, 0, charsForResrcName);
126+
127+
return IdentifierUtils.generateResourceIdentifier(prefix.toString(), clientRequestToken, maxLength);
128+
}
129+
130+
private static boolean isStackArn(String stackId) {
131+
return STACK_ARN_PATTERN.matcher(stackId).matches() && Iterables.size(STACKID_SPLITTER.split(stackId)) == 3;
132+
}
133+
134+
private static boolean isValidStackName(String stackName) {
135+
return STACK_NAME_PATTERN.matcher(stackName).matches();
136+
}
137+
138+
private static int[] fairSplit(final int cap, final int[] buckets) {
139+
int remaining = cap;
140+
141+
int[] allocated = new int[buckets.length];
142+
Arrays.fill(allocated, 0);
143+
144+
while (remaining > 0) {
145+
// at least one capacity unit
146+
int maxAllocation = remaining < buckets.length ? 1 : remaining / buckets.length;
147+
148+
int bucketSatisfied = 0; // reset on each cap
149+
150+
for (int i = -1; ++i < buckets.length;) {
151+
if (allocated[i] < buckets[i]) {
152+
final int incrementalAllocation = Math.min(maxAllocation, buckets[i] - allocated[i]);
153+
allocated[i] += incrementalAllocation;
154+
remaining -= incrementalAllocation;
155+
} else {
156+
bucketSatisfied++;
157+
}
158+
159+
if (remaining <= 0 || bucketSatisfied == buckets.length) {
160+
return allocated;
161+
}
162+
}
163+
}
164+
return allocated;
165+
}
69166
}

src/test/java/software/amazon/cloudformation/resource/IdentifierUtilsTest.java

+88
Original file line numberDiff line numberDiff line change
@@ -48,4 +48,92 @@ public void generateResourceIdentifier_withShortLength_prefixTruncated() {
4848
// string that is left to fix the max length.
4949
assertThat(result).startsWith("my-re-");
5050
}
51+
52+
@Test
53+
public void generateResourceIdentifier_withStackNameStackId() {
54+
String result = IdentifierUtils.generateResourceIdentifier("my-stack-name", "my-resource", "123456", 18);
55+
assertThat(result.length()).isLessThanOrEqualTo(18);
56+
57+
// to ensure randomness in the identity, the result will always be a random
58+
// string PREFIXED by the size of
59+
// string that is left to fix the max length.
60+
assertThat(result).startsWith("my-my-");
61+
}
62+
63+
@Test
64+
public void generateResourceIdentifier_withStackName() {
65+
String result = IdentifierUtils.generateResourceIdentifier("my-stack-name", "my-resource", "123456", 50);
66+
assertThat(result.length()).isLessThanOrEqualTo(49);
67+
68+
// to ensure randomness in the identity, the result will always be a random
69+
// string PREFIXED by the size of
70+
// string that is left to fix the max length.
71+
assertThat(result).startsWith("my-stack-name-my-resource-");
72+
}
73+
74+
@Test
75+
public void generateResourceIdentifier_withStackNameLessThanPreferredLen() {
76+
String result = IdentifierUtils.generateResourceIdentifier("my-stack-name", "my-resource", "123456", 16);
77+
assertThat(result.length()).isLessThanOrEqualTo(16);
78+
79+
// to ensure randomness in the identity, the result will always be a random
80+
// string PREFIXED by the size of
81+
// string that is left to fix the max length.
82+
assertThat(result).startsWith("mym-");
83+
}
84+
85+
@Test
86+
public void generateResourceIdentifier_withStackNameBothFitMaxLen() {
87+
String result = IdentifierUtils.generateResourceIdentifier(
88+
"arn:aws:cloudformation:us-east-1:123456789012:stack/my-stack-name/084c0bd1-082b-11eb-afdc-0a2fadfa68a5",
89+
"my-resource", "123456", 255);
90+
assertThat(result.length()).isLessThanOrEqualTo(44);
91+
assertThat(result).isEqualTo("my-stack-name-my-resource-hDoP0dahAFjd");
92+
}
93+
94+
@Test
95+
public void generateResourceIdentifier_withLongStackNameAndShotLogicalId() {
96+
String result = IdentifierUtils.generateResourceIdentifier(
97+
"arn:aws:cloudformation:us-east-1:123456789012:stack/my-very-very-very-very-very-very-long-custom-stack-name/084c0bd1-082b-11eb-afdc-0a2fadfa68a5",
98+
"abc", "123456", 36);
99+
assertThat(result.length()).isLessThanOrEqualTo(36);
100+
assertThat(result).isEqualTo("my-very-very-very-v-abc-hDoP0dahAFjd");
101+
}
102+
103+
@Test
104+
public void generateResourceIdentifier_withShortStackNameAndLongLogicalId() {
105+
String result = IdentifierUtils.generateResourceIdentifier("abc",
106+
"my-very-very-very-very-very-very-long-custom-logical-id", "123456", 36);
107+
assertThat(result.length()).isLessThanOrEqualTo(36);
108+
assertThat(result).isEqualTo("abc-my-very-very-very-v-hDoP0dahAFjd");
109+
}
110+
111+
@Test
112+
public void generateResourceIdentifier_withLongStackNameAndLongLogicalId() {
113+
String result = IdentifierUtils.generateResourceIdentifier(
114+
"arn:aws:cloudformation:us-east-1:123456789012:stack/my-very-very-very-very-very-very-long-custom-stack-name/084c0bd1-082b-11eb-afdc-0a2fadfa68a5",
115+
"my-very-very-very-very-very-very-long-custom-logical-id", "123456", 36);
116+
assertThat(result.length()).isEqualTo(36);
117+
assertThat(result).isEqualTo("my-very-ver-my-very-ver-hDoP0dahAFjd");
118+
}
119+
120+
@Test
121+
public void generateResourceIdentifier_withStackInValidInput() {
122+
try {
123+
IdentifierUtils.generateResourceIdentifier("stack/my-stack-name", "my-resource", "123456", 255);
124+
} catch (IllegalArgumentException e) {
125+
assertThat(e.getMessage()).isEqualTo("stack/my-stack-name is not a valid Stack name");
126+
}
127+
}
128+
129+
@Test
130+
public void generateResourceIdentifier_withStackValidStackId() {
131+
try {
132+
IdentifierUtils.generateResourceIdentifier(
133+
"arn:aws:cloudformation:us-east-1:123456789012:stack/my-stack-name/084c0bd1-082b-11eb-afdc-0a2fadfa68a5",
134+
"my-resource", "123456", 14);
135+
} catch (IllegalArgumentException e) {
136+
assertThat(e.getMessage()).isEqualTo("Cannot generate resource IDs shorter than 15 characters.");
137+
}
138+
}
51139
}

0 commit comments

Comments
 (0)