diff --git a/changelogs/fragments/245-add-dry-run.yaml b/changelogs/fragments/245-add-dry-run.yaml new file mode 100644 index 00000000..cf3c40c0 --- /dev/null +++ b/changelogs/fragments/245-add-dry-run.yaml @@ -0,0 +1,3 @@ +--- +minor_changes: + - add support for dry run with kubernetes client version >=18.20 (https://github.com/ansible-collections/kubernetes.core/pull/245). diff --git a/molecule/default/tasks/full.yml b/molecule/default/tasks/full.yml index 03dfa402..b6b1bd26 100644 --- a/molecule/default/tasks/full.yml +++ b/molecule/default/tasks/full.yml @@ -60,11 +60,16 @@ environment: K8S_AUTH_KUBECONFIG: ~/.kube/customconfig + - name: Get currently installed version of kubernetes + ansible.builtin.command: python -c "import kubernetes; print(kubernetes.__version__)" + register: kubernetes_version + - name: Using in-memory kubeconfig should succeed kubernetes.core.k8s: name: testing kind: Namespace kubeconfig: "{{ lookup('file', '~/.kube/customconfig') | from_yaml }}" + when: kubernetes_version.stdout is version("17.17.0", ">=") always: - name: Return kubeconfig diff --git a/plugins/module_utils/apply.py b/plugins/module_utils/apply.py index 486d9a5a..034859cd 100644 --- a/plugins/module_utils/apply.py +++ b/plugins/module_utils/apply.py @@ -110,16 +110,17 @@ def apply_object(resource, definition): return apply_patch(actual.to_dict(), definition) -def k8s_apply(resource, definition): +def k8s_apply(resource, definition, **kwargs): existing, desired = apply_object(resource, definition) if not existing: - return resource.create(body=desired, namespace=definition['metadata'].get('namespace')) + return resource.create(body=desired, namespace=definition['metadata'].get('namespace'), **kwargs) if existing == desired: return resource.get(name=definition['metadata']['name'], namespace=definition['metadata'].get('namespace')) return resource.patch(body=desired, name=definition['metadata']['name'], namespace=definition['metadata'].get('namespace'), - content_type='application/merge-patch+json') + content_type='application/merge-patch+json', + **kwargs) # The patch is the difference from actual to desired without deletions, plus deletions diff --git a/plugins/module_utils/common.py b/plugins/module_utils/common.py index 2ace5779..c3ca5217 100644 --- a/plugins/module_utils/common.py +++ b/plugins/module_utils/common.py @@ -229,6 +229,7 @@ class K8sAnsibleMixin(object): module.fail_json(msg=missing_required_lib('kubernetes'), exception=K8S_IMP_ERR, error=to_native(k8s_import_exception)) self.kubernetes_version = kubernetes.__version__ + self.supports_dry_run = LooseVersion(self.kubernetes_version) >= LooseVersion("18.20.0") if pyyaml_required and not HAS_YAML: module.fail_json(msg=missing_required_lib("PyYAML"), exception=YAML_IMP_ERR) @@ -686,7 +687,9 @@ class K8sAnsibleMixin(object): else: # Delete the object result['changed'] = True - if not self.check_mode: + if self.check_mode and not self.supports_dry_run: + return result + else: if delete_options: body = { 'apiVersion': 'v1', @@ -694,6 +697,8 @@ class K8sAnsibleMixin(object): } body.update(delete_options) params['body'] = body + if self.check_mode: + params['dry_run'] = "All" try: k8s_obj = resource.delete(**params) result['result'] = k8s_obj.to_dict() @@ -705,7 +710,7 @@ class K8sAnsibleMixin(object): return result else: self.fail_json(msg=build_error_msg(definition['kind'], origin_name, msg), error=exc.status, status=exc.status, reason=exc.reason) - if wait: + if wait and not self.check_mode: success, resource, duration = self.wait(resource, definition, wait_sleep, wait_timeout, 'absent', label_selectors=label_selectors) result['duration'] = duration if not success: @@ -726,7 +731,7 @@ class K8sAnsibleMixin(object): kind=definition['kind'], name=origin_name, namespace=namespace) return result if apply: - if self.check_mode: + if self.check_mode and not self.supports_dry_run: ignored, patch = apply_object(resource, _encode_stringdata(definition)) if existing: k8s_obj = dict_merge(existing.to_dict(), patch) @@ -734,7 +739,10 @@ class K8sAnsibleMixin(object): k8s_obj = patch else: try: - k8s_obj = resource.apply(definition, namespace=namespace).to_dict() + params = {} + if self.check_mode: + params['dry_run'] = 'All' + k8s_obj = resource.apply(definition, namespace=namespace, **params).to_dict() except DynamicApiError as exc: msg = "Failed to apply object: {0}".format(exc.body) if self.warnings: @@ -775,11 +783,14 @@ class K8sAnsibleMixin(object): parameter has been set to '{state}'".format( kind=definition['kind'], name=origin_name, state=state) return result - elif self.check_mode: + elif self.check_mode and not self.supports_dry_run: k8s_obj = _encode_stringdata(definition) else: + params = {} + if self.check_mode: + params['dry_run'] = "All" try: - k8s_obj = resource.create(definition, namespace=namespace).to_dict() + k8s_obj = resource.create(definition, namespace=namespace, **params).to_dict() except ConflictError: # Some resources, like ProjectRequests, can't be created multiple times, # because the resources that they create don't match their kind @@ -826,11 +837,14 @@ class K8sAnsibleMixin(object): diffs = [] if state == 'present' and existing and force: - if self.check_mode: + if self.check_mode and not self.supports_dry_run: k8s_obj = _encode_stringdata(definition) else: + params = {} + if self.check_mode: + params['dry_run'] = "All" try: - k8s_obj = resource.replace(definition, name=name, namespace=namespace, append_hash=append_hash).to_dict() + k8s_obj = resource.replace(definition, name=name, namespace=namespace, append_hash=append_hash, **params).to_dict() except DynamicApiError as exc: msg = "Failed to replace object: {0}".format(exc.body) if self.warnings: @@ -861,7 +875,7 @@ class K8sAnsibleMixin(object): return result # Differences exist between the existing obj and requested params - if self.check_mode: + if self.check_mode and not self.supports_dry_run: k8s_obj = dict_merge(existing.to_dict(), _encode_stringdata(definition)) else: for merge_type in self.params['merge_type'] or ['strategic-merge', 'merge']: @@ -903,6 +917,8 @@ class K8sAnsibleMixin(object): version="3.0.0", collection_name="kubernetes.core") try: params = dict(name=name, namespace=namespace) + if self.check_mode: + params['dry_run'] = 'All' if merge_type: params['content_type'] = 'application/{0}-patch+json'.format(merge_type) k8s_obj = resource.patch(definition, **params).to_dict() diff --git a/plugins/module_utils/k8sdynamicclient.py b/plugins/module_utils/k8sdynamicclient.py index 0827009c..73fe6197 100644 --- a/plugins/module_utils/k8sdynamicclient.py +++ b/plugins/module_utils/k8sdynamicclient.py @@ -24,7 +24,7 @@ from ansible_collections.kubernetes.core.plugins.module_utils.exceptions import class K8SDynamicClient(DynamicClient): - def apply(self, resource, body=None, name=None, namespace=None): + def apply(self, resource, body=None, name=None, namespace=None, **kwargs): body = super().serialize_body(body) body['metadata'] = body.get('metadata', dict()) name = name or body['metadata'].get('name') @@ -33,7 +33,7 @@ class K8SDynamicClient(DynamicClient): if resource.namespaced: body['metadata']['namespace'] = super().ensure_namespace(resource, namespace, body) try: - return k8s_apply(resource, body) + return k8s_apply(resource, body, **kwargs) except ApplyException as e: raise ValueError("Could not apply strategic merge to %s/%s: %s" % (body['kind'], body['metadata']['name'], e)) diff --git a/plugins/modules/k8s_json_patch.py b/plugins/modules/k8s_json_patch.py index c38e2eb6..f3adf8a3 100644 --- a/plugins/modules/k8s_json_patch.py +++ b/plugins/modules/k8s_json_patch.py @@ -234,13 +234,16 @@ def execute_module(k8s_module, module): msg = 'Failed to retrieve requested object: {0}'.format(to_native(exc)) module.fail_json(msg=build_error_msg(kind, name, msg), error='', status='', reason='') - if module.check_mode: + if module.check_mode and not k8s_module.supports_dry_run: obj, error = json_patch(existing.to_dict(), patch) if error: module.fail_json(**error) else: + params = {} + if module.check_mode: + params["dry_run"] = "All" try: - obj = resource.patch(patch, name=name, namespace=namespace, content_type="application/json-patch+json").to_dict() + obj = resource.patch(patch, name=name, namespace=namespace, content_type="application/json-patch+json", **params).to_dict() except DynamicApiError as exc: msg = "Failed to patch existing object: {0}".format(exc.body) module.fail_json(msg=msg, error=exc.status, status=exc.status, reason=exc.reason)