diff --git a/cloudfoundry_client/client.py b/cloudfoundry_client/client.py index 98d16f4..46944d2 100644 --- a/cloudfoundry_client/client.py +++ b/cloudfoundry_client/client.py @@ -46,6 +46,7 @@ from cloudfoundry_client.v3.service_offerings import ServiceOfferingsManager from cloudfoundry_client.v3.service_plans import ServicePlanManager from cloudfoundry_client.v3.spaces import SpaceManager +from cloudfoundry_client.v3.stacks import StackMananager from cloudfoundry_client.v3.tasks import TaskManager from cloudfoundry_client.v3.jobs import JobManager from cloudfoundry_client.v3.users import UserManager @@ -137,6 +138,7 @@ def __init__(self, cloud_controller_v3_url: str, credential_manager: "CloudFound self.service_offerings = ServiceOfferingsManager(target_endpoint, credential_manager) self.service_plans = ServicePlanManager(target_endpoint, credential_manager) self.spaces = SpaceManager(target_endpoint, credential_manager) + self.stacks = StackMananager(target_endpoint, credential_manager) self.tasks = TaskManager(target_endpoint, credential_manager) self.users = UserManager(target_endpoint, credential_manager) diff --git a/cloudfoundry_client/v3/stacks.py b/cloudfoundry_client/v3/stacks.py new file mode 100644 index 0000000..6004f2e --- /dev/null +++ b/cloudfoundry_client/v3/stacks.py @@ -0,0 +1,42 @@ +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 StackMananager(EntityManager[Entity]): + def __init__(self, target_endpoint: str, client: "CloudFoundryClient"): + super(StackMananager, self).__init__(target_endpoint, client, "/v3/stacks") + + def create( + self, + name: str, + description: str | None = None, + meta_labels: dict | None = None, + meta_annotations: dict | None = None, + ) -> Entity: + data = {"name": name} + if description is not None: + data["description"] = description + self._metadata(data, meta_labels, meta_annotations) + return super(StackMananager, self)._create(data) + + def update( + self, + stack_guid: str, + meta_labels: dict | None = None, + meta_annotations: dict | None = None, + ) -> Entity: + data = {} + self._metadata(data, meta_labels, meta_annotations) + return super(StackMananager, self)._update(stack_guid, data) + + def list_apps(self, stack_guid: str, **kwargs) -> Pagination[Entity]: + uri: str = "%s/%s/apps" % (self.entity_uri, stack_guid) + return super(StackMananager, self)._list(requested_path=uri, **kwargs) + + def remove(self, stack_guid: str): + super(StackMananager, self)._remove(stack_guid) diff --git a/tests/fixtures/v3/stacks/GET_response.json b/tests/fixtures/v3/stacks/GET_response.json new file mode 100644 index 0000000..10aaa83 --- /dev/null +++ b/tests/fixtures/v3/stacks/GET_response.json @@ -0,0 +1,54 @@ +{ + "pagination": { + "total_results": 3, + "total_pages": 2, + "first": { + "href": "https://api.example.org/v3/stacks?page=1&per_page=2" + }, + "last": { + "href": "https://api.example.org/v3/stacks?page=2&per_page=2" + }, + "next": null, + "previous": null + }, + "resources": [ + { + "guid": "11c916c9-c2f9-440e-8e73-102e79c4704d", + "created_at": "2018-11-09T22:43:28Z", + "updated_at": "2018-11-09T22:43:28Z", + "name": "my-stack-1", + "build_rootfs_image": "my-stack-1-build", + "run_rootfs_image": "my-stack-1-run", + "description": "This is my first stack!", + "default": true, + "metadata": { + "labels": {}, + "annotations": {} + }, + "links": { + "self": { + "href": "https://api.example.org/v3/stacks/11c916c9-c2f9-440e-8e73-102e79c4704d" + } + } + }, + { + "guid": "81c916c9-c2f9-440e-8e73-102e79c4704h", + "created_at": "2018-11-09T22:43:29Z", + "updated_at": "2018-11-09T22:43:29Z", + "name": "my-stack-2", + "description": "This is my second stack!", + "build_rootfs_image": "my-stack-2-build", + "run_rootfs_image": "my-stack-2-run", + "default": false, + "metadata": { + "labels": {}, + "annotations": {} + }, + "links": { + "self": { + "href": "https://api.example.org/v3/stacks/81c916c9-c2f9-440e-8e73-102e79c4704h" + } + } + } + ] +} \ No newline at end of file diff --git a/tests/fixtures/v3/stacks/GET_{id}_apps_response.json b/tests/fixtures/v3/stacks/GET_{id}_apps_response.json new file mode 100644 index 0000000..a542fa6 --- /dev/null +++ b/tests/fixtures/v3/stacks/GET_{id}_apps_response.json @@ -0,0 +1,162 @@ +{ + "pagination": { + "total_results": 3, + "total_pages": 2, + "first": { + "href": "https://api.example.org/v3/stacks/[guid]/apps?page=1&per_page=2" + }, + "last": { + "href": "https://api.example.org/v3/stacks/[guid]/apps?page=2&per_page=2" + }, + "next": null, + "previous": null + }, + "resources": [ + { + "guid": "1cb006ee-fb05-47e1-b541-c34179ddc446", + "name": "my_app", + "state": "STARTED", + "created_at": "2016-03-17T21:41:30Z", + "updated_at": "2016-03-18T11:32:30Z", + "lifecycle": { + "type": "buildpack", + "data": { + "buildpacks": ["java_buildpack"], + "stack": "cflinuxfs4" + } + }, + "relationships": { + "space": { + "data": { + "guid": "2f35885d-0c9d-4423-83ad-fd05066f8576" + } + }, + "current_droplet": { + "data": { + "guid": "585bc3c1-3743-497d-88b0-403ad6b56d16" + } + } + }, + "links": { + "self": { + "href": "https://api.example.org/v3/apps/1cb006ee-fb05-47e1-b541-c34179ddc446" + }, + "space": { + "href": "https://api.example.org/v3/spaces/2f35885d-0c9d-4423-83ad-fd05066f8576" + }, + "processes": { + "href": "https://api.example.org/v3/apps/1cb006ee-fb05-47e1-b541-c34179ddc446/processes" + }, + "packages": { + "href": "https://api.example.org/v3/apps/1cb006ee-fb05-47e1-b541-c34179ddc446/packages" + }, + "environment_variables": { + "href": "https://api.example.org/v3/apps/1cb006ee-fb05-47e1-b541-c34179ddc446/environment_variables" + }, + "current_droplet": { + "href": "https://api.example.org/v3/apps/1cb006ee-fb05-47e1-b541-c34179ddc446/droplets/current" + }, + "droplets": { + "href": "https://api.example.org/v3/apps/1cb006ee-fb05-47e1-b541-c34179ddc446/droplets" + }, + "tasks": { + "href": "https://api.example.org/v3/apps/1cb006ee-fb05-47e1-b541-c34179ddc446/tasks" + }, + "start": { + "href": "https://api.example.org/v3/apps/1cb006ee-fb05-47e1-b541-c34179ddc446/actions/start", + "method": "POST" + }, + "stop": { + "href": "https://api.example.org/v3/apps/1cb006ee-fb05-47e1-b541-c34179ddc446/actions/stop", + "method": "POST" + }, + "revisions": { + "href": "https://api.example.org/v3/apps/1cb006ee-fb05-47e1-b541-c34179ddc446/revisions" + }, + "deployed_revisions": { + "href": "https://api.example.org/v3/apps/1cb006ee-fb05-47e1-b541-c34179ddc446/revisions/deployed" + }, + "features": { + "href": "https://api.example.org/v3/apps/1cb006ee-fb05-47e1-b541-c34179ddc446/features" + } + }, + "metadata": { + "labels": {}, + "annotations": {} + } + }, + { + "guid": "02b4ec9b-94c7-4468-9c23-4e906191a0f8", + "name": "my_app2", + "state": "STOPPED", + "created_at": "1970-01-01T00:00:02Z", + "updated_at": "2016-06-08T16:41:26Z", + "lifecycle": { + "type": "buildpack", + "data": { + "buildpacks": ["ruby_buildpack"], + "stack": "cflinuxfs4" + } + }, + "relationships": { + "space": { + "data": { + "guid": "2f35885d-0c9d-4423-83ad-fd05066f8576" + } + }, + "droplet": { + "data": { + "guid": "585bc3c1-3743-497d-88b0-403ad6b56d16" + } + } + }, + "links": { + "self": { + "href": "https://api.example.org/v3/stacks/[guid]/apps/02b4ec9b-94c7-4468-9c23-4e906191a0f8" + }, + "space": { + "href": "https://api.example.org/v3/spaces/2f35885d-0c9d-4423-83ad-fd05066f8576" + }, + "processes": { + "href": "https://api.example.org/v3/stacks/[guid]/apps/02b4ec9b-94c7-4468-9c23-4e906191a0f8/processes" + }, + "packages": { + "href": "https://api.example.org/v3/stacks/[guid]/apps/02b4ec9b-94c7-4468-9c23-4e906191a0f8/packages" + }, + "environment_variables": { + "href": "https://api.example.org/v3/stacks/[guid]/apps/02b4ec9b-94c7-4468-9c23-4e906191a0f8/environment_variables" + }, + "current_droplet": { + "href": "https://api.example.org/v3/stacks/[guid]/apps/02b4ec9b-94c7-4468-9c23-4e906191a0f8/droplets/current" + }, + "droplets": { + "href": "https://api.example.org/v3/stacks/[guid]/apps/02b4ec9b-94c7-4468-9c23-4e906191a0f8/droplets" + }, + "tasks": { + "href": "https://api.example.org/v3/stacks/[guid]/apps/02b4ec9b-94c7-4468-9c23-4e906191a0f8/tasks" + }, + "start": { + "href": "https://api.example.org/v3/stacks/[guid]/apps/02b4ec9b-94c7-4468-9c23-4e906191a0f8/actions/start", + "method": "POST" + }, + "stop": { + "href": "https://api.example.org/v3/stacks/[guid]/apps/02b4ec9b-94c7-4468-9c23-4e906191a0f8/actions/stop", + "method": "POST" + }, + "revisions": { + "href": "https://api.example.org//v3/stacks/[guid]/apps/02b4ec9b-94c7-4468-9c23-4e906191a0f8/revisions" + }, + "deployed_revisions": { + "href": "https://api.example.org//v3/stacks/[guid]/apps/02b4ec9b-94c7-4468-9c23-4e906191a0f8/revisions/deployed" + }, + "features": { + "href": "https://api.example.org//v3/stacks/[guid]/apps/02b4ec9b-94c7-4468-9c23-4e906191a0f8/features" + } + }, + "metadata": { + "labels": {}, + "annotations": {} + } + } + ] + } \ No newline at end of file diff --git a/tests/fixtures/v3/stacks/GET_{id}_response.json b/tests/fixtures/v3/stacks/GET_{id}_response.json new file mode 100644 index 0000000..b6a38b4 --- /dev/null +++ b/tests/fixtures/v3/stacks/GET_{id}_response.json @@ -0,0 +1,19 @@ +{ + "guid": "11c916c9-c2f9-440e-8e73-102e79c4704d", + "created_at": "2018-11-09T22:43:28Z", + "updated_at": "2018-11-09T22:43:28Z", + "name": "my-stack", + "description": "Here is my stack!", + "build_rootfs_image": "my-stack", + "run_rootfs_image": "my-stack", + "default": true, + "metadata": { + "labels": {}, + "annotations": {} + }, + "links": { + "self": { + "href": "https://api.example.com/v3/stacks/11c916c9-c2f9-440e-8e73-102e79c4704d" + } + } +} diff --git a/tests/fixtures/v3/stacks/PATCH_{id}_response.json b/tests/fixtures/v3/stacks/PATCH_{id}_response.json new file mode 100644 index 0000000..f2e2f01 --- /dev/null +++ b/tests/fixtures/v3/stacks/PATCH_{id}_response.json @@ -0,0 +1,19 @@ +{ + "guid": "11c916c9-c2f9-440e-8e73-102e79c4704d", + "created_at": "2018-11-09T22:43:28Z", + "updated_at": "2018-11-09T22:43:28Z", + "name": "my-stack", + "description": "Here is my stack!", + "build_rootfs_image": "my-stack", + "run_rootfs_image": "my-stack", + "default": true, + "metadata": { + "labels": {"key":"value"}, + "annotations": {"note":"detailed information"} + }, + "links": { + "self": { + "href": "https://api.example.com/v3/stacks/11c916c9-c2f9-440e-8e73-102e79c4704d" + } + } +} diff --git a/tests/fixtures/v3/stacks/POST_response.json b/tests/fixtures/v3/stacks/POST_response.json new file mode 100644 index 0000000..80cce02 --- /dev/null +++ b/tests/fixtures/v3/stacks/POST_response.json @@ -0,0 +1,19 @@ +{ + "guid": "11c916c9-c2f9-440e-8e73-102e79c4704d", + "created_at": "2018-11-09T22:43:28Z", + "updated_at": "2018-11-09T22:43:28Z", + "name": "my-stack", + "description": "Here is my stack!", + "build_rootfs_image": "my-stack", + "run_rootfs_image": "my-stack", + "default": true, + "metadata": { + "labels": {}, + "annotations": {} + }, + "links": { + "self": { + "href": "https://api.example.com/v3/stacks/11c916c9-c2f9-440e-8e73-102e79c4704d" + } + } +} \ No newline at end of file diff --git a/tests/v3/test_security_groups.py b/tests/v3/test_security_groups.py index 885ae12..6670a15 100644 --- a/tests/v3/test_security_groups.py +++ b/tests/v3/test_security_groups.py @@ -42,7 +42,7 @@ def test_list(self): self.client.get.return_value = self.mock_response( "/v3/security_groups", HTTPStatus.OK, None, "v3", "security_groups", "GET_response.json" ) - all_security_groups = [service_broker for service_broker in self.client.v3.security_groups.list()] + all_security_groups = [security_group for security_group in self.client.v3.security_groups.list()] self.client.get.assert_called_with(self.client.get.return_value.url) self.assertEqual(2, len(all_security_groups)) self.assertEqual(all_security_groups[0]["name"], "my-group0") diff --git a/tests/v3/test_stacks.py b/tests/v3/test_stacks.py new file mode 100644 index 0000000..a5cd581 --- /dev/null +++ b/tests/v3/test_stacks.py @@ -0,0 +1,88 @@ +import unittest +from http import HTTPStatus + +from abstract_test_case import AbstractTestCase +from cloudfoundry_client.v3.entities import Entity + + +class TestStacks(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/stacks", HTTPStatus.CREATED, None, "v3", "stacks", "POST_response.json" + ) + result = self.client.v3.stacks.create("my-stack", "Here is my stack!") + self.client.post.assert_called_with( + self.client.post.return_value.url, + json={ + "name": "my-stack", + "description": "Here is my stack!", + }, + files=None + ) + self.assertIsNotNone(result) + self.assertIsInstance(result, Entity) + + def test_list(self): + self.client.get.return_value = self.mock_response( + "/v3/stacks", HTTPStatus.OK, None, "v3", "stacks", "GET_response.json" + ) + all_stacks = [stack for stack in self.client.v3.stacks.list()] + self.client.get.assert_called_with(self.client.get.return_value.url) + self.assertEqual(2, len(all_stacks)) + self.assertEqual(all_stacks[0]["name"], "my-stack-1") + self.assertIsInstance(all_stacks[0], Entity) + + def test_get(self): + self.client.get.return_value = self.mock_response( + "/v3/stacks/stack-id", HTTPStatus.OK, None, "v3", "stacks", + "GET_{id}_response.json" + ) + stack = self.client.v3.stacks.get("stack-id") + self.client.get.assert_called_with(self.client.get.return_value.url) + self.assertEqual("my-stack", stack["name"]) + self.assertIsInstance(stack, Entity) + + def test_update(self): + self.client.patch.return_value = self.mock_response( + "/v3/stacks/stack-id", HTTPStatus.OK, None, + "v3", "stacks", "PATCH_{id}_response.json" + ) + result = self.client.v3.stacks.update("stack-id", + {"key": "value"}, + {"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_list_apps(self): + self.client.get.return_value = self.mock_response( + "/v3/stacks/stack-id/apps", HTTPStatus.OK, None, + "v3", "stacks", "GET_{id}_apps_response.json" + ) + all_apps = [app for app in self.client.v3.stacks.list_apps('stack-id')] + self.client.get.assert_called_with(self.client.get.return_value.url) + self.assertEqual(2, len(all_apps)) + self.assertEqual(all_apps[0]["name"], "my_app") + self.assertIsInstance(all_apps[0], Entity) + + def test_remove(self): + self.client.delete.return_value = self.mock_response("/v3/stacks/stack-id", + HTTPStatus.NO_CONTENT, None) + self.client.v3.stacks.remove("stack-id") + self.client.delete.assert_called_with(self.client.delete.return_value.url)