diff --git a/cloudfoundry_client/client.py b/cloudfoundry_client/client.py index 998acbe..80212ae 100644 --- a/cloudfoundry_client/client.py +++ b/cloudfoundry_client/client.py @@ -18,7 +18,7 @@ from cloudfoundry_client.v2.events import EventManager from cloudfoundry_client.v2.jobs import JobManager as JobManagerV2 from cloudfoundry_client.v2.resources import ResourceManager -from cloudfoundry_client.v2.routes import RouteManager +from cloudfoundry_client.v2.routes import RouteManager as RouteManagerV2 from cloudfoundry_client.v2.service_bindings import ServiceBindingManager from cloudfoundry_client.v2.service_brokers import ServiceBrokerManager as ServiceBrokerManagerV2 from cloudfoundry_client.v2.service_instances import ServiceInstanceManager as ServiceInstanceManagerV2 @@ -36,6 +36,7 @@ from cloudfoundry_client.v3.processes import ProcessManager from cloudfoundry_client.v3.organizations import OrganizationManager from cloudfoundry_client.v3.roles import RoleManager +from cloudfoundry_client.v3.routes import RouteManager from cloudfoundry_client.v3.security_groups import SecurityGroupManager from cloudfoundry_client.v3.service_brokers import ServiceBrokerManager from cloudfoundry_client.v3.service_credential_bindings import ServiceCredentialBindingManager @@ -97,7 +98,7 @@ def __init__(self, cloud_controller_v2_url: str, credential_manager: "CloudFound self.event = EventManager(target_endpoint, credential_manager) self.organizations = EntityManagerV2(target_endpoint, credential_manager, "/v2/organizations") self.private_domains = EntityManagerV2(target_endpoint, credential_manager, "/v2/private_domains") - self.routes = RouteManager(target_endpoint, credential_manager) + self.routes = RouteManagerV2(target_endpoint, credential_manager) self.services = EntityManagerV2(target_endpoint, credential_manager, "/v2/services") self.shared_domains = EntityManagerV2(target_endpoint, credential_manager, "/v2/shared_domains") self.spaces = SpaceManagerV2(target_endpoint, credential_manager) @@ -124,6 +125,7 @@ def __init__(self, cloud_controller_v3_url: str, credential_manager: "CloudFound self.organization_quotas = OrganizationQuotaManager(target_endpoint, credential_manager) self.processes = ProcessManager(target_endpoint, credential_manager) self.roles = RoleManager(target_endpoint, credential_manager) + self.routes = RouteManager(target_endpoint, credential_manager) self.security_groups = SecurityGroupManager(target_endpoint, credential_manager) self.service_brokers = ServiceBrokerManager(target_endpoint, credential_manager) self.service_credential_bindings = ServiceCredentialBindingManager(target_endpoint, credential_manager) diff --git a/cloudfoundry_client/v3/routes.py b/cloudfoundry_client/v3/routes.py new file mode 100644 index 0000000..4063971 --- /dev/null +++ b/cloudfoundry_client/v3/routes.py @@ -0,0 +1,58 @@ +from enum import Enum +from typing import TYPE_CHECKING, Any + +from cloudfoundry_client.v3.entities import EntityManager, Entity, ToOneRelationship + +if TYPE_CHECKING: + from cloudfoundry_client.client import CloudFoundryClient + + +class LoadBalancing(Enum): + ROUND_ROBIN = 'round-robin' + LEAST_CONNECTION = 'least-connection' + + +class RouteManager(EntityManager[Entity]): + def __init__(self, target_endpoint: str, client: "CloudFoundryClient"): + super(RouteManager, self).__init__(target_endpoint, client, "/v3/routes") + + def create(self, + space_guid: str, + domain_guid: str, + host: str | None = None, + path: str | None = None, + port: int | None = None, + load_balancing: LoadBalancing | None = None, + meta_labels: dict | None = None, + meta_annotations: dict | None = None, + ) -> Entity: + data: dict[str, Any] = { + "relationships": { + "space": ToOneRelationship(space_guid), "domain": ToOneRelationship(domain_guid) + }, + } + if host is not None: + data["host"] = host + if port is not None: + data["port"] = port + if path is not None: + data["path"] = path + if load_balancing is not None: + data["options"] = {"loadbalancing": load_balancing.value} + self._metadata(data, meta_labels, meta_annotations) + return super(RouteManager, self)._create(data) + + def update(self, + route_gid: str, + load_balancing: LoadBalancing | None = None, + meta_labels: dict | None = None, + meta_annotations: dict | None = None, + ) -> Entity: + data: dict[str, Any] = {} + if load_balancing is not None: + data["options"] = {"loadbalancing": load_balancing.value} + self._metadata(data, meta_labels, meta_annotations) + return super(RouteManager, self)._update(route_gid, data) + + def remove(self, route_gid: str): + return super(RouteManager, self)._remove(route_gid) diff --git a/tests/fixtures/v3/routes/GET_response.json b/tests/fixtures/v3/routes/GET_response.json new file mode 100644 index 0000000..680e80e --- /dev/null +++ b/tests/fixtures/v3/routes/GET_response.json @@ -0,0 +1,88 @@ +{ + "pagination": { + "total_results": 1, + "total_pages": 1, + "first": { + "href": "https://api.example.org/v3/routes?page=1&per_page=2" + }, + "last": { + "href": "https://api.example.org/v3/routes?page=1&per_page=2" + }, + "next": null, + "previous": null + }, + "resources": [ + { + "guid": "cbad697f-cac1-48f4-9017-ac08f39dfb31", + "protocol": "http", + "created_at": "2019-05-10T17:17:48Z", + "updated_at": "2019-05-10T17:17:48Z", + "host": "a-hostname", + "path": "/some_path", + "url": "a-hostname.a-domain.com/some_path", + "destinations": [ + { + "guid": "385bf117-17f5-4689-8c5c-08c6cc821fed", + "app": { + "guid": "0a6636b5-7fc4-44d8-8752-0db3e40b35a5", + "process": { + "type": "web" + } + }, + "weight": null, + "port": 8080, + "protocol": "http1", + "created_at": "2019-05-10T17:17:48Z", + "updated_at": "2019-05-10T17:17:48Z" + }, + { + "guid": "27e96a3b-5bcf-49ed-8048-351e0be23e6f", + "app": { + "guid": "f61e59fa-2121-4217-8c7b-15bfd75baf25", + "process": { + "type": "web" + } + }, + "weight": null, + "port": 8080, + "protocol": "http1", + "created_at": "2019-05-10T17:17:48Z", + "updated_at": "2019-05-10T17:17:48Z" + } + ], + "options": { + "loadbalancing": "round-robin" + }, + "metadata": { + "labels": {}, + "annotations": {} + }, + "relationships": { + "space": { + "data": { + "guid": "885a8cb3-c07b-4856-b448-eeb10bf36236" + } + }, + "domain": { + "data": { + "guid": "0b5f3633-194c-42d2-9408-972366617e0e" + } + } + }, + "links": { + "self": { + "href": "https://api.example.org/v3/routes/cbad697f-cac1-48f4-9017-ac08f39dfb31" + }, + "space": { + "href": "https://api.example.org/v3/spaces/885a8cb3-c07b-4856-b448-eeb10bf36236" + }, + "domain": { + "href": "https://api.example.org/v3/domains/0b5f3633-194c-42d2-9408-972366617e0e" + }, + "destinations": { + "href": "https://api.example.org/v3/routes/cbad697f-cac1-48f4-9017-ac08f39dfb31/destinations" + } + } + } + ] +} diff --git a/tests/fixtures/v3/routes/GET_{id}_response.json b/tests/fixtures/v3/routes/GET_{id}_response.json new file mode 100644 index 0000000..8284103 --- /dev/null +++ b/tests/fixtures/v3/routes/GET_{id}_response.json @@ -0,0 +1,73 @@ +{ + "guid": "cbad697f-cac1-48f4-9017-ac08f39dfb31", + "protocol": "tcp", + "port": 6666, + "created_at": "2019-05-10T17:17:48Z", + "updated_at": "2019-05-10T17:17:48Z", + "host": "a-hostname", + "path": "/some_path", + "url": "a-hostname.a-domain.com/some_path", + "destinations": [ + { + "guid": "385bf117-17f5-4689-8c5c-08c6cc821fed", + "app": { + "guid": "0a6636b5-7fc4-44d8-8752-0db3e40b35a5", + "process": { + "type": "web" + } + }, + "weight": null, + "port": 8080, + "protocol": "tcp", + "created_at": "2019-05-10T17:17:48Z", + "updated_at": "2019-05-10T17:17:48Z" + }, + { + "guid": "27e96a3b-5bcf-49ed-8048-351e0be23e6f", + "app": { + "guid": "f61e59fa-2121-4217-8c7b-15bfd75baf25", + "process": { + "type": "web" + } + }, + "weight": null, + "port": 8080, + "protocol": "tcp", + "created_at": "2019-05-10T17:17:48Z", + "updated_at": "2019-05-10T17:17:48Z" + } + ], + "options": { + "loadbalancing": "round-robin" + }, + "metadata": { + "labels": {}, + "annotations": {} + }, + "relationships": { + "space": { + "data": { + "guid": "885a8cb3-c07b-4856-b448-eeb10bf36236" + } + }, + "domain": { + "data": { + "guid": "0b5f3633-194c-42d2-9408-972366617e0e" + } + } + }, + "links": { + "self": { + "href": "https://api.example.org/v3/routes/cbad697f-cac1-48f4-9017-ac08f39dfb31" + }, + "space": { + "href": "https://api.example.org/v3/spaces/885a8cb3-c07b-4856-b448-eeb10bf36236" + }, + "domain": { + "href": "https://api.example.org/v3/domains/0b5f3633-194c-42d2-9408-972366617e0e" + }, + "destinations": { + "href": "https://api.example.org/v3/routes/cbad697f-cac1-48f4-9017-ac08f39dfb31/destinations" + } + } +} \ No newline at end of file diff --git a/tests/fixtures/v3/routes/PATCH_{id}_response.json b/tests/fixtures/v3/routes/PATCH_{id}_response.json new file mode 100644 index 0000000..82d9734 --- /dev/null +++ b/tests/fixtures/v3/routes/PATCH_{id}_response.json @@ -0,0 +1,77 @@ +{ + "guid": "cbad697f-cac1-48f4-9017-ac08f39dfb31", + "protocol": "tcp", + "port": 6666, + "created_at": "2019-05-10T17:17:48Z", + "updated_at": "2019-05-10T17:17:48Z", + "host": "a-hostname", + "path": "/some_path", + "url": "a-hostname.a-domain.com/some_path", + "destinations": [ + { + "guid": "385bf117-17f5-4689-8c5c-08c6cc821fed", + "app": { + "guid": "0a6636b5-7fc4-44d8-8752-0db3e40b35a5", + "process": { + "type": "web" + } + }, + "weight": null, + "port": 8080, + "protocol": "tcp", + "created_at": "2019-05-10T17:17:48Z", + "updated_at": "2019-05-10T17:17:48Z" + }, + { + "guid": "27e96a3b-5bcf-49ed-8048-351e0be23e6f", + "app": { + "guid": "f61e59fa-2121-4217-8c7b-15bfd75baf25", + "process": { + "type": "web" + } + }, + "weight": null, + "port": 8080, + "protocol": "tcp", + "created_at": "2019-05-10T17:17:48Z", + "updated_at": "2019-05-10T17:17:48Z" + } + ], + "options": { + "loadbalancing": "round-robin" + }, + "metadata": { + "labels": { + "key": "value" + }, + "annotations": { + "note": "detailed information" + } + }, + "relationships": { + "space": { + "data": { + "guid": "885a8cb3-c07b-4856-b448-eeb10bf36236" + } + }, + "domain": { + "data": { + "guid": "0b5f3633-194c-42d2-9408-972366617e0e" + } + } + }, + "links": { + "self": { + "href": "https://api.example.org/v3/routes/cbad697f-cac1-48f4-9017-ac08f39dfb31" + }, + "space": { + "href": "https://api.example.org/v3/spaces/885a8cb3-c07b-4856-b448-eeb10bf36236" + }, + "domain": { + "href": "https://api.example.org/v3/domains/0b5f3633-194c-42d2-9408-972366617e0e" + }, + "destinations": { + "href": "https://api.example.org/v3/routes/cbad697f-cac1-48f4-9017-ac08f39dfb31/destinations" + } + } +} \ No newline at end of file diff --git a/tests/fixtures/v3/routes/POST_response.json b/tests/fixtures/v3/routes/POST_response.json new file mode 100644 index 0000000..ec6f099 --- /dev/null +++ b/tests/fixtures/v3/routes/POST_response.json @@ -0,0 +1,73 @@ +{ + "guid": "cbad697f-cac1-48f4-9017-ac08f39dfb31", + "protocol": "tcp", + "port": 6666, + "created_at": "2019-05-10T17:17:48Z", + "updated_at": "2019-05-10T17:17:48Z", + "host": "a-hostname", + "path": "/some_path", + "url": "a-hostname.a-domain.com/some_path", + "destinations": [ + { + "guid": "385bf117-17f5-4689-8c5c-08c6cc821fed", + "app": { + "guid": "0a6636b5-7fc4-44d8-8752-0db3e40b35a5", + "process": { + "type": "web" + } + }, + "weight": null, + "port": 8080, + "protocol": "tcp", + "created_at": "2019-05-10T17:17:48Z", + "updated_at": "2019-05-10T17:17:48Z" + }, + { + "guid": "27e96a3b-5bcf-49ed-8048-351e0be23e6f", + "app": { + "guid": "f61e59fa-2121-4217-8c7b-15bfd75baf25", + "process": { + "type": "web" + } + }, + "weight": null, + "port": 8080, + "protocol": "tcp", + "created_at": "2019-05-10T17:17:48Z", + "updated_at": "2019-05-10T17:17:48Z" + } + ], + "options": { + "loadbalancing": "round-robin" + }, + "metadata": { + "labels": {"key":"value"}, + "annotations": {"note":"detailed information"} + }, + "relationships": { + "space": { + "data": { + "guid": "885a8cb3-c07b-4856-b448-eeb10bf36236" + } + }, + "domain": { + "data": { + "guid": "0b5f3633-194c-42d2-9408-972366617e0e" + } + } + }, + "links": { + "self": { + "href": "https://api.example.org/v3/routes/cbad697f-cac1-48f4-9017-ac08f39dfb31" + }, + "space": { + "href": "https://api.example.org/v3/spaces/885a8cb3-c07b-4856-b448-eeb10bf36236" + }, + "domain": { + "href": "https://api.example.org/v3/domains/0b5f3633-194c-42d2-9408-972366617e0e" + }, + "destinations": { + "href": "https://api.example.org/v3/routes/cbad697f-cac1-48f4-9017-ac08f39dfb31/destinations" + } + } +} \ No newline at end of file diff --git a/tests/v3/test_routes.py b/tests/v3/test_routes.py new file mode 100644 index 0000000..1f9c24d --- /dev/null +++ b/tests/v3/test_routes.py @@ -0,0 +1,142 @@ +import unittest +from http import HTTPStatus + +from abstract_test_case import AbstractTestCase +from cloudfoundry_client.v3.entities import Entity +from cloudfoundry_client.v3.routes import LoadBalancing + + +class TestRoutes(unittest.TestCase, AbstractTestCase): + @classmethod + def setUpClass(cls): + cls.mock_client_class() + + def setUp(self): + self.build_client() + + def test_create(self): + self.client.post.return_value = self.mock_response( + "/v3/routes", + HTTPStatus.OK, + None, + "v3", "routes", "POST_response.json" + ) + result = self.client.v3.routes.create( + space_guid="space-guid", + domain_guid="domain-guid", + host="a-hostname", + path="/some_path", + port=6666, + load_balancing=LoadBalancing.ROUND_ROBIN, + meta_labels={"key": "value"}, + meta_annotations={"note": "detailed information"}, + ) + self.client.post.assert_called_with( + self.client.post.return_value.url, + json={ + "host": "a-hostname", + "path": "/some_path", + "port": 6666, + "relationships": { + "domain": { + "data": {"guid": "domain-guid"} + }, + "space": { + "data": {"guid": "space-guid"} + } + }, + "options": { + "loadbalancing": "round-robin" + }, + "metadata": { + "labels": {"key": "value"}, + "annotations": {"note": "detailed information"} + } + }, + files=None, + ) + self.client.post.assert_called_with( + self.client.post.return_value.url, + files=None, + json={ + "host": "a-hostname", + "path": "/some_path", + "port": 6666, + "relationships": { + "domain": { + "data": {"guid": "domain-guid"} + }, + "space": { + "data": {"guid": "space-guid"} + } + }, + "options": { + "loadbalancing": "round-robin" + }, + "metadata": { + "labels": {"key": "value"}, + "annotations": {"note": "detailed information"} + } + }, + ) + self.assertIsNotNone(result) + self.assertIsInstance(result, Entity) + + def test_list(self): + self.client.get.return_value = self.mock_response( + "/v3/routes", + HTTPStatus.OK, + None, + "v3", "routes", "GET_response.json" + ) + all_routes = [route for route in self.client.v3.routes.list()] + self.client.get.assert_called_with(self.client.get.return_value.url) + self.assertEqual(1, len(all_routes)) + self.assertEqual(all_routes[0]["protocol"], "http") + for route in all_routes: + self.assertIsInstance(route, Entity) + + def test_get(self): + self.client.get.return_value = self.mock_response( + "/v3/routes/route_id", + HTTPStatus.OK, + None, + "v3", "routes", "GET_{id}_response.json" + ) + result = self.client.v3.routes.get("route_id") + self.client.get.assert_called_with(self.client.get.return_value.url) + self.assertIsNotNone(result) + self.assertIsInstance(result, Entity) + + def test_update(self): + self.client.patch.return_value = self.mock_response( + "/v3/routes/route_id", + HTTPStatus.OK, + None, + "v3", "routes", "PATCH_{id}_response.json" + ) + result = self.client.v3.routes.update( + "route_id", + LoadBalancing.LEAST_CONNECTION, + meta_labels={"key": "value"}, + meta_annotations={"note": "detailed information"}, + ) + self.client.patch.assert_called_with( + self.client.patch.return_value.url, + json={ + "options": { + "loadbalancing": "least-connection" + }, + "metadata": { + "labels": {"key": "value"}, + "annotations": {"note": "detailed information"} + } + } + ) + self.assertIsNotNone(result) + self.assertIsInstance(result, Entity) + + def test_remove(self): + self.client.delete.return_value = self.mock_response("/v3/routes/route_id", HTTPStatus.NO_CONTENT, None) + self.client.v3.routes.remove("route_id") + self.client.delete.assert_called_with(self.client.delete.return_value.url)