Skip to content

Commit

Permalink
Add support for dry run (ansible-collections#245)
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
gravesm authored Oct 15, 2021
1 parent 281ff56 commit 4010987
Show file tree
Hide file tree
Showing 6 changed files with 44 additions and 16 deletions.
3 changes: 3 additions & 0 deletions changelogs/fragments/245-add-dry-run.yaml
Original file line number Diff line number Diff line change
@@ -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).
5 changes: 5 additions & 0 deletions molecule/default/tasks/full.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 4 additions & 3 deletions plugins/module_utils/apply.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
34 changes: 25 additions & 9 deletions plugins/module_utils/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@ def __init__(self, module, pyyaml_required=True, *args, **kwargs):
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)
Expand Down Expand Up @@ -686,14 +687,18 @@ def _empty_resource_list():
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',
'kind': 'DeleteOptions',
}
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()
Expand All @@ -705,7 +710,7 @@ def _empty_resource_list():
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:
Expand All @@ -726,15 +731,18 @@ def _empty_resource_list():
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)
else:
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:
Expand Down Expand Up @@ -775,11 +783,14 @@ def _empty_resource_list():
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
Expand Down Expand Up @@ -826,11 +837,14 @@ def _empty_resource_list():
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:
Expand Down Expand Up @@ -861,7 +875,7 @@ def _empty_resource_list():
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']:
Expand Down Expand Up @@ -903,6 +917,8 @@ def patch_resource(self, resource, definition, existing, name, namespace, merge_
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()
Expand Down
4 changes: 2 additions & 2 deletions plugins/module_utils/k8sdynamicclient.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@


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')
Expand All @@ -33,7 +33,7 @@ def apply(self, resource, body=None, name=None, namespace=None):
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))
7 changes: 5 additions & 2 deletions plugins/modules/k8s_json_patch.py
Original file line number Diff line number Diff line change
Expand Up @@ -234,13 +234,16 @@ def build_error_msg(kind, name, msg):
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)
Expand Down

0 comments on commit 4010987

Please sign in to comment.