mirror of
https://github.com/ansible-collections/kubernetes.core.git
synced 2026-04-21 08:11:25 +00:00
save
This commit is contained in:
@@ -64,9 +64,21 @@ options:
|
||||
- The possible reasons in a condition are specific to each resource type in Kubernetes.
|
||||
- See the API documentation of the status field for a given resource to see possible choices.
|
||||
type: dict
|
||||
wait_for:
|
||||
wait_property:
|
||||
description:
|
||||
- Specifies a property on the resource to wait for.
|
||||
- Ignored if C(wait) is not set or is set to I(False).
|
||||
type: str
|
||||
type: dict
|
||||
suboptions:
|
||||
property:
|
||||
type: str
|
||||
required: True
|
||||
description:
|
||||
- The property name to wait for.
|
||||
- This value must be jmespath valid expression, see details here U(http://jmespath.org).
|
||||
value:
|
||||
type: str
|
||||
description:
|
||||
- The expected value of the C(property).
|
||||
- If this is missing, we will check only that the attribute C(property) is present.
|
||||
'''
|
||||
|
||||
@@ -71,7 +71,14 @@ WAIT_ARG_SPEC = dict(
|
||||
reason=dict()
|
||||
)
|
||||
),
|
||||
wait_for=dict()
|
||||
wait_property=dict(
|
||||
type='dict',
|
||||
default=None,
|
||||
options=dict(
|
||||
property=dict(),
|
||||
value=dict()
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
# Map kubernetes-client parameters to ansible parameters
|
||||
|
||||
@@ -29,7 +29,7 @@ from distutils.version import LooseVersion
|
||||
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.args_common import (AUTH_ARG_MAP, AUTH_ARG_SPEC, AUTH_PROXY_HEADERS_SPEC)
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.hashes import generate_hash
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.exceptions import WaitException
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.jsonpath import match_json_property
|
||||
|
||||
from ansible.module_utils.basic import missing_required_lib
|
||||
from ansible.module_utils.six import iteritems, string_types
|
||||
@@ -98,14 +98,6 @@ except ImportError as e:
|
||||
k8s_import_exception = e
|
||||
K8S_IMP_ERR = traceback.format_exc()
|
||||
|
||||
try:
|
||||
import jsonpath_rw
|
||||
HAS_JSONPATH_RW = True
|
||||
jsonpath_import_exception = None
|
||||
except ImportError as e:
|
||||
HAS_JSONPATH_RW = False
|
||||
jsonpath_import_exception = e
|
||||
|
||||
JSON_PATCH_IMP_ERR = None
|
||||
try:
|
||||
import jsonpatch
|
||||
@@ -250,7 +242,7 @@ class K8sAnsibleMixin(object):
|
||||
self.fail(msg='Failed to find exact match for {0}.{1} by [kind, name, singularName, shortNames]'.format(api_version, kind))
|
||||
|
||||
def kubernetes_facts(self, kind, api_version, name=None, namespace=None, label_selectors=None, field_selectors=None,
|
||||
wait=False, wait_sleep=5, wait_timeout=120, state='present', condition=None, wait_for=None):
|
||||
wait=False, wait_sleep=5, wait_timeout=120, state='present', condition=None, wait_property=None):
|
||||
resource = self.find_resource(kind, api_version)
|
||||
api_found = bool(resource)
|
||||
if not api_found:
|
||||
@@ -306,7 +298,7 @@ class K8sAnsibleMixin(object):
|
||||
for resource_instance in resource_list:
|
||||
success, res, duration = self.wait(resource, resource_instance,
|
||||
sleep=wait_sleep, timeout=wait_timeout,
|
||||
state=state, condition=condition, wait_for=wait_for)
|
||||
state=state, condition=condition, wait_property=wait_property)
|
||||
if not success:
|
||||
self.fail(msg="Failed to gather information about %s(s) even"
|
||||
" after waiting for %s seconds" % (res.get('kind'), duration))
|
||||
@@ -391,7 +383,7 @@ class K8sAnsibleMixin(object):
|
||||
response = response.to_dict()
|
||||
return False, response, _wait_for_elapsed()
|
||||
|
||||
def wait(self, resource, definition, sleep, timeout, state='present', condition=None, wait_for=None):
|
||||
def wait(self, resource, definition, sleep, timeout, state='present', condition=None, property=None):
|
||||
|
||||
def _deployment_ready(deployment):
|
||||
# FIXME: frustratingly bool(deployment.status) is True even if status is empty
|
||||
@@ -441,28 +433,16 @@ class K8sAnsibleMixin(object):
|
||||
def _resource_absent(resource):
|
||||
return not resource
|
||||
|
||||
# wait_for requires jsonpath-rw library
|
||||
jsonpath_expr = None
|
||||
if wait_for is not None:
|
||||
if not HAS_JSONPATH_RW:
|
||||
if hasattr(self, 'fail_json'):
|
||||
self.fail_json(msg=missing_required_lib('jsonpath_rw'), error=to_native(jsonpath_import_exception))
|
||||
raise WaitException("wait_for option requires 'jsonpath_rw' library")
|
||||
try:
|
||||
wait_expr = wait_for
|
||||
if wait_for.startswith("."):
|
||||
wait_expr = "$" + wait_for
|
||||
jsonpath_expr = jsonpath_rw.parse(wait_expr)
|
||||
except Exception as parse_err:
|
||||
if hasattr(self, 'fail_json'):
|
||||
self.fail_json(msg="Failed to parse wait_for attribute {0}".format(wait_for), error=to_native(parse_err))
|
||||
raise WaitException("Failed to parse wait_for attribute {0} error is {1}".format(wait_for, to_native(parse_err)))
|
||||
with open("/tmp/resource.txt", "w+") as f:
|
||||
import json
|
||||
f.write("------- Property -------\n{}".format(json.dumps(property, indent=2)))
|
||||
|
||||
def _wait_for_property(resource):
|
||||
try:
|
||||
return all([match.value for match in jsonpath_expr.find(resource)])
|
||||
except Exception as e:
|
||||
return False
|
||||
test = match_json_property(self, resource.to_dict(), property.get('property'), property.get('value', None))
|
||||
with open("/tmp/resource.txt", "w+") as f:
|
||||
import json
|
||||
f.write("------- test = {}\n{}".format(test, json.dumps(resource.to_dict(), indent=2)))
|
||||
return test
|
||||
|
||||
waiter = dict(
|
||||
Deployment=_deployment_ready,
|
||||
@@ -472,17 +452,17 @@ class K8sAnsibleMixin(object):
|
||||
kind = definition['kind']
|
||||
predicates = []
|
||||
if state == 'present':
|
||||
if condition is None and wait_for is None:
|
||||
if condition is None and property is None:
|
||||
predicates.append(waiter.get(kind, lambda x: x))
|
||||
else:
|
||||
if condition:
|
||||
# add waiter on custom condition
|
||||
predicates.append(_custom_condition)
|
||||
if wait_for:
|
||||
if property:
|
||||
# json path predicate
|
||||
predicates.append(_wait_for_property)
|
||||
else:
|
||||
predicates.append(_resource_absent)
|
||||
predicates = [_resource_absent]
|
||||
return self._wait_for(resource, definition['metadata']['name'], definition['metadata'].get('namespace'), predicates, sleep, timeout, state)
|
||||
|
||||
def set_resource_definitions(self, module):
|
||||
@@ -625,7 +605,7 @@ class K8sAnsibleMixin(object):
|
||||
continue_on_error = self.params.get('continue_on_error')
|
||||
if self.params.get('wait_condition') and self.params['wait_condition'].get('type'):
|
||||
wait_condition = self.params['wait_condition']
|
||||
wait_for = self.params.get('wait_for')
|
||||
wait_property = self.params.get('wait_property')
|
||||
|
||||
def build_error_msg(kind, name, msg):
|
||||
return "%s %s: %s" % (kind, name, msg)
|
||||
@@ -735,7 +715,7 @@ class K8sAnsibleMixin(object):
|
||||
result['result'] = k8s_obj
|
||||
if wait and not self.check_mode:
|
||||
success, result['result'], result['duration'] = self.wait(resource, definition, wait_sleep, wait_timeout,
|
||||
condition=wait_condition, wait_for=wait_for)
|
||||
condition=wait_condition, property=wait_property)
|
||||
if existing:
|
||||
existing = existing.to_dict()
|
||||
else:
|
||||
@@ -788,8 +768,8 @@ class K8sAnsibleMixin(object):
|
||||
success = True
|
||||
result['result'] = k8s_obj
|
||||
if wait and not self.check_mode:
|
||||
success, result['result'], result['duration'] = self.wait(resource, definition, wait_sleep,
|
||||
wait_timeout, condition=wait_condition, wait_for=wait_for)
|
||||
success, result['result'], result['duration'] = self.wait(resource, definition, wait_sleep, wait_timeout,
|
||||
condition=wait_condition, property=wait_property)
|
||||
result['changed'] = True
|
||||
result['method'] = 'create'
|
||||
if not success:
|
||||
@@ -824,8 +804,8 @@ class K8sAnsibleMixin(object):
|
||||
success = True
|
||||
result['result'] = k8s_obj
|
||||
if wait and not self.check_mode:
|
||||
success, result['result'], result['duration'] = self.wait(resource, definition, wait_sleep,
|
||||
wait_timeout, condition=wait_condition, wait_for=wait_for)
|
||||
success, result['result'], result['duration'] = self.wait(resource, definition, wait_sleep, wait_timeout,
|
||||
condition=wait_condition, property=wait_property)
|
||||
match, diffs = self.diff_objects(existing.to_dict(), result['result'])
|
||||
result['changed'] = not match
|
||||
result['method'] = 'replace'
|
||||
@@ -859,8 +839,8 @@ class K8sAnsibleMixin(object):
|
||||
success = True
|
||||
result['result'] = k8s_obj
|
||||
if wait and not self.check_mode:
|
||||
success, result['result'], result['duration'] = self.wait(resource, definition,
|
||||
wait_sleep, wait_timeout, condition=wait_condition, wait_for=wait_for)
|
||||
success, result['result'], result['duration'] = self.wait(resource, definition, wait_sleep, wait_timeout,
|
||||
condition=wait_condition, property=wait_property)
|
||||
match, diffs = self.diff_objects(existing.to_dict(), result['result'])
|
||||
result['changed'] = not match
|
||||
result['method'] = 'patch'
|
||||
|
||||
@@ -19,7 +19,3 @@ __metaclass__ = type
|
||||
|
||||
class ApplyException(Exception):
|
||||
""" Could not apply patch """
|
||||
|
||||
|
||||
class WaitException(Exception):
|
||||
""" Bad parameters for Wait operation """
|
||||
|
||||
62
plugins/module_utils/jsonpath.py
Normal file
62
plugins/module_utils/jsonpath.py
Normal file
@@ -0,0 +1,62 @@
|
||||
# Copyright [2021] [Red Hat, Inc.]
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
import traceback
|
||||
from ansible.module_utils.basic import missing_required_lib
|
||||
from ansible.module_utils._text import to_native
|
||||
|
||||
try:
|
||||
import jmespath
|
||||
HAS_JMESPATH_LIB = True
|
||||
jmespath_import_exception = None
|
||||
except ImportError as e:
|
||||
HAS_JMESPATH_LIB = False
|
||||
jmespath_import_exception = e
|
||||
JMESPATH_IMP_ERR = traceback.format_exc()
|
||||
|
||||
|
||||
def match_json_property(module, data, expr, value=None):
|
||||
"""
|
||||
This function uses jmespath to validate json data
|
||||
- module: running the function (used to fail in case of error)
|
||||
- data: JSON document
|
||||
- expr: Specify how to extract elements from a JSON document (jmespath, http://jmespath.org)
|
||||
- value: the matching JSON element should have this value, if set to None this is ignored
|
||||
"""
|
||||
def _raise_or_fail(err, **kwargs):
|
||||
if module and hasattr(module, "fail_json"):
|
||||
module.fail_json(error=to_native(err), **kwargs)
|
||||
raise err
|
||||
|
||||
def _match_value(buf, v):
|
||||
# convert all values from bool to str and lowercase them
|
||||
return v.lower() in [str(i).lower() for i in buf]
|
||||
|
||||
if not HAS_JMESPATH_LIB:
|
||||
_raise_or_fail(jmespath_import_exception, msg=missing_required_lib('jmespath'), exception=JMESPATH_IMP_ERR)
|
||||
|
||||
jmespath.functions.REVERSE_TYPES_MAP['string'] = jmespath.functions.REVERSE_TYPES_MAP['string'] + ('AnsibleUnicode', 'AnsibleUnsafeText', )
|
||||
try:
|
||||
content = jmespath.search(expr, data)
|
||||
if not content:
|
||||
return False
|
||||
if not value or _match_value(content, value):
|
||||
return True
|
||||
return False
|
||||
except Exception as err:
|
||||
_raise_or_fail(err, msg="JMESPathError failed to extract from JSON document using expr: {}".format(expr))
|
||||
@@ -238,11 +238,19 @@ EXAMPLES = r'''
|
||||
reason: DeploymentPaused
|
||||
|
||||
# Wait for this service to have acquired an External IP
|
||||
- name: Deploy the dashboard service (lb)
|
||||
- name: Create ingress and wait for ip to be assigned
|
||||
kubernetes.core.k8s:
|
||||
template: dash-service.yaml
|
||||
wait: yes
|
||||
wait_for: .status.loadBalancer.ingress[*].ip
|
||||
wait_property:
|
||||
property: status.loadBalancer.ingress[*].ip
|
||||
|
||||
- name: Create Pod and wait for containers for be running
|
||||
kubernetes.core.k8s:
|
||||
template: pod.yaml
|
||||
wait: yes
|
||||
wait_property:
|
||||
property: status.containerStatuses[*].state.running
|
||||
'''
|
||||
|
||||
RETURN = r'''
|
||||
|
||||
69
tests/unit/module_utils/test_jsonpath.py
Normal file
69
tests/unit/module_utils/test_jsonpath.py
Normal file
@@ -0,0 +1,69 @@
|
||||
# Copyright [2021] [Red Hat, Inc.]
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
from ansible_collections.kubernetes.core.plugins.module_utils.jsonpath import match_json_property
|
||||
|
||||
import pytest
|
||||
jmespath = pytest.importorskip("jmespath")
|
||||
|
||||
|
||||
def test_property_present():
|
||||
data = {
|
||||
"Kind": "Pod",
|
||||
"containers": [
|
||||
{"name": "t0", "image": "nginx"},
|
||||
{"name": "t1", "image": "python"},
|
||||
{"name": "t2", "image": "mongo", "state": "running"}
|
||||
]
|
||||
}
|
||||
assert match_json_property(None, data, "containers[*].state")
|
||||
assert not match_json_property(None, data, "containers[*].status")
|
||||
|
||||
|
||||
def test_property_value():
|
||||
data = {
|
||||
"Kind": "Pod",
|
||||
"containers": [
|
||||
{"name": "t0", "image": "nginx"},
|
||||
{"name": "t1", "image": "python"},
|
||||
{"name": "t2", "image": "mongo", "state": "running"}
|
||||
]
|
||||
}
|
||||
assert match_json_property(None, data, "containers[*].state", "running")
|
||||
assert match_json_property(None, data, "containers[*].state", "Running")
|
||||
assert not match_json_property(None, data, "containers[*].state", "off")
|
||||
|
||||
|
||||
def test_boolean_value():
|
||||
data = {
|
||||
"containers": [
|
||||
{"image": "nginx"},
|
||||
{"image": "python"},
|
||||
{"image": "mongo", "connected": True}
|
||||
]
|
||||
}
|
||||
assert match_json_property(None, data, "containers[*].connected", "true")
|
||||
assert match_json_property(None, data, "containers[*].connected", "True")
|
||||
assert match_json_property(None, data, "containers[*].connected", "TRUE")
|
||||
|
||||
|
||||
def test_valid_expression():
|
||||
data = dict(key="ansible", value="unit-test")
|
||||
with pytest.raises(jmespath.exceptions.ParseError) as parsing_err:
|
||||
match_json_property(None, data, ".ansible")
|
||||
assert "Parse error" in str(parsing_err.value)
|
||||
@@ -1,3 +1,4 @@
|
||||
pytest
|
||||
PyYAML
|
||||
kubernetes
|
||||
jmespath
|
||||
|
||||
Reference in New Issue
Block a user