diff --git a/changelogs/fragments/264-kubernetes-project.yaml b/changelogs/fragments/264-kubernetes-project.yaml new file mode 100644 index 00000000..1f58cd63 --- /dev/null +++ b/changelogs/fragments/264-kubernetes-project.yaml @@ -0,0 +1,3 @@ +minor_changes: + - digital_ocean_kubernetes - add project_name parameter + (https://github.com/ansible-collections/community.digitalocean/issues/264). \ No newline at end of file diff --git a/plugins/module_utils/digital_ocean.py b/plugins/module_utils/digital_ocean.py index 2e3e3c55..f7bb4204 100644 --- a/plugins/module_utils/digital_ocean.py +++ b/plugins/module_utils/digital_ocean.py @@ -283,6 +283,7 @@ def assign_to_project(self, project_name, urn): Domain | do:domain:example.com Droplet | do:droplet:4126873 Floating IP | do:floatingip:192.168.99.100 + Kubernetes | do:kubernetes:bd5f5959-5e1e-4205-a714-a914373942af Load Balancer | do:loadbalancer:39052d89-8dd4-4d49-8d5a-3c3b6b365b5b Space | do:space:my-website-assets Volume | do:volume:6fc4c277-ea5c-448a-93cd-dd496cfef71f diff --git a/plugins/modules/digital_ocean_kubernetes.py b/plugins/modules/digital_ocean_kubernetes.py index 85adb08f..b286930f 100644 --- a/plugins/modules/digital_ocean_kubernetes.py +++ b/plugins/modules/digital_ocean_kubernetes.py @@ -155,6 +155,14 @@ - Highly available control planes incur less downtime. type: bool default: false + project_name: + aliases: ["project"] + description: + - Project to assign the resource to (project name, not UUID). + - Defaults to the default project of the account (empty string). + type: str + required: false + default: "" """ @@ -171,7 +179,7 @@ count: 3 return_kubeconfig: true wait_timeout: 600 - register: my_cluster + register: my_cluster - name: Show the kubeconfig for the cluster we just created debug: @@ -182,6 +190,21 @@ state: absent oauth_token: "{{ lookup('env', 'DO_API_TOKEN') }}" name: hacktoberfest + +- name: Create a new DigitalOcean Kubernetes cluster assigned to Project "test" + community.digitalocean.digital_ocean_kubernetes: + state: present + oauth_token: "{{ lookup('env', 'DO_API_TOKEN') }}" + name: hacktoberfest + region: nyc1 + node_pools: + - name: hacktoberfest-workers + size: s-1vcpu-2gb + count: 3 + return_kubeconfig: true + project: test + wait_timeout: 600 + register: my_cluster """ @@ -263,6 +286,7 @@ from ansible.module_utils.basic import AnsibleModule, env_fallback from ansible_collections.community.digitalocean.plugins.module_utils.digital_ocean import ( DigitalOceanHelper, + DigitalOceanProjects, ) @@ -276,6 +300,8 @@ def __init__(self, module): self.wait_timeout = self.module.params.pop("wait_timeout", 600) self.module.params.pop("oauth_token") self.cluster_id = None + if self.module.params.get("project_name"): + self.projects = DigitalOceanProjects(module, self.rest) def get_by_id(self): """Returns an existing DigitalOcean Kubernetes cluster matching on id""" @@ -374,16 +400,32 @@ def create(self): node_pool["size"], ", ".join(valid_sizes) ) ) - + if self.module.check_mode: + self.module.exit_json(changed=True) # Create the Kubernetes cluster json_data = self.get_kubernetes() if json_data: # Add the kubeconfig to the return if self.return_kubeconfig: json_data["kubeconfig"] = self.get_kubernetes_kubeconfig() + # Assign kubernetes to project + project_name = self.module.params.get("project_name") + # empty string is the default project, skip project assignment + if project_name: + urn = "do:kubernetes:{0}".format(self.cluster_id) + ( + assign_status, + error_message, + resources, + ) = self.projects.assign_to_project(project_name, urn) + if assign_status not in {"ok", "assigned", "already_assigned"}: + self.module.fail_json( + changed=False, + msg=error_message, + assign_status=assign_status, + resources=resources, + ) self.module.exit_json(changed=False, data=json_data) - if self.module.check_mode: - self.module.exit_json(changed=True) request_params = dict(self.module.params) response = self.rest.post("kubernetes/clusters", data=request_params) json_data = response.json @@ -395,6 +437,22 @@ def create(self): json_data = self.ensure_running() # Add the kubeconfig to the return if self.return_kubeconfig: + json_data["kubeconfig"] = self.get_kubernetes_kubeconfig() + # Assign kubernetes to project + project_name = self.module.params.get("project_name") + # empty string is the default project, skip project assignment + if project_name: + urn = "do:kubernetes:{0}".format(self.cluster_id) + assign_status, error_message, resources = self.projects.assign_to_project( + project_name, urn + ) + if assign_status not in {"ok", "assigned", "already_assigned"}: + self.module.fail_json( + changed=True, + msg=error_message, + assign_status=assign_status, + resources=resources, + ) json_data["kubernetes_cluster"][ "kubeconfig" ] = self.get_kubernetes_kubeconfig() @@ -474,6 +532,9 @@ def main(): wait=dict(type="bool", default=True), wait_timeout=dict(type="int", default=600), ha=dict(type="bool", default=False), + project_name=dict( + type="str", aliases=["project"], required=False, default="" + ), ), required_if=( [ diff --git a/tests/integration/targets/digital_ocean_kubernetes/defaults/main.yml b/tests/integration/targets/digital_ocean_kubernetes/defaults/main.yml index 3281135d..8b69c43c 100644 --- a/tests/integration/targets/digital_ocean_kubernetes/defaults/main.yml +++ b/tests/integration/targets/digital_ocean_kubernetes/defaults/main.yml @@ -1,4 +1,5 @@ do_region: nyc1 +test_project_name: test-kubernetes cluster_name: gh-ci-k8s-0-{{ pr_number }} cluster_version: latest diff --git a/tests/integration/targets/digital_ocean_kubernetes/tasks/main.yml b/tests/integration/targets/digital_ocean_kubernetes/tasks/main.yml index 70d30301..3635a824 100644 --- a/tests/integration/targets/digital_ocean_kubernetes/tasks/main.yml +++ b/tests/integration/targets/digital_ocean_kubernetes/tasks/main.yml @@ -57,6 +57,28 @@ - result.failed - result.msg == "Kubernetes cluster not found" + - name: Ensure the test project is absent + community.digitalocean.digital_ocean_project: + oauth_token: "{{ do_api_key }}" + state: absent + name: "{{ test_project_name }}" + register: project + + - name: Verify test project is absent + ansible.builtin.assert: + that: + - not project.changed + + - name: Create test project + community.digitalocean.digital_ocean_project: + oauth_token: "{{ do_api_key }}" + state: present + name: "{{ test_project_name }}" + purpose: Just trying out DigitalOcean + description: This is a test project + environment: Development + register: project + - name: Create the Kubernetes cluster community.digitalocean.digital_ocean_kubernetes: oauth_token: "{{ do_api_key }}" @@ -65,7 +87,8 @@ version: "{{ cluster_version }}" region: "{{ do_region }}" node_pools: "{{ cluster_node_pools }}" - return_kubeconfig: true + project_name: "{{ test_project_name }}" + return_kubeconfig: false wait_timeout: 600 register: result @@ -105,6 +128,19 @@ - result.data.kubeconfig is defined - result.data.kubeconfig | length > 0 + - name: Get test project resources + community.digitalocean.digital_ocean_project_resource_info: + oauth_token: "{{ do_api_key }}" + name: "{{ test_project_name }}" + register: resources + + - name: Verify kubernetes cluster is present + ansible.builtin.assert: + that: + - resources.data is defined + - resources.data | length == 1 + - resources.data[0].urn == 'do:kubernetes:' + result.data.id + - name: Give the cloud a minute to settle ansible.builtin.pause: minutes: 1 @@ -229,3 +265,10 @@ return_kubeconfig: false wait_timeout: 600 ignore_errors: true # Should this fail, we'll clean it up next run + + - name: Delete test project + community.digitalocean.digital_ocean_project: + oauth_token: "{{ do_api_key }}" + state: absent + name: "{{ test_project_name }}" + ignore_errors: true # Should this fail, we'll clean it up next run diff --git a/tests/unit/plugins/modules/test_digital_ocean_kubernetes.py b/tests/unit/plugins/modules/test_digital_ocean_kubernetes.py index cfb0f59b..e919aa45 100644 --- a/tests/unit/plugins/modules/test_digital_ocean_kubernetes.py +++ b/tests/unit/plugins/modules/test_digital_ocean_kubernetes.py @@ -4,6 +4,7 @@ from ansible_collections.community.general.tests.unit.compat import unittest from ansible_collections.community.general.tests.unit.compat.mock import MagicMock +from ansible_collections.community.general.tests.unit.compat.mock import DEFAULT from ansible_collections.community.digitalocean.plugins.modules.digital_ocean_kubernetes import ( DOKubernetes, ) @@ -12,6 +13,7 @@ class TestDOKubernetes(unittest.TestCase): def test_get_by_id_when_ok(self): module = MagicMock() + module.params.get.return_value = False k = DOKubernetes(module) k.rest = MagicMock() k.rest.get = MagicMock() @@ -21,6 +23,7 @@ def test_get_by_id_when_ok(self): def test_get_by_id_when_not_ok(self): module = MagicMock() + module.params.get.return_value = False k = DOKubernetes(module) k.rest = MagicMock() k.rest.get = MagicMock() @@ -30,6 +33,7 @@ def test_get_by_id_when_not_ok(self): def test_get_all_clusters_when_ok(self): module = MagicMock() + module.params.get.return_value = False k = DOKubernetes(module) k.rest = MagicMock() k.rest.get = MagicMock() @@ -39,6 +43,7 @@ def test_get_all_clusters_when_ok(self): def test_get_all_clusters_when_not_ok(self): module = MagicMock() + module.params.get.return_value = False k = DOKubernetes(module) k.rest = MagicMock() k.rest.get = MagicMock() @@ -48,11 +53,13 @@ def test_get_all_clusters_when_not_ok(self): def test_get_by_name_none(self): module = MagicMock() + module.params.get.return_value = False k = DOKubernetes(module) self.assertIsNone(k.get_by_name(None)) def test_get_by_name_found(self): module = MagicMock() + module.params.get.return_value = False k = DOKubernetes(module) k.get_all_clusters = MagicMock() k.get_all_clusters.return_value = {"kubernetes_clusters": [{"name": "foo"}]} @@ -60,6 +67,7 @@ def test_get_by_name_found(self): def test_get_by_name_not_found(self): module = MagicMock() + module.params.get.return_value = False k = DOKubernetes(module) k.get_all_clusters = MagicMock() k.get_all_clusters.return_value = {"kubernetes_clusters": [{"name": "foo"}]} @@ -67,6 +75,7 @@ def test_get_by_name_not_found(self): def test_get_kubernetes_kubeconfig_when_ok(self): module = MagicMock() + module.params.get.return_value = False k = DOKubernetes(module) k.rest = MagicMock() k.rest.get = MagicMock() @@ -76,6 +85,7 @@ def test_get_kubernetes_kubeconfig_when_ok(self): def test_get_kubernetes_kubeconfig_when_not_ok(self): module = MagicMock() + module.params.get.return_value = False k = DOKubernetes(module) k.rest = MagicMock() k.rest.get = MagicMock() @@ -85,6 +95,7 @@ def test_get_kubernetes_kubeconfig_when_not_ok(self): def test_get_kubernetes_when_found(self): module = MagicMock() + module.params.get.return_value = False k = DOKubernetes(module) k.get_by_name = MagicMock() k.get_by_name.return_value = {"id": 42} @@ -92,6 +103,7 @@ def test_get_kubernetes_when_found(self): def test_get_kubernetes_when_not_found(self): module = MagicMock() + module.params.get.return_value = False k = DOKubernetes(module) k.get_by_name = MagicMock() k.get_by_name.return_value = None @@ -99,6 +111,7 @@ def test_get_kubernetes_when_not_found(self): def test_get_kubernetes_options_when_ok(self): module = MagicMock() + module.params.get.return_value = False k = DOKubernetes(module) k.rest = MagicMock() k.rest.get = MagicMock() @@ -108,6 +121,7 @@ def test_get_kubernetes_options_when_ok(self): def test_get_kubernetes_options_when_not_ok(self): module = MagicMock() + module.params.get.return_value = False k = DOKubernetes(module) k.rest = MagicMock() k.rest.get = MagicMock() @@ -117,6 +131,7 @@ def test_get_kubernetes_options_when_not_ok(self): def test_ensure_running_when_running(self): module = MagicMock() + module.params.get.return_value = False module.fail_json = MagicMock() k = DOKubernetes(module) @@ -137,6 +152,7 @@ def test_ensure_running_when_running(self): def test_ensure_running_when_not_running(self): module = MagicMock() + module.params.get.return_value = False module.fail_json = MagicMock() k = DOKubernetes(module) @@ -158,14 +174,19 @@ def test_ensure_running_when_not_running(self): def test_create_ok(self): module = MagicMock() + + def side_effect(*args, **kwargs): + if "project_name" == args[0]: + return False + if "regions" == args[0]: + return "nyc1" + return DEFAULT + + module.params.get.side_effect = side_effect module.exit_json = MagicMock() module.fail_json = MagicMock() k = DOKubernetes(module) - k.module = MagicMock() - k.module.params = MagicMock() - - k.module.params.return_value = {"region": "nyc1"} k.get_kubernetes_options = MagicMock() @@ -190,21 +211,25 @@ def test_create_ok(self): k.rest.post.return_value.status_code = 200 k.ensure_running = MagicMock() k.cluster_id = MagicMock() - k.module = MagicMock() k.create() k.module.exit_json.assert_called() def test_create_not_ok(self): module = MagicMock() + + def side_effect(*args, **kwargs): + if "project_name" == args[0]: + return False + if "regions" == args[0]: + return "nyc1" + return DEFAULT + + module.params.get.side_effect = side_effect module.exit_json = MagicMock() module.fail_json = MagicMock() k = DOKubernetes(module) - k.module = MagicMock() - k.module.params = MagicMock() - - k.module.params.return_value = {"region": "nyc1"} k.get_kubernetes_options = MagicMock() @@ -229,13 +254,13 @@ def test_create_not_ok(self): k.rest.post.return_value.status_code = 400 k.ensure_running = MagicMock() k.cluster_id = MagicMock() - k.module = MagicMock() k.create() k.module.exit_json.assert_called() def test_delete_ok(self): module = MagicMock() + module.params.get.return_value = False module.exit_json = MagicMock() k = DOKubernetes(module) @@ -252,6 +277,7 @@ def test_delete_ok(self): def test_delete_not_ok(self): module = MagicMock() + module.params.get.return_value = False module.exit_json = MagicMock() k = DOKubernetes(module)