Skip to content

Commit 2306d7e

Browse files
authored
Identifier Utils Module (#126)
1 parent e6a6b78 commit 2306d7e

File tree

6 files changed

+134
-0
lines changed

6 files changed

+134
-0
lines changed

python/rpdk/python/templates/handlers.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
Resource,
1010
SessionProxy,
1111
exceptions,
12+
identifier_utils,
1213
)
1314

1415
from .models import ResourceHandlerRequest, ResourceModel
@@ -36,6 +37,19 @@ def create_handler(
3637

3738
# Example:
3839
try:
40+
41+
# primary identifier from example
42+
primary_identifier = None
43+
44+
# setting up random primary identifier compliant with cfn standard
45+
if primary_identifier is None:
46+
primary_identifier = identifier_utils.generate_resource_identifier(
47+
stack_id_or_name=request.stackId,
48+
logical_resource_id=request.logicalResourceIdentifier,
49+
client_request_token=request.clientRequestToken,
50+
max_length=255
51+
)
52+
3953
if isinstance(session, SessionProxy):
4054
client = session.client("s3")
4155
# Setting Status to success will signal to cfn that the operation is complete
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import hashlib
2+
import re
3+
from typing import Optional
4+
5+
STACK_ARN_PATTERN = "^[a-z0-9-:]*stack/[-a-z0-9A-Z]*/[-a-z0-9A-Z]*"
6+
7+
MIN_PHYSICAL_RESOURCE_ID_LENGTH = 15
8+
MIN_PREFERRED_LENGTH = 17
9+
HASH_LENGTH = 12
10+
11+
12+
def _get_hash(client_request_token: str) -> str:
13+
return hashlib.sha1(str.encode(client_request_token)).hexdigest() # nosec
14+
15+
16+
def generate_resource_identifier(
17+
stack_id_or_name: Optional[str],
18+
logical_resource_id: Optional[str],
19+
client_request_token: Optional[str],
20+
max_length: int,
21+
) -> str:
22+
if max_length < MIN_PHYSICAL_RESOURCE_ID_LENGTH:
23+
raise Exception(
24+
f"Cannot generate resource IDs shorter than\
25+
{MIN_PHYSICAL_RESOURCE_ID_LENGTH} characters."
26+
)
27+
28+
strinct_logical_resource_id: str = logical_resource_id or ""
29+
strinct_client_request_token: str = client_request_token or ""
30+
31+
stack_name: str = stack_id_or_name or ""
32+
33+
pattern = re.compile(STACK_ARN_PATTERN)
34+
35+
if pattern.match(stack_name):
36+
stack_name = stack_name.split("/")[1]
37+
38+
separate: bool = max_length > MIN_PREFERRED_LENGTH
39+
40+
clean_stack_name: str = stack_name.replace("^-+", "", 1).replace("--", "-")
41+
free_chars: int = max_length - (HASH_LENGTH + 1) - (1 if separate else 0)
42+
43+
chars_for_resource_name: int = min(
44+
free_chars // 2, len(strinct_logical_resource_id)
45+
)
46+
chars_for_stack_name: int = min(
47+
free_chars - chars_for_resource_name, len(clean_stack_name)
48+
)
49+
50+
hash_value: str = _get_hash(strinct_client_request_token)
51+
52+
identifier: str = (
53+
clean_stack_name[:chars_for_stack_name]
54+
+ ("-" if separate else "")
55+
+ strinct_logical_resource_id[:chars_for_resource_name]
56+
+ "-"
57+
+ hash_value[:HASH_LENGTH]
58+
)
59+
return identifier

src/cloudformation_cli_python_lib/interface.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,3 +138,4 @@ class BaseResourceHandlerRequest:
138138
nextToken: Optional[str]
139139
region: Optional[str]
140140
awsPartition: Optional[str]
141+
stackId: Optional[str]

src/cloudformation_cli_python_lib/resource.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@ def _cast_resource_request(
163163
systemTags=request.requestData.systemTags,
164164
awsAccountId=request.awsAccountId,
165165
logicalResourceIdentifier=request.requestData.logicalResourceId,
166+
stackId=request.stackId,
166167
region=request.region,
167168
).to_modelled(self._model_cls)
168169
except Exception as e: # pylint: disable=broad-except

src/cloudformation_cli_python_lib/utils.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ class UnmodelledRequest:
124124
awsAccountId: Optional[str] = None
125125
logicalResourceIdentifier: Optional[str] = None
126126
nextToken: Optional[str] = None
127+
stackId: Optional[str] = None
127128
region: Optional[str] = None
128129

129130
def to_modelled(self, model_cls: Type[BaseModel]) -> BaseResourceHandlerRequest:
@@ -138,6 +139,7 @@ def to_modelled(self, model_cls: Type[BaseModel]) -> BaseResourceHandlerRequest:
138139
awsAccountId=self.awsAccountId,
139140
logicalResourceIdentifier=self.logicalResourceIdentifier,
140141
nextToken=self.nextToken,
142+
stackId=self.stackId,
141143
region=self.region,
142144
awsPartition=self.get_partition(self.region),
143145
)

tests/lib/identifier_utils_test.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import pytest
2+
from cloudformation_cli_python_lib.identifier_utils import generate_resource_identifier
3+
4+
5+
def test_generated_name_with_stack_name_and_long_logical_id():
6+
result: str = generate_resource_identifier(
7+
stack_id_or_name="my-custom-stack-name",
8+
logical_resource_id="my-long-long-long-long-long-logical-id-name",
9+
client_request_token="123456789",
10+
max_length=36,
11+
)
12+
assert len(result) == 36
13+
assert result.startswith("my-custom-s-my-long-lon-")
14+
15+
16+
def test_generated_name_with_stack_id_and_long_logical_id():
17+
result: str = generate_resource_identifier(
18+
stack_id_or_name="arn:aws:cloudformation:us-east-1:123456789012:stack/my-stack-name/084c0bd1-082b-11eb-afdc-0a2fadfa68a5", # noqa: B950 # pylint: disable=line-too-long
19+
logical_resource_id="my-long-long-long-long-long-logical-id-name",
20+
client_request_token="123456789",
21+
max_length=36,
22+
)
23+
assert len(result) == 36
24+
assert result.startswith("my-stack-na-my-long-lon-")
25+
26+
27+
def test_generated_name_with_short_stack_name_and_short_logical_id():
28+
result: str = generate_resource_identifier(
29+
stack_id_or_name="abc",
30+
logical_resource_id="abc",
31+
client_request_token="123456789",
32+
max_length=255,
33+
)
34+
assert len(result) == 20 # "abc" + "-" + "abc" + "-" + 12 char hash
35+
assert result.startswith("abc-abc-")
36+
37+
38+
def test_generated_name_with_max_len_shorter_than_preferred():
39+
result: str = generate_resource_identifier(
40+
stack_id_or_name="abc",
41+
logical_resource_id="abc",
42+
client_request_token="123456789",
43+
max_length=16,
44+
)
45+
assert len(result) == 16
46+
assert result.startswith("aba-f7c3bc1d808e")
47+
48+
49+
def test_generated_name_with_invalid_len():
50+
with pytest.raises(Exception) as excinfo:
51+
generate_resource_identifier(
52+
stack_id_or_name="my-stack-name",
53+
logical_resource_id="my-logical-id",
54+
client_request_token="123456789",
55+
max_length=13,
56+
)
57+
assert "Cannot generate resource IDs shorter than" in str(excinfo.value)

0 commit comments

Comments
 (0)