From ff8f2917b27345602957af7512c1d40878183a37 Mon Sep 17 00:00:00 2001 From: Benjamin Einaudi Date: Fri, 9 Jan 2026 22:11:26 +0100 Subject: [PATCH] feature(v3) add droplets + fixes list app routes + initiate packages See #280 --- cloudfoundry_client/client.py | 4 + cloudfoundry_client/v3/apps.py | 9 +- cloudfoundry_client/v3/droplets.py | 57 ++++++ cloudfoundry_client/v3/entities.py | 9 +- cloudfoundry_client/v3/packages.py | 16 ++ .../v3/apps/GET_{id}_droplets_response.json | 119 +++++++++++++ .../v3/apps/GET_{id}_routes_response.json | 166 +++++++++--------- tests/fixtures/v3/droplets/GET_response.json | 119 +++++++++++++ .../v3/droplets/GET_{id}_response.json | 59 +++++++ .../v3/droplets/PATCH_{id}_response.json | 63 +++++++ tests/fixtures/v3/droplets/POST_response.json | 57 ++++++ tests/fixtures/v3/packages/GET_response.json | 88 ++++++++++ .../packages/GET_{id}_droplets_response.json | 119 +++++++++++++ .../v3/packages/GET_{id}_response.json | 41 +++++ tests/v3/test_apps.py | 17 +- tests/v3/test_droplets.py | 136 ++++++++++++++ tests/v3/test_packages.py | 48 +++++ tests/v3/test_routes.py | 24 --- 18 files changed, 1040 insertions(+), 111 deletions(-) create mode 100644 cloudfoundry_client/v3/droplets.py create mode 100644 cloudfoundry_client/v3/packages.py create mode 100644 tests/fixtures/v3/apps/GET_{id}_droplets_response.json create mode 100644 tests/fixtures/v3/droplets/GET_response.json create mode 100644 tests/fixtures/v3/droplets/GET_{id}_response.json create mode 100644 tests/fixtures/v3/droplets/PATCH_{id}_response.json create mode 100644 tests/fixtures/v3/droplets/POST_response.json create mode 100644 tests/fixtures/v3/packages/GET_response.json create mode 100644 tests/fixtures/v3/packages/GET_{id}_droplets_response.json create mode 100644 tests/fixtures/v3/packages/GET_{id}_response.json create mode 100644 tests/v3/test_droplets.py create mode 100644 tests/v3/test_packages.py diff --git a/cloudfoundry_client/client.py b/cloudfoundry_client/client.py index 80212ae..98d16f4 100644 --- a/cloudfoundry_client/client.py +++ b/cloudfoundry_client/client.py @@ -30,9 +30,11 @@ from cloudfoundry_client.v3.apps import AppManager from cloudfoundry_client.v3.buildpacks import BuildpackManager from cloudfoundry_client.v3.domains import DomainManager +from cloudfoundry_client.v3.droplets import DropletManager from cloudfoundry_client.v3.feature_flags import FeatureFlagManager from cloudfoundry_client.v3.isolation_segments import IsolationSegmentManager from cloudfoundry_client.v3.organization_quotas import OrganizationQuotaManager +from cloudfoundry_client.v3.packages import PackageManager from cloudfoundry_client.v3.processes import ProcessManager from cloudfoundry_client.v3.organizations import OrganizationManager from cloudfoundry_client.v3.roles import RoleManager @@ -118,11 +120,13 @@ def __init__(self, cloud_controller_v3_url: str, credential_manager: "CloudFound self.apps = AppManager(target_endpoint, credential_manager) self.buildpacks = BuildpackManager(target_endpoint, credential_manager) self.domains = DomainManager(target_endpoint, credential_manager) + self.droplets = DropletManager(target_endpoint, credential_manager) self.feature_flags = FeatureFlagManager(target_endpoint, credential_manager) self.isolation_segments = IsolationSegmentManager(target_endpoint, credential_manager) self.jobs = JobManager(target_endpoint, credential_manager) self.organizations = OrganizationManager(target_endpoint, credential_manager) self.organization_quotas = OrganizationQuotaManager(target_endpoint, credential_manager) + self.packages = PackageManager(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) diff --git a/cloudfoundry_client/v3/apps.py b/cloudfoundry_client/v3/apps.py index 46b9dce..50eb5ca 100644 --- a/cloudfoundry_client/v3/apps.py +++ b/cloudfoundry_client/v3/apps.py @@ -22,8 +22,13 @@ def remove(self, application_guid: str, asynchronous: bool = True) -> str | None def get_env(self, application_guid: str) -> JsonObject: return super(AppManager, self)._get("%s%s/%s/env" % (self.target_endpoint, self.entity_uri, application_guid)) - def get_routes(self, application_guid: str) -> JsonObject: - return super(AppManager, self)._get("%s%s/%s/routes" % (self.target_endpoint, self.entity_uri, application_guid)) + def list_routes(self, application_guid: str, **kwargs) -> Pagination[Entity]: + uri: str = "%s/%s/routes" % (self.entity_uri, application_guid) + return super(AppManager, self)._list(requested_path=uri, **kwargs) + + def list_droplets(self, application_guid: str, **kwargs) -> Pagination[Entity]: + uri: str = "%s/%s/droplets" % (self.entity_uri, application_guid) + return super(AppManager, self)._list(requested_path=uri, **kwargs) def get_manifest(self, application_guid: str) -> str: return self.client.get(url="%s%s/%s/manifest" % (self.target_endpoint, self.entity_uri, application_guid)).text diff --git a/cloudfoundry_client/v3/droplets.py b/cloudfoundry_client/v3/droplets.py new file mode 100644 index 0000000..7f4bb69 --- /dev/null +++ b/cloudfoundry_client/v3/droplets.py @@ -0,0 +1,57 @@ +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 DropletManager(EntityManager[Entity]): + def __init__(self, target_endpoint: str, client: "CloudFoundryClient"): + super(DropletManager, self).__init__(target_endpoint, client, "/v3/droplets") + + def create(self, + app_guid: str, + process_types: dict[str, str] | None = None, + meta_labels: dict | None = None, + meta_annotations: dict | None = None, + ) -> Entity: + data: dict[str, Any] = { + "relationships": { + "app": ToOneRelationship(app_guid) + }, + } + if process_types is not None: + data["process_types"] = process_types + self._metadata(data, meta_labels, meta_annotations) + return super(DropletManager, self)._create(data) + + def copy(self, + droplet_guid: str, + app_guid: str, + meta_labels: dict | None = None, + meta_annotations: dict | None = None, + ) -> Entity: + data: dict[str, Any] = { + "relationships": { + "app": ToOneRelationship(app_guid) + }, + } + self._metadata(data, meta_labels, meta_annotations) + url = EntityManager._get_url_with_encoded_params( + "%s%s" % (self.target_endpoint, self.entity_uri), + source_guid=droplet_guid + ) + return self._post(url, data=data) + + def update(self, + droplet_gid: str, + meta_labels: dict | None = None, + meta_annotations: dict | None = None, + ) -> Entity: + data: dict[str, Any] = {} + self._metadata(data, meta_labels, meta_annotations) + return super(DropletManager, self)._update(droplet_gid, data) + + def remove(self, route_gid: str): + return super(DropletManager, self)._remove(route_gid) diff --git a/cloudfoundry_client/v3/entities.py b/cloudfoundry_client/v3/entities.py index 8a7ba6f..e7924ac 100644 --- a/cloudfoundry_client/v3/entities.py +++ b/cloudfoundry_client/v3/entities.py @@ -135,11 +135,16 @@ def _get(self, url: str, entity_type: type[ENTITY_TYPE] | None = None, **kwargs) def _post( self, url: str, - data: dict | None = None, entity_type: type[ENTITY_TYPE] | None = None, + data: dict | None = None, + params: dict | None = None, files: Any = None ) -> ENTITY_TYPE: - response = self.client.post(url, json=data, files=files) + response = self.client.post( + url if params is None else EntityManager._get_url_with_encoded_params(url, **params), + json=data, + files=files + ) return self._read_response(response, entity_type) def _put(self, url: str, data: dict, entity_type: type[ENTITY_TYPE] | None = None) -> ENTITY_TYPE: diff --git a/cloudfoundry_client/v3/packages.py b/cloudfoundry_client/v3/packages.py new file mode 100644 index 0000000..9e8e3d3 --- /dev/null +++ b/cloudfoundry_client/v3/packages.py @@ -0,0 +1,16 @@ +from typing import TYPE_CHECKING + +from cloudfoundry_client.common_objects import Pagination +from cloudfoundry_client.v3.entities import EntityManager, Entity + +if TYPE_CHECKING: + from cloudfoundry_client.client import CloudFoundryClient + + +class PackageManager(EntityManager[Entity]): + def __init__(self, target_endpoint: str, client: "CloudFoundryClient"): + super(PackageManager, self).__init__(target_endpoint, client, "/v3/packages") + + def list_droplets(self, package_guid: str, **kwargs) -> Pagination[Entity]: + uri: str = "%s/%s/droplets" % (self.entity_uri, package_guid) + return super(PackageManager, self)._list(requested_path=uri, **kwargs) diff --git a/tests/fixtures/v3/apps/GET_{id}_droplets_response.json b/tests/fixtures/v3/apps/GET_{id}_droplets_response.json new file mode 100644 index 0000000..578a68b --- /dev/null +++ b/tests/fixtures/v3/apps/GET_{id}_droplets_response.json @@ -0,0 +1,119 @@ +{ + "pagination": { + "total_results": 2, + "total_pages": 1, + "first": { + "href": "https://api.example.org/v3/app/7b34f1cf-7e73-428a-bb5a-8a17a8058396/droplets?page=1&per_page=50" + }, + "last": { + "href": "https://api.example.org/v3/app/7b34f1cf-7e73-428a-bb5a-8a17a8058396/droplets?page=1&per_page=50" + }, + "next": null, + "previous": null + }, + "resources": [ + { + "guid": "585bc3c1-3743-497d-88b0-403ad6b56d16", + "state": "STAGED", + "error": null, + "lifecycle": { + "type": "buildpack", + "data": {} + }, + "image": null, + "execution_metadata": "PRIVATE DATA HIDDEN", + "process_types": { + "redacted_message": "[PRIVATE DATA HIDDEN IN LISTS]" + }, + "checksum": { + "type": "sha256", + "value": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "buildpacks": [ + { + "name": "ruby_buildpack", + "detect_output": "ruby 1.6.14", + "version": "1.1.1.", + "buildpack_name": "ruby" + } + ], + "stack": "cflinuxfs4", + "created_at": "2016-03-28T23:39:34Z", + "updated_at": "2016-03-28T23:39:47Z", + "relationships": { + "app": { + "data": { + "guid": "7b34f1cf-7e73-428a-bb5a-8a17a8058396" + } + } + }, + "links": { + "self": { + "href": "https://api.example.org/v3/droplets/585bc3c1-3743-497d-88b0-403ad6b56d16" + }, + "package": { + "href": "https://api.example.org/v3/packages/8222f76a-9e09-4360-b3aa-1ed329945e92" + }, + "app": { + "href": "https://api.example.org/v3/apps/7b34f1cf-7e73-428a-bb5a-8a17a8058396" + }, + "assign_current_droplet": { + "href": "https://api.example.org/v3/apps/7b34f1cf-7e73-428a-bb5a-8a17a8058396/relationships/current_droplet", + "method": "PATCH" + }, + "download": { + "href": "https://api.example.org/v3/droplets/585bc3c1-3743-497d-88b0-403ad6b56d16/download" + } + }, + "metadata": { + "labels": {}, + "annotations": {} + } + }, + { + "guid": "fdf3851c-def8-4de1-87f1-6d4543189e22", + "state": "STAGED", + "error": null, + "lifecycle": { + "type": "docker", + "data": {} + }, + "execution_metadata": "[PRIVATE DATA HIDDEN IN LISTS]", + "process_types": { + "redacted_message": "[PRIVATE DATA HIDDEN IN LISTS]" + }, + "image": "cloudfoundry/diego-docker-app-custom:latest", + "checksum": null, + "buildpacks": null, + "stack": null, + "created_at": "2016-03-17T00:00:01Z", + "updated_at": "2016-03-17T21:41:32Z", + "relationships": { + "app": { + "data": { + "guid": "7b34f1cf-7e73-428a-bb5a-8a17a8058396" + } + } + }, + "links": { + "self": { + "href": "https://api.example.org/v3/droplets/fdf3851c-def8-4de1-87f1-6d4543189e22" + }, + "package": { + "href": "https://api.example.org/v3/packages/c5725684-a02f-4e59-bc67-8f36ae944688" + }, + "app": { + "href": "https://api.example.org/v3/apps/7b34f1cf-7e73-428a-bb5a-8a17a8058396" + }, + "assign_current_droplet": { + "href": "https://api.example.org/v3/apps/7b34f1cf-7e73-428a-bb5a-8a17a8058396/relationships/current_droplet", + "method": "PATCH" + } + }, + "metadata": { + "labels": {}, + "annotations": {} + } + } + ] +} \ No newline at end of file diff --git a/tests/fixtures/v3/apps/GET_{id}_routes_response.json b/tests/fixtures/v3/apps/GET_{id}_routes_response.json index 5a54cf0..9f8d95a 100644 --- a/tests/fixtures/v3/apps/GET_{id}_routes_response.json +++ b/tests/fixtures/v3/apps/GET_{id}_routes_response.json @@ -1,80 +1,88 @@ { - "pagination": { - "total_results": 3, - "total_pages": 2, - "first": { - "href": "http://somewhere.org/v3/apps/1cb006ee-fb05-47e1-b541-c34179ddc446/routes?page=1&per_page=2" - }, - "last": { - "href": "http://somewhere.org/v3/apps/1cb006ee-fb05-47e1-b541-c34179ddc446/routes?page=2&per_page=2" - }, - "next": { - "href": "http://somewhere.org/v3/apps/1cb006ee-fb05-47e1-b541-c34179ddc446/routes?page=2&per_page=2" - }, - "previous": null - }, - "resources": [ - { - "guid": "cbad697f-cac1-48f4-9017-ac08f39dfb31", - "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 - }, - { - "guid": "27e96a3b-5bcf-49ed-8048-351e0be23e6f", - "app": { - "guid": "f61e59fa-2121-4217-8c7b-15bfd75baf25", - "process": { - "type": "web" - } - }, - "weight": null, - "port": 8080 - } - ], - "metadata": { - "labels": {}, - "annotations": {} - }, - "relationships": { - "space": { - "data": { - "guid": "885a8cb3-c07b-4856-b448-eeb10bf36236" - } - }, - "domain": { - "data": { - "guid": "0b5f3633-194c-42d2-9408-972366617e0e" - } - } - }, - "links": { - "self": { - "href": "http://somewhere.org/v3/routes/cbad697f-cac1-48f4-9017-ac08f39dfb31" - }, - "space": { - "href": "http://somewhere.org/v3/spaces/885a8cb3-c07b-4856-b448-eeb10bf36236" - }, - "domain": { - "href": "http://somewhere.org/v3/domains/0b5f3633-194c-42d2-9408-972366617e0e" - }, - "destinations": { - "href": "http://somewhere.org/v3/routes/cbad697f-cac1-48f4-9017-ac08f39dfb31/destinations" - } - } - } - ] -} + "pagination": { + "total_results": 3, + "total_pages": 2, + "first": { + "href": "https://api.example.org/v3/apps/1cb006ee-fb05-47e1-b541-c34179ddc446/routes?page=1&per_page=2" + }, + "last": { + "href": "https://api.example.org/v3/apps/1cb006ee-fb05-47e1-b541-c34179ddc446/routes?page=2&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" + } + } + } + ] +} \ No newline at end of file diff --git a/tests/fixtures/v3/droplets/GET_response.json b/tests/fixtures/v3/droplets/GET_response.json new file mode 100644 index 0000000..4d3121f --- /dev/null +++ b/tests/fixtures/v3/droplets/GET_response.json @@ -0,0 +1,119 @@ +{ + "pagination": { + "total_results": 2, + "total_pages": 1, + "first": { + "href": "https://api.example.org/v3/droplets?page=1&per_page=50" + }, + "last": { + "href": "https://api.example.org/v3/droplets?page=1&per_page=50" + }, + "next": null, + "previous": null + }, + "resources": [ + { + "guid": "585bc3c1-3743-497d-88b0-403ad6b56d16", + "state": "STAGED", + "error": null, + "lifecycle": { + "type": "buildpack", + "data": {} + }, + "image": null, + "execution_metadata": "PRIVATE DATA HIDDEN", + "process_types": { + "redacted_message": "[PRIVATE DATA HIDDEN IN LISTS]" + }, + "checksum": { + "type": "sha256", + "value": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "buildpacks": [ + { + "name": "ruby_buildpack", + "detect_output": "ruby 1.6.14", + "version": "1.1.1.", + "buildpack_name": "ruby" + } + ], + "stack": "cflinuxfs4", + "created_at": "2016-03-28T23:39:34Z", + "updated_at": "2016-03-28T23:39:47Z", + "relationships": { + "app": { + "data": { + "guid": "7b34f1cf-7e73-428a-bb5a-8a17a8058396" + } + } + }, + "links": { + "self": { + "href": "https://api.example.org/v3/droplets/585bc3c1-3743-497d-88b0-403ad6b56d16" + }, + "package": { + "href": "https://api.example.org/v3/packages/8222f76a-9e09-4360-b3aa-1ed329945e92" + }, + "app": { + "href": "https://api.example.org/v3/apps/7b34f1cf-7e73-428a-bb5a-8a17a8058396" + }, + "assign_current_droplet": { + "href": "https://api.example.org/v3/apps/7b34f1cf-7e73-428a-bb5a-8a17a8058396/relationships/current_droplet", + "method": "PATCH" + }, + "download": { + "href": "https://api.example.org/v3/droplets/585bc3c1-3743-497d-88b0-403ad6b56d16/download" + } + }, + "metadata": { + "labels": {}, + "annotations": {} + } + }, + { + "guid": "fdf3851c-def8-4de1-87f1-6d4543189e22", + "state": "STAGED", + "error": null, + "lifecycle": { + "type": "docker", + "data": {} + }, + "execution_metadata": "[PRIVATE DATA HIDDEN IN LISTS]", + "process_types": { + "redacted_message": "[PRIVATE DATA HIDDEN IN LISTS]" + }, + "image": "cloudfoundry/diego-docker-app-custom:latest", + "checksum": null, + "buildpacks": null, + "stack": null, + "created_at": "2016-03-17T00:00:01Z", + "updated_at": "2016-03-17T21:41:32Z", + "relationships": { + "app": { + "data": { + "guid": "7b34f1cf-7e73-428a-bb5a-8a17a8058396" + } + } + }, + "links": { + "self": { + "href": "https://api.example.org/v3/droplets/fdf3851c-def8-4de1-87f1-6d4543189e22" + }, + "package": { + "href": "https://api.example.org/v3/packages/c5725684-a02f-4e59-bc67-8f36ae944688" + }, + "app": { + "href": "https://api.example.org/v3/apps/7b34f1cf-7e73-428a-bb5a-8a17a8058396" + }, + "assign_current_droplet": { + "href": "https://api.example.org/v3/apps/7b34f1cf-7e73-428a-bb5a-8a17a8058396/relationships/current_droplet", + "method": "PATCH" + } + }, + "metadata": { + "labels": {}, + "annotations": {} + } + } + ] +} diff --git a/tests/fixtures/v3/droplets/GET_{id}_response.json b/tests/fixtures/v3/droplets/GET_{id}_response.json new file mode 100644 index 0000000..8c2ab9c --- /dev/null +++ b/tests/fixtures/v3/droplets/GET_{id}_response.json @@ -0,0 +1,59 @@ +{ + "guid": "585bc3c1-3743-497d-88b0-403ad6b56d16", + "state": "STAGED", + "error": null, + "lifecycle": { + "type": "buildpack", + "data": {} + }, + "execution_metadata": "", + "process_types": { + "rake": "bundle exec rake", + "web": "bundle exec rackup config.ru -p $PORT" + }, + "checksum": { + "type": "sha256", + "value": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "buildpacks": [ + { + "name": "ruby_buildpack", + "detect_output": "ruby 1.6.14", + "version": "1.1.1.", + "buildpack_name": "ruby" + } + ], + "stack": "cflinuxfs4", + "image": null, + "created_at": "2016-03-28T23:39:34Z", + "updated_at": "2016-03-28T23:39:47Z", + "relationships": { + "app": { + "data": { + "guid": "7b34f1cf-7e73-428a-bb5a-8a17a8058396" + } + } + }, + "links": { + "self": { + "href": "https://api.example.org/v3/droplets/585bc3c1-3743-497d-88b0-403ad6b56d16" + }, + "package": { + "href": "https://api.example.org/v3/packages/8222f76a-9e09-4360-b3aa-1ed329945e92" + }, + "app": { + "href": "https://api.example.org/v3/apps/7b34f1cf-7e73-428a-bb5a-8a17a8058396" + }, + "assign_current_droplet": { + "href": "https://api.example.org/v3/apps/7b34f1cf-7e73-428a-bb5a-8a17a8058396/relationships/current_droplet", + "method": "PATCH" + }, + "download": { + "href": "https://api.example.org/v3/droplets/585bc3c1-3743-497d-88b0-403ad6b56d16/download" + } + }, + "metadata": { + "labels": {}, + "annotations": {} + } +} \ No newline at end of file diff --git a/tests/fixtures/v3/droplets/PATCH_{id}_response.json b/tests/fixtures/v3/droplets/PATCH_{id}_response.json new file mode 100644 index 0000000..e3d6353 --- /dev/null +++ b/tests/fixtures/v3/droplets/PATCH_{id}_response.json @@ -0,0 +1,63 @@ +{ + "guid": "585bc3c1-3743-497d-88b0-403ad6b56d16", + "state": "STAGED", + "error": null, + "lifecycle": { + "type": "buildpack", + "data": {} + }, + "execution_metadata": "", + "process_types": { + "rake": "bundle exec rake", + "web": "bundle exec rackup config.ru -p $PORT" + }, + "checksum": { + "type": "sha256", + "value": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "buildpacks": [ + { + "name": "ruby_buildpack", + "detect_output": "ruby 1.6.14", + "version": "1.1.1.", + "buildpack_name": "ruby" + } + ], + "stack": "cflinuxfs4", + "image": null, + "created_at": "2016-03-28T23:39:34Z", + "updated_at": "2016-03-28T23:39:47Z", + "relationships": { + "app": { + "data": { + "guid": "7b34f1cf-7e73-428a-bb5a-8a17a8058396" + } + } + }, + "links": { + "self": { + "href": "https://api.example.org/v3/droplets/585bc3c1-3743-497d-88b0-403ad6b56d16" + }, + "package": { + "href": "https://api.example.org/v3/packages/8222f76a-9e09-4360-b3aa-1ed329945e92" + }, + "app": { + "href": "https://api.example.org/v3/apps/7b34f1cf-7e73-428a-bb5a-8a17a8058396" + }, + "assign_current_droplet": { + "href": "https://api.example.org/v3/apps/7b34f1cf-7e73-428a-bb5a-8a17a8058396/relationships/current_droplet", + "method": "PATCH" + }, + "download": { + "href": "https://api.example.org/v3/droplets/585bc3c1-3743-497d-88b0-403ad6b56d16/download" + } + }, + "metadata": { + "labels": { + "release": "stable" + }, + "annotations": { + "note": "detailed information" + } + } + } \ No newline at end of file diff --git a/tests/fixtures/v3/droplets/POST_response.json b/tests/fixtures/v3/droplets/POST_response.json new file mode 100644 index 0000000..9707e9b --- /dev/null +++ b/tests/fixtures/v3/droplets/POST_response.json @@ -0,0 +1,57 @@ +{ + "guid": "585bc3c1-3743-497d-88b0-403ad6b56d16", + "state": "AWAITING_UPLOAD", + "error": null, + "lifecycle": { + "type": "buildpack", + "data": {} + }, + "execution_metadata": "", + "process_types": { + "rake": "bundle exec rake", + "web": "bundle exec rackup config.ru -p $PORT" + }, + "checksum": { + "type": "sha256", + "value": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "buildpacks": [ + { + "name": "ruby_buildpack", + "detect_output": "ruby 1.6.14", + "version": "1.1.1.", + "buildpack_name": "ruby" + } + ], + "stack": "cflinuxfs4", + "image": null, + "created_at": "2016-03-28T23:39:34Z", + "updated_at": "2016-03-28T23:39:47Z", + "relationships": { + "app": { + "data": { + "guid": "7b34f1cf-7e73-428a-bb5a-8a17a8058396" + } + } + }, + "links": { + "self": { + "href": "https://api.example.org/v3/droplets/585bc3c1-3743-497d-88b0-403ad6b56d16" + }, + "app": { + "href": "https://api.example.org/v3/apps/7b34f1cf-7e73-428a-bb5a-8a17a8058396" + }, + "assign_current_droplet": { + "href": "https://api.example.org/v3/apps/7b34f1cf-7e73-428a-bb5a-8a17a8058396/relationships/current_droplet", + "method": "PATCH" + }, + "upload": { + "href": "https://api.example.org/v3/droplets/585bc3c1-3743-497d-88b0-403ad6b56d16/upload", + "method": "POST" + } + }, + "metadata": { + "labels": {}, + "annotations": {} + } +} \ No newline at end of file diff --git a/tests/fixtures/v3/packages/GET_response.json b/tests/fixtures/v3/packages/GET_response.json new file mode 100644 index 0000000..89964d7 --- /dev/null +++ b/tests/fixtures/v3/packages/GET_response.json @@ -0,0 +1,88 @@ +{ + "pagination": { + "total_results": 2, + "total_pages": 1, + "first": { + "href": "https://api.example.org/v3/packages?types=bits%2Cdocker&page=1&per_page=2" + }, + "last": { + "href": "https://api.example.org/v3/packages?types=bits%2Cdocker&page=1&per_page=2" + }, + "next": null, + "previous": null + }, + "resources": [ + { + "guid": "a57fd932-85db-483a-a27e-b00efbb3b0a4", + "type": "bits", + "data": { + "checksum": { + "type": "sha256", + "value": null + }, + "error": null + }, + "state": "AWAITING_UPLOAD", + "created_at": "2015-11-03T00:53:54Z", + "updated_at": "2016-06-08T16:41:26Z", + "relationships": { + "app": { + "data": { + "guid": "fa3558ce-1c4d-46fc-9776-54b9c8021745" + } + } + }, + "links": { + "self": { + "href": "https://api.example.org/v3/packages/a57fd932-85db-483a-a27e-b00efbb3b0a4" + }, + "upload": { + "href": "https://api.example.org/v3/packages/a57fd932-85db-483a-a27e-b00efbb3b0a4/upload", + "method": "POST" + }, + "download": { + "href": "https://api.example.org/v3/packages/a57fd932-85db-483a-a27e-b00efbb3b0a4/download", + "method": "GET" + }, + "app": { + "href": "https://api.example.org/v3/apps/fa3558ce-1c4d-46fc-9776-54b9c8021745" + } + }, + "metadata": { + "labels": {}, + "annotations": {} + } + }, + { + "guid": "8f1f294d-cef8-4c11-9f0b-3bcdc0bd2691", + "type": "docker", + "data": { + "image": "registry/image:latest", + "username": "username", + "password": "***" + }, + "state": "READY", + "created_at": "2015-11-03T00:53:54Z", + "updated_at": "2016-06-08T16:41:26Z", + "relationships": { + "app": { + "data": { + "guid": "fa3558ce-1c4d-46fc-9776-54b9c8021745" + } + } + }, + "links": { + "self": { + "href": "https://api.example.org/v3/packages/8f1f294d-cef8-4c11-9f0b-3bcdc0bd2691" + }, + "app": { + "href": "https://api.example.org/v3/apps/fa3558ce-1c4d-46fc-9776-54b9c8021745" + } + }, + "metadata": { + "labels": {}, + "annotations": {} + } + } + ] +} \ No newline at end of file diff --git a/tests/fixtures/v3/packages/GET_{id}_droplets_response.json b/tests/fixtures/v3/packages/GET_{id}_droplets_response.json new file mode 100644 index 0000000..b8d2c83 --- /dev/null +++ b/tests/fixtures/v3/packages/GET_{id}_droplets_response.json @@ -0,0 +1,119 @@ +{ + "pagination": { + "total_results": 2, + "total_pages": 1, + "first": { + "href": "https://api.example.org/v3/packages/7b34f1cf-7e73-428a-bb5a-8a17a8058396/droplets?page=1&per_page=50" + }, + "last": { + "href": "https://api.example.org/v3/packages/7b34f1cf-7e73-428a-bb5a-8a17a8058396/droplets?page=1&per_page=50" + }, + "next": null, + "previous": null + }, + "resources": [ + { + "guid": "585bc3c1-3743-497d-88b0-403ad6b56d16", + "state": "STAGED", + "error": null, + "lifecycle": { + "type": "buildpack", + "data": {} + }, + "image": null, + "execution_metadata": "PRIVATE DATA HIDDEN", + "process_types": { + "redacted_message": "[PRIVATE DATA HIDDEN IN LISTS]" + }, + "checksum": { + "type": "sha256", + "value": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "buildpacks": [ + { + "name": "ruby_buildpack", + "detect_output": "ruby 1.6.14", + "version": "1.1.1.", + "buildpack_name": "ruby" + } + ], + "stack": "cflinuxfs4", + "created_at": "2016-03-28T23:39:34Z", + "updated_at": "2016-03-28T23:39:47Z", + "relationships": { + "app": { + "data": { + "guid": "7b34f1cf-7e73-428a-bb5a-8a17a8058396" + } + } + }, + "links": { + "self": { + "href": "https://api.example.org/v3/droplets/585bc3c1-3743-497d-88b0-403ad6b56d16" + }, + "package": { + "href": "https://api.example.org/v3/packages/8222f76a-9e09-4360-b3aa-1ed329945e92" + }, + "app": { + "href": "https://api.example.org/v3/apps/7b34f1cf-7e73-428a-bb5a-8a17a8058396" + }, + "assign_current_droplet": { + "href": "https://api.example.org/v3/apps/7b34f1cf-7e73-428a-bb5a-8a17a8058396/relationships/current_droplet", + "method": "PATCH" + }, + "download": { + "href": "https://api.example.org/v3/droplets/585bc3c1-3743-497d-88b0-403ad6b56d16/download" + } + }, + "metadata": { + "labels": {}, + "annotations": {} + } + }, + { + "guid": "fdf3851c-def8-4de1-87f1-6d4543189e22", + "state": "STAGED", + "error": null, + "lifecycle": { + "type": "docker", + "data": {} + }, + "execution_metadata": "[PRIVATE DATA HIDDEN IN LISTS]", + "process_types": { + "redacted_message": "[PRIVATE DATA HIDDEN IN LISTS]" + }, + "image": "cloudfoundry/diego-docker-app-custom:latest", + "checksum": null, + "buildpacks": null, + "stack": null, + "created_at": "2016-03-17T00:00:01Z", + "updated_at": "2016-03-17T21:41:32Z", + "relationships": { + "app": { + "data": { + "guid": "7b34f1cf-7e73-428a-bb5a-8a17a8058396" + } + } + }, + "links": { + "self": { + "href": "https://api.example.org/v3/droplets/fdf3851c-def8-4de1-87f1-6d4543189e22" + }, + "package": { + "href": "https://api.example.org/v3/packages/c5725684-a02f-4e59-bc67-8f36ae944688" + }, + "app": { + "href": "https://api.example.org/v3/apps/7b34f1cf-7e73-428a-bb5a-8a17a8058396" + }, + "assign_current_droplet": { + "href": "https://api.example.org/v3/apps/7b34f1cf-7e73-428a-bb5a-8a17a8058396/relationships/current_droplet", + "method": "PATCH" + } + }, + "metadata": { + "labels": {}, + "annotations": {} + } + } + ] +} \ No newline at end of file diff --git a/tests/fixtures/v3/packages/GET_{id}_response.json b/tests/fixtures/v3/packages/GET_{id}_response.json new file mode 100644 index 0000000..3e72e01 --- /dev/null +++ b/tests/fixtures/v3/packages/GET_{id}_response.json @@ -0,0 +1,41 @@ +{ + "guid": "44f7c078-0934-470f-9883-4fcddc5b8f13", + "type": "bits", + "data": { + "checksum": { + "type": "sha256", + "value": null + }, + "error": null + }, + "state": "PROCESSING_UPLOAD", + "created_at": "2015-11-13T17:02:56Z", + "updated_at": "2016-06-08T16:41:26Z", + "relationships": { + "app": { + "data": { + "guid": "1d3bf0ec-5806-43c4-b64e-8364dba1086a" + } + } + }, + "links": { + "self": { + "href": "https://api.example.org/v3/packages/44f7c078-0934-470f-9883-4fcddc5b8f13" + }, + "upload": { + "href": "https://api.example.org/v3/packages/44f7c078-0934-470f-9883-4fcddc5b8f13/upload", + "method": "POST" + }, + "download": { + "href": "https://api.example.org/v3/packages/44f7c078-0934-470f-9883-4fcddc5b8f13/download", + "method": "GET" + }, + "app": { + "href": "https://api.example.org/v3/apps/1d3bf0ec-5806-43c4-b64e-8364dba1086a" + } + }, + "metadata": { + "labels": {}, + "annotations": {} + } +} diff --git a/tests/v3/test_apps.py b/tests/v3/test_apps.py index 6039f1b..67d0827 100644 --- a/tests/v3/test_apps.py +++ b/tests/v3/test_apps.py @@ -97,13 +97,22 @@ def test_get_env(self): self.assertIsInstance(env, JsonObject) self.assertEqual(env["application_env_json"]["VCAP_APPLICATION"]["limits"]["fds"], 16384) - def test_get_routes(self): + def test_list_routes(self): self.client.get.return_value = self.mock_response( "/v3/apps/app_id/routes", HTTPStatus.OK, None, "v3", "apps", "GET_{id}_routes_response.json" ) - routes = self.client.v3.apps.get_routes("app_id") - self.assertIsInstance(routes, JsonObject) - self.assertEqual(routes["resources"][0]["destinations"][0]["guid"], "385bf117-17f5-4689-8c5c-08c6cc821fed") + routes: list[dict] = [revision for revision in self.client.v3.apps.list_routes("app_id")] + self.assertEqual(1, len(routes)) + + self.assertEqual(routes[0]["destinations"][0]["guid"], "385bf117-17f5-4689-8c5c-08c6cc821fed") + + def test_list_droplets(self): + self.client.get.return_value = self.mock_response( + "/v3/apps/app_id/droplets", HTTPStatus.OK, None, "v3", "apps", "GET_{id}_droplets_response.json" + ) + droplets: list[dict] = [revision for revision in self.client.v3.apps.list_droplets("app_id")] + self.assertEqual(2, len(droplets)) + self.assertEqual(droplets[0]["state"], "STAGED") def test_get_include_space_and_org(self): self.client.get.return_value = self.mock_response( diff --git a/tests/v3/test_droplets.py b/tests/v3/test_droplets.py new file mode 100644 index 0000000..ebfd2be --- /dev/null +++ b/tests/v3/test_droplets.py @@ -0,0 +1,136 @@ +import unittest +from http import HTTPStatus + +from abstract_test_case import AbstractTestCase +from cloudfoundry_client.v3.entities import Entity + + +class TestDroplets(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/droplets", + HTTPStatus.OK, + None, + "v3", "droplets", "POST_response.json" + ) + result = self.client.v3.droplets.create( + app_guid="app-guid", + process_types={ + "rake": "bundle exec rake", + "web": "bundle exec rackup config.ru -p $PORT" + }, + meta_labels={"key": "value"}, + meta_annotations={"note": "detailed information"}, + ) + self.client.post.assert_called_with( + self.client.post.return_value.url, + json={ + "relationships": { + "app": { + "data": { + "guid": "app-guid" + } + } + }, + "process_types": { + "rake": "bundle exec rake", + "web": "bundle exec rackup config.ru -p $PORT" + }, + "metadata": {"labels": {"key": "value"}, "annotations": {"note": "detailed information"}} + }, + files=None, + ) + self.assertIsNotNone(result) + self.assertIsInstance(result, Entity) + + def test_copy(self): + self.client.post.return_value = self.mock_response( + "/v3/droplets?source_guid=droplet_id", + HTTPStatus.OK, + None, + "v3", "droplets", "POST_response.json" + ) + result = self.client.v3.droplets.copy( + droplet_guid="droplet_id", + app_guid="app-guid", + meta_labels={"key": "value"}, + meta_annotations={"note": "detailed information"}, + ) + self.client.post.assert_called_with( + self.client.post.return_value.url, + json={ + "relationships": { + "app": { + "data": { + "guid": "app-guid" + } + } + }, + "metadata": {"labels": {"key": "value"}, "annotations": {"note": "detailed information"}} + }, + files=None, + ) + self.assertIsNotNone(result) + self.assertIsInstance(result, Entity) + + def test_list(self): + self.client.get.return_value = self.mock_response( + "/v3/droplets", + HTTPStatus.OK, + None, + "v3", "droplets", "GET_response.json" + ) + all_droplets = [droplet for droplet in self.client.v3.droplets.list()] + self.client.get.assert_called_with(self.client.get.return_value.url) + self.assertEqual(2, len(all_droplets)) + self.assertEqual(all_droplets[0]["state"], "STAGED") + for droplet in all_droplets: + self.assertIsInstance(droplet, Entity) + + def test_get(self): + self.client.get.return_value = self.mock_response( + "/v3/droplets/route_id", + HTTPStatus.OK, + None, + "v3", "droplets", "GET_{id}_response.json" + ) + result = self.client.v3.droplets.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/droplets/droplet_id", + HTTPStatus.OK, + None, + "v3", "droplets", "PATCH_{id}_response.json" + ) + result = self.client.v3.droplets.update( + "droplet_id", + meta_labels={"key": "value"}, + meta_annotations={"note": "detailed information"}, + ) + self.client.patch.assert_called_with( + self.client.patch.return_value.url, + json={ + "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/droplets/droplet_id", HTTPStatus.NO_CONTENT, None) + self.client.v3.droplets.remove("droplet_id") + self.client.delete.assert_called_with(self.client.delete.return_value.url) diff --git a/tests/v3/test_packages.py b/tests/v3/test_packages.py new file mode 100644 index 0000000..0db7c46 --- /dev/null +++ b/tests/v3/test_packages.py @@ -0,0 +1,48 @@ +import unittest +from http import HTTPStatus + +from abstract_test_case import AbstractTestCase +from cloudfoundry_client.v3.entities import Entity + + +class TestPackages(unittest.TestCase, AbstractTestCase): + @classmethod + def setUpClass(cls): + cls.mock_client_class() + + def setUp(self): + self.build_client() + + def test_list(self): + self.client.get.return_value = self.mock_response( + "/v3/packages", + HTTPStatus.OK, + None, + "v3", "packages", "GET_response.json" + ) + all_packages = [package for package in self.client.v3.packages.list()] + self.client.get.assert_called_with(self.client.get.return_value.url) + self.assertEqual(2, len(all_packages)) + self.assertEqual(all_packages[0]["type"], "bits") + for droplet in all_packages: + self.assertIsInstance(droplet, Entity) + + def test_get(self): + self.client.get.return_value = self.mock_response( + "/v3/packages/package_id", + HTTPStatus.OK, + None, + "v3", "packages", "GET_{id}_response.json" + ) + result = self.client.v3.packages.get("package_id") + self.client.get.assert_called_with(self.client.get.return_value.url) + self.assertIsNotNone(result) + self.assertIsInstance(result, Entity) + + def test_list_droplets(self): + self.client.get.return_value = self.mock_response( + "/v3/packages/package_id/droplets", HTTPStatus.OK, None, "v3", "packages", "GET_{id}_droplets_response.json" + ) + droplets: list[dict] = [droplet for droplet in self.client.v3.packages.list_droplets("package_id")] + self.assertEqual(2, len(droplets)) + self.assertEqual(droplets[0]["state"], "STAGED") diff --git a/tests/v3/test_routes.py b/tests/v3/test_routes.py index 1f9c24d..4bb60f8 100644 --- a/tests/v3/test_routes.py +++ b/tests/v3/test_routes.py @@ -55,30 +55,6 @@ def test_create(self): }, 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)