diff --git a/moto/apigateway/models.py b/moto/apigateway/models.py index 1bfb5111793f..74a075f6b8d1 100644 --- a/moto/apigateway/models.py +++ b/moto/apigateway/models.py @@ -1312,9 +1312,11 @@ def create_deployment( ) -> Deployment: if stage_variables is None: stage_variables = {} + # Since there are no unique values to a deployment, we will use the stage name for the deployment. + # We are also passing a list of deployment ids to generate to prevent overwriting deployments. deployment_id = ApigwDeploymentIdentifier( - self.account_id, self.region_name, name - ).generate() + self.account_id, self.region_name, stage_name=name + ).generate(list(self.deployments.keys())) deployment = Deployment(deployment_id, name, description) self.deployments[deployment_id] = deployment if name: @@ -2177,7 +2179,7 @@ def create_api_key(self, payload: Dict[str, Any]) -> ApiKey: self.account_id, self.region_name, # The value of an api key must be unique on aws - payload.get("value"), + payload.get("value", ""), ).generate() key = ApiKey(api_key_id=api_key_id, **payload) self.keys[key.id] = key diff --git a/moto/apigateway/utils.py b/moto/apigateway/utils.py index 3ee7a1eb4db8..c01529235b96 100644 --- a/moto/apigateway/utils.py +++ b/moto/apigateway/utils.py @@ -1,6 +1,6 @@ import json import string -from typing import Any, Dict +from typing import Any, Dict, List, Union import yaml @@ -14,9 +14,10 @@ class ApigwIdentifier(ResourceIdentifier): def __init__(self, account_id: str, region: str, name: str): super().__init__(account_id, region, name) - def generate(self) -> str: + def generate(self, existing_ids: Union[List[str], None] = None) -> str: return generate_str_id( self, + existing_ids, length=10, include_digits=True, lower_case=True, @@ -37,6 +38,9 @@ class ApigwAuthorizerIdentifier(ApigwIdentifier): class ApigwDeploymentIdentifier(ApigwIdentifier): resource = "deployment" + def __init__(self, account_id: str, region: str, stage_name: str): + super().__init__(account_id, region, stage_name) + class ApigwModelIdentifier(ApigwIdentifier): resource = "model" diff --git a/moto/secretsmanager/utils.py b/moto/secretsmanager/utils.py index 196f19eb1809..84d34d4a80dd 100644 --- a/moto/secretsmanager/utils.py +++ b/moto/secretsmanager/utils.py @@ -2,7 +2,7 @@ import string from moto.moto_api._internal import mock_random as random -from moto.utilities.id_generator import ResourceIdentifier, generate_str_id +from moto.utilities.id_generator import ExistingIds, ResourceIdentifier, generate_str_id from moto.utilities.utils import ARN_PARTITION_REGEX, get_partition @@ -104,9 +104,12 @@ class SecretsManagerSecretIdentifier(ResourceIdentifier): def __init__(self, account_id: str, region: str, secret_id: str): super().__init__(account_id, region, name=secret_id) - def generate(self) -> str: + def generate(self, existing_ids: ExistingIds = None) -> str: id_string = generate_str_id( - resource_identifier=self, length=6, include_digits=False + existing_ids=existing_ids, + resource_identifier=self, + length=6, + include_digits=False, ) return ( f"arn:{get_partition(self.region)}:secretsmanager:{self.region}:" diff --git a/moto/utilities/id_generator.py b/moto/utilities/id_generator.py index dadefdc64fb1..5d8f9cd3c080 100644 --- a/moto/utilities/id_generator.py +++ b/moto/utilities/id_generator.py @@ -4,6 +4,8 @@ from moto.moto_api._internal import mock_random +ExistingIds = Union[List[str], None] + class ResourceIdentifier(abc.ABC): """ @@ -23,7 +25,9 @@ def __init__(self, account_id: str, region: str, name: str): self.name = name or "" @abc.abstractmethod - def generate(self) -> str: ... + def generate(self, existing_ids: ExistingIds = None) -> str: ... + + """ If `existing_ids` is provided, we will not return a custom id if it is already on the list""" @property def unique_identifier(self) -> str: @@ -74,10 +78,12 @@ def add_id_source( self._id_sources.append(id_source) def find_id_from_sources( - self, resource_identifier: ResourceIdentifier + self, resource_identifier: ResourceIdentifier, existing_ids: List[str] ) -> Union[str, None]: for id_source in self._id_sources: - if found_id := id_source(resource_identifier): + if ( + found_id := id_source(resource_identifier) + ) and found_id not in existing_ids: return found_id return None @@ -88,11 +94,15 @@ def find_id_from_sources( def moto_id(fn: Callable[..., str]) -> Callable[..., str]: # Decorator for helping in creation of static ids within Moto. def _wrapper( - resource_identifier: ResourceIdentifier, **kwargs: Dict[str, Any] + resource_identifier: ResourceIdentifier, + existing_ids: ExistingIds, + **kwargs: Dict[str, Any], ) -> str: - if found_id := moto_id_manager.find_id_from_sources(resource_identifier): + if found_id := moto_id_manager.find_id_from_sources( + resource_identifier, existing_ids or [] + ): return found_id - return fn(resource_identifier, **kwargs) + return fn(resource_identifier, existing_ids, **kwargs) return _wrapper @@ -100,6 +110,7 @@ def _wrapper( @moto_id def generate_str_id( # type: ignore resource_identifier: ResourceIdentifier, + existing_ids: ExistingIds, length: int = 20, include_digits: bool = True, lower_case: bool = False, diff --git a/tests/conftest.py b/tests/conftest.py index f723cf5f3d16..3a1f20cb0fb8 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,6 +2,7 @@ import pytest from moto import mock_aws +from moto.utilities.id_generator import ResourceIdentifier, moto_id_manager @pytest.fixture(scope="function") @@ -16,3 +17,19 @@ def account_id(): with mock_aws(): identity = boto3.client("sts", "us-east-1").get_caller_identity() yield identity["Account"] + + +@pytest.fixture +def set_custom_id(): + set_ids = [] + + def _set_custom_id(resource_identifier: ResourceIdentifier, custom_id): + moto_id_manager.set_custom_id( + resource_identifier=resource_identifier, custom_id=custom_id + ) + set_ids.append(resource_identifier) + + yield _set_custom_id + + for resource_identifier in set_ids: + moto_id_manager.unset_custom_id(resource_identifier) diff --git a/tests/test_apigateway/test_apigateway_custom_ids.py b/tests/test_apigateway/test_apigateway_custom_ids.py new file mode 100644 index 000000000000..e864c0781c35 --- /dev/null +++ b/tests/test_apigateway/test_apigateway_custom_ids.py @@ -0,0 +1,133 @@ +import boto3 + +from moto import mock_aws +from moto.apigateway.utils import ( + ApigwApiKeyIdentifier, + ApigwDeploymentIdentifier, + ApigwModelIdentifier, + ApigwRequestValidatorIdentifier, + ApigwResourceIdentifier, + ApigwRestApiValidatorIdentifier, + ApigwUsagePlanIdentifier, +) + +API_ID = "ApiId" +API_KEY_ID = "ApiKeyId" +DEPLOYMENT_ID = "DeployId" +MODEL_ID = "ModelId" +PET_1_RESOURCE_ID = "Pet1Id" +PET_2_RESOURCE_ID = "Pet2Id" +REQUEST_VALIDATOR_ID = "ReqValId" +ROOT_RESOURCE_ID = "RootId" +USAGE_PLAN_ID = "UPlanId" + + +@mock_aws +def test_custom_id_rest_api(set_custom_id, account_id): + region_name = "us-west-2" + rest_api_name = "my-api" + model_name = "modelName" + request_validator_name = "request-validator-name" + stage_name = "stage-name" + + client = boto3.client("apigateway", region_name=region_name) + + set_custom_id( + ApigwRestApiValidatorIdentifier(account_id, region_name, rest_api_name), API_ID + ) + set_custom_id( + ApigwResourceIdentifier(account_id, region_name, path_name="/"), + ROOT_RESOURCE_ID, + ) + set_custom_id( + ApigwResourceIdentifier( + account_id, region_name, parent_id=ROOT_RESOURCE_ID, path_name="pet" + ), + PET_1_RESOURCE_ID, + ) + set_custom_id( + ApigwResourceIdentifier( + account_id, region_name, parent_id=PET_1_RESOURCE_ID, path_name="pet" + ), + PET_2_RESOURCE_ID, + ) + set_custom_id(ApigwModelIdentifier(account_id, region_name, model_name), MODEL_ID) + set_custom_id( + ApigwRequestValidatorIdentifier( + account_id, region_name, request_validator_name + ), + REQUEST_VALIDATOR_ID, + ) + set_custom_id( + ApigwDeploymentIdentifier(account_id, region_name, stage_name=stage_name), + DEPLOYMENT_ID, + ) + + rest_api = client.create_rest_api(name=rest_api_name) + assert rest_api["id"] == API_ID + assert rest_api["rootResourceId"] == ROOT_RESOURCE_ID + + pet_resource_1 = client.create_resource( + restApiId=API_ID, parentId=ROOT_RESOURCE_ID, pathPart="pet" + ) + assert pet_resource_1["id"] == PET_1_RESOURCE_ID + + # we create a second resource with the same path part to ensure we can pass different ids + pet_resource_2 = client.create_resource( + restApiId=API_ID, parentId=PET_1_RESOURCE_ID, pathPart="pet" + ) + assert pet_resource_2["id"] == PET_2_RESOURCE_ID + + model = client.create_model( + restApiId=API_ID, + name=model_name, + schema="EMPTY", + contentType="application/json", + ) + assert model["id"] == MODEL_ID + + request_validator = client.create_request_validator( + restApiId=API_ID, name=request_validator_name + ) + assert request_validator["id"] == REQUEST_VALIDATOR_ID + + # Creating the resource to make a deployment + client.put_method( + restApiId=API_ID, + httpMethod="ANY", + resourceId=PET_2_RESOURCE_ID, + authorizationType="NONE", + ) + client.put_integration( + restApiId=API_ID, resourceId=PET_2_RESOURCE_ID, httpMethod="ANY", type="MOCK" + ) + deployment = client.create_deployment(restApiId=API_ID, stageName=stage_name) + assert deployment["id"] == DEPLOYMENT_ID + + +@mock_aws +def test_custom_id_api_key(account_id, set_custom_id): + region_name = "us-west-2" + api_key_value = "01234567890123456789" + usage_plan_name = "usage-plan" + + client = boto3.client("apigateway", region_name=region_name) + + set_custom_id( + ApigwApiKeyIdentifier(account_id, region_name, value=api_key_value), API_KEY_ID + ) + set_custom_id( + ApigwUsagePlanIdentifier(account_id, region_name, usage_plan_name), + USAGE_PLAN_ID, + ) + + api_key = client.create_api_key(name="api-key", value=api_key_value) + usage_plan = client.create_usage_plan(name=usage_plan_name) + + # verify that we can create a usage plan key using the custom ids + client.create_usage_plan_key( + usagePlanId=USAGE_PLAN_ID, keyId=API_KEY_ID, keyType="API_KEY" + ) + + assert api_key["id"] == API_KEY_ID + assert usage_plan["id"] == USAGE_PLAN_ID diff --git a/tests/test_secretsmanager/test_secretsmanager.py b/tests/test_secretsmanager/test_secretsmanager.py index e386aff25405..b22b192012a2 100644 --- a/tests/test_secretsmanager/test_secretsmanager.py +++ b/tests/test_secretsmanager/test_secretsmanager.py @@ -13,6 +13,7 @@ from moto import mock_aws, settings from moto.core import DEFAULT_ACCOUNT_ID as ACCOUNT_ID +from moto.secretsmanager.utils import SecretsManagerSecretIdentifier from . import secretsmanager_aws_verified @@ -1950,3 +1951,20 @@ def test_update_secret_version_stage_dont_specify_current_stage(secret_arn=None) err["Message"] == f"The parameter RemoveFromVersionId can't be empty. Staging label AWSCURRENT is currently attached to version {current_version}, so you must explicitly reference that version in RemoveFromVersionId." ) + + +@mock_aws +def test_create_secret_custom_id(account_id, set_custom_id): + secret_suffix = "randomSuffix" + secret_name = "secret-name" + region_name = "us-east-1" + + client = boto3.client("secretsmanager", region_name=region_name) + + set_custom_id( + SecretsManagerSecretIdentifier(account_id, region_name, secret_name), + secret_suffix, + ) + secret = client.create_secret(Name=secret_name, SecretString="my secret") + + assert secret["ARN"].split(":")[-1] == f"{secret_name}-{secret_suffix}"