diff --git a/IMPLEMENTATION_COVERAGE.md b/IMPLEMENTATION_COVERAGE.md index 946ad7970..06de16edc 100644 --- a/IMPLEMENTATION_COVERAGE.md +++ b/IMPLEMENTATION_COVERAGE.md @@ -4810,6 +4810,38 @@ - [ ] validate_resource_policy +## servicediscovery +
+61% implemented + +- [X] create_http_namespace +- [X] create_private_dns_namespace +- [X] create_public_dns_namespace +- [X] create_service +- [X] delete_namespace +- [X] delete_service +- [ ] deregister_instance +- [ ] discover_instances +- [ ] get_instance +- [ ] get_instances_health_status +- [X] get_namespace +- [X] get_operation +- [X] get_service +- [ ] list_instances +- [X] list_namespaces +- [X] list_operations +- [X] list_services +- [X] list_tags_for_resource +- [ ] register_instance +- [X] tag_resource +- [X] untag_resource +- [ ] update_http_namespace +- [ ] update_instance_custom_health_status +- [ ] update_private_dns_namespace +- [ ] update_public_dns_namespace +- [X] update_service +
+ ## ses
32% implemented @@ -5544,7 +5576,6 @@ - service-quotas - servicecatalog - servicecatalog-appregistry -- servicediscovery - sesv2 - shield - signer diff --git a/docs/docs/services/servicediscovery.rst b/docs/docs/services/servicediscovery.rst new file mode 100644 index 000000000..196dcc94a --- /dev/null +++ b/docs/docs/services/servicediscovery.rst @@ -0,0 +1,68 @@ +.. _implementedservice_servicediscovery: + +.. |start-h3| raw:: html + +

+ +.. |end-h3| raw:: html + +

+ +================ +servicediscovery +================ + +.. autoclass:: moto.servicediscovery.models.ServiceDiscoveryBackend + +|start-h3| Example usage |end-h3| + +.. sourcecode:: python + + @mock_servicediscovery + def test_servicediscovery_behaviour: + boto3.client("servicediscovery") + ... + + + +|start-h3| Implemented features for this service |end-h3| + +- [X] create_http_namespace +- [X] create_private_dns_namespace +- [X] create_public_dns_namespace +- [X] create_service +- [X] delete_namespace +- [X] delete_service +- [ ] deregister_instance +- [ ] discover_instances +- [ ] get_instance +- [ ] get_instances_health_status +- [X] get_namespace +- [X] get_operation +- [X] get_service +- [ ] list_instances +- [X] list_namespaces + + Pagination or the Filters-parameter is not yet implemented + + +- [X] list_operations + + Pagination or the Filters-argument is not yet implemented + + +- [X] list_services + + Pagination or the Filters-argument is not yet implemented + + +- [X] list_tags_for_resource +- [ ] register_instance +- [X] tag_resource +- [X] untag_resource +- [ ] update_http_namespace +- [ ] update_instance_custom_health_status +- [ ] update_private_dns_namespace +- [ ] update_public_dns_namespace +- [X] update_service + diff --git a/moto/__init__.py b/moto/__init__.py index 3178dc64c..00036dbaa 100644 --- a/moto/__init__.py +++ b/moto/__init__.py @@ -130,6 +130,7 @@ mock_sagemaker = lazy_load(".sagemaker", "mock_sagemaker") mock_sdb = lazy_load(".sdb", "mock_sdb") mock_secretsmanager = lazy_load(".secretsmanager", "mock_secretsmanager") mock_ses = lazy_load(".ses", "mock_ses") +mock_servicediscovery = lazy_load(".servicediscovery", "mock_servicediscovery") mock_sns = lazy_load(".sns", "mock_sns") mock_sqs = lazy_load(".sqs", "mock_sqs") mock_ssm = lazy_load(".ssm", "mock_ssm") diff --git a/moto/backend_index.py b/moto/backend_index.py index f58ad44d5..7a60336e3 100644 --- a/moto/backend_index.py +++ b/moto/backend_index.py @@ -124,6 +124,10 @@ backend_url_patterns = [ ("sagemaker", re.compile("https?://api.sagemaker\\.(.+)\\.amazonaws.com")), ("sdb", re.compile("https?://sdb\\.(.+)\\.amazonaws\\.com")), ("secretsmanager", re.compile("https?://secretsmanager\\.(.+)\\.amazonaws\\.com")), + ( + "servicediscovery", + re.compile("https?://servicediscovery\\.(.+)\\.amazonaws\\.com"), + ), ("ses", re.compile("https?://email\\.(.+)\\.amazonaws\\.com")), ("ses", re.compile("https?://ses\\.(.+)\\.amazonaws\\.com")), ("sns", re.compile("https?://sns\\.(.+)\\.amazonaws\\.com")), diff --git a/moto/servicediscovery/__init__.py b/moto/servicediscovery/__init__.py new file mode 100644 index 000000000..983db1ab4 --- /dev/null +++ b/moto/servicediscovery/__init__.py @@ -0,0 +1,5 @@ +"""servicediscovery module initialization; sets value for base decorator.""" +from .models import servicediscovery_backends +from ..core.models import base_decorator + +mock_servicediscovery = base_decorator(servicediscovery_backends) diff --git a/moto/servicediscovery/exceptions.py b/moto/servicediscovery/exceptions.py new file mode 100644 index 000000000..f816fcc2c --- /dev/null +++ b/moto/servicediscovery/exceptions.py @@ -0,0 +1,22 @@ +"""Exceptions raised by the servicediscovery service.""" +from moto.core.exceptions import JsonRESTError + + +class OperationNotFound(JsonRESTError): + def __init__(self): + super().__init__("OperationNotFound", "") + + +class NamespaceNotFound(JsonRESTError): + def __init__(self, ns_id): + super().__init__("NamespaceNotFound", f"{ns_id}") + + +class ServiceNotFound(JsonRESTError): + def __init__(self, ns_id): + super().__init__("ServiceNotFound", f"{ns_id}") + + +class ConflictingDomainExists(JsonRESTError): + def __init__(self, vpc_id): + super().__init__("ConflictingDomainExists", f"{vpc_id}") diff --git a/moto/servicediscovery/models.py b/moto/servicediscovery/models.py new file mode 100644 index 000000000..c76c459db --- /dev/null +++ b/moto/servicediscovery/models.py @@ -0,0 +1,330 @@ +import random +import string + +from moto.core import ACCOUNT_ID, BaseBackend, BaseModel +from moto.core.utils import BackendDict, unix_time +from moto.utilities.tagging_service import TaggingService + +from .exceptions import ( + ConflictingDomainExists, + NamespaceNotFound, + OperationNotFound, + ServiceNotFound, +) + + +def random_id(size): + return "".join( + [random.choice(string.ascii_lowercase + string.digits) for _ in range(size)] + ) + + +class Namespace(BaseModel): + def __init__( + self, + region, + name, + ns_type, + creator_request_id, + description, + dns_properties, + http_properties, + vpc=None, + ): + super().__init__() + self.id = f"ns-{random_id(20)}" + self.arn = f"arn:aws:servicediscovery:{region}:{ACCOUNT_ID}:namespace/{self.id}" + self.name = name + self.type = ns_type + self.creator_request_id = creator_request_id + self.description = description + self.dns_properties = dns_properties + self.http_properties = http_properties + self.vpc = vpc + self.created = unix_time() + self.updated = unix_time() + + def to_json(self): + return { + "Arn": self.arn, + "Id": self.id, + "Name": self.name, + "Description": self.description, + "Type": self.type, + "Properties": { + "DnsProperties": self.dns_properties, + "HttpProperties": self.http_properties, + }, + "CreateDate": self.created, + "UpdateDate": self.updated, + "CreatorRequestId": self.creator_request_id, + } + + +class Service(BaseModel): + def __init__( + self, + region, + name, + namespace_id, + description, + creator_request_id, + dns_config, + health_check_config, + health_check_custom_config, + service_type, + ): + super().__init__() + self.id = f"srv-{random_id(8)}" + self.arn = f"arn:aws:servicediscovery:{region}:{ACCOUNT_ID}:service/{self.id}" + self.name = name + self.namespace_id = namespace_id + self.description = description + self.creator_request_id = creator_request_id + self.dns_config = dns_config + self.health_check_config = health_check_config + self.health_check_custom_config = health_check_custom_config + self.service_type = service_type + self.created = unix_time() + + def update(self, details): + if "Description" in details: + self.description = details["Description"] + if "DnsConfig" in details: + if self.dns_config is None: + self.dns_config = {} + self.dns_config["DnsRecords"] = details["DnsConfig"]["DnsRecords"] + else: + # From the docs: + # If you omit any existing DnsRecords or HealthCheckConfig configurations from an UpdateService request, + # the configurations are deleted from the service. + self.dns_config = None + if "HealthCheckConfig" in details: + self.health_check_config = details["HealthCheckConfig"] + + def to_json(self): + return { + "Arn": self.arn, + "Id": self.id, + "Name": self.name, + "NamespaceId": self.namespace_id, + "CreateDate": self.created, + "Description": self.description, + "CreatorRequestId": self.creator_request_id, + "DnsConfig": self.dns_config, + "HealthCheckConfig": self.health_check_config, + "HealthCheckCustomConfig": self.health_check_custom_config, + "Type": self.service_type, + } + + +class Operation(BaseModel): + def __init__(self, operation_type, targets): + super().__init__() + self.id = f"{random_id(32)}-{random_id(8)}" + self.status = "SUCCESS" + self.operation_type = operation_type + self.created = unix_time() + self.updated = unix_time() + self.targets = targets + + def to_json(self, short=False): + if short: + return {"Id": self.id, "Status": self.status} + else: + return { + "Id": self.id, + "Status": self.status, + "Type": self.operation_type, + "CreateDate": self.created, + "UpdateDate": self.updated, + "Targets": self.targets, + } + + +class ServiceDiscoveryBackend(BaseBackend): + """Implementation of ServiceDiscovery APIs.""" + + def __init__(self, region_name=None): + self.region_name = region_name + self.operations = dict() + self.namespaces = dict() + self.services = dict() + self.tagger = TaggingService() + + def reset(self): + """Re-initialize all attributes for this instance.""" + region_name = self.region_name + self.__dict__ = {} + self.__init__(region_name) + + def list_namespaces(self): + """ + Pagination or the Filters-parameter is not yet implemented + """ + return self.namespaces.values() + + def create_http_namespace(self, name, creator_request_id, description, tags): + namespace = Namespace( + region=self.region_name, + name=name, + ns_type="HTTP", + creator_request_id=creator_request_id, + description=description, + dns_properties={"SOA": {}}, + http_properties={"HttpName": name}, + ) + self.namespaces[namespace.id] = namespace + if tags: + self.tagger.tag_resource(namespace.arn, tags) + operation_id = self._create_operation( + "CREATE_NAMESPACE", targets={"NAMESPACE": namespace.id} + ) + return operation_id + + def _create_operation(self, op_type, targets): + operation = Operation(operation_type=op_type, targets=targets) + self.operations[operation.id] = operation + operation_id = operation.id + return operation_id + + def delete_namespace(self, namespace_id): + if namespace_id not in self.namespaces: + raise NamespaceNotFound(namespace_id) + del self.namespaces[namespace_id] + operation_id = self._create_operation( + op_type="DELETE_NAMESPACE", targets={"NAMESPACE": namespace_id} + ) + return operation_id + + def get_namespace(self, namespace_id): + if namespace_id not in self.namespaces: + raise NamespaceNotFound(namespace_id) + return self.namespaces[namespace_id] + + def list_operations(self): + """ + Pagination or the Filters-argument is not yet implemented + """ + # Operations for namespaces will only be listed as long as namespaces exist + self.operations = { + op_id: op + for op_id, op in self.operations.items() + if op.targets.get("NAMESPACE") in self.namespaces + } + return self.operations.values() + + def get_operation(self, operation_id): + if operation_id not in self.operations: + raise OperationNotFound() + return self.operations[operation_id] + + def tag_resource(self, resource_arn, tags): + self.tagger.tag_resource(resource_arn, tags) + + def untag_resource(self, resource_arn, tag_keys): + self.tagger.untag_resource_using_names(resource_arn, tag_keys) + + def list_tags_for_resource(self, resource_arn): + return self.tagger.list_tags_for_resource(resource_arn) + + def create_private_dns_namespace( + self, name, creator_request_id, description, vpc, tags, properties + ): + for namespace in self.namespaces.values(): + if namespace.vpc == vpc: + raise ConflictingDomainExists(vpc) + dns_properties = (properties or {}).get("DnsProperties", {}) + dns_properties["HostedZoneId"] = "hzi" + namespace = Namespace( + region=self.region_name, + name=name, + ns_type="DNS_PRIVATE", + creator_request_id=creator_request_id, + description=description, + dns_properties=dns_properties, + http_properties={}, + vpc=vpc, + ) + self.namespaces[namespace.id] = namespace + if tags: + self.tagger.tag_resource(namespace.arn, tags) + operation_id = self._create_operation( + "CREATE_NAMESPACE", targets={"NAMESPACE": namespace.id} + ) + return operation_id + + def create_public_dns_namespace( + self, name, creator_request_id, description, tags, properties + ): + dns_properties = (properties or {}).get("DnsProperties", {}) + dns_properties["HostedZoneId"] = "hzi" + namespace = Namespace( + region=self.region_name, + name=name, + ns_type="DNS_PUBLIC", + creator_request_id=creator_request_id, + description=description, + dns_properties=dns_properties, + http_properties={}, + ) + self.namespaces[namespace.id] = namespace + if tags: + self.tagger.tag_resource(namespace.arn, tags) + operation_id = self._create_operation( + "CREATE_NAMESPACE", targets={"NAMESPACE": namespace.id} + ) + return operation_id + + def create_service( + self, + name, + namespace_id, + creator_request_id, + description, + dns_config, + health_check_config, + health_check_custom_config, + tags, + service_type, + ): + service = Service( + region=self.region_name, + name=name, + namespace_id=namespace_id, + description=description, + creator_request_id=creator_request_id, + dns_config=dns_config, + health_check_config=health_check_config, + health_check_custom_config=health_check_custom_config, + service_type=service_type, + ) + self.services[service.id] = service + if tags: + self.tagger.tag_resource(service.arn, tags) + return service + + def get_service(self, service_id): + if service_id not in self.services: + raise ServiceNotFound(service_id) + return self.services[service_id] + + def delete_service(self, service_id): + self.services.pop(service_id, None) + + def list_services(self): + """ + Pagination or the Filters-argument is not yet implemented + """ + return self.services.values() + + def update_service(self, service_id, details): + service = self.get_service(service_id) + service.update(details=details) + operation_id = self._create_operation( + "UPDATE_SERVICE", targets={"SEVICE": service.id} + ) + return operation_id + + +servicediscovery_backends = BackendDict(ServiceDiscoveryBackend, "servicediscovery") diff --git a/moto/servicediscovery/responses.py b/moto/servicediscovery/responses.py new file mode 100644 index 000000000..38873294c --- /dev/null +++ b/moto/servicediscovery/responses.py @@ -0,0 +1,171 @@ +"""Handles incoming servicediscovery requests, invokes methods, returns responses.""" +import json + +from moto.core.responses import BaseResponse +from .models import servicediscovery_backends + + +class ServiceDiscoveryResponse(BaseResponse): + @property + def servicediscovery_backend(self): + """Return backend instance specific for this region.""" + return servicediscovery_backends[self.region] + + def list_namespaces(self): + namespaces = self.servicediscovery_backend.list_namespaces() + return 200, {}, json.dumps({"Namespaces": [ns.to_json() for ns in namespaces]}) + + def create_http_namespace(self): + params = json.loads(self.body) + name = params.get("Name") + creator_request_id = params.get("CreatorRequestId") + description = params.get("Description") + tags = params.get("Tags") + operation_id = self.servicediscovery_backend.create_http_namespace( + name=name, + creator_request_id=creator_request_id, + description=description, + tags=tags, + ) + return json.dumps(dict(OperationId=operation_id)) + + def delete_namespace(self): + params = json.loads(self.body) + namespace_id = params.get("Id") + operation_id = self.servicediscovery_backend.delete_namespace( + namespace_id=namespace_id, + ) + return json.dumps(dict(OperationId=operation_id)) + + def list_operations(self): + operations = self.servicediscovery_backend.list_operations() + return ( + 200, + {}, + json.dumps({"Operations": [o.to_json(short=True) for o in operations]}), + ) + + def get_operation(self): + params = json.loads(self.body) + operation_id = params.get("OperationId") + operation = self.servicediscovery_backend.get_operation( + operation_id=operation_id, + ) + return json.dumps(dict(Operation=operation.to_json())) + + def get_namespace(self): + params = json.loads(self.body) + namespace_id = params.get("Id") + namespace = self.servicediscovery_backend.get_namespace( + namespace_id=namespace_id, + ) + return json.dumps(dict(Namespace=namespace.to_json())) + + def tag_resource(self): + params = json.loads(self.body) + resource_arn = params.get("ResourceARN") + tags = params.get("Tags") + self.servicediscovery_backend.tag_resource( + resource_arn=resource_arn, tags=tags, + ) + return json.dumps(dict()) + + def untag_resource(self): + params = json.loads(self.body) + resource_arn = params.get("ResourceARN") + tag_keys = params.get("TagKeys") + self.servicediscovery_backend.untag_resource( + resource_arn=resource_arn, tag_keys=tag_keys, + ) + return json.dumps(dict()) + + def list_tags_for_resource(self): + params = json.loads(self.body) + resource_arn = params.get("ResourceARN") + tags = self.servicediscovery_backend.list_tags_for_resource( + resource_arn=resource_arn, + ) + return 200, {}, json.dumps(tags) + + def create_private_dns_namespace(self): + params = json.loads(self.body) + name = params.get("Name") + creator_request_id = params.get("CreatorRequestId") + description = params.get("Description") + vpc = params.get("Vpc") + tags = params.get("Tags") + properties = params.get("Properties") + operation_id = self.servicediscovery_backend.create_private_dns_namespace( + name=name, + creator_request_id=creator_request_id, + description=description, + vpc=vpc, + tags=tags, + properties=properties, + ) + return json.dumps(dict(OperationId=operation_id)) + + def create_public_dns_namespace(self): + params = json.loads(self.body) + name = params.get("Name") + creator_request_id = params.get("CreatorRequestId") + description = params.get("Description") + tags = params.get("Tags") + properties = params.get("Properties") + operation_id = self.servicediscovery_backend.create_public_dns_namespace( + name=name, + creator_request_id=creator_request_id, + description=description, + tags=tags, + properties=properties, + ) + return json.dumps(dict(OperationId=operation_id)) + + def create_service(self): + params = json.loads(self.body) + name = params.get("Name") + namespace_id = params.get("NamespaceId") + creator_request_id = params.get("CreatorRequestId") + description = params.get("Description") + dns_config = params.get("DnsConfig") + health_check_config = params.get("HealthCheckConfig") + health_check_custom_config = params.get("HealthCheckCustomConfig") + tags = params.get("Tags") + service_type = params.get("Type") + service = self.servicediscovery_backend.create_service( + name=name, + namespace_id=namespace_id, + creator_request_id=creator_request_id, + description=description, + dns_config=dns_config, + health_check_config=health_check_config, + health_check_custom_config=health_check_custom_config, + tags=tags, + service_type=service_type, + ) + return json.dumps(dict(Service=service.to_json())) + + def get_service(self): + params = json.loads(self.body) + service_id = params.get("Id") + service = self.servicediscovery_backend.get_service(service_id=service_id) + return json.dumps(dict(Service=service.to_json())) + + def delete_service(self): + params = json.loads(self.body) + service_id = params.get("Id") + self.servicediscovery_backend.delete_service(service_id=service_id) + return json.dumps(dict()) + + def list_services(self): + services = self.servicediscovery_backend.list_services() + return json.dumps(dict(Services=[s.to_json() for s in services])) + + def update_service(self): + params = json.loads(self.body) + service_id = params.get("Id") + details = params.get("Service") + operation_id = self.servicediscovery_backend.update_service( + service_id=service_id, details=details, + ) + return json.dumps(dict(OperationId=operation_id)) diff --git a/moto/servicediscovery/urls.py b/moto/servicediscovery/urls.py new file mode 100644 index 000000000..53e1b0ffb --- /dev/null +++ b/moto/servicediscovery/urls.py @@ -0,0 +1,11 @@ +"""servicediscovery base URL and path.""" +from .responses import ServiceDiscoveryResponse + +url_bases = [ + r"https?://servicediscovery\.(.+)\.amazonaws\.com", +] + + +url_paths = { + "{0}/$": ServiceDiscoveryResponse.dispatch, +} diff --git a/tests/terraform-tests.success.txt b/tests/terraform-tests.success.txt index 9e98761e0..a5865a903 100644 --- a/tests/terraform-tests.success.txt +++ b/tests/terraform-tests.success.txt @@ -85,6 +85,7 @@ TestAccAWSRedshiftServiceAccount TestAccAWSRolePolicyAttachment TestAccAWSSNSSMSPreferences TestAccAWSSageMakerPrebuiltECRImage +TestAccAWSServiceDiscovery TestAccAWSSQSQueuePolicy TestAccAWSSSMDocument TestValidateSSMDocumentPermissions diff --git a/tests/test_servicediscovery/__init__.py b/tests/test_servicediscovery/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test_servicediscovery/test_server.py b/tests/test_servicediscovery/test_server.py new file mode 100644 index 000000000..8967f11ba --- /dev/null +++ b/tests/test_servicediscovery/test_server.py @@ -0,0 +1,15 @@ +import json +import sure # noqa # pylint: disable=unused-import + +import moto.server as server + + +def test_servicediscovery_list(): + backend = server.create_backend_app("servicediscovery") + test_client = backend.test_client() + + headers = {"X-Amz-Target": "Route53AutoNaming_v20170314.ListNamespaces"} + + resp = test_client.get("/", headers=headers) + resp.status_code.should.equal(200) + json.loads(resp.data).should.equal({"Namespaces": []}) diff --git a/tests/test_servicediscovery/test_servicediscovery_httpnamespaces.py b/tests/test_servicediscovery/test_servicediscovery_httpnamespaces.py new file mode 100644 index 000000000..97998c2ea --- /dev/null +++ b/tests/test_servicediscovery/test_servicediscovery_httpnamespaces.py @@ -0,0 +1,233 @@ +"""Unit tests for servicediscovery-supported APIs.""" +import boto3 +import pytest +import sure # noqa # pylint: disable=unused-import + +from botocore.exceptions import ClientError +from moto import mock_servicediscovery +from moto.core import ACCOUNT_ID + +# See our Development Tips on writing tests for hints on how to write good tests: +# http://docs.getmoto.org/en/latest/docs/contributing/development_tips/tests.html + + +@mock_servicediscovery +def test_create_http_namespace(): + client = boto3.client("servicediscovery", region_name="eu-west-1") + client.create_http_namespace(Name="mynamespace") + + resp = client.list_namespaces() + resp.should.have.key("Namespaces").length_of(1) + + namespace = resp["Namespaces"][0] + namespace.should.have.key("Id").match("ns-[a-z0-9]{16}") + namespace.should.have.key("Arn").match( + f"arn:aws:servicediscovery:eu-west-1:{ACCOUNT_ID}:namespace/{namespace['Id']}" + ) + namespace.should.have.key("Name").equals("mynamespace") + namespace.should.have.key("Type").equals("HTTP") + namespace.should.have.key("CreateDate") + + namespace.should.have.key("Properties") + props = namespace["Properties"] + props.should.have.key("DnsProperties").equals({"SOA": {}}) + props.should.have.key("HttpProperties").equals({"HttpName": "mynamespace"}) + + +@mock_servicediscovery +def test_get_http_namespace_minimal(): + client = boto3.client("servicediscovery", region_name="eu-west-1") + client.create_http_namespace(Name="mynamespace") + + ns_id = client.list_namespaces()["Namespaces"][0]["Id"] + + resp = client.get_namespace(Id=ns_id) + resp.should.have.key("Namespace") + + namespace = resp["Namespace"] + namespace.should.have.key("Id").match(ns_id) + namespace.should.have.key("Arn").match( + f"arn:aws:servicediscovery:eu-west-1:{ACCOUNT_ID}:namespace/{namespace['Id']}" + ) + namespace.should.have.key("Name").equals("mynamespace") + namespace.should.have.key("Type").equals("HTTP") + namespace.should.have.key("CreateDate") + namespace.should.have.key("CreatorRequestId") + + namespace.should.have.key("Properties") + props = namespace["Properties"] + props.should.have.key("DnsProperties").equals({"SOA": {}}) + props.should.have.key("HttpProperties").equals({"HttpName": "mynamespace"}) + + namespace.shouldnt.have.key("Description") + + +@mock_servicediscovery +def test_get_http_namespace(): + client = boto3.client("servicediscovery", region_name="eu-west-1") + client.create_http_namespace( + Name="mynamespace", CreatorRequestId="crid", Description="mu fancy namespace" + ) + + ns_id = client.list_namespaces()["Namespaces"][0]["Id"] + + resp = client.get_namespace(Id=ns_id) + resp.should.have.key("Namespace") + + namespace = resp["Namespace"] + namespace.should.have.key("Id").match(ns_id) + namespace.should.have.key("Arn").match( + f"arn:aws:servicediscovery:eu-west-1:{ACCOUNT_ID}:namespace/{namespace['Id']}" + ) + namespace.should.have.key("Name").equals("mynamespace") + namespace.should.have.key("Type").equals("HTTP") + namespace.should.have.key("CreateDate") + namespace.should.have.key("CreatorRequestId").equals("crid") + namespace.should.have.key("Description").equals("mu fancy namespace") + + namespace.should.have.key("Properties") + props = namespace["Properties"] + props.should.have.key("DnsProperties").equals({"SOA": {}}) + props.should.have.key("HttpProperties").equals({"HttpName": "mynamespace"}) + + +@mock_servicediscovery +def test_delete_namespace(): + client = boto3.client("servicediscovery", region_name="eu-west-1") + client.create_http_namespace(Name="mynamespace") + ns_id = client.list_namespaces()["Namespaces"][0]["Id"] + + resp = client.delete_namespace(Id=ns_id) + resp.should.have.key("OperationId") + + # Calling delete again while this is in progress results in an error: + # Another operation of type DeleteNamespace and id dlmpkcn33aovnztwdpsdplgtheuhgcap-k6x64euq is in progress + # list_operations is empty after successfull deletion - old operations from this namespace should be deleted + # list_namespaces is also empty (obvs) + + client.list_namespaces()["Namespaces"].should.equal([]) + client.list_operations()["Operations"].should.equal([]) + + +@mock_servicediscovery +def test_delete_unknown_namespace(): + client = boto3.client("servicediscovery", region_name="eu-west-1") + with pytest.raises(ClientError) as exc: + client.delete_namespace(Id="unknown") + err = exc.value.response["Error"] + err["Code"].should.equal("NamespaceNotFound") + err["Message"].should.equal("unknown") + + +@mock_servicediscovery +def test_get_unknown_namespace(): + client = boto3.client("servicediscovery", region_name="eu-west-1") + with pytest.raises(ClientError) as exc: + client.get_namespace(Id="unknown") + err = exc.value.response["Error"] + err["Code"].should.equal("NamespaceNotFound") + err["Message"].should.equal("unknown") + + +@mock_servicediscovery +def test_create_private_dns_namespace_minimal(): + client = boto3.client("servicediscovery", region_name="eu-west-1") + client.create_private_dns_namespace(Name="dns_ns", Vpc="vpc_id") + + ns_id = client.list_namespaces()["Namespaces"][0]["Id"] + + resp = client.get_namespace(Id=ns_id) + resp.should.have.key("Namespace") + + namespace = resp["Namespace"] + namespace.should.have.key("Id").match(ns_id) + namespace.should.have.key("Name").equals("dns_ns") + namespace.should.have.key("Type").equals("DNS_PRIVATE") + + namespace.should.have.key("Properties") + props = namespace["Properties"] + props.should.have.key("DnsProperties") + props["DnsProperties"].should.have.key("HostedZoneId") + props["DnsProperties"].shouldnt.have.key("SOA") + + +@mock_servicediscovery +def test_create_private_dns_namespace(): + client = boto3.client("servicediscovery", region_name="eu-west-1") + client.create_private_dns_namespace( + Name="dns_ns", + Vpc="vpc_id", + Description="my private dns", + Properties={"DnsProperties": {"SOA": {"TTL": 123}}}, + ) + + ns_id = client.list_namespaces()["Namespaces"][0]["Id"] + + resp = client.get_namespace(Id=ns_id) + resp.should.have.key("Namespace") + + namespace = resp["Namespace"] + namespace.should.have.key("Id").match(ns_id) + namespace.should.have.key("Name").equals("dns_ns") + namespace.should.have.key("Type").equals("DNS_PRIVATE") + namespace.should.have.key("Description").equals("my private dns") + + namespace.should.have.key("Properties") + props = namespace["Properties"] + props.should.have.key("DnsProperties") + props["DnsProperties"].should.have.key("HostedZoneId") + props["DnsProperties"].should.have.key("SOA").equals({"TTL": 123}) + + +@mock_servicediscovery +def test_create_private_dns_namespace_with_duplicate_vpc(): + client = boto3.client("servicediscovery", region_name="eu-west-1") + client.create_private_dns_namespace(Name="dns_ns", Vpc="vpc_id") + + with pytest.raises(ClientError) as exc: + client.create_private_dns_namespace(Name="sth else", Vpc="vpc_id") + err = exc.value.response["Error"] + err["Code"].should.equal("ConflictingDomainExists") + + +@mock_servicediscovery +def test_create_public_dns_namespace_minimal(): + client = boto3.client("servicediscovery", region_name="us-east-2") + client.create_public_dns_namespace(Name="public_dns_ns") + + ns_id = client.list_namespaces()["Namespaces"][0]["Id"] + + resp = client.get_namespace(Id=ns_id) + resp.should.have.key("Namespace") + + namespace = resp["Namespace"] + namespace.should.have.key("Id").match(ns_id) + namespace.should.have.key("Name").equals("public_dns_ns") + namespace.should.have.key("Type").equals("DNS_PUBLIC") + + +@mock_servicediscovery +def test_create_public_dns_namespace(): + client = boto3.client("servicediscovery", region_name="us-east-2") + client.create_public_dns_namespace( + Name="public_dns_ns", + CreatorRequestId="cri", + Description="my public dns", + Properties={"DnsProperties": {"SOA": {"TTL": 124}}}, + ) + + ns_id = client.list_namespaces()["Namespaces"][0]["Id"] + + resp = client.get_namespace(Id=ns_id) + resp.should.have.key("Namespace") + + namespace = resp["Namespace"] + namespace.should.have.key("Id").match(ns_id) + namespace.should.have.key("Name").equals("public_dns_ns") + namespace.should.have.key("Type").equals("DNS_PUBLIC") + namespace.should.have.key("Description").equals("my public dns") + namespace.should.have.key("CreatorRequestId").equals("cri") + + namespace.should.have.key("Properties").should.have.key("DnsProperties") + dns_props = namespace["Properties"]["DnsProperties"] + dns_props.should.equal({"HostedZoneId": "hzi", "SOA": {"TTL": 124}}) diff --git a/tests/test_servicediscovery/test_servicediscovery_operations.py b/tests/test_servicediscovery/test_servicediscovery_operations.py new file mode 100644 index 000000000..f42dce14b --- /dev/null +++ b/tests/test_servicediscovery/test_servicediscovery_operations.py @@ -0,0 +1,135 @@ +"""Unit tests for servicediscovery-supported APIs.""" +import boto3 +import pytest +import sure # noqa # pylint: disable=unused-import + +from botocore.exceptions import ClientError +from moto import mock_servicediscovery + +# See our Development Tips on writing tests for hints on how to write good tests: +# http://docs.getmoto.org/en/latest/docs/contributing/development_tips/tests.html + + +@mock_servicediscovery +def test_list_operations_initial(): + client = boto3.client("servicediscovery", region_name="eu-west-1") + resp = client.list_operations() + + resp.should.have.key("Operations").equals([]) + + +@mock_servicediscovery +def test_list_operations(): + client = boto3.client("servicediscovery", region_name="eu-west-2") + + resp = client.create_http_namespace(Name="n/a") + resp.should.have.key("OperationId") + op_id = resp["OperationId"] + + resp = client.list_operations() + resp.should.have.key("Operations").length_of(1) + resp["Operations"].should.equal([{"Id": op_id, "Status": "SUCCESS"}]) + + +@mock_servicediscovery +def test_get_create_http_namespace_operation(): + client = boto3.client("servicediscovery", region_name="eu-west-1") + resp = client.create_http_namespace(Name="mynamespace") + + resp["OperationId"].should.match("[a-z0-9]{32}-[a-z0-9]{8}") + + operation_id = resp["OperationId"] + + resp = client.get_operation(OperationId=operation_id) + + resp.should.have.key("Operation") + operation = resp["Operation"] + operation.should.have.key("Id").equals(operation_id) + operation.should.have.key("Type").equals("CREATE_NAMESPACE") + operation.should.have.key("Status").equals("SUCCESS") + operation.should.have.key("CreateDate") + operation.should.have.key("UpdateDate") + operation.should.have.key("Targets") + + targets = operation["Targets"] + targets.should.have.key("NAMESPACE") + + namespaces = client.list_namespaces()["Namespaces"] + [ns["Id"] for ns in namespaces].should.contain(targets["NAMESPACE"]) + + +@mock_servicediscovery +def test_get_private_dns_namespace_operation(): + client = boto3.client("servicediscovery", region_name="eu-west-1") + resp = client.create_private_dns_namespace(Name="dns_ns", Vpc="vpc_id") + + resp["OperationId"].should.match("[a-z0-9]{32}-[a-z0-9]{8}") + + operation_id = resp["OperationId"] + + resp = client.get_operation(OperationId=operation_id) + + resp.should.have.key("Operation") + operation = resp["Operation"] + operation.should.have.key("Id").equals(operation_id) + operation.should.have.key("Type").equals("CREATE_NAMESPACE") + operation.should.have.key("Status").equals("SUCCESS") + operation.should.have.key("CreateDate") + operation.should.have.key("UpdateDate") + operation.should.have.key("Targets") + + +@mock_servicediscovery +def test_get_public_dns_namespace_operation(): + client = boto3.client("servicediscovery", region_name="eu-west-1") + resp = client.create_public_dns_namespace(Name="dns_ns") + + resp["OperationId"].should.match("[a-z0-9]{32}-[a-z0-9]{8}") + + operation_id = resp["OperationId"] + + resp = client.get_operation(OperationId=operation_id) + + resp.should.have.key("Operation") + operation = resp["Operation"] + operation.should.have.key("Id").equals(operation_id) + operation.should.have.key("Type").equals("CREATE_NAMESPACE") + operation.should.have.key("Status").equals("SUCCESS") + operation.should.have.key("CreateDate") + operation.should.have.key("UpdateDate") + operation.should.have.key("Targets") + + +@mock_servicediscovery +def test_get_update_service_operation(): + client = boto3.client("servicediscovery", region_name="eu-west-1") + service_id = client.create_service( + Name="my service", NamespaceId="ns_id", Description="first desc", + )["Service"]["Id"] + + resp = client.update_service(Id=service_id, Service={"Description": "updated desc"}) + + resp["OperationId"].should.match("[a-z0-9]{32}-[a-z0-9]{8}") + + operation_id = resp["OperationId"] + + resp = client.get_operation(OperationId=operation_id) + + resp.should.have.key("Operation") + operation = resp["Operation"] + operation.should.have.key("Id").equals(operation_id) + operation.should.have.key("Type").equals("UPDATE_SERVICE") + operation.should.have.key("Status").equals("SUCCESS") + operation.should.have.key("CreateDate") + operation.should.have.key("UpdateDate") + operation.should.have.key("Targets") + + +@mock_servicediscovery +def test_get_unknown_operation(): + client = boto3.client("servicediscovery", region_name="eu-west-1") + + with pytest.raises(ClientError) as exc: + client.get_operation(OperationId="unknown") + err = exc.value.response["Error"] + err["Code"].should.equal("OperationNotFound") diff --git a/tests/test_servicediscovery/test_servicediscovery_service.py b/tests/test_servicediscovery/test_servicediscovery_service.py new file mode 100644 index 000000000..66f1c943b --- /dev/null +++ b/tests/test_servicediscovery/test_servicediscovery_service.py @@ -0,0 +1,208 @@ +"""Unit tests for servicediscovery-supported APIs.""" +import boto3 +import pytest +import sure # noqa # pylint: disable=unused-import + +from botocore.exceptions import ClientError +from moto import mock_servicediscovery + +# See our Development Tips on writing tests for hints on how to write good tests: +# http://docs.getmoto.org/en/latest/docs/contributing/development_tips/tests.html + + +@mock_servicediscovery +def test_create_service_minimal(): + client = boto3.client("servicediscovery", region_name="ap-southeast-1") + operation_id = client.create_http_namespace(Name="mynamespace")["OperationId"] + namespace_id = client.get_operation(OperationId=operation_id)["Operation"][ + "Targets" + ]["NAMESPACE"] + + resp = client.create_service(Name="my service", NamespaceId=namespace_id) + + resp.should.have.key("Service") + resp["Service"].should.have.key("Id") + resp["Service"].should.have.key("Arn") + resp["Service"].should.have.key("Name").equals("my service") + resp["Service"].should.have.key("NamespaceId").equals(namespace_id) + resp["Service"].should.have.key("CreateDate") + + +@mock_servicediscovery +def test_create_service(): + client = boto3.client("servicediscovery", region_name="ap-southeast-1") + operation_id = client.create_http_namespace(Name="mynamespace")["OperationId"] + namespace_id = client.get_operation(OperationId=operation_id)["Operation"][ + "Targets" + ]["NAMESPACE"] + + resp = client.create_service( + Name="my service", + CreatorRequestId="crid", + Description="my service", + DnsConfig={ + "NamespaceId": namespace_id, + "RoutingPolicy": "WEIGHTED", + "DnsRecords": [{"Type": "SRV", "TTL": 0}], + }, + HealthCheckConfig={"Type": "TCP", "ResourcePath": "/sth"}, + HealthCheckCustomConfig={"FailureThreshold": 125}, + Type="HTTP", + ) + + resp.should.have.key("Service") + resp["Service"].should.have.key("Id") + resp["Service"].should.have.key("Arn") + resp["Service"].should.have.key("Name").equals("my service") + resp["Service"].shouldnt.have.key("NamespaceId") + resp["Service"].should.have.key("Description").equals("my service") + resp["Service"].should.have.key("DnsConfig").equals( + { + "NamespaceId": namespace_id, + "RoutingPolicy": "WEIGHTED", + "DnsRecords": [{"Type": "SRV", "TTL": 0}], + } + ) + resp["Service"].should.have.key("HealthCheckConfig").equals( + {"Type": "TCP", "ResourcePath": "/sth"} + ) + resp["Service"].should.have.key("HealthCheckCustomConfig").equals( + {"FailureThreshold": 125} + ) + resp["Service"].should.have.key("Type").equals("HTTP") + resp["Service"].should.have.key("CreatorRequestId").equals("crid") + + +@mock_servicediscovery +def test_get_service(): + client = boto3.client("servicediscovery", region_name="ap-southeast-1") + + operation_id = client.create_http_namespace(Name="mynamespace")["OperationId"] + namespace_id = client.get_operation(OperationId=operation_id)["Operation"][ + "Targets" + ]["NAMESPACE"] + + service_id = client.create_service(Name="my service", NamespaceId=namespace_id)[ + "Service" + ]["Id"] + + resp = client.get_service(Id=service_id) + + resp.should.have.key("Service") + resp["Service"].should.have.key("Id") + resp["Service"].should.have.key("Arn") + resp["Service"].should.have.key("Name").equals("my service") + resp["Service"].should.have.key("NamespaceId").equals(namespace_id) + + +@mock_servicediscovery +def test_get_unknown_service(): + client = boto3.client("servicediscovery", region_name="ap-southeast-1") + + with pytest.raises(ClientError) as exc: + client.get_service(Id="unknown") + err = exc.value.response["Error"] + err["Code"].should.equal("ServiceNotFound") + err["Message"].should.equal("unknown") + + +@mock_servicediscovery +def test_delete_service(): + client = boto3.client("servicediscovery", region_name="eu-west-1") + + operation_id = client.create_http_namespace(Name="mynamespace")["OperationId"] + namespace_id = client.get_operation(OperationId=operation_id)["Operation"][ + "Targets" + ]["NAMESPACE"] + service_id = client.create_service(Name="my service", NamespaceId=namespace_id)[ + "Service" + ]["Id"] + + client.delete_service(Id=service_id) + + with pytest.raises(ClientError) as exc: + client.get_service(Id=service_id) + err = exc.value.response["Error"] + err["Code"].should.equal("ServiceNotFound") + err["Message"].should.equal(service_id) + + +@mock_servicediscovery +def test_update_service_description(): + client = boto3.client("servicediscovery", region_name="ap-southeast-1") + operation_id = client.create_http_namespace(Name="mynamespace")["OperationId"] + namespace_id = client.get_operation(OperationId=operation_id)["Operation"][ + "Targets" + ]["NAMESPACE"] + + service_id = client.create_service( + Name="my service", + NamespaceId=namespace_id, + Description="first desc", + DnsConfig={ + "NamespaceId": namespace_id, + "RoutingPolicy": "WEIGHTED", + "DnsRecords": [{"Type": "SRV", "TTL": 0}], + }, + HealthCheckConfig={"Type": "TCP", "ResourcePath": "/sth"}, + )["Service"]["Id"] + + client.update_service(Id=service_id, Service={"Description": "updated desc"}) + + resp = client.get_service(Id=service_id) + + resp.should.have.key("Service") + resp["Service"].should.have.key("Id").equals(service_id) + resp["Service"].should.have.key("Arn") + resp["Service"].should.have.key("Name").equals("my service") + resp["Service"].should.have.key("NamespaceId").equals(namespace_id) + resp["Service"].should.have.key("Description").equals("updated desc") + # From the docs: + # If you omit any existing DnsRecords or HealthCheckConfig configurations from an UpdateService request, + # the configurations are deleted from the service. + resp["Service"].shouldnt.have.key("DnsConfig") + resp["Service"].should.have.key("HealthCheckConfig").equals( + {"Type": "TCP", "ResourcePath": "/sth"} + ) + + +@mock_servicediscovery +def test_update_service_others(): + client = boto3.client("servicediscovery", region_name="ap-southeast-1") + operation_id = client.create_http_namespace(Name="mynamespace")["OperationId"] + namespace_id = client.get_operation(OperationId=operation_id)["Operation"][ + "Targets" + ]["NAMESPACE"] + + service_id = client.create_service( + Name="my service", + NamespaceId=namespace_id, + Description="first desc", + DnsConfig={ + "RoutingPolicy": "WEIGHTED", + "DnsRecords": [{"Type": "SRV", "TTL": 0}], + }, + )["Service"]["Id"] + + client.update_service( + Id=service_id, + Service={ + "DnsConfig": {"DnsRecords": [{"Type": "SRV", "TTL": 12}]}, + "HealthCheckConfig": {"Type": "TCP", "ResourcePath": "/sth"}, + }, + ) + + resp = client.get_service(Id=service_id) + + resp.should.have.key("Service") + resp["Service"].should.have.key("Id").equals(service_id) + resp["Service"].should.have.key("Arn") + resp["Service"].should.have.key("Name").equals("my service") + resp["Service"].should.have.key("NamespaceId").equals(namespace_id) + resp["Service"].should.have.key("Description").equals("first desc") + resp["Service"].should.have.key("DnsConfig").equals( + {"RoutingPolicy": "WEIGHTED", "DnsRecords": [{"Type": "SRV", "TTL": 12}]} + ) + resp["Service"].should.have.key("HealthCheckConfig").equals( + {"Type": "TCP", "ResourcePath": "/sth"} + ) diff --git a/tests/test_servicediscovery/test_servicediscovery_tags.py b/tests/test_servicediscovery/test_servicediscovery_tags.py new file mode 100644 index 000000000..1f11e26fd --- /dev/null +++ b/tests/test_servicediscovery/test_servicediscovery_tags.py @@ -0,0 +1,103 @@ +"""Unit tests for servicediscovery-supported APIs.""" +import boto3 + +import sure # noqa # pylint: disable=unused-import +from moto import mock_servicediscovery + +# See our Development Tips on writing tests for hints on how to write good tests: +# http://docs.getmoto.org/en/latest/docs/contributing/development_tips/tests.html + + +@mock_servicediscovery +def test_create_http_namespace_with_tags(): + client = boto3.client("servicediscovery", region_name="eu-west-1") + client.create_http_namespace( + Name="mynamespace", Tags=[{"Key": "key1", "Value": "val1"}] + ) + + ns_arn = client.list_namespaces()["Namespaces"][0]["Arn"] + + resp = client.list_tags_for_resource(ResourceARN=ns_arn) + resp.should.have.key("Tags") + + resp["Tags"].should.equal([{"Key": "key1", "Value": "val1"}]) + + +@mock_servicediscovery +def test_create_public_dns_namespace_with_tags(): + client = boto3.client("servicediscovery", region_name="eu-west-1") + client.create_public_dns_namespace( + Name="mynamespace", Tags=[{"Key": "key1", "Value": "val1"}] + ) + + ns_arn = client.list_namespaces()["Namespaces"][0]["Arn"] + + resp = client.list_tags_for_resource(ResourceARN=ns_arn) + resp.should.have.key("Tags") + + resp["Tags"].should.equal([{"Key": "key1", "Value": "val1"}]) + + +@mock_servicediscovery +def test_create_private_dns_namespace_with_tags(): + client = boto3.client("servicediscovery", region_name="eu-west-1") + client.create_private_dns_namespace( + Name="mynamespace", Vpc="vpc", Tags=[{"Key": "key1", "Value": "val1"}] + ) + + ns_arn = client.list_namespaces()["Namespaces"][0]["Arn"] + + resp = client.list_tags_for_resource(ResourceARN=ns_arn) + resp.should.have.key("Tags") + + resp["Tags"].should.equal([{"Key": "key1", "Value": "val1"}]) + + +@mock_servicediscovery +def test_create_service_with_tags(): + client = boto3.client("servicediscovery", region_name="eu-west-1") + client.create_service(Name="myservice", Tags=[{"Key": "key1", "Value": "val1"}]) + + ns_arn = client.list_services()["Services"][0]["Arn"] + + resp = client.list_tags_for_resource(ResourceARN=ns_arn) + resp.should.have.key("Tags") + + resp["Tags"].should.equal([{"Key": "key1", "Value": "val1"}]) + + +@mock_servicediscovery +def test_tag_resource(): + client = boto3.client("servicediscovery", region_name="ap-southeast-1") + client.create_http_namespace( + Name="mynamespace", Tags=[{"Key": "key1", "Value": "val1"}] + ) + + ns_arn = client.list_namespaces()["Namespaces"][0]["Arn"] + client.tag_resource(ResourceARN=ns_arn, Tags=[{"Key": "key2", "Value": "val2"}]) + + resp = client.list_tags_for_resource(ResourceARN=ns_arn) + resp.should.have.key("Tags") + + resp["Tags"].should.equal( + [{"Key": "key1", "Value": "val1"}, {"Key": "key2", "Value": "val2"}] + ) + + +@mock_servicediscovery +def test_untag_resource(): + client = boto3.client("servicediscovery", region_name="us-east-2") + client.create_http_namespace(Name="mynamespace") + + ns_arn = client.list_namespaces()["Namespaces"][0]["Arn"] + client.tag_resource( + ResourceARN=ns_arn, + Tags=[{"Key": "key1", "Value": "val1"}, {"Key": "key2", "Value": "val2"}], + ) + + client.untag_resource(ResourceARN=ns_arn, TagKeys=["key1"]) + + resp = client.list_tags_for_resource(ResourceARN=ns_arn) + resp.should.have.key("Tags") + + resp["Tags"].should.equal([{"Key": "key2", "Value": "val2"}])