Skip to content

Commit 4010987

Browse files
authored
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 <[email protected]> Reviewed-by: Mike Graves <[email protected]> Reviewed-by: Alina Buzachis <None> Reviewed-by: None <None> Reviewed-by: None <None>
1 parent 281ff56 commit 4010987

File tree

6 files changed

+44
-16
lines changed

6 files changed

+44
-16
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
---
2+
minor_changes:
3+
- add support for dry run with kubernetes client version >=18.20 (https://github.com/ansible-collections/kubernetes.core/pull/245).

molecule/default/tasks/full.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,11 +60,16 @@
6060
environment:
6161
K8S_AUTH_KUBECONFIG: ~/.kube/customconfig
6262

63+
- name: Get currently installed version of kubernetes
64+
ansible.builtin.command: python -c "import kubernetes; print(kubernetes.__version__)"
65+
register: kubernetes_version
66+
6367
- name: Using in-memory kubeconfig should succeed
6468
kubernetes.core.k8s:
6569
name: testing
6670
kind: Namespace
6771
kubeconfig: "{{ lookup('file', '~/.kube/customconfig') | from_yaml }}"
72+
when: kubernetes_version.stdout is version("17.17.0", ">=")
6873

6974
always:
7075
- name: Return kubeconfig

plugins/module_utils/apply.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -110,16 +110,17 @@ def apply_object(resource, definition):
110110
return apply_patch(actual.to_dict(), definition)
111111

112112

113-
def k8s_apply(resource, definition):
113+
def k8s_apply(resource, definition, **kwargs):
114114
existing, desired = apply_object(resource, definition)
115115
if not existing:
116-
return resource.create(body=desired, namespace=definition['metadata'].get('namespace'))
116+
return resource.create(body=desired, namespace=definition['metadata'].get('namespace'), **kwargs)
117117
if existing == desired:
118118
return resource.get(name=definition['metadata']['name'], namespace=definition['metadata'].get('namespace'))
119119
return resource.patch(body=desired,
120120
name=definition['metadata']['name'],
121121
namespace=definition['metadata'].get('namespace'),
122-
content_type='application/merge-patch+json')
122+
content_type='application/merge-patch+json',
123+
**kwargs)
123124

124125

125126
# The patch is the difference from actual to desired without deletions, plus deletions

plugins/module_utils/common.py

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,7 @@ def __init__(self, module, pyyaml_required=True, *args, **kwargs):
229229
module.fail_json(msg=missing_required_lib('kubernetes'), exception=K8S_IMP_ERR,
230230
error=to_native(k8s_import_exception))
231231
self.kubernetes_version = kubernetes.__version__
232+
self.supports_dry_run = LooseVersion(self.kubernetes_version) >= LooseVersion("18.20.0")
232233

233234
if pyyaml_required and not HAS_YAML:
234235
module.fail_json(msg=missing_required_lib("PyYAML"), exception=YAML_IMP_ERR)
@@ -686,14 +687,18 @@ def _empty_resource_list():
686687
else:
687688
# Delete the object
688689
result['changed'] = True
689-
if not self.check_mode:
690+
if self.check_mode and not self.supports_dry_run:
691+
return result
692+
else:
690693
if delete_options:
691694
body = {
692695
'apiVersion': 'v1',
693696
'kind': 'DeleteOptions',
694697
}
695698
body.update(delete_options)
696699
params['body'] = body
700+
if self.check_mode:
701+
params['dry_run'] = "All"
697702
try:
698703
k8s_obj = resource.delete(**params)
699704
result['result'] = k8s_obj.to_dict()
@@ -705,7 +710,7 @@ def _empty_resource_list():
705710
return result
706711
else:
707712
self.fail_json(msg=build_error_msg(definition['kind'], origin_name, msg), error=exc.status, status=exc.status, reason=exc.reason)
708-
if wait:
713+
if wait and not self.check_mode:
709714
success, resource, duration = self.wait(resource, definition, wait_sleep, wait_timeout, 'absent', label_selectors=label_selectors)
710715
result['duration'] = duration
711716
if not success:
@@ -726,15 +731,18 @@ def _empty_resource_list():
726731
kind=definition['kind'], name=origin_name, namespace=namespace)
727732
return result
728733
if apply:
729-
if self.check_mode:
734+
if self.check_mode and not self.supports_dry_run:
730735
ignored, patch = apply_object(resource, _encode_stringdata(definition))
731736
if existing:
732737
k8s_obj = dict_merge(existing.to_dict(), patch)
733738
else:
734739
k8s_obj = patch
735740
else:
736741
try:
737-
k8s_obj = resource.apply(definition, namespace=namespace).to_dict()
742+
params = {}
743+
if self.check_mode:
744+
params['dry_run'] = 'All'
745+
k8s_obj = resource.apply(definition, namespace=namespace, **params).to_dict()
738746
except DynamicApiError as exc:
739747
msg = "Failed to apply object: {0}".format(exc.body)
740748
if self.warnings:
@@ -775,11 +783,14 @@ def _empty_resource_list():
775783
parameter has been set to '{state}'".format(
776784
kind=definition['kind'], name=origin_name, state=state)
777785
return result
778-
elif self.check_mode:
786+
elif self.check_mode and not self.supports_dry_run:
779787
k8s_obj = _encode_stringdata(definition)
780788
else:
789+
params = {}
790+
if self.check_mode:
791+
params['dry_run'] = "All"
781792
try:
782-
k8s_obj = resource.create(definition, namespace=namespace).to_dict()
793+
k8s_obj = resource.create(definition, namespace=namespace, **params).to_dict()
783794
except ConflictError:
784795
# Some resources, like ProjectRequests, can't be created multiple times,
785796
# because the resources that they create don't match their kind
@@ -826,11 +837,14 @@ def _empty_resource_list():
826837
diffs = []
827838

828839
if state == 'present' and existing and force:
829-
if self.check_mode:
840+
if self.check_mode and not self.supports_dry_run:
830841
k8s_obj = _encode_stringdata(definition)
831842
else:
843+
params = {}
844+
if self.check_mode:
845+
params['dry_run'] = "All"
832846
try:
833-
k8s_obj = resource.replace(definition, name=name, namespace=namespace, append_hash=append_hash).to_dict()
847+
k8s_obj = resource.replace(definition, name=name, namespace=namespace, append_hash=append_hash, **params).to_dict()
834848
except DynamicApiError as exc:
835849
msg = "Failed to replace object: {0}".format(exc.body)
836850
if self.warnings:
@@ -861,7 +875,7 @@ def _empty_resource_list():
861875
return result
862876

863877
# Differences exist between the existing obj and requested params
864-
if self.check_mode:
878+
if self.check_mode and not self.supports_dry_run:
865879
k8s_obj = dict_merge(existing.to_dict(), _encode_stringdata(definition))
866880
else:
867881
for merge_type in self.params['merge_type'] or ['strategic-merge', 'merge']:
@@ -903,6 +917,8 @@ def patch_resource(self, resource, definition, existing, name, namespace, merge_
903917
version="3.0.0", collection_name="kubernetes.core")
904918
try:
905919
params = dict(name=name, namespace=namespace)
920+
if self.check_mode:
921+
params['dry_run'] = 'All'
906922
if merge_type:
907923
params['content_type'] = 'application/{0}-patch+json'.format(merge_type)
908924
k8s_obj = resource.patch(definition, **params).to_dict()

plugins/module_utils/k8sdynamicclient.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424

2525

2626
class K8SDynamicClient(DynamicClient):
27-
def apply(self, resource, body=None, name=None, namespace=None):
27+
def apply(self, resource, body=None, name=None, namespace=None, **kwargs):
2828
body = super().serialize_body(body)
2929
body['metadata'] = body.get('metadata', dict())
3030
name = name or body['metadata'].get('name')
@@ -33,7 +33,7 @@ def apply(self, resource, body=None, name=None, namespace=None):
3333
if resource.namespaced:
3434
body['metadata']['namespace'] = super().ensure_namespace(resource, namespace, body)
3535
try:
36-
return k8s_apply(resource, body)
36+
return k8s_apply(resource, body, **kwargs)
3737
except ApplyException as e:
3838
raise ValueError("Could not apply strategic merge to %s/%s: %s" %
3939
(body['kind'], body['metadata']['name'], e))

plugins/modules/k8s_json_patch.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -234,13 +234,16 @@ def build_error_msg(kind, name, msg):
234234
msg = 'Failed to retrieve requested object: {0}'.format(to_native(exc))
235235
module.fail_json(msg=build_error_msg(kind, name, msg), error='', status='', reason='')
236236

237-
if module.check_mode:
237+
if module.check_mode and not k8s_module.supports_dry_run:
238238
obj, error = json_patch(existing.to_dict(), patch)
239239
if error:
240240
module.fail_json(**error)
241241
else:
242+
params = {}
243+
if module.check_mode:
244+
params["dry_run"] = "All"
242245
try:
243-
obj = resource.patch(patch, name=name, namespace=namespace, content_type="application/json-patch+json").to_dict()
246+
obj = resource.patch(patch, name=name, namespace=namespace, content_type="application/json-patch+json", **params).to_dict()
244247
except DynamicApiError as exc:
245248
msg = "Failed to patch existing object: {0}".format(exc.body)
246249
module.fail_json(msg=msg, error=exc.status, status=exc.status, reason=exc.reason)

0 commit comments

Comments
 (0)