mirror of
https://github.com/ansible-collections/kubernetes.core.git
synced 2026-03-26 21:33:02 +00:00
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>
285 lines
8.2 KiB
Python
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()
|