mirror of
https://github.com/ansible-collections/kubernetes.core.git
synced 2026-07-28 18:34:42 +00:00
Add support for dry run (#245)
Add support for dry run SUMMARY Kubernetes server-side dry run will be used when the kubernetes client version is >=18.20.0. For older versions of the client, the existing client side speculative change implementation will be used. The effect of this change should be mostly transparent to the end user and is reflected in the fact the tests have not changed but should still pass. With this change, there are a few edge cases that will be improved. One example of these edge cases is to use check mode on an existing Service resource. With dry run this will correctly report no changes, while the older client side implementation will erroneously report changes to the port spec. ISSUE TYPE Feature Pull Request COMPONENT NAME ADDITIONAL INFORMATION Reviewed-by: Gonéri Le Bouder <goneri@lebouder.net> Reviewed-by: Mike Graves <mgraves@redhat.com> Reviewed-by: Alina Buzachis <None> Reviewed-by: None <None> Reviewed-by: None <None>
This commit is contained in:
3
changelogs/fragments/245-add-dry-run.yaml
Normal file
3
changelogs/fragments/245-add-dry-run.yaml
Normal file
@@ -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).
|
||||||
@@ -60,11 +60,16 @@
|
|||||||
environment:
|
environment:
|
||||||
K8S_AUTH_KUBECONFIG: ~/.kube/customconfig
|
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
|
- name: Using in-memory kubeconfig should succeed
|
||||||
kubernetes.core.k8s:
|
kubernetes.core.k8s:
|
||||||
name: testing
|
name: testing
|
||||||
kind: Namespace
|
kind: Namespace
|
||||||
kubeconfig: "{{ lookup('file', '~/.kube/customconfig') | from_yaml }}"
|
kubeconfig: "{{ lookup('file', '~/.kube/customconfig') | from_yaml }}"
|
||||||
|
when: kubernetes_version.stdout is version("17.17.0", ">=")
|
||||||
|
|
||||||
always:
|
always:
|
||||||
- name: Return kubeconfig
|
- name: Return kubeconfig
|
||||||
|
|||||||
@@ -110,16 +110,17 @@ def apply_object(resource, definition):
|
|||||||
return apply_patch(actual.to_dict(), 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)
|
existing, desired = apply_object(resource, definition)
|
||||||
if not existing:
|
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:
|
if existing == desired:
|
||||||
return resource.get(name=definition['metadata']['name'], namespace=definition['metadata'].get('namespace'))
|
return resource.get(name=definition['metadata']['name'], namespace=definition['metadata'].get('namespace'))
|
||||||
return resource.patch(body=desired,
|
return resource.patch(body=desired,
|
||||||
name=definition['metadata']['name'],
|
name=definition['metadata']['name'],
|
||||||
namespace=definition['metadata'].get('namespace'),
|
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
|
# The patch is the difference from actual to desired without deletions, plus deletions
|
||||||
|
|||||||
@@ -229,6 +229,7 @@ class K8sAnsibleMixin(object):
|
|||||||
module.fail_json(msg=missing_required_lib('kubernetes'), exception=K8S_IMP_ERR,
|
module.fail_json(msg=missing_required_lib('kubernetes'), exception=K8S_IMP_ERR,
|
||||||
error=to_native(k8s_import_exception))
|
error=to_native(k8s_import_exception))
|
||||||
self.kubernetes_version = kubernetes.__version__
|
self.kubernetes_version = kubernetes.__version__
|
||||||
|
self.supports_dry_run = LooseVersion(self.kubernetes_version) >= LooseVersion("18.20.0")
|
||||||
|
|
||||||
if pyyaml_required and not HAS_YAML:
|
if pyyaml_required and not HAS_YAML:
|
||||||
module.fail_json(msg=missing_required_lib("PyYAML"), exception=YAML_IMP_ERR)
|
module.fail_json(msg=missing_required_lib("PyYAML"), exception=YAML_IMP_ERR)
|
||||||
@@ -686,7 +687,9 @@ class K8sAnsibleMixin(object):
|
|||||||
else:
|
else:
|
||||||
# Delete the object
|
# Delete the object
|
||||||
result['changed'] = True
|
result['changed'] = True
|
||||||
if not self.check_mode:
|
if self.check_mode and not self.supports_dry_run:
|
||||||
|
return result
|
||||||
|
else:
|
||||||
if delete_options:
|
if delete_options:
|
||||||
body = {
|
body = {
|
||||||
'apiVersion': 'v1',
|
'apiVersion': 'v1',
|
||||||
@@ -694,6 +697,8 @@ class K8sAnsibleMixin(object):
|
|||||||
}
|
}
|
||||||
body.update(delete_options)
|
body.update(delete_options)
|
||||||
params['body'] = body
|
params['body'] = body
|
||||||
|
if self.check_mode:
|
||||||
|
params['dry_run'] = "All"
|
||||||
try:
|
try:
|
||||||
k8s_obj = resource.delete(**params)
|
k8s_obj = resource.delete(**params)
|
||||||
result['result'] = k8s_obj.to_dict()
|
result['result'] = k8s_obj.to_dict()
|
||||||
@@ -705,7 +710,7 @@ class K8sAnsibleMixin(object):
|
|||||||
return result
|
return result
|
||||||
else:
|
else:
|
||||||
self.fail_json(msg=build_error_msg(definition['kind'], origin_name, msg), error=exc.status, status=exc.status, reason=exc.reason)
|
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)
|
success, resource, duration = self.wait(resource, definition, wait_sleep, wait_timeout, 'absent', label_selectors=label_selectors)
|
||||||
result['duration'] = duration
|
result['duration'] = duration
|
||||||
if not success:
|
if not success:
|
||||||
@@ -726,7 +731,7 @@ class K8sAnsibleMixin(object):
|
|||||||
kind=definition['kind'], name=origin_name, namespace=namespace)
|
kind=definition['kind'], name=origin_name, namespace=namespace)
|
||||||
return result
|
return result
|
||||||
if apply:
|
if apply:
|
||||||
if self.check_mode:
|
if self.check_mode and not self.supports_dry_run:
|
||||||
ignored, patch = apply_object(resource, _encode_stringdata(definition))
|
ignored, patch = apply_object(resource, _encode_stringdata(definition))
|
||||||
if existing:
|
if existing:
|
||||||
k8s_obj = dict_merge(existing.to_dict(), patch)
|
k8s_obj = dict_merge(existing.to_dict(), patch)
|
||||||
@@ -734,7 +739,10 @@ class K8sAnsibleMixin(object):
|
|||||||
k8s_obj = patch
|
k8s_obj = patch
|
||||||
else:
|
else:
|
||||||
try:
|
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:
|
except DynamicApiError as exc:
|
||||||
msg = "Failed to apply object: {0}".format(exc.body)
|
msg = "Failed to apply object: {0}".format(exc.body)
|
||||||
if self.warnings:
|
if self.warnings:
|
||||||
@@ -775,11 +783,14 @@ class K8sAnsibleMixin(object):
|
|||||||
parameter has been set to '{state}'".format(
|
parameter has been set to '{state}'".format(
|
||||||
kind=definition['kind'], name=origin_name, state=state)
|
kind=definition['kind'], name=origin_name, state=state)
|
||||||
return result
|
return result
|
||||||
elif self.check_mode:
|
elif self.check_mode and not self.supports_dry_run:
|
||||||
k8s_obj = _encode_stringdata(definition)
|
k8s_obj = _encode_stringdata(definition)
|
||||||
else:
|
else:
|
||||||
|
params = {}
|
||||||
|
if self.check_mode:
|
||||||
|
params['dry_run'] = "All"
|
||||||
try:
|
try:
|
||||||
k8s_obj = resource.create(definition, namespace=namespace).to_dict()
|
k8s_obj = resource.create(definition, namespace=namespace, **params).to_dict()
|
||||||
except ConflictError:
|
except ConflictError:
|
||||||
# Some resources, like ProjectRequests, can't be created multiple times,
|
# Some resources, like ProjectRequests, can't be created multiple times,
|
||||||
# because the resources that they create don't match their kind
|
# because the resources that they create don't match their kind
|
||||||
@@ -826,11 +837,14 @@ class K8sAnsibleMixin(object):
|
|||||||
diffs = []
|
diffs = []
|
||||||
|
|
||||||
if state == 'present' and existing and force:
|
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)
|
k8s_obj = _encode_stringdata(definition)
|
||||||
else:
|
else:
|
||||||
|
params = {}
|
||||||
|
if self.check_mode:
|
||||||
|
params['dry_run'] = "All"
|
||||||
try:
|
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:
|
except DynamicApiError as exc:
|
||||||
msg = "Failed to replace object: {0}".format(exc.body)
|
msg = "Failed to replace object: {0}".format(exc.body)
|
||||||
if self.warnings:
|
if self.warnings:
|
||||||
@@ -861,7 +875,7 @@ class K8sAnsibleMixin(object):
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
# Differences exist between the existing obj and requested params
|
# 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))
|
k8s_obj = dict_merge(existing.to_dict(), _encode_stringdata(definition))
|
||||||
else:
|
else:
|
||||||
for merge_type in self.params['merge_type'] or ['strategic-merge', 'merge']:
|
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")
|
version="3.0.0", collection_name="kubernetes.core")
|
||||||
try:
|
try:
|
||||||
params = dict(name=name, namespace=namespace)
|
params = dict(name=name, namespace=namespace)
|
||||||
|
if self.check_mode:
|
||||||
|
params['dry_run'] = 'All'
|
||||||
if merge_type:
|
if merge_type:
|
||||||
params['content_type'] = 'application/{0}-patch+json'.format(merge_type)
|
params['content_type'] = 'application/{0}-patch+json'.format(merge_type)
|
||||||
k8s_obj = resource.patch(definition, **params).to_dict()
|
k8s_obj = resource.patch(definition, **params).to_dict()
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ from ansible_collections.kubernetes.core.plugins.module_utils.exceptions import
|
|||||||
|
|
||||||
|
|
||||||
class K8SDynamicClient(DynamicClient):
|
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 = super().serialize_body(body)
|
||||||
body['metadata'] = body.get('metadata', dict())
|
body['metadata'] = body.get('metadata', dict())
|
||||||
name = name or body['metadata'].get('name')
|
name = name or body['metadata'].get('name')
|
||||||
@@ -33,7 +33,7 @@ class K8SDynamicClient(DynamicClient):
|
|||||||
if resource.namespaced:
|
if resource.namespaced:
|
||||||
body['metadata']['namespace'] = super().ensure_namespace(resource, namespace, body)
|
body['metadata']['namespace'] = super().ensure_namespace(resource, namespace, body)
|
||||||
try:
|
try:
|
||||||
return k8s_apply(resource, body)
|
return k8s_apply(resource, body, **kwargs)
|
||||||
except ApplyException as e:
|
except ApplyException as e:
|
||||||
raise ValueError("Could not apply strategic merge to %s/%s: %s" %
|
raise ValueError("Could not apply strategic merge to %s/%s: %s" %
|
||||||
(body['kind'], body['metadata']['name'], e))
|
(body['kind'], body['metadata']['name'], e))
|
||||||
|
|||||||
@@ -234,13 +234,16 @@ def execute_module(k8s_module, module):
|
|||||||
msg = 'Failed to retrieve requested object: {0}'.format(to_native(exc))
|
msg = 'Failed to retrieve requested object: {0}'.format(to_native(exc))
|
||||||
module.fail_json(msg=build_error_msg(kind, name, msg), error='', status='', reason='')
|
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)
|
obj, error = json_patch(existing.to_dict(), patch)
|
||||||
if error:
|
if error:
|
||||||
module.fail_json(**error)
|
module.fail_json(**error)
|
||||||
else:
|
else:
|
||||||
|
params = {}
|
||||||
|
if module.check_mode:
|
||||||
|
params["dry_run"] = "All"
|
||||||
try:
|
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:
|
except DynamicApiError as exc:
|
||||||
msg = "Failed to patch existing object: {0}".format(exc.body)
|
msg = "Failed to patch existing object: {0}".format(exc.body)
|
||||||
module.fail_json(msg=msg, error=exc.status, status=exc.status, reason=exc.reason)
|
module.fail_json(msg=msg, error=exc.status, status=exc.status, reason=exc.reason)
|
||||||
|
|||||||
Reference in New Issue
Block a user