Files
kubernetes.core/plugins/modules/k8s_json_patch.py
Mike Graves 4010987d1f 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>
2021-10-15 14:20:43 +00:00

285 lines
8.2 KiB
Python

#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (C), 2018 Red Hat | Ansible
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = r'''
module: k8s_json_patch
short_description: Apply JSON patch operations to existing objects
description:
- This module is used to apply RFC 6902 JSON patch operations only.
- Use the M(kubernetes.core.k8s) module for strategic merge or JSON merge operations.
- The jsonpatch library is required for check mode.
version_added: 2.0.0
author:
- Mike Graves (@gravesm)
options:
api_version:
description:
- Use to specify the API version.
- Use in conjunction with I(kind), I(name), and I(namespace) to identify a specific object.
type: str
default: v1
aliases:
- api
- version
kind:
description:
- Use to specify an object model.
- Use in conjunction with I(api_version), I(name), and I(namespace) to identify a specific object.
type: str
required: yes
namespace:
description:
- Use to specify an object namespace.
- Use in conjunction with I(api_version), I(kind), and I(name) to identify a specific object.
type: str
name:
description:
- Use to specify an object name.
- Use in conjunction with I(api_version), I(kind), and I(namespace) to identify a specific object.
type: str
required: yes
patch:
description:
- List of JSON patch operations.
required: yes
type: list
elements: dict
extends_documentation_fragment:
- kubernetes.core.k8s_auth_options
- kubernetes.core.k8s_wait_options
requirements:
- "python >= 3.6"
- "kubernetes >= 12.0.0"
- "PyYAML >= 3.11"
- "jsonpatch"
'''
EXAMPLES = r'''
- name: Apply multiple patch operations to an existing Pod
kubernetes.core.k8s_json_patch:
kind: Pod
namespace: testing
name: mypod
patch:
- op: add
path: /metadata/labels/app
value: myapp
- op: replace
patch: /spec/containers/0/image
value: nginx
'''
RETURN = r'''
result:
description: The modified object.
returned: success
type: dict
contains:
api_version:
description: The versioned schema of this representation of an object.
returned: success
type: str
kind:
description: The REST resource this object represents.
returned: success
type: str
metadata:
description: Standard object metadata. Includes name, namespace, annotations, labels, etc.
returned: success
type: dict
spec:
description: Specific attributes of the object. Will vary based on the I(api_version) and I(kind).
returned: success
type: dict
status:
description: Current status details for the object.
returned: success
type: dict
duration:
description: Elapsed time of task in seconds.
returned: when C(wait) is true
type: int
sample: 48
error:
description: The error when patching the object.
returned: error
type: dict
sample: {
"msg": "Failed to import the required Python library (jsonpatch) ...",
"exception": "Traceback (most recent call last): ..."
}
'''
import copy
import traceback
from ansible.module_utils.basic import missing_required_lib
from ansible.module_utils._text import to_native
from ansible_collections.kubernetes.core.plugins.module_utils.ansiblemodule import AnsibleModule
from ansible_collections.kubernetes.core.plugins.module_utils.args_common import AUTH_ARG_SPEC, WAIT_ARG_SPEC
from ansible_collections.kubernetes.core.plugins.module_utils.common import (
get_api_client, K8sAnsibleMixin)
try:
from kubernetes.dynamic.exceptions import DynamicApiError
except ImportError:
# kubernetes library check happens in common.py
pass
JSON_PATCH_IMPORT_ERR = None
try:
import jsonpatch
HAS_JSON_PATCH = True
except ImportError:
HAS_JSON_PATCH = False
JSON_PATCH_IMPORT_ERR = traceback.format_exc()
JSON_PATCH_ARGS = {
'api_version': {
'default': 'v1',
'aliases': ['api', 'version'],
},
"kind": {
"type": "str",
"required": True,
},
"namespace": {
"type": "str",
},
"name": {
"type": "str",
"required": True,
},
"patch": {
"type": "list",
"required": True,
"elements": "dict",
},
}
def json_patch(existing, patch):
if not HAS_JSON_PATCH:
error = {
"msg": missing_required_lib('jsonpatch'),
"exception": JSON_PATCH_IMPORT_ERR,
}
return None, error
try:
patch = jsonpatch.JsonPatch(patch)
patched = patch.apply(existing)
return patched, None
except jsonpatch.InvalidJsonPatch as e:
error = {
"msg": "Invalid JSON patch",
"exception": e
}
return None, error
except jsonpatch.JsonPatchConflict as e:
error = {
"msg": "Patch could not be applied due to a conflict",
"exception": e
}
return None, error
def execute_module(k8s_module, module):
kind = module.params.get("kind")
api_version = module.params.get("api_version")
name = module.params.get("name")
namespace = module.params.get("namespace")
patch = module.params.get("patch")
wait = module.params.get("wait")
wait_sleep = module.params.get("wait_sleep")
wait_timeout = module.params.get("wait_timeout")
wait_condition = None
if module.params.get("wait_condition") and module.params.get("wait_condition").get("type"):
wait_condition = module.params['wait_condition']
# definition is needed for wait
definition = {
"kind": kind,
"metadata": {
"name": name,
"namespace": namespace,
}
}
def build_error_msg(kind, name, msg):
return "%s %s: %s" % (kind, name, msg)
resource = k8s_module.find_resource(kind, api_version, fail=True)
try:
existing = resource.get(name=name, namespace=namespace)
except DynamicApiError as exc:
msg = 'Failed to retrieve requested object: {0}'.format(exc.body)
module.fail_json(msg=build_error_msg(kind, name, msg), error=exc.status, status=exc.status, reason=exc.reason)
except ValueError as 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='')
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", **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)
except Exception as exc:
msg = "Failed to patch existing object: {0}".format(exc)
module.fail_json(msg=msg, error=to_native(exc), status='', reason='')
success = True
result = {"result": obj}
if wait and not module.check_mode:
success, result['result'], result['duration'] = k8s_module.wait(resource, definition, wait_sleep, wait_timeout, condition=wait_condition)
match, diffs = k8s_module.diff_objects(existing.to_dict(), obj)
result["changed"] = not match
if module._diff:
result["diff"] = diffs
if not success:
msg = "Resource update timed out"
module.fail_json(msg=msg, **result)
module.exit_json(**result)
def main():
args = copy.deepcopy(AUTH_ARG_SPEC)
args.update(copy.deepcopy(WAIT_ARG_SPEC))
args.update(JSON_PATCH_ARGS)
module = AnsibleModule(argument_spec=args, supports_check_mode=True)
k8s_module = K8sAnsibleMixin(module)
k8s_module.params = module.params
k8s_module.check_library_version()
client = get_api_client(module)
k8s_module.client = client
execute_module(k8s_module, module)
if __name__ == "__main__":
main()