diff --git a/cloudfoundry_client/client.py b/cloudfoundry_client/client.py index 9163f1b..edeaa04 100644 --- a/cloudfoundry_client/client.py +++ b/cloudfoundry_client/client.py @@ -44,6 +44,7 @@ from cloudfoundry_client.v3.spaces import SpaceManager from cloudfoundry_client.v3.tasks import TaskManager from cloudfoundry_client.v3.jobs import JobManager +from cloudfoundry_client.v3.users import UserManager _logger = logging.getLogger(__name__) @@ -130,6 +131,7 @@ def __init__(self, cloud_controller_v3_url: str, credential_manager: "CloudFound self.service_plans = ServicePlanManager(target_endpoint, credential_manager) self.spaces = SpaceManager(target_endpoint, credential_manager) self.tasks = TaskManager(target_endpoint, credential_manager) + self.users = UserManager(target_endpoint, credential_manager) class CloudFoundryClient(CredentialManager): diff --git a/cloudfoundry_client/v3/buildpacks.py b/cloudfoundry_client/v3/buildpacks.py index ba14c9a..a8b3e68 100644 --- a/cloudfoundry_client/v3/buildpacks.py +++ b/cloudfoundry_client/v3/buildpacks.py @@ -26,8 +26,8 @@ def create( "enabled": enabled, "locked": locked, "stack": stack, - "metadata": {"labels": meta_labels, "annotations": meta_annotations}, } + self._metadata(data, meta_labels, meta_annotations) return super(BuildpackManager, self)._create(data) def remove(self, buildpack_guid: str, asynchronous: bool = True) -> str | None: @@ -50,8 +50,8 @@ def update( "enabled": enabled, "locked": locked, "stack": stack, - "metadata": {"labels": meta_labels, "annotations": meta_annotations}, } + self._metadata(data, meta_labels, meta_annotations) return super(BuildpackManager, self)._update(buildpack_guid, data) def upload(self, buildpack_guid: str, buildpack_zip: str, asynchronous: bool = False) -> Entity: diff --git a/cloudfoundry_client/v3/domains.py b/cloudfoundry_client/v3/domains.py index 9068cd1..aa752a0 100644 --- a/cloudfoundry_client/v3/domains.py +++ b/cloudfoundry_client/v3/domains.py @@ -39,8 +39,8 @@ def create( "organization": organization, "shared_organizations": shared_organizations, }, - "metadata": {"labels": meta_labels, "annotations": meta_annotations}, } + self._metadata(data, meta_labels, meta_annotations) return super(DomainManager, self)._create(data) def list_domains_for_org(self, org_guid: str, **kwargs) -> Pagination[Entity]: diff --git a/cloudfoundry_client/v3/entities.py b/cloudfoundry_client/v3/entities.py index 6ddacd8..be2b257 100644 --- a/cloudfoundry_client/v3/entities.py +++ b/cloudfoundry_client/v3/entities.py @@ -312,3 +312,12 @@ def _append_encoded_parameter(parameters: list[str], args: tuple[str, Any]) -> l return "%s?%s" % (url, "&".join(functools.reduce(_append_encoded_parameter, sorted(list(kwargs.items())), []))) else: return url + + def _metadata(self, data, meta_labels, meta_annotations): + if meta_labels or meta_annotations: + metadata = dict() + if meta_labels: + metadata["labels"] = meta_labels + if meta_annotations: + metadata["annotations"] = meta_annotations + data["metadata"] = metadata diff --git a/cloudfoundry_client/v3/isolation_segments.py b/cloudfoundry_client/v3/isolation_segments.py index e11edb9..772e4cc 100644 --- a/cloudfoundry_client/v3/isolation_segments.py +++ b/cloudfoundry_client/v3/isolation_segments.py @@ -11,13 +11,15 @@ def __init__(self, target_endpoint: str, client: "CloudFoundryClient"): super(IsolationSegmentManager, self).__init__(target_endpoint, client, "/v3/isolation_segments") def create(self, name: str, meta_labels: dict | None = None, meta_annotations: dict | None = None) -> Entity: - data = {"name": name, "metadata": {"labels": meta_labels, "annotations": meta_annotations}} + data = {"name": name} + self._metadata(data, meta_labels, meta_annotations) return super(IsolationSegmentManager, self)._create(data) def update( self, isolation_segment_guid: str, name: str, meta_labels: dict | None = None, meta_annotations: dict | None = None ) -> Entity: - data = {"name": name, "metadata": {"labels": meta_labels, "annotations": meta_annotations}} + data = {"name": name} + self._metadata(data, meta_labels, meta_annotations) return super(IsolationSegmentManager, self)._update(isolation_segment_guid, data) def entitle_organizations(self, isolation_segment_guid: str, *org_guids: str) -> ToManyRelationship: diff --git a/cloudfoundry_client/v3/organizations.py b/cloudfoundry_client/v3/organizations.py index 252411d..18562ee 100644 --- a/cloudfoundry_client/v3/organizations.py +++ b/cloudfoundry_client/v3/organizations.py @@ -13,7 +13,8 @@ def __init__(self, target_endpoint: str, client: "CloudFoundryClient"): def create( self, name: str, suspended: bool, meta_labels: dict | None = None, meta_annotations: dict | None = None ) -> Entity: - data = {"name": name, "suspended": suspended, "metadata": {"labels": meta_labels, "annotations": meta_annotations}} + data = {"name": name, "suspended": suspended} + self._metadata(data, meta_labels, meta_annotations) return super(OrganizationManager, self)._create(data) def update( @@ -27,13 +28,7 @@ def update( data = {"name": name} if suspended is not None: data["suspended"] = suspended - metadata = {} - if meta_labels is not None: - metadata["labels"] = meta_labels - if meta_annotations is not None: - metadata["annotations"] = meta_annotations - if len(metadata) > 0: - data["metadata"] = metadata + self._metadata(data, meta_labels, meta_annotations) return super(OrganizationManager, self)._update(guid, data) def remove(self, guid: str, asynchronous: bool = True) -> str | None: diff --git a/cloudfoundry_client/v3/service_brokers.py b/cloudfoundry_client/v3/service_brokers.py index f1febc6..c507f18 100644 --- a/cloudfoundry_client/v3/service_brokers.py +++ b/cloudfoundry_client/v3/service_brokers.py @@ -22,13 +22,7 @@ def create( ) -> Entity: credentials = {"type": "basic", "credentials": {"username": auth_username, "password": auth_password}} payload = dict(name=name, url=url, authentication=credentials) - if meta_labels or meta_annotations: - metadata = dict() - if meta_labels: - metadata["labels"] = meta_labels - if meta_annotations: - metadata["annotations"] = meta_annotations - payload["metadata"] = metadata + self._metadata(payload, meta_labels, meta_annotations) if space_guid: payload["relationships"] = dict(space=ToOneRelationship(space_guid)) return super(ServiceBrokerManager, self)._create(payload) @@ -50,13 +44,7 @@ def update( payload["url"] = url if auth_username and auth_password: payload["authentication"] = {"type": "basic", "credentials": {"username": auth_username, "password": auth_password}} - if meta_labels or meta_annotations: - metadata = dict() - if meta_labels: - metadata["labels"] = meta_labels - if meta_annotations: - metadata["annotations"] = meta_annotations - payload["metadata"] = metadata + self._metadata(payload, meta_labels, meta_annotations) return super(ServiceBrokerManager, self)._update(guid, payload) def remove(self, guid: str, asynchronous: bool = True) -> str | None: diff --git a/cloudfoundry_client/v3/service_instances.py b/cloudfoundry_client/v3/service_instances.py index d882604..189870e 100644 --- a/cloudfoundry_client/v3/service_instances.py +++ b/cloudfoundry_client/v3/service_instances.py @@ -30,13 +30,7 @@ def create( data["parameters"] = parameters if tags: data["tags"] = tags - if meta_labels or meta_annotations: - metadata = dict() - if meta_labels: - metadata["labels"] = meta_labels - if meta_annotations: - metadata["annotations"] = meta_annotations - data["metadata"] = metadata + self._metadata(data, meta_labels, meta_annotations) return super(ServiceInstanceManager, self)._create(data) def update( @@ -62,17 +56,11 @@ def update( data["maintenance_info"] = {"version": maintenance_info} if tags: data["tags"] = tags - if meta_labels or meta_annotations: - metadata = dict() - if meta_labels: - metadata["labels"] = meta_labels - if meta_annotations: - metadata["annotations"] = meta_annotations - data["metadata"] = metadata - return super(ServiceInstanceManager, self)._update(instance_guid, data) + super()._metadata(data, meta_labels, meta_annotations) + return super()._update(instance_guid, data) def remove(self, guid: str, asynchronous: bool = True): - super(ServiceInstanceManager, self)._remove(guid, asynchronous) + super()._remove(guid, asynchronous) def get_permissions(self, instance_guid: str) -> JsonObject: return super(ServiceInstanceManager, self)._get( diff --git a/cloudfoundry_client/v3/service_offerings.py b/cloudfoundry_client/v3/service_offerings.py index c47ca66..bc067aa 100644 --- a/cloudfoundry_client/v3/service_offerings.py +++ b/cloudfoundry_client/v3/service_offerings.py @@ -12,13 +12,7 @@ def __init__(self, target_endpoint: str, client: "CloudFoundryClient"): def update(self, guid: str, meta_labels: dict | None = None, meta_annotations: dict | None = None) -> Entity: payload = dict() - if meta_labels or meta_annotations: - metadata = dict() - if meta_labels: - metadata["labels"] = meta_labels - if meta_annotations: - metadata["annotations"] = meta_annotations - payload["metadata"] = metadata + self._metadata(payload, meta_labels, meta_annotations) return super(ServiceOfferingsManager, self)._update(guid, payload) def remove(self, guid: str, purge: bool = False) -> None: diff --git a/cloudfoundry_client/v3/service_plans.py b/cloudfoundry_client/v3/service_plans.py index f2333aa..0c6f56a 100644 --- a/cloudfoundry_client/v3/service_plans.py +++ b/cloudfoundry_client/v3/service_plans.py @@ -17,11 +17,7 @@ def update( meta_annotations: dict | None = None, ) -> Entity: payload = {"metadata": {}} - - if meta_labels: - payload["metadata"]["labels"] = meta_labels - if meta_annotations: - payload["metadata"]["annotations"] = meta_annotations + self._metadata(payload, meta_labels, meta_annotations) return super(ServicePlanManager, self)._update(guid, payload) def remove(self, guid: str): diff --git a/cloudfoundry_client/v3/users.py b/cloudfoundry_client/v3/users.py new file mode 100644 index 0000000..431bd19 --- /dev/null +++ b/cloudfoundry_client/v3/users.py @@ -0,0 +1,40 @@ +from typing import TYPE_CHECKING + +from cloudfoundry_client.v3.entities import Entity, EntityManager + +if TYPE_CHECKING: + from cloudfoundry_client.client import CloudFoundryClient + + +class UserManager(EntityManager): + def __init__(self, target_endpoint: str, client: "CloudFoundryClient"): + super(UserManager, self).__init__(target_endpoint, client, "/v3/users") + + def create( + self, + user_info: str | tuple[str, str], + meta_labels: dict | None = None, + meta_annotations: dict | None = None, + ) -> Entity: + data = {} + if isinstance(user_info, str): + data["guid"] = user_info + else: + username, origin = user_info + data["username"] = username + data["origin"] = origin + self._metadata(data, meta_labels, meta_annotations) + return super(UserManager, self)._create(data) + + def update( + self, + guid: str, + meta_labels: dict | None = None, + meta_annotations: dict | None = None, + ) -> Entity: + data = {} + self._metadata(data, meta_labels, meta_annotations) + return super(UserManager, self)._update(guid, data) + + def remove(self, guid: str) -> str | None: + return super(UserManager, self)._remove(guid) diff --git a/tests/fixtures/v3/users/GET_response.json b/tests/fixtures/v3/users/GET_response.json new file mode 100644 index 0000000..de19877 --- /dev/null +++ b/tests/fixtures/v3/users/GET_response.json @@ -0,0 +1,50 @@ +{ + "pagination": { + "total_results": 3, + "total_pages": 2, + "first": { + "href": "https://api.example.org/v3/users?page=1&per_page=2" + }, + "last": { + "href": "https://api.example.org/v3/users?page=2&per_page=2" + }, + "next": null, + "previous": null + }, + "resources": [ + { + "guid": "client_id", + "created_at": "2019-03-08T01:06:19Z", + "updated_at": "2019-03-08T01:06:19Z", + "username": null, + "presentation_name": "client_id", + "origin": null, + "metadata": { + "labels": {}, + "annotations": {} + }, + "links": { + "self": { + "href": "https://api.example.org/v3/users/3a5d3d89-3f89-4f05-8188-8a2b298c79d5" + } + } + }, + { + "guid": "9da93b89-3f89-4f05-7238-8a2b123c79l9", + "created_at": "2019-03-08T01:06:19Z", + "updated_at": "2019-03-08T01:06:19Z", + "username": "some-name", + "presentation_name": "some-name", + "origin": "uaa", + "metadata": { + "labels": {}, + "annotations": {} + }, + "links": { + "self": { + "href": "https://api.example.org/v3/users/9da93b89-3f89-4f05-7238-8a2b123c79l9" + } + } + } + ] +} diff --git a/tests/fixtures/v3/users/GET_{id}_response.json b/tests/fixtures/v3/users/GET_{id}_response.json new file mode 100644 index 0000000..b60ce5a --- /dev/null +++ b/tests/fixtures/v3/users/GET_{id}_response.json @@ -0,0 +1,17 @@ +{ + "guid": "3a5d3d89-3f89-4f05-8188-8a2b298c79d5", + "created_at": "2019-03-08T01:06:19Z", + "updated_at": "2019-03-08T01:06:19Z", + "username": "some-name", + "presentation_name": "some-name", + "origin": "uaa", + "metadata": { + "labels": {}, + "annotations": {} + }, + "links": { + "self": { + "href": "https://api.example.org/v3/users/3a5d3d89-3f89-4f05-8188-8a2b298c79d5" + } + } +} \ No newline at end of file diff --git a/tests/fixtures/v3/users/PATCH_{id}_response.json b/tests/fixtures/v3/users/PATCH_{id}_response.json new file mode 100644 index 0000000..d516fb4 --- /dev/null +++ b/tests/fixtures/v3/users/PATCH_{id}_response.json @@ -0,0 +1,21 @@ +{ + "guid": "3a5d3d89-3f89-4f05-8188-8a2b298c79d5", + "created_at": "2019-03-08T01:06:19Z", + "updated_at": "2019-03-08T01:06:19Z", + "username": "some-name", + "presentation_name": "some-name", + "origin": "uaa", + "metadata": { + "labels": { + "enviroment": "production" + }, + "annotations": { + "note": "detailed information" + } + }, + "links": { + "self": { + "href": "https://api.example.org/v3/users/3a5d3d89-3f89-4f05-8188-8a2b298c79d5" + } + } +} diff --git a/tests/fixtures/v3/users/POST_response.json b/tests/fixtures/v3/users/POST_response.json new file mode 100644 index 0000000..b60ce5a --- /dev/null +++ b/tests/fixtures/v3/users/POST_response.json @@ -0,0 +1,17 @@ +{ + "guid": "3a5d3d89-3f89-4f05-8188-8a2b298c79d5", + "created_at": "2019-03-08T01:06:19Z", + "updated_at": "2019-03-08T01:06:19Z", + "username": "some-name", + "presentation_name": "some-name", + "origin": "uaa", + "metadata": { + "labels": {}, + "annotations": {} + }, + "links": { + "self": { + "href": "https://api.example.org/v3/users/3a5d3d89-3f89-4f05-8188-8a2b298c79d5" + } + } +} \ No newline at end of file diff --git a/tests/v3/test_buildpacks.py b/tests/v3/test_buildpacks.py index d7d9ba5..cec5228 100644 --- a/tests/v3/test_buildpacks.py +++ b/tests/v3/test_buildpacks.py @@ -47,7 +47,6 @@ def test_update(self): "enabled": True, "position": 42, "stack": "windows64", - "metadata": {"labels": None, "annotations": None}, }, ) self.assertIsNotNone(result) @@ -66,7 +65,6 @@ def test_create(self): "enabled": True, "position": 42, "stack": "windows64", - "metadata": {"labels": None, "annotations": None}, }, ) self.assertIsNotNone(result) diff --git a/tests/v3/test_domains.py b/tests/v3/test_domains.py index 4e6c8dc..57a28f9 100644 --- a/tests/v3/test_domains.py +++ b/tests/v3/test_domains.py @@ -76,7 +76,6 @@ def test_create(self): ] }, }, - "metadata": {"labels": None, "annotations": None}, }, ) self.assertIsNotNone(result) diff --git a/tests/v3/test_isolation_segments.py b/tests/v3/test_isolation_segments.py index 2c26bf1..344da89 100644 --- a/tests/v3/test_isolation_segments.py +++ b/tests/v3/test_isolation_segments.py @@ -51,7 +51,7 @@ def test_update(self): result = self.client.v3.isolation_segments.update("isolation_segment_id", "new-name", meta_labels=dict(key="value")) self.client.patch.assert_called_with( self.client.patch.return_value.url, - json={"name": "new-name", "metadata": {"labels": {"key": "value"}, "annotations": None}}, + json={"name": "new-name", "metadata": {"labels": {"key": "value"}}}, ) self.assertIsNotNone(result) self.assertIsInstance(result, Entity) diff --git a/tests/v3/test_organizations.py b/tests/v3/test_organizations.py index edc08b4..e8c3f73 100644 --- a/tests/v3/test_organizations.py +++ b/tests/v3/test_organizations.py @@ -78,7 +78,7 @@ def test_create(self): self.client.post.assert_called_with( self.client.post.return_value.url, files=None, - json={"name": "my-organization", "suspended": False, "metadata": {"labels": None, "annotations": None}}, + json={"name": "my-organization", "suspended": False}, ) self.assertIsNotNone(result) diff --git a/tests/v3/test_users.py b/tests/v3/test_users.py new file mode 100644 index 0000000..a295e1e --- /dev/null +++ b/tests/v3/test_users.py @@ -0,0 +1,73 @@ +import unittest +from http import HTTPStatus + +from abstract_test_case import AbstractTestCase +from cloudfoundry_client.v3.entities import Entity + + +class TestUsers(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/users", HTTPStatus.OK, None, "v3", "users", "POST_response.json" + ) + result = self.client.v3.users.create("3a5d3d89-3f89-4f05-8188-8a2b298c79d5") + self.client.post.assert_called_with( + self.client.post.return_value.url, + files=None, + json={"guid": "3a5d3d89-3f89-4f05-8188-8a2b298c79d5"}, + ) + self.assertIsNotNone(result) + self.assertIsInstance(result, Entity) + + def test_list(self): + self.client.get.return_value = self.mock_response( + "/v3/users", HTTPStatus.OK, None, "v3", "users", "GET_response.json" + ) + all_users = [user for user in self.client.v3.users.list()] + self.client.get.assert_called_with(self.client.get.return_value.url) + self.assertEqual(2, len(all_users)) + self.assertEqual(all_users[0]["guid"], "client_id") + self.assertIsInstance(all_users[0], Entity) + + def test_get(self): + self.client.get.return_value = self.mock_response( + "/v3/users/user_id", HTTPStatus.OK, None, "v3", "users", "GET_{id}_response.json" + ) + user = self.client.v3.users.get("user_id") + self.client.get.assert_called_with(self.client.get.return_value.url) + self.assertEqual("uaa", user["origin"]) + self.assertIsInstance(user, Entity) + + def test_update(self): + self.client.patch.return_value = self.mock_response( + "/v3/users/user_id", HTTPStatus.OK, None, "v3", "users", "PATCH_{id}_response.json" + ) + result = self.client.v3.users.update( + "user_id", + {"environment": "production"}, + {"note": "detailed information"} + ) + + self.client.patch.assert_called_with( + self.client.patch.return_value.url, + json={ + "metadata": { + "labels": {"environment": "production"}, + "annotations": {"note": "detailed information"} + } + }, + ) + self.assertIsNotNone(result) + self.assertIsInstance(result, Entity) + + def test_remove(self): + self.client.delete.return_value = self.mock_response("/v3/users/user_id", HTTPStatus.NO_CONTENT, None) + self.client.v3.users.remove("user_id") + self.client.delete.assert_called_with(self.client.delete.return_value.url)