diff --git a/IMPLEMENTATION_COVERAGE.md b/IMPLEMENTATION_COVERAGE.md index c4b2b9e2bcb7..e6dedaaf9045 100644 --- a/IMPLEMENTATION_COVERAGE.md +++ b/IMPLEMENTATION_COVERAGE.md @@ -7553,7 +7553,7 @@ ## servicediscovery
-66% implemented +100% implemented - [X] create_http_namespace - [X] create_private_dns_namespace @@ -7561,24 +7561,24 @@ - [X] create_service - [X] delete_namespace - [X] delete_service -- [ ] deregister_instance -- [ ] discover_instances -- [ ] discover_instances_revision -- [ ] get_instance -- [ ] get_instances_health_status +- [X] deregister_instance +- [X] discover_instances +- [X] discover_instances_revision +- [X] get_instance +- [X] get_instances_health_status - [X] get_namespace - [X] get_operation - [X] get_service -- [ ] list_instances +- [X] list_instances - [X] list_namespaces - [X] list_operations - [X] list_services - [X] list_tags_for_resource -- [ ] register_instance +- [X] register_instance - [X] tag_resource - [X] untag_resource -- [ ] update_http_namespace -- [ ] update_instance_custom_health_status +- [X] update_http_namespace +- [X] update_instance_custom_health_status - [X] update_private_dns_namespace - [X] update_public_dns_namespace - [X] update_service diff --git a/docs/docs/services/cf.rst b/docs/docs/services/cf.rst index 5ecd6c6a6d68..599c8148a56d 100644 --- a/docs/docs/services/cf.rst +++ b/docs/docs/services/cf.rst @@ -55,7 +55,7 @@ Please let us know if you'd like support for a resource not yet listed here. +---------------------------------------+--------+--------+--------+----------------------------------------+ |AWS::EC2::InternetGateway | x | | | - [ ] InternetGatewayId | +---------------------------------------+--------+--------+--------+----------------------------------------+ - |AWS::EC2::LaunchTemplate | x | x | x | - [x] LatestVersionNumber | + |AWS::EC2::LaunchTemplate | x | x | | - [x] LatestVersionNumber | +---------------------------------------+--------+--------+--------+ - [x] LaunchTemplateId | | | | | | - [x] DefaultVersionNumber | +---------------------------------------+--------+--------+--------+----------------------------------------+ diff --git a/docs/docs/services/servicediscovery.rst b/docs/docs/services/servicediscovery.rst index 489b459d1c37..604be7a04236 100644 --- a/docs/docs/services/servicediscovery.rst +++ b/docs/docs/services/servicediscovery.rst @@ -22,15 +22,15 @@ servicediscovery - [X] create_service - [X] delete_namespace - [X] delete_service -- [ ] deregister_instance -- [ ] discover_instances -- [ ] discover_instances_revision -- [ ] get_instance -- [ ] get_instances_health_status +- [X] deregister_instance +- [X] discover_instances +- [X] discover_instances_revision +- [X] get_instance +- [X] get_instances_health_status - [X] get_namespace - [X] get_operation - [X] get_service -- [ ] list_instances +- [X] list_instances - [X] list_namespaces Pagination or the Filters-parameter is not yet implemented @@ -47,11 +47,11 @@ servicediscovery - [X] list_tags_for_resource -- [ ] register_instance +- [X] register_instance - [X] tag_resource - [X] untag_resource -- [ ] update_http_namespace -- [ ] update_instance_custom_health_status +- [X] update_http_namespace +- [X] update_instance_custom_health_status - [X] update_private_dns_namespace - [X] update_public_dns_namespace - [X] update_service diff --git a/moto/backend_index.py b/moto/backend_index.py index 56ad29caff15..6b63bea9322c 100644 --- a/moto/backend_index.py +++ b/moto/backend_index.py @@ -175,7 +175,7 @@ ("secretsmanager", re.compile("https?://secretsmanager\\.(.+)\\.amazonaws\\.com")), ( "servicediscovery", - re.compile("https?://servicediscovery\\.(.+)\\.amazonaws\\.com"), + re.compile("https?://(data-)?servicediscovery\\.(.+)\\.amazonaws\\.com"), ), ("servicequotas", re.compile("https?://servicequotas\\.(.+)\\.amazonaws\\.com")), ("ses", re.compile("https?://email\\.(.+)\\.amazonaws\\.com")), diff --git a/moto/servicediscovery/exceptions.py b/moto/servicediscovery/exceptions.py index 979aca4e939f..38da40a2048b 100644 --- a/moto/servicediscovery/exceptions.py +++ b/moto/servicediscovery/exceptions.py @@ -18,6 +18,21 @@ def __init__(self, ns_id: str): super().__init__("ServiceNotFound", f"{ns_id}") +class InstanceNotFound(JsonRESTError): + def __init__(self, ns_id: str): + super().__init__("InstanceNotFound", f"{ns_id}") + + class ConflictingDomainExists(JsonRESTError): def __init__(self, vpc_id: str): super().__init__("ConflictingDomainExists", f"{vpc_id}") + + +class CustomHealthNotFound(JsonRESTError): + def __init__(self, ns_id: str): + super().__init__("CustomHealthNotFound", f"{ns_id}") + + +class InvalidInput(JsonRESTError): + def __init__(self, message: str): + super().__init__("InvalidInput", message) diff --git a/moto/servicediscovery/models.py b/moto/servicediscovery/models.py index 444c9a6f8447..fbf5e7557437 100644 --- a/moto/servicediscovery/models.py +++ b/moto/servicediscovery/models.py @@ -1,5 +1,5 @@ import string -from typing import Any, Dict, Iterable, List, Optional +from typing import Any, Dict, Iterable, List, Optional, Tuple from moto.core.base_backend import BackendDict, BaseBackend from moto.core.common_models import BaseModel @@ -10,6 +10,9 @@ from .exceptions import ( ConflictingDomainExists, + CustomHealthNotFound, + InstanceNotFound, + InvalidInput, NamespaceNotFound, OperationNotFound, ServiceNotFound, @@ -89,6 +92,8 @@ def __init__( self.health_check_custom_config = health_check_custom_config self.service_type = service_type self.created = unix_time() + self.instances: List[ServiceInstance] = [] + self.instances_revision: Dict[str, int] = {} def update(self, details: Dict[str, Any]) -> None: if "Description" in details: @@ -121,6 +126,30 @@ def to_json(self) -> Dict[str, Any]: } +class ServiceInstance(BaseModel): + def __init__( + self, + service_id: str, + instance_id: str, + creator_request_id: Optional[str] = None, + attributes: Optional[Dict[str, str]] = None, + ): + self.service_id = service_id + self.instance_id = instance_id + self.attributes = attributes if attributes else {} + self.creator_request_id = ( + creator_request_id if creator_request_id else random_id(32) + ) + self.health_status = "HEALTHY" + + def to_json(self) -> Dict[str, Any]: + return { + "Id": self.instance_id, + "CreatorRequestId": self.creator_request_id, + "Attributes": self.attributes, + } + + class Operation(BaseModel): def __init__(self, operation_type: str, targets: Dict[str, str]): super().__init__() @@ -159,7 +188,7 @@ def list_namespaces(self) -> Iterable[Namespace]: """ Pagination or the Filters-parameter is not yet implemented """ - return self.namespaces.values() + return list(self.namespaces.values()) def create_http_namespace( self, @@ -345,6 +374,30 @@ def update_service(self, service_id: str, details: Dict[str, Any]) -> str: ) return operation_id + def update_http_namespace( + self, + _id: str, + namespace_dict: Dict[str, Any], + updater_request_id: Optional[str] = None, + ) -> str: + if "Description" not in namespace_dict: + raise InvalidInput("Description is required") + + namespace = self.get_namespace(namespace_id=_id) + if updater_request_id is None: + # Unused as the operation cannot fail + updater_request_id = random_id(32) + namespace.description = namespace_dict["Description"] + if "Properties" in namespace_dict: + if "HttpProperties" in namespace_dict["Properties"]: + namespace.http_properties = namespace_dict["Properties"][ + "HttpProperties" + ] + operation_id = self._create_operation( + "UPDATE_NAMESPACE", targets={"NAMESPACE": namespace.id} + ) + return operation_id + def update_private_dns_namespace( self, _id: str, description: str, properties: Dict[str, Any] ) -> str: @@ -371,5 +424,212 @@ def update_public_dns_namespace( ) return operation_id + def register_instance( + self, + service_id: str, + instance_id: str, + creator_request_id: str, + attributes: Dict[str, str], + ) -> str: + service = self.get_service(service_id) + instance = ServiceInstance( + service_id=service_id, + instance_id=instance_id, + creator_request_id=creator_request_id, + attributes=attributes, + ) + service.instances.append(instance) + service.instances_revision[instance_id] = ( + service.instances_revision.get(instance_id, 0) + 1 + ) + operation_id = self._create_operation( + "REGISTER_INSTANCE", targets={"INSTANCE": instance_id} + ) + return operation_id + + def deregister_instance(self, service_id: str, instance_id: str) -> str: + service = self.get_service(service_id) + i = 0 + while i < len(service.instances): + instance = service.instances[i] + if instance.instance_id == instance_id: + service.instances.remove(instance) + service.instances_revision[instance_id] = ( + service.instances_revision.get(instance_id, 0) + 1 + ) + operation_id = self._create_operation( + "DEREGISTER_INSTANCE", targets={"INSTANCE": instance_id} + ) + return operation_id + i += 1 + raise InstanceNotFound(instance_id) + + def list_instances(self, service_id: str) -> List[ServiceInstance]: + service = self.get_service(service_id) + return service.instances + + def get_instance(self, service_id: str, instance_id: str) -> ServiceInstance: + for instance in self.list_instances(service_id): + if instance.instance_id == instance_id: + return instance + raise InstanceNotFound(instance_id) + + def get_instances_health_status( + self, + service_id: str, + instances: Optional[List[str]] = None, + ) -> List[Tuple[str, str]]: + service = self.get_service(service_id) + status = [] + if instances is None: + instances = [instance.instance_id for instance in service.instances] + if not isinstance(instances, list): + raise InvalidInput("Instances must be a list") + filtered_instances = [ + instance + for instance in service.instances + if instance.instance_id in instances + ] + for instance in filtered_instances: + status.append((instance.instance_id, instance.health_status)) + return status + + def update_instance_custom_health_status( + self, service_id: str, instance_id: str, status: str + ) -> None: + if status not in ["HEALTHY", "UNHEALTHY"]: + raise CustomHealthNotFound(service_id) + instance = self.get_instance(service_id, instance_id) + instance.health_status = status + + def _filter_instances( + self, + instances: List[ServiceInstance], + query_parameters: Optional[Dict[str, str]] = None, + optional_parameters: Optional[Dict[str, str]] = None, + health_status: Optional[str] = None, + ) -> List[ServiceInstance]: + if query_parameters is None: + query_parameters = {} + if optional_parameters is None: + optional_parameters = {} + if health_status is None: + health_status = "ALL" + + filtered_instances = [] + has_healthy = False + for instance in instances: + # Filter out instances with mismatching health status + if ( + health_status not in ["ALL", "HEALTHY_OR_ELSE_ALL"] + and instance.health_status != health_status + ): + continue + # Record if there is at least one healthy instance for HEALTHY_OR_ELSE_ALL + if instance.health_status == "HEALTHY": + has_healthy = True + # Filter out instances with mismatching query parameters + matches_query = True + for param in query_parameters: + if instance.attributes.get(param) != query_parameters[param]: + matches_query = False + break + if not matches_query: + continue + # Add instance to the list if it passed all filters + filtered_instances.append(instance) + # Handle HEALTHY_OR_ELSE_ALL + if has_healthy and health_status == "HEALTHY_OR_ELSE_ALL": + filtered_instances = [ + instance + for instance in filtered_instances + if instance.health_status == "HEALTHY" + ] + # Filter out instances with mismatching optional parameters + opt_filtered_instances = [] + for instance in filtered_instances: + matches_optional = True + for param in optional_parameters: + if instance.attributes.get(param) != optional_parameters[param]: + matches_optional = False + break + if matches_optional: + opt_filtered_instances.append(instance) + # If no instances passed the optional parameters, return the original filtered list + return opt_filtered_instances if opt_filtered_instances else filtered_instances + + def discover_instances( + self, + namespace_name: str, + service_name: str, + query_parameters: Optional[Dict[str, str]] = None, + optional_parameters: Optional[Dict[str, str]] = None, + health_status: Optional[str] = None, + ) -> Tuple[List[ServiceInstance], Dict[str, int]]: + if query_parameters is None: + query_parameters = {} + if optional_parameters is None: + optional_parameters = {} + if health_status is None: + health_status = "ALL" + if health_status not in ["HEALTHY", "UNHEALTHY", "ALL", "HEALTHY_OR_ELSE_ALL"]: + raise InvalidInput("Invalid health status") + try: + namespace = [ + ns for ns in self.list_namespaces() if ns.name == namespace_name + ][0] + except IndexError: + raise NamespaceNotFound(namespace_name) + try: + service = [ + srv + for srv in self.list_services() + if srv.name == service_name and srv.namespace_id == namespace.id + ][0] + except IndexError: + raise ServiceNotFound(service_name) + instances = self.list_instances(service.id) + # Filter instances based on query parameters, optional parameters, and health status + final_instances = self._filter_instances( + instances, query_parameters, optional_parameters, health_status + ) + # Get the revision number for each instance that passed the filters + instance_revisions = { + instance.instance_id: service.instances_revision.get( + instance.instance_id, 0 + ) + for instance in final_instances + } + return final_instances, instance_revisions + + def discover_instances_revision( + self, namespace_name: str, service_name: str + ) -> int: + return sum(self.discover_instances(namespace_name, service_name)[1].values()) + + def paginate( + self, + items: List[Any], + max_results: Optional[int] = None, + next_token: Optional[str] = None, + ) -> Tuple[List[Any], Optional[str]]: + """ + Paginates a list of items. If called without optional parameters, the entire list is returned as-is. + """ + # Default to beginning of list + if next_token is None: + next_token = "0" + # Return empty list if next_token is invalid + if not next_token.isdigit(): + return [], None + # Default to the entire list + if max_results is None: + max_results = len(items) + new_token = int(next_token) + max_results + # If the new token overflows the list, return the rest of the list + if new_token >= len(items): + return items[int(next_token) :], None + return items[int(next_token) : new_token], str(new_token) + servicediscovery_backends = BackendDict(ServiceDiscoveryBackend, "servicediscovery") diff --git a/moto/servicediscovery/responses.py b/moto/servicediscovery/responses.py index 54afb29b5869..8e277c9d7a72 100644 --- a/moto/servicediscovery/responses.py +++ b/moto/servicediscovery/responses.py @@ -1,6 +1,7 @@ """Handles incoming servicediscovery requests, invokes methods, returns responses.""" import json +from typing import Any, Dict, List from moto.core.common_types import TYPE_RESPONSE from moto.core.responses import BaseResponse @@ -174,6 +175,18 @@ def update_service(self) -> str: ) return json.dumps(dict(OperationId=operation_id)) + def update_http_namespace(self) -> str: + params = json.loads(self.body) + _id = params.get("Id") + updater_request_id = params.get("UpdaterRequestId") + namespace = params.get("Namespace") + operation_id = self.servicediscovery_backend.update_http_namespace( + _id=_id, + updater_request_id=updater_request_id, + namespace_dict=namespace, + ) + return json.dumps(dict(operationId=operation_id)) + def update_private_dns_namespace(self) -> str: params = json.loads(self.body) _id = params.get("Id") @@ -197,3 +210,135 @@ def update_public_dns_namespace(self) -> str: properties=properties, ) return json.dumps(dict(OperationId=operation_id)) + + def register_instance(self) -> str: + params = json.loads(self.body) + service_id = params.get("ServiceId") + instance_id = params.get("InstanceId") + creator_request_id = params.get("CreatorRequestId") + attributes = params.get("Attributes") + operation_id = self.servicediscovery_backend.register_instance( + service_id=service_id, + instance_id=instance_id, + creator_request_id=creator_request_id, + attributes=attributes, + ) + return json.dumps(dict(OperationId=operation_id)) + + def deregister_instance(self) -> str: + params = json.loads(self.body) + service_id = params.get("ServiceId") + instance_id = params.get("InstanceId") + operation_id = self.servicediscovery_backend.deregister_instance( + service_id=service_id, + instance_id=instance_id, + ) + return json.dumps(dict(OperationId=operation_id)) + + def get_instance(self) -> str: + params = json.loads(self.body) + service_id = params.get("ServiceId") + instance_id = params.get("InstanceId") + instance = self.servicediscovery_backend.get_instance( + service_id=service_id, + instance_id=instance_id, + ) + return json.dumps(dict(Instance=instance.to_json())) + + def get_instances_health_status(self) -> str: + params = json.loads(self.body) + service_id = params.get("ServiceId") + instances = params.get("Instances") + max_results = params.get("MaxResults") + next_token = params.get("NextToken") + status_records = self.servicediscovery_backend.get_instances_health_status( + service_id=service_id, + instances=instances, + ) + page, new_token = self.servicediscovery_backend.paginate( + status_records, max_results=max_results, next_token=next_token + ) + result: Dict[str, Any] = {"Status": {}} + for record in page: + result["Status"][record[0]] = record[1] + if new_token: + result["NextToken"] = new_token + return json.dumps(result) + + def update_instance_custom_health_status(self) -> str: + params = json.loads(self.body) + service_id = params.get("ServiceId") + instance_id = params.get("InstanceId") + status = params.get("Status") + self.servicediscovery_backend.update_instance_custom_health_status( + service_id=service_id, + instance_id=instance_id, + status=status, + ) + return "{}" + + def list_instances(self) -> str: + params = json.loads(self.body) + service_id = params.get("ServiceId") + next_token = params.get("NextToken") + max_results = params.get("MaxResults") + instances = self.servicediscovery_backend.list_instances(service_id=service_id) + page, new_token = self.servicediscovery_backend.paginate( + instances, max_results=max_results, next_token=next_token + ) + result: Dict[str, Any] = {"Instances": []} + for instance in page: + result["Instances"].append(instance.to_json()) + if new_token: + result["NextToken"] = new_token + return json.dumps(result) + + def discover_instances(self) -> str: + params = json.loads(self.body) + namespace_name = params.get("NamespaceName") + service_name = params.get("ServiceName") + max_results = params.get("MaxResults") + query_parameters = params.get("QueryParameters") + optional_parameters = params.get("OptionalParameters") + health_status = params.get("HealthStatus") + instances, instances_revision = ( + self.servicediscovery_backend.discover_instances( + namespace_name=namespace_name, + service_name=service_name, + query_parameters=query_parameters, + optional_parameters=optional_parameters, + health_status=health_status, + ) + ) + page, new_token = self.servicediscovery_backend.paginate( + instances, max_results=max_results + ) + result_instances: List[Dict[str, Any]] = [] + instances_revision_total = 0 + for instance in page: + result_instances.append( + { + "InstanceId": instance.instance_id, + "NamespaceName": namespace_name, + "ServiceName": service_name, + "Attributes": instance.attributes, + "HealthStatus": instance.health_status, + } + ) + instances_revision_total += instances_revision[instance.instance_id] + return json.dumps( + { + "Instances": result_instances, + "InstancesRevision": instances_revision_total, + } + ) + + def discover_instances_revision(self) -> str: + params = json.loads(self.body) + namespace_name = params.get("NamespaceName") + service_name = params.get("ServiceName") + instances_revision = self.servicediscovery_backend.discover_instances_revision( + namespace_name=namespace_name, + service_name=service_name, + ) + return json.dumps(dict(InstancesRevision=instances_revision)) diff --git a/moto/servicediscovery/urls.py b/moto/servicediscovery/urls.py index 5da02f027b60..62e4d2ad278a 100644 --- a/moto/servicediscovery/urls.py +++ b/moto/servicediscovery/urls.py @@ -3,7 +3,7 @@ from .responses import ServiceDiscoveryResponse url_bases = [ - r"https?://servicediscovery\.(.+)\.amazonaws\.com", + r"https?://(data-)?servicediscovery\.(.+)\.amazonaws\.com", ] diff --git a/tests/test_servicediscovery/test_servicediscovery_httpnamespaces.py b/tests/test_servicediscovery/test_servicediscovery_httpnamespaces.py index aa4fc72526f8..8e00d2aba528 100644 --- a/tests/test_servicediscovery/test_servicediscovery_httpnamespaces.py +++ b/tests/test_servicediscovery/test_servicediscovery_httpnamespaces.py @@ -290,3 +290,23 @@ def test_update_public_dns_namespace(): dns_props = namespace["Properties"]["DnsProperties"] assert dns_props == {"SOA": {"TTL": 987}} + + +@mock_aws +def test_update_http_namespace(): + client = boto3.client("servicediscovery", region_name="us-east-2") + client.create_http_namespace( + Name="mynamespace", CreatorRequestId="crid", Description="mu fancy namespace" + ) + + ns_id = client.list_namespaces()["Namespaces"][0]["Id"] + + client.update_http_namespace( + Id=ns_id, + Namespace={ + "Description": "updated http", + }, + ) + + namespace = client.get_namespace(Id=ns_id)["Namespace"] + assert namespace["Description"] == "updated http" diff --git a/tests/test_servicediscovery/test_servicediscovery_instance.py b/tests/test_servicediscovery/test_servicediscovery_instance.py new file mode 100644 index 000000000000..dbb39a163fa5 --- /dev/null +++ b/tests/test_servicediscovery/test_servicediscovery_instance.py @@ -0,0 +1,423 @@ +from unittest import SkipTest + +import boto3 +import pytest +from botocore.exceptions import ClientError + +from moto import mock_aws, settings + + +@pytest.fixture(name="client") +def client_fixture(): + with mock_aws(): + yield boto3.client("servicediscovery", region_name="eu-west-1") + + +@pytest.fixture(name="ns_resp") +def ns_resp_fixture(client): + client.create_http_namespace(Name="mynamespace") + namespace = [ + ns + for ns in client.list_namespaces()["Namespaces"] + if ns["Name"] == "mynamespace" + ][0] + return dict(Namespace=namespace) + + +@pytest.fixture(name="srv_resp") +def srv_resp_fixture(client, ns_resp): + return client.create_service( + Name="myservice", + NamespaceId=ns_resp["Namespace"]["Id"], + DnsConfig={"DnsRecords": [{"Type": "A", "TTL": 60}]}, + ) + + +@mock_aws +def test_register_instance(client, ns_resp, srv_resp): + instance_id = "i-123" + creator_request_id = "crid" + attributes = {"attr1": "value1"} + inst_resp = client.register_instance( + ServiceId=srv_resp["Service"]["Id"], + InstanceId=instance_id, + CreatorRequestId=creator_request_id, + Attributes=attributes, + ) + + assert "OperationId" in inst_resp + + operation = client.get_operation(OperationId=inst_resp["OperationId"]) + assert operation["Operation"]["Targets"]["INSTANCE"] == instance_id + + instance = client.get_instance( + ServiceId=srv_resp["Service"]["Id"], InstanceId=instance_id + ) + assert instance["Instance"]["CreatorRequestId"] == creator_request_id + assert instance["Instance"]["Attributes"] == attributes + assert instance["Instance"]["Id"] == instance_id + + +@mock_aws +def test_deregister_instance(client, ns_resp, srv_resp): + instance_id = "i-123" + creator_request_id = "crid" + attributes = {"attr1": "value1"} + client.register_instance( + ServiceId=srv_resp["Service"]["Id"], + InstanceId=instance_id, + CreatorRequestId=creator_request_id, + Attributes=attributes, + ) + + dereg_resp = client.deregister_instance( + ServiceId=srv_resp["Service"]["Id"], InstanceId=instance_id + ) + assert "OperationId" in dereg_resp + + operation = client.get_operation(OperationId=dereg_resp["OperationId"]) + assert operation["Operation"]["Targets"]["INSTANCE"] == instance_id + + with pytest.raises(ClientError) as exc: + client.get_instance(ServiceId=srv_resp["Service"]["Id"], InstanceId=instance_id) + assert exc.value.response["Error"]["Code"] == "InstanceNotFound" + assert exc.value.response["Error"]["Message"] == instance_id + + +@mock_aws +def test_get_instance(client, ns_resp, srv_resp): + instance_id = "i-123" + creator_request_id = "crid" + attributes = {"attr1": "value1"} + client.register_instance( + ServiceId=srv_resp["Service"]["Id"], + InstanceId=instance_id, + CreatorRequestId=creator_request_id, + Attributes=attributes, + ) + + instance = client.get_instance( + ServiceId=srv_resp["Service"]["Id"], InstanceId=instance_id + ) + assert "Instance" in instance + assert instance["Instance"]["CreatorRequestId"] == creator_request_id + assert instance["Instance"]["Attributes"] == attributes + assert instance["Instance"]["Id"] == instance_id + + +@mock_aws +def test_get_unknown_instance(client, srv_resp): + with pytest.raises(ClientError) as exc: + client.get_instance(ServiceId=srv_resp["Service"]["Id"], InstanceId="unknown") + err = exc.value.response["Error"] + assert err["Code"] == "InstanceNotFound" + assert err["Message"] == "unknown" + + +@mock_aws +def test_list_instances(client, ns_resp, srv_resp): + instance_ids = ["i-123", "i-456", "i-789", "i-012"] + for instance_id in instance_ids: + client.register_instance( + ServiceId=srv_resp["Service"]["Id"], InstanceId=instance_id, Attributes={} + ) + + instances = client.list_instances(ServiceId=srv_resp["Service"]["Id"]) + assert len(instances["Instances"]) == 4 + assert set(inst["Id"] for inst in instances["Instances"]) == set(instance_ids) + + +@mock_aws +def test_paginate_list_instances(client, ns_resp, srv_resp): + instance_ids = ["i-123", "i-456", "i-789", "i-012"] + for instance_id in instance_ids: + client.register_instance( + ServiceId=srv_resp["Service"]["Id"], InstanceId=instance_id, Attributes={} + ) + + instances = client.list_instances(ServiceId=srv_resp["Service"]["Id"], MaxResults=2) + assert len(instances["Instances"]) == 2 + assert "NextToken" in instances + assert set(inst["Id"] for inst in instances["Instances"]) == set(instance_ids[:2]) + + instances = client.list_instances( + ServiceId=srv_resp["Service"]["Id"], NextToken=instances["NextToken"] + ) + assert len(instances["Instances"]) == 2 + assert "NextToken" not in instances + assert set(inst["Id"] for inst in instances["Instances"]) == set(instance_ids[2:]) + + +@mock_aws +def test_get_instances_health_status(client, ns_resp, srv_resp): + instance_ids = ["i-123", "i-456", "i-789", "i-012"] + for instance_id in instance_ids: + client.register_instance( + ServiceId=srv_resp["Service"]["Id"], InstanceId=instance_id, Attributes={} + ) + + health_status = client.get_instances_health_status( + ServiceId=srv_resp["Service"]["Id"], Instances=instance_ids + ) + assert len(health_status["Status"]) == 4 + for inst_id in instance_ids: + assert health_status["Status"][inst_id] == "HEALTHY" + + +@mock_aws +def test_update_instance_custom_health_status(client, ns_resp, srv_resp): + instance_id = "i-123" + client.register_instance( + ServiceId=srv_resp["Service"]["Id"], InstanceId=instance_id, Attributes={} + ) + + client.update_instance_custom_health_status( + ServiceId=srv_resp["Service"]["Id"], InstanceId=instance_id, Status="UNHEALTHY" + ) + + health_status = client.get_instances_health_status( + ServiceId=srv_resp["Service"]["Id"], Instances=[instance_id] + ) + assert health_status["Status"][instance_id] == "UNHEALTHY" + + +@mock_aws +def test_discover_instances_formatting(client, ns_resp, srv_resp): + if not settings.TEST_DECORATOR_MODE: + raise SkipTest( + "Endpoint for discovering instances is prefixed with 'data-', and we can't intercept calls to 'data-localhost'" + ) + + attr_dict = {"attr1": "value1", "attr2": "value1"} + client.register_instance( + ServiceId=srv_resp["Service"]["Id"], InstanceId="i-123", Attributes=attr_dict + ) + + instances = client.discover_instances( + NamespaceName=ns_resp["Namespace"]["Name"], + ServiceName=srv_resp["Service"]["Name"], + MaxResults=2, + ) + + assert len(instances["Instances"]) == 1 + assert instances["Instances"][0]["InstanceId"] == "i-123" + assert instances["Instances"][0]["NamespaceName"] == ns_resp["Namespace"]["Name"] + assert instances["Instances"][0]["ServiceName"] == srv_resp["Service"]["Name"] + assert instances["Instances"][0]["Attributes"] == attr_dict + assert instances["Instances"][0]["HealthStatus"] == "HEALTHY" + assert instances["InstancesRevision"] == 1 + + +@mock_aws +def test_discover_instances_attr_filters(client, ns_resp, srv_resp): + if not settings.TEST_DECORATOR_MODE: + raise SkipTest( + "Endpoint for discovering instances is prefixed with 'data-', and we can't intercept calls to 'data-localhost'" + ) + + instance_dicts = [ + {"id": "i-123", "attributes": {"attr1": "value1", "attr2": "value1"}}, + {"id": "i-456", "attributes": {"attr1": "value2"}}, + {"id": "i-789", "attributes": {"attr1": "value3", "attr2": "value1"}}, + {"id": "i-012", "attributes": {"attr1": "value3"}}, + ] + for inst_dict in instance_dicts: + client.register_instance( + ServiceId=srv_resp["Service"]["Id"], + InstanceId=inst_dict["id"], + Attributes=inst_dict["attributes"], + ) + + instances = client.discover_instances( + NamespaceName=ns_resp["Namespace"]["Name"], + ServiceName=srv_resp["Service"]["Name"], + ) + assert len(instances["Instances"]) == 4 + assert set(inst["InstanceId"] for inst in instances["Instances"]) == { + "i-123", + "i-456", + "i-789", + "i-012", + } + + instances = client.discover_instances( + NamespaceName=ns_resp["Namespace"]["Name"], + ServiceName=srv_resp["Service"]["Name"], + QueryParameters={"attr1": "value3"}, + ) + assert len(instances["Instances"]) == 2 + assert set(inst["InstanceId"] for inst in instances["Instances"]) == { + "i-789", + "i-012", + } + + instances = client.discover_instances( + NamespaceName=ns_resp["Namespace"]["Name"], + ServiceName=srv_resp["Service"]["Name"], + QueryParameters={"attr1": "value3"}, + OptionalParameters={"attr2": "value1"}, + ) + assert len(instances["Instances"]) == 1 + assert instances["Instances"][0]["InstanceId"] == "i-789" + + instances = client.discover_instances( + NamespaceName=ns_resp["Namespace"]["Name"], + ServiceName=srv_resp["Service"]["Name"], + QueryParameters={"attr1": "value3"}, + OptionalParameters={"attr2": "value2"}, + ) + assert len(instances["Instances"]) == 2 + assert set(inst["InstanceId"] for inst in instances["Instances"]) == { + "i-789", + "i-012", + } + + +@mock_aws +def test_discover_instances_health_filters(client, ns_resp, srv_resp): + if not settings.TEST_DECORATOR_MODE: + raise SkipTest( + "Endpoint for discovering instances is prefixed with 'data-', and we can't intercept calls to 'data-localhost'" + ) + + instance_dicts = [ + {"id": "i-123"}, + {"id": "i-456"}, + {"id": "i-789", "health": "UNHEALTHY"}, + {"id": "i-012", "health": "UNHEALTHY"}, + ] + for inst_dict in instance_dicts: + client.register_instance( + ServiceId=srv_resp["Service"]["Id"], + InstanceId=inst_dict["id"], + Attributes={}, + ) + if "health" in inst_dict: + client.update_instance_custom_health_status( + ServiceId=srv_resp["Service"]["Id"], + InstanceId=inst_dict["id"], + Status=inst_dict["health"], + ) + + instances = client.discover_instances( + NamespaceName=ns_resp["Namespace"]["Name"], + ServiceName=srv_resp["Service"]["Name"], + HealthStatus="ALL", + ) + assert len(instances["Instances"]) == 4 + assert set(inst["InstanceId"] for inst in instances["Instances"]) == { + "i-123", + "i-456", + "i-789", + "i-012", + } + + instances = client.discover_instances( + NamespaceName=ns_resp["Namespace"]["Name"], + ServiceName=srv_resp["Service"]["Name"], + HealthStatus="UNHEALTHY", + ) + assert len(instances["Instances"]) == 2 + assert set(inst["InstanceId"] for inst in instances["Instances"]) == { + "i-789", + "i-012", + } + + instances = client.discover_instances( + NamespaceName=ns_resp["Namespace"]["Name"], + ServiceName=srv_resp["Service"]["Name"], + HealthStatus="HEALTHY", + ) + assert len(instances["Instances"]) == 2 + assert set(inst["InstanceId"] for inst in instances["Instances"]) == { + "i-123", + "i-456", + } + + instances = client.discover_instances( + NamespaceName=ns_resp["Namespace"]["Name"], + ServiceName=srv_resp["Service"]["Name"], + HealthStatus="HEALTHY_OR_ELSE_ALL", + ) + assert len(instances["Instances"]) == 2 + assert set(inst["InstanceId"] for inst in instances["Instances"]) == { + "i-123", + "i-456", + } + + client.update_instance_custom_health_status( + ServiceId=srv_resp["Service"]["Id"], + InstanceId="i-123", + Status="UNHEALTHY", + ) + client.update_instance_custom_health_status( + ServiceId=srv_resp["Service"]["Id"], + InstanceId="i-456", + Status="UNHEALTHY", + ) + instances = client.discover_instances( + NamespaceName=ns_resp["Namespace"]["Name"], + ServiceName=srv_resp["Service"]["Name"], + HealthStatus="HEALTHY_OR_ELSE_ALL", + ) + assert len(instances["Instances"]) == 4 + assert set(inst["InstanceId"] for inst in instances["Instances"]) == { + "i-123", + "i-456", + "i-789", + "i-012", + } + + +@mock_aws +def test_max_results_discover_instances(client, ns_resp, srv_resp): + if not settings.TEST_DECORATOR_MODE: + raise SkipTest( + "Endpoint for discovering instances is prefixed with 'data-', and we can't intercept calls to 'data-localhost'" + ) + + instance_ids = ["i-123", "i-456", "i-789", "i-012"] + for instance_id in instance_ids: + client.register_instance( + ServiceId=srv_resp["Service"]["Id"], InstanceId=instance_id, Attributes={} + ) + + instances = client.discover_instances( + NamespaceName=ns_resp["Namespace"]["Name"], + ServiceName=srv_resp["Service"]["Name"], + MaxResults=2, + ) + assert len(instances["Instances"]) == 2 + assert set(inst["InstanceId"] for inst in instances["Instances"]) == set( + instance_ids[:2] + ) + + +@mock_aws +def test_discover_instances_revision(client, ns_resp, srv_resp): + if not settings.TEST_DECORATOR_MODE: + raise SkipTest( + "Endpoint for discovering instances is prefixed with 'data-', and we can't intercept calls to 'data-localhost'" + ) + + instance_ids = ["i-123", "i-456", "i-789", "i-012"] + for instance_id in instance_ids: + client.register_instance( + ServiceId=srv_resp["Service"]["Id"], InstanceId=instance_id, Attributes={} + ) + + revisions = client.discover_instances_revision( + NamespaceName=ns_resp["Namespace"]["Name"], + ServiceName=srv_resp["Service"]["Name"], + ) + assert revisions["InstancesRevision"] == 4 + + client.deregister_instance(ServiceId=srv_resp["Service"]["Id"], InstanceId="i-123") + client.register_instance( + ServiceId=srv_resp["Service"]["Id"], InstanceId="i-123", Attributes={} + ) + revisions = client.discover_instances_revision( + NamespaceName=ns_resp["Namespace"]["Name"], + ServiceName=srv_resp["Service"]["Name"], + ) + assert revisions["InstancesRevision"] == 6