8000 Replicator user defined id generator (#11626) · localstack/localstack@bf8fafe · GitHub
[go: up one dir, main page]

Skip to content

Commit bf8fafe

Browse files
authored
Replicator user defined id generator (#11626)
1 parent 1bc0db5 commit bf8fafe

File tree

9 files changed

+321
-4
lines changed

9 files changed

+321
-4
lines changed

localstack-core/localstack/services/apigateway/helpers.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from moto.apigateway import models as apigw_models
1313
from moto.apigateway.models import APIGatewayBackend, Integration, Resource
1414
from moto.apigateway.models import RestAPI as MotoRestAPI
15-
from moto.apigateway.utils import create_id as create_resource_id
15+
from moto.apigateway.utils import ApigwAuthorizerIdentifier, ApigwResourceIdentifier
1616

1717
from localstack import config
1818
from localstack.aws.api import RequestContext
@@ -472,6 +472,8 @@ def import_api_from_openapi_spec(
472472

473473
query_params: dict = context.request.values.to_dict()
474474
resolved_schema = resolve_references(copy.deepcopy(body), rest_api_id=rest_api.id)
475+
account_id = context.account_id
476+
region_name = context.region
475477

476478
# TODO:
477479
# 1. validate the "mode" property of the spec document, "merge" or "overwrite"
@@ -525,7 +527,9 @@ def create_authorizers(security_schemes: dict) -> None:
525527
authorizer_type = aws_apigateway_authorizer.get("type", "").upper()
526528
# TODO: do we need validation of resources here?
527529
authorizer = Authorizer(
528-
id=create_resource_id(),
530+
id=ApigwAuthorizerIdentifier(
531+
account_id, region_name, security_scheme_name
532+
).generate(),
529533
name=security_scheme_name,
530534
type=authorizer_type,
531535
authorizerResultTtlInSeconds=aws_apigateway_authorizer.get(
@@ -584,8 +588,8 @@ def get_or_create_path(abs_path: str, base_path: str):
584588
return add_path_methods(rel_path, parts, parent_id=parent_id)
585589

586590
def add_path_methods(rel_path: str, parts: List[str], parent_id=""):
587-
child_id = create_resource_id()
588591
rel_path = rel_path or "/"
592+
child_id = ApigwResourceIdentifier(account_id, region_name, parent_id, rel_path).generate()
589593

590594
# Create a `Resource` for the passed `rel_path`
591595
resource = Resource(

localstack-core/localstack/services/apigateway/patches.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ def apigateway_models_stage_to_json(fn, self):
145145

146146
return result
147147

148+
# TODO remove this patch when the behavior is implemented in moto
148149
@patch(apigateway_models.APIGatewayBackend.create_rest_api)
149150
def create_rest_api(fn, self, *args, tags=None, **kwargs):
150151
"""

localstack-core/localstack/services/cloudformation/engine/entities.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
)
1010
from localstack.utils.aws import arns
1111
from localstack.utils.collections import select_attributes
12+
from localstack.utils.id_generator import ExistingIds, ResourceIdentifier, Tags, generate_short_uid
1213
from localstack.utils.json import clone_safe
1314
from localstack.utils.objects import recurse_object
1415
from localstack.utils.strings import long_uid, short_uid
@@ -58,6 +59,17 @@ class StackTemplate(TypedDict):
5859
Resources: dict
5960

6061

62+
class StackIdentifier(ResourceIdentifier):
63+
service = "cloudformation"
64+
resource = "stack"
65+
66+
def __init__(self, account_id: str, region: str, stack_name: str):
67+
super().__init__(account_id, region, stack_name)
68+
69+
def generate(self, existing_ids: ExistingIds = None, tags: Tags = None) -> str:
70+
return generate_short_uid(resource_identifier=self, existing_ids=existing_ids, tags=tags)
71+
72+
6173
# TODO: remove metadata (flatten into individual fields)
6274
class Stack:
6375
change_sets: list["StackChangeSet"]
@@ -93,7 +105,9 @@ def __init__(
93105
# initialize stack template attributes
94106
stack_id = self.metadata.get("StackId") or arns.cloudformation_stack_arn(
95107
self.stack_name,
96-
stack_id=short_uid(),
108+
stack_id=StackIdentifier(
109+
account_id=account_id, region=region_name, stack_name=metadata.get("StackName")
110+
).generate(tags=metadata.get("tags")),
97111
account_id=account_id,
98112
region_name=region_name,
99113
)

localstack-core/localstack/testing/pytest/fixtures.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
from localstack.utils.collections import ensure_list
4646
from localstack.utils.functions import call_safe, run_safe
4747
from localstack.utils.http import safe_requests as requests
48+
from localstack.utils.id_generator import ResourceIdentifier, localstack_id_manager
4849
from localstack.utils.json import CustomEncoder, json_safe
4950
from localstack.utils.net import wait_for_port_open
5051
from localstack.utils.strings import short_uid, to_str
@@ -2291,3 +2292,19 @@ def _delete_log_group():
22912292
def openapi_validate(monkeypatch):
22922293
monkeypatch.setattr(config, "OPENAPI_VALIDATE_RESPONSE", "true")
22932294
monkeypatch.setattr(config, "OPENAPI_VALIDATE_REQUEST", "true")
2295+
2296+
2297+
@pytest.fixture
2298+
def set_resource_custom_id():
2299+
set_ids = []
2300+
2301+
def _set_custom_id(resource_identifier: ResourceIdentifier, custom_id):
2302+
localstack_id_manager.set_custom_id(
2303+
resource_identifier=resource_identifier, custom_id=custom_id
2304+
)
2305+
set_ids.append(resource_identifier)
2306+
2307+
yield _set_custom_id
2308+
2309+
for resource_identifier in set_ids:
2310+
localstack_id_manager.unset_custom_id(resource_identifier)
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import random
2+
import string
3+
4+
from moto.utilities import id_generator as moto_id_generator
5+
from moto.utilities.id_generator import MotoIdManager, moto_id
6+
from moto.utilities.id_generator import ResourceIdentifier as MotoResourceIdentifier
7+
8+
from localstack.utils.strings import long_uid, short_uid
9+
10+
ExistingIds = list[str] | None
11+
Tags = dict[str, str] | None
12+
13+
14+
class LocalstackIdManager(MotoIdManager):
15+
def set_custom_id_by_unique_identifier(self, unique_identifier: str, custom_id: str):
16+
with self._lock:
17+
self._custom_ids[unique_identifier] = custom_id
18+
19+
20+
localstack_id_manager = LocalstackIdManager()
21+
moto_id_generator.moto_id_manager = localstack_id_manager
22+
localstack_id = moto_id
23+
24+
ResourceIdentifier = MotoResourceIdentifier
25+
26+
27+
@localstack_id
28+
def generate_uid(
29+
resource_identifier: ResourceIdentifier,
30+
existing_ids: ExistingIds = None,
31+
tags: Tags = None,
32+
length=36,
33+
) -> str:
34+
return long_uid()[:length]
35+
36+
37+
@localstack_id
38+
def generate_short_uid(
39+
resource_identifier: ResourceIdentifier,
40+
existing_ids: ExistingIds = None,
41+
tags: Tags = None,
42+
) -> str:
43+
return short_uid()
44+
45+
46+
@localstack_id
47+
def generate_str_id(
48+
resource_identifier: ResourceIdentifier,
49+
existing_ids: ExistingIds = None,
50+
tags: Tags = None,
51+
length=8,
52+
) -> str:
53+
return "".join(random.choice(string.ascii_letters) for _ in range(length))
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
from moto.apigateway.utils import (
2+
ApigwApiKeyIdentifier,
3+
ApigwResourceIdentifier,
4+
ApigwRestApiIdentifier,
5+
)
6+
7+
from localstack.testing.pytest import markers
8+
from localstack.utils.strings import long_uid, short_uid
9+
10+
API_ID = "ApiId"
11+
ROOT_RESOURCE_ID = "RootId"
12+
PET_1_RESOURCE_ID = "Pet1Id"
13+
PET_2_RESOURCE_ID = "Pet2Id"
14+
API_KEY_ID = "ApiKeyId"
15+
16+
17+
# Custom ids can't be set on aws.
18+
@markers.aws.only_localstack
19+
def test_apigateway_custom_ids(
20+
aws_client, set_resource_custom_id, create_rest_apigw, account_id, region_name, cleanups
21+
):
22+
rest_api_name = f"apigw-{short_uid()}"
23+
api_key_value = long_uid()
24+
25+
set_resource_custom_id(ApigwRestApiIdentifier(account_id, region_name, rest_api_name), API_ID)
26+
set_resource_custom_id(
27+
ApigwResourceIdentifier(account_id, region_name, path_name="/"), ROOT_RESOURCE_ID
28+
)
29+
set_resource_custom_id(
30+
ApigwResourceIdentifier(
31+
account_id, region_name, parent_id=ROOT_RESOURCE_ID, path_name="pet"
32+
),
33+
PET_1_RESOURCE_ID,
34+
)
35+
set_resource_custom_id(
36+
ApigwResourceIdentifier(
37+
account_id, region_name, parent_id=PET_1_RESOURCE_ID, path_name="pet"
38+
),
39+
PET_2_RESOURCE_ID,
40+
)
41+
set_resource_custom_id(
42+
ApigwApiKeyIdentifier(account_id, region_name, value=api_key_value), API_KEY_ID
43+
)
44+
45+
api_id, name, root_id = create_rest_apigw(name=rest_api_name)
46+
pet_resource_1 = aws_client.apigateway.create_resource(
47+
restApiId=api_id, parentId=ROOT_RESOURCE_ID, pathPart="pet"
48+
)
49+
# we create a second resource with the same path part to ensure we can pass different ids
50+
pet_resource_2 = aws_client.apigateway.create_resource(
51+
restApiId=api_id, parentId=PET_1_RESOURCE_ID, pathPart="pet"
52+
)
53+
api_key = aws_client.apigateway.create_api_key(name="api-key", value=api_key_value)
54+
cleanups.append(lambda: aws_client.apigateway.delete_api_key(apiKey=api_key["id"]))
55+
56+
assert api_id == API_ID
57+
assert name == rest_api_name
58+
assert root_id == ROOT_RESOURCE_ID
59+
assert pet_resource_1["id"] == PET_1_RESOURCE_ID
60+
assert pet_resource_2["id"] == PET_2_RESOURCE_ID
61+
assert api_key["id"] == API_KEY_ID

tests/aws/services/cloudformation/api/test_stacks.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from localstack_snapshot.snapshots.transformer import SortingTransformer
1111

1212
from localstack.aws.api.cloudformation import Capability
13+
from localstack.services.cloudformation.engine.entities import StackIdentifier
1314
from localstack.services.cloudformation.engine.yaml_parser import parse_yaml
1415
from localstack.testing.aws.util import is_aws_cloud
1516
from localstack.testing.pytest import markers
@@ -400,6 +401,33 @@ def _assert_stack_process_finished():
400401
]
401402
assert len(updated_resources) == length_expected
402403

404+
@markers.aws.only_localstack
405+
def test_create_stack_with_custom_id(
406+
self, aws_client, cleanups, account_id, region_name, set_resource_custom_id
407+
):
408+
stack_name = f"stack-{short_uid()}"
409+
custom_id = short_uid()
410+
411+
set_resource_custom_id(
412+
StackIdentifier(account_id, region_name, stack_name), custom_id=custom_id
413+
)
414+
template = open(
415+
os.path.join(os.path.dirname(__file__), "../../../templates/sns_topic_simple.yaml"),
416+
"r",
417+
).read()
418+
419+
stack = aws_client.cloudformation.create_stack(
420+
StackName=stack_name,
421+
TemplateBody=template,
422+
)
423+
cleanups.append(lambda: aws_client.cloudformation.delete_stack(StackName=stack_name))
424+
425+
assert stack["StackId"].split("/")[-1] == custom_id
426+
427+
# We need to wait until the stack is created otherwise we can end up in a scenario
428+
# where we try to delete the stack before creating its resources, failing the test
429+
aws_client.cloudformation.get_waiter("stack_create_complete").wait(StackName=stack_name)
430+
403431

404432
def stack_process_is_finished(cfn_client, stack_name):
405433
return (

tests/aws/services/secretsmanager/test_secretsmanager.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import requests
1212
from botocore.auth import SigV4Auth
1313
from botocore.exceptions import ClientError
14+
from moto.secretsmanager.utils import SecretsManagerSecretIdentifier
1415

1516
from localstack.aws.api.lambda_ import Runtime
1617
from localstack.aws.api.secretsmanager import (
@@ -2446,6 +2447,23 @@ def test_get_secret_value(
24462447
)
24472448
sm_snapshot.match("secret_value_http_response", json_response)
24482449

2450+
@markers.aws.only_localstack
2451+
def test_create_secret_with_custom_id(
2452+
self, account_id, region_name, create_secret, set_resource_custom_id
2453+
):
2454+
secret_name = short_uid()
2455+
custom_id = "TestID"
2456+
set_resource_custom_id(
2457+
SecretsManagerSecretIdentifier(
2458+
account_id=account_id, region=region_name, secret_id=secret_name
2459+
),
2460+
custom_id,
2461+
)
2462+
2463+
secret = create_secret(Name=secret_name, SecretBinary="test-secret")
2464+
2465+
assert secret["ARN"].split(":")[-1] == "-".join((secret_name, custom_id))
2466+
24492467
@markers.aws.validated
24502468
def test_force_delete_deleted_secret(self, sm_snapshot, secret_name, aws_client):
24512469
"""Test if a deleted secret can be force deleted afterwards."""

0 commit comments

Comments
 (0)
0