From 4a9eca576d60b53458d35b8d539da387a65c4250 Mon Sep 17 00:00:00 2001 From: aubin Date: Mon, 3 May 2021 10:55:24 +0200 Subject: [PATCH 01/17] missing implementation of jsonpath library --- plugins/doc_fragments/k8s_wait_options.py | 6 +++ plugins/module_utils/args_common.py | 3 +- plugins/module_utils/common.py | 53 ++++++++++++++++------- plugins/modules/k8s.py | 8 ++++ plugins/modules/k8s_info.py | 2 + plugins/modules/k8s_rollback.py | 1 + 6 files changed, 57 insertions(+), 16 deletions(-) diff --git a/plugins/doc_fragments/k8s_wait_options.py b/plugins/doc_fragments/k8s_wait_options.py index 06600564c3..d556f36dd2 100644 --- a/plugins/doc_fragments/k8s_wait_options.py +++ b/plugins/doc_fragments/k8s_wait_options.py @@ -64,4 +64,10 @@ class ModuleDocFragment(object): - 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: + description: + - Specifies a property on the resource to wait for. + - Ignored if C(wait) is not set or is set to False. + default: 120 + type: int ''' diff --git a/plugins/module_utils/args_common.py b/plugins/module_utils/args_common.py index 67c183db74..398a6c1098 100644 --- a/plugins/module_utils/args_common.py +++ b/plugins/module_utils/args_common.py @@ -70,7 +70,8 @@ def list_dict_str(value): status=dict(default=True, choices=[True, False, "Unknown"]), reason=dict() ) - ) + ), + wait_for=dict(type='list') ) # Map kubernetes-client parameters to ansible parameters diff --git a/plugins/module_utils/common.py b/plugins/module_utils/common.py index 14d13e5c39..1fbb46b74b 100644 --- a/plugins/module_utils/common.py +++ b/plugins/module_utils/common.py @@ -37,6 +37,7 @@ from ansible.module_utils._text import to_native, to_bytes, to_text from ansible.module_utils.common.dict_transformations import dict_merge from ansible.module_utils.parsing.convert_bool import boolean +from ansible.errors import AnsibleError K8S_IMP_ERR = None try: @@ -105,6 +106,13 @@ k8s_import_exception = e K8S_IMP_ERR = traceback.format_exc() +try: + import jsonpath_rw as jsonpath + HAS_JSONPATH_RW = True + jsonpath_import_exception = None +except ImportError as e: + HAS_JSONPATH_RW = False + jsonpath_import_exception = e def configuration_digest(configuration): m = hashlib.sha256() @@ -246,7 +254,7 @@ def find_resource(self, kind, api_version, fail=False): 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=False, wait_sleep=5, wait_timeout=120, state='present', condition=None, wait_for=None): resource = self.find_resource(kind, api_version) api_found = bool(resource) if not api_found: @@ -302,7 +310,7 @@ def _elapsed(): for resource_instance in resource_list: success, res, duration = self.wait(resource, resource_instance, sleep=wait_sleep, timeout=wait_timeout, - state=state, condition=condition) + state=state, condition=condition, wait_for=wait_for) if not success: self.fail(msg="Failed to gather information about %s(s) even" " after waiting for %s seconds" % (res.get('kind'), duration)) @@ -365,7 +373,7 @@ def diff_objects(self, existing, new): def fail(self, msg=None): self.fail_json(msg=msg) - def _wait_for(self, resource, name, namespace, predicate, sleep, timeout, state): + def _wait_for(self, resource, name, namespace, predicates, sleep, timeout, state): start = datetime.now() def _wait_for_elapsed(): @@ -375,7 +383,7 @@ def _wait_for_elapsed(): while _wait_for_elapsed() < timeout: try: response = resource.get(name=name, namespace=namespace) - if predicate(response): + if all([ predicate(response) for predicate in predicates ]): if response: return True, response.to_dict(), _wait_for_elapsed() return True, {}, _wait_for_elapsed() @@ -387,7 +395,7 @@ def _wait_for_elapsed(): response = response.to_dict() return False, response, _wait_for_elapsed() - def wait(self, resource, definition, sleep, timeout, state='present', condition=None): + def wait(self, resource, definition, sleep, timeout, state='present', condition=None, wait_for=None): def _deployment_ready(deployment): # FIXME: frustratingly bool(deployment.status) is True even if status is empty @@ -434,22 +442,36 @@ def _custom_condition(resource): return True return False + def _wait_for_property(resource): + return all([ jsonpath.match("$.{}".format(item),json_data) for item in wait_for ]) + def _resource_absent(resource): return not resource + # wait_for requires jsonpath-rw library + if wait_for is not None and 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 AnsibleError("wait_for option requires 'jsonpath_rw' library") + waiter = dict( Deployment=_deployment_ready, DaemonSet=_daemonset_ready, Pod=_pod_ready ) kind = definition['kind'] - if state == 'present' and not condition: - predicate = waiter.get(kind, lambda x: x) - elif state == 'present' and condition: - predicate = _custom_condition + predicates = [] + if state == 'present': + if condition: + predicates.append(_custom_condition) + if wait_for: + # json path predicate + predicates.append(_wait_for_property) + if condition is None and wait_for is None: + predicates.append(waiter.get(kind, lambda x: x)) else: - predicate = _resource_absent - return self._wait_for(resource, definition['metadata']['name'], definition['metadata'].get('namespace'), predicate, sleep, timeout, state) + predicates.append(_resource_absent) + return self._wait_for(resource, definition['metadata']['name'], definition['metadata'].get('namespace'), predicates, sleep, timeout, state) def set_resource_definitions(self, module): resource_definition = module.params.get('resource_definition') @@ -602,6 +624,7 @@ def perform_action(self, resource, definition): 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') def build_error_msg(kind, name, msg): return "%s %s: %s" % (kind, name, msg) @@ -710,7 +733,7 @@ def build_error_msg(kind, name, msg): 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) + success, result['result'], result['duration'] = self.wait(resource, definition, wait_sleep, wait_timeout, condition=wait_condition, wait_for=wait_for) if existing: existing = existing.to_dict() else: @@ -763,7 +786,7 @@ def build_error_msg(kind, name, msg): 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) + success, result['result'], result['duration'] = self.wait(resource, definition, wait_sleep, wait_timeout, condition=wait_condition, wait_for=wait_for) result['changed'] = True result['method'] = 'create' if not success: @@ -798,7 +821,7 @@ def build_error_msg(kind, name, msg): 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) + success, result['result'], result['duration'] = self.wait(resource, definition, wait_sleep, wait_timeout, condition=wait_condition, wait_for=wait_for) match, diffs = self.diff_objects(existing.to_dict(), result['result']) result['changed'] = not match result['method'] = 'replace' @@ -836,7 +859,7 @@ def build_error_msg(kind, name, msg): 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) + success, result['result'], result['duration'] = self.wait(resource, definition, wait_sleep, wait_timeout, condition=wait_condition, wait_for=wait_for) match, diffs = self.diff_objects(existing.to_dict(), result['result']) result['changed'] = not match result['method'] = 'patch' diff --git a/plugins/modules/k8s.py b/plugins/modules/k8s.py index 0672aa9301..15756d56b7 100644 --- a/plugins/modules/k8s.py +++ b/plugins/modules/k8s.py @@ -136,6 +136,7 @@ - "python >= 2.7" - "openshift >= 0.6" - "PyYAML >= 3.11" + - "jsonpath-rw" ''' EXAMPLES = r''' @@ -246,6 +247,13 @@ type: Progressing status: Unknown reason: DeploymentPaused + +# Wait for this service to have acquired an External IP +- name: Deploy the dashboard service (lb) + kubernetes.core.k8s: + template: dash-service.yaml + wait: yes + wait_for: .status.loadBalancer.ingress[*].ip ''' RETURN = r''' diff --git a/plugins/modules/k8s_info.py b/plugins/modules/k8s_info.py index 28c639a673..bcb1dc4741 100644 --- a/plugins/modules/k8s_info.py +++ b/plugins/modules/k8s_info.py @@ -52,6 +52,7 @@ - "python >= 2.7" - "openshift >= 0.6" - "PyYAML >= 3.11" + - "jsonpath-rw" ''' EXAMPLES = r''' @@ -164,6 +165,7 @@ def execute_module(module, k8s_ansible_mixin): wait_sleep=module.params["wait_sleep"], wait_timeout=module.params["wait_timeout"], condition=module.params["wait_condition"], + wait_for=module.params["wait_for"] ) module.exit_json(changed=False, **facts) diff --git a/plugins/modules/k8s_rollback.py b/plugins/modules/k8s_rollback.py index e41da81037..537a1381ad 100644 --- a/plugins/modules/k8s_rollback.py +++ b/plugins/modules/k8s_rollback.py @@ -34,6 +34,7 @@ - "python >= 2.7" - "openshift >= 0.6" - "PyYAML >= 3.11" + - "jsonpath-rw" ''' EXAMPLES = r''' From ce037ce3ba1ec0e8e54d1336aa69597a661cd733 Mon Sep 17 00:00:00 2001 From: abikouo Date: Tue, 18 May 2021 09:35:49 +0200 Subject: [PATCH 02/17] not tested --- plugins/doc_fragments/k8s_wait_options.py | 4 +--- plugins/module_utils/common.py | 16 +++++++++------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/plugins/doc_fragments/k8s_wait_options.py b/plugins/doc_fragments/k8s_wait_options.py index d556f36dd2..263986ec8a 100644 --- a/plugins/doc_fragments/k8s_wait_options.py +++ b/plugins/doc_fragments/k8s_wait_options.py @@ -67,7 +67,5 @@ class ModuleDocFragment(object): wait_for: description: - Specifies a property on the resource to wait for. - - Ignored if C(wait) is not set or is set to False. - default: 120 - type: int + - Ignored if C(wait) is not set or is set to I(False). ''' diff --git a/plugins/module_utils/common.py b/plugins/module_utils/common.py index bea2a39b93..b64f475dfc 100644 --- a/plugins/module_utils/common.py +++ b/plugins/module_utils/common.py @@ -373,7 +373,7 @@ def _wait_for(self, resource, name, namespace, predicates, sleep, timeout, state def _wait_for_elapsed(): return (datetime.now() - start).seconds - + response = None while _wait_for_elapsed() < timeout: try: @@ -438,7 +438,7 @@ def _custom_condition(resource): return False def _wait_for_property(resource): - return all([ jsonpath.match("$.{}".format(item),json_data) for item in wait_for ]) + return all([ jsonpath.match("$.{}".format(item),resource) for item in wait_for ]) def _resource_absent(resource): return not resource @@ -457,13 +457,15 @@ def _resource_absent(resource): kind = definition['kind'] predicates = [] if state == 'present': - if condition: - predicates.append(_custom_condition) - if wait_for: - # json path predicate - predicates.append(_wait_for_property) if condition is None and wait_for 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: + # json path predicate + predicates.append(_wait_for_property) else: predicates.append(_resource_absent) return self._wait_for(resource, definition['metadata']['name'], definition['metadata'].get('namespace'), predicates, sleep, timeout, state) From b453f791e926695bbfde703d4a5c763de950bc12 Mon Sep 17 00:00:00 2001 From: abikouo Date: Tue, 18 May 2021 12:10:23 +0200 Subject: [PATCH 03/17] sanity --- plugins/doc_fragments/k8s_wait_options.py | 1 + plugins/filter/k8s.py | 2 +- plugins/module_utils/args_common.py | 2 +- plugins/module_utils/common.py | 49 ++++++++++++++++------- plugins/module_utils/exceptions.py | 4 ++ 5 files changed, 41 insertions(+), 17 deletions(-) diff --git a/plugins/doc_fragments/k8s_wait_options.py b/plugins/doc_fragments/k8s_wait_options.py index 263986ec8a..34456e44c0 100644 --- a/plugins/doc_fragments/k8s_wait_options.py +++ b/plugins/doc_fragments/k8s_wait_options.py @@ -68,4 +68,5 @@ class ModuleDocFragment(object): 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 ''' diff --git a/plugins/filter/k8s.py b/plugins/filter/k8s.py index bf19ba3bd2..b3eddc6615 100644 --- a/plugins/filter/k8s.py +++ b/plugins/filter/k8s.py @@ -23,7 +23,7 @@ def k8s_config_resource_name(resource): # ---- Ansible filters ---- class FilterModule(object): """ - + Kubernetes filter Module """ def filters(self): diff --git a/plugins/module_utils/args_common.py b/plugins/module_utils/args_common.py index 398a6c1098..1284c6f8d6 100644 --- a/plugins/module_utils/args_common.py +++ b/plugins/module_utils/args_common.py @@ -71,7 +71,7 @@ def list_dict_str(value): reason=dict() ) ), - wait_for=dict(type='list') + wait_for=dict() ) # Map kubernetes-client parameters to ansible parameters diff --git a/plugins/module_utils/common.py b/plugins/module_utils/common.py index b64f475dfc..d1fa17a89c 100644 --- a/plugins/module_utils/common.py +++ b/plugins/module_utils/common.py @@ -29,13 +29,14 @@ 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.module_utils.basic import missing_required_lib from ansible.module_utils.six import iteritems, string_types from ansible.module_utils._text import to_native, to_bytes, to_text from ansible.module_utils.common.dict_transformations import dict_merge from ansible.module_utils.parsing.convert_bool import boolean -from ansible.errors import AnsibleError + K8S_IMP_ERR = None try: @@ -98,7 +99,7 @@ K8S_IMP_ERR = traceback.format_exc() try: - import jsonpath_rw as jsonpath + import jsonpath_rw HAS_JSONPATH_RW = True jsonpath_import_exception = None except ImportError as e: @@ -373,12 +374,12 @@ def _wait_for(self, resource, name, namespace, predicates, sleep, timeout, state def _wait_for_elapsed(): return (datetime.now() - start).seconds - + response = None while _wait_for_elapsed() < timeout: try: response = resource.get(name=name, namespace=namespace) - if all([ predicate(response) for predicate in predicates ]): + if all([predicate(response) for predicate in predicates]): if response: return True, response.to_dict(), _wait_for_elapsed() return True, {}, _wait_for_elapsed() @@ -437,17 +438,31 @@ def _custom_condition(resource): return True return False - def _wait_for_property(resource): - return all([ jsonpath.match("$.{}".format(item),resource) for item in wait_for ]) - def _resource_absent(resource): return not resource # wait_for requires jsonpath-rw library - if wait_for is not None and 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 AnsibleError("wait_for option 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))) + + def _wait_for_property(resource): + try: + return all([match.value for match in jsonpath_expr.find(resource)]) + except Exception as e: + return False waiter = dict( Deployment=_deployment_ready, @@ -719,7 +734,8 @@ def build_error_msg(kind, name, msg): 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, wait_for=wait_for) if existing: existing = existing.to_dict() else: @@ -772,7 +788,8 @@ def build_error_msg(kind, name, msg): 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, wait_for=wait_for) result['changed'] = True result['method'] = 'create' if not success: @@ -807,7 +824,8 @@ def build_error_msg(kind, name, msg): 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, wait_for=wait_for) match, diffs = self.diff_objects(existing.to_dict(), result['result']) result['changed'] = not match result['method'] = 'replace' @@ -841,7 +859,8 @@ def build_error_msg(kind, name, msg): 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, wait_for=wait_for) match, diffs = self.diff_objects(existing.to_dict(), result['result']) result['changed'] = not match result['method'] = 'patch' diff --git a/plugins/module_utils/exceptions.py b/plugins/module_utils/exceptions.py index 35d3c2fd63..fba4165c7a 100644 --- a/plugins/module_utils/exceptions.py +++ b/plugins/module_utils/exceptions.py @@ -19,3 +19,7 @@ class ApplyException(Exception): """ Could not apply patch """ + + +class WaitException(Exception): + """ Bad parameters for Wait operation """ From dd821f06aa59c5750692bcf2aad3a856065abfd2 Mon Sep 17 00:00:00 2001 From: abikouo Date: Wed, 19 May 2021 15:58:43 +0200 Subject: [PATCH 04/17] save --- plugins/doc_fragments/k8s_wait_options.py | 16 +++++- plugins/module_utils/args_common.py | 9 ++- plugins/module_utils/common.py | 66 ++++++++-------------- plugins/module_utils/exceptions.py | 4 -- plugins/module_utils/jsonpath.py | 62 ++++++++++++++++++++ plugins/modules/k8s.py | 12 +++- tests/unit/module_utils/test_jsonpath.py | 69 +++++++++++++++++++++++ tests/unit/requirements.txt | 1 + 8 files changed, 187 insertions(+), 52 deletions(-) create mode 100644 plugins/module_utils/jsonpath.py create mode 100644 tests/unit/module_utils/test_jsonpath.py diff --git a/plugins/doc_fragments/k8s_wait_options.py b/plugins/doc_fragments/k8s_wait_options.py index 34456e44c0..6b1f4d5386 100644 --- a/plugins/doc_fragments/k8s_wait_options.py +++ b/plugins/doc_fragments/k8s_wait_options.py @@ -64,9 +64,21 @@ class ModuleDocFragment(object): - 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. ''' diff --git a/plugins/module_utils/args_common.py b/plugins/module_utils/args_common.py index 1284c6f8d6..02d1ba0f80 100644 --- a/plugins/module_utils/args_common.py +++ b/plugins/module_utils/args_common.py @@ -71,7 +71,14 @@ def list_dict_str(value): 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 diff --git a/plugins/module_utils/common.py b/plugins/module_utils/common.py index d1fa17a89c..5e4a3b87fe 100644 --- a/plugins/module_utils/common.py +++ b/plugins/module_utils/common.py @@ -29,7 +29,7 @@ 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 @@ 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 @@ def find_resource(self, kind, api_version, fail=False): 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 @@ def _elapsed(): 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 @@ def _wait_for_elapsed(): 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 @@ def _custom_condition(resource): 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 @@ def _wait_for_property(resource): 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 @@ def perform_action(self, resource, definition): 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 @@ def build_error_msg(kind, name, msg): 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 @@ def build_error_msg(kind, name, msg): 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 @@ def build_error_msg(kind, name, msg): 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 @@ def build_error_msg(kind, name, msg): 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' diff --git a/plugins/module_utils/exceptions.py b/plugins/module_utils/exceptions.py index fba4165c7a..35d3c2fd63 100644 --- a/plugins/module_utils/exceptions.py +++ b/plugins/module_utils/exceptions.py @@ -19,7 +19,3 @@ class ApplyException(Exception): """ Could not apply patch """ - - -class WaitException(Exception): - """ Bad parameters for Wait operation """ diff --git a/plugins/module_utils/jsonpath.py b/plugins/module_utils/jsonpath.py new file mode 100644 index 0000000000..faa6e39b43 --- /dev/null +++ b/plugins/module_utils/jsonpath.py @@ -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)) diff --git a/plugins/modules/k8s.py b/plugins/modules/k8s.py index f98044b45f..d5a4f45ac6 100644 --- a/plugins/modules/k8s.py +++ b/plugins/modules/k8s.py @@ -238,11 +238,19 @@ 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''' diff --git a/tests/unit/module_utils/test_jsonpath.py b/tests/unit/module_utils/test_jsonpath.py new file mode 100644 index 0000000000..59be299a64 --- /dev/null +++ b/tests/unit/module_utils/test_jsonpath.py @@ -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) diff --git a/tests/unit/requirements.txt b/tests/unit/requirements.txt index 55c7255f3e..f46d016c23 100644 --- a/tests/unit/requirements.txt +++ b/tests/unit/requirements.txt @@ -1,3 +1,4 @@ pytest PyYAML kubernetes +jmespath From 0a28aa02e8548939cabce91cfed4693ab36f015d Mon Sep 17 00:00:00 2001 From: abikouo Date: Thu, 20 May 2021 10:29:30 +0200 Subject: [PATCH 05/17] updates --- changelogs/fragments/105-wait_property.yaml | 3 + molecule/default/tasks/waiter.yml | 87 +++++++++++++++++++++ plugins/doc_fragments/k8s_wait_options.py | 2 + plugins/module_utils/common.py | 10 +-- plugins/module_utils/jsonpath.py | 26 ++++-- tests/unit/module_utils/test_jsonpath.py | 5 +- 6 files changed, 113 insertions(+), 20 deletions(-) create mode 100644 changelogs/fragments/105-wait_property.yaml diff --git a/changelogs/fragments/105-wait_property.yaml b/changelogs/fragments/105-wait_property.yaml new file mode 100644 index 0000000000..4ecabd1aa8 --- /dev/null +++ b/changelogs/fragments/105-wait_property.yaml @@ -0,0 +1,3 @@ +--- +minor_changes: + - k8s - add new option ``wait_property`` to support ability to wait on arbitrary property (https://github.com/ansible-collections/kubernetes.core/pull/105). diff --git a/molecule/default/tasks/waiter.yml b/molecule/default/tasks/waiter.yml index 44fc42b3ff..e2c1da0c81 100644 --- a/molecule/default/tasks/waiter.yml +++ b/molecule/default/tasks/waiter.yml @@ -364,6 +364,93 @@ that: - short_wait_remove_pod is failed + - name: add a simple crashing pod and wait until container is running + k8s: + definition: + apiVersion: v1 + kind: Pod + metadata: + name: pod-crash-0 + namespace: "{{ wait_namespace }}" + spec: + containers: + - name: crashing-container + image: busybox + command: ['/dummy/dummy-shell', '-c', 'sleep 2000'] + wait: yes + wait_timeout: 10 + wait_property: + property: status.containerStatuses[*].state.running + ignore_errors: true + register: crash_pod + + - name: assert that task failed + assert: + that: + - crash_pod is failed + - crash_pod.changed + - '"Resource creation timed out" in crash_pod.msg' + + - name: add a valid pod and wait until container is running + k8s: + definition: + apiVersion: v1 + kind: Pod + metadata: + name: pod-valid-0 + namespace: "{{ wait_namespace }}" + spec: + containers: + - name: crashing-container + image: busybox + command: ['/bin/sh', '-c', 'sleep 10000'] + wait: yes + wait_timeout: 10 + wait_property: + property: status.containerStatuses[*].state.running + ignore_errors: true + register: valid_pod + + - name: assert that task failed + assert: + that: + - valid_pod is successful + - valid_pod.changed + - valid_pod.result.status.containerStatuses[0].state.running is defined + + - name: create pod (waiting for container.ready set to false) + k8s: + definition: + apiVersion: v1 + kind: Pod + metadata: + name: redis-pod + namespace: "{{ wait_namespace }}" + spec: + containers: + - name: redis-container + image: redis + volumeMounts: + - name: test + mountPath: "/etc/test" + readOnly: true + volumes: + - name: test + configMap: + name: redis-config + wait: yes + wait_timeout: 10 + wait_property: + property: status.containerStatuses[0].ready + value: "false" + register: wait_boolean + + - name: assert that pod was created but not running + assert: + that: + - wait_boolean.changed + - wait_boolean.result.status.phase == 'Pending' + always: - name: Remove namespace k8s: diff --git a/plugins/doc_fragments/k8s_wait_options.py b/plugins/doc_fragments/k8s_wait_options.py index 6b1f4d5386..6303bc631b 100644 --- a/plugins/doc_fragments/k8s_wait_options.py +++ b/plugins/doc_fragments/k8s_wait_options.py @@ -69,6 +69,7 @@ class ModuleDocFragment(object): - Specifies a property on the resource to wait for. - Ignored if C(wait) is not set or is set to I(False). type: dict + version_added: '2.0.0' suboptions: property: type: str @@ -80,5 +81,6 @@ class ModuleDocFragment(object): type: str description: - The expected value of the C(property). + - The value is not case-sensitive. - If this is missing, we will check only that the attribute C(property) is present. ''' diff --git a/plugins/module_utils/common.py b/plugins/module_utils/common.py index c868ee3534..db5ddc62e8 100644 --- a/plugins/module_utils/common.py +++ b/plugins/module_utils/common.py @@ -434,16 +434,8 @@ def _custom_condition(resource): def _resource_absent(resource): return not resource - 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): - 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 + return match_json_property(self, resource.to_dict(), property.get('property'), property.get('value', None)) waiter = dict( Deployment=_deployment_ready, diff --git a/plugins/module_utils/jsonpath.py b/plugins/module_utils/jsonpath.py index faa6e39b43..411e2289cd 100644 --- a/plugins/module_utils/jsonpath.py +++ b/plugins/module_utils/jsonpath.py @@ -23,11 +23,10 @@ try: import jmespath HAS_JMESPATH_LIB = True - jmespath_import_exception = None + JMESPATH_IMP_ERR = None except ImportError as e: HAS_JMESPATH_LIB = False - jmespath_import_exception = e - JMESPATH_IMP_ERR = traceback.format_exc() + JMESPATH_IMP_ERR = e def match_json_property(module, data, expr, value=None): @@ -44,18 +43,29 @@ def _raise_or_fail(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 isinstance(buf, list): + # convert all values from bool to str and lowercase them + return v.lower() in [str(i).lower() for i in buf] + elif isinstance(buf, str): + return v.lower() == content.lower() + elif isinstance(buf, bool): + return v.lower() == str(content).lower() + else: + # unable to test single value against dict + return False if not HAS_JMESPATH_LIB: - _raise_or_fail(jmespath_import_exception, msg=missing_required_lib('jmespath'), exception=JMESPATH_IMP_ERR) + _raise_or_fail(JMESPATH_IMP_ERR, msg=missing_required_lib('jmespath')) jmespath.functions.REVERSE_TYPES_MAP['string'] = jmespath.functions.REVERSE_TYPES_MAP['string'] + ('AnsibleUnicode', 'AnsibleUnsafeText', ) try: content = jmespath.search(expr, data) - if not content: + with open("/tmp/play.cont", "w") as f: + f.write("{}".format(content)) + if content is None or content == []: return False - if not value or _match_value(content, value): + if value is None or _match_value(content, value): + # looking for state present return True return False except Exception as err: diff --git a/tests/unit/module_utils/test_jsonpath.py b/tests/unit/module_utils/test_jsonpath.py index 59be299a64..aa7fb3f1be 100644 --- a/tests/unit/module_utils/test_jsonpath.py +++ b/tests/unit/module_utils/test_jsonpath.py @@ -24,7 +24,6 @@ def test_property_present(): data = { - "Kind": "Pod", "containers": [ {"name": "t0", "image": "nginx"}, {"name": "t1", "image": "python"}, @@ -37,7 +36,6 @@ def test_property_present(): def test_property_value(): data = { - "Kind": "Pod", "containers": [ {"name": "t0", "image": "nginx"}, {"name": "t1", "image": "python"}, @@ -52,7 +50,7 @@ def test_property_value(): def test_boolean_value(): data = { "containers": [ - {"image": "nginx"}, + {"image": "nginx", "poweron": False}, {"image": "python"}, {"image": "mongo", "connected": True} ] @@ -60,6 +58,7 @@ def test_boolean_value(): 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") + assert match_json_property(None, data, "containers[0].poweron", "false") def test_valid_expression(): From 4db9724057fc42123d9919687318779531e94801 Mon Sep 17 00:00:00 2001 From: abikouo <79859644+abikouo@users.noreply.github.com> Date: Thu, 20 May 2021 10:39:19 +0200 Subject: [PATCH 06/17] Update args_common.py --- plugins/module_utils/args_common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/module_utils/args_common.py b/plugins/module_utils/args_common.py index 02d1ba0f80..10171ae9f1 100644 --- a/plugins/module_utils/args_common.py +++ b/plugins/module_utils/args_common.py @@ -75,7 +75,7 @@ def list_dict_str(value): type='dict', default=None, options=dict( - property=dict(), + property=dict(required=True), value=dict() ) ) From 2691b0f43db3b77636fd5dfbf7ae8adfb972633c Mon Sep 17 00:00:00 2001 From: abikouo Date: Thu, 20 May 2021 11:17:10 +0200 Subject: [PATCH 07/17] lint validation --- .github/workflows/ci.yml | 2 +- molecule/default/tasks/waiter.yml | 32 +++++++++++++++---------------- plugins/module_utils/jsonpath.py | 5 +---- 3 files changed, 18 insertions(+), 21 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e98f925c0f..393d224ec5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -92,7 +92,7 @@ jobs: # The 3.3.0 release of molecule introduced a breaking change. See # https://github.com/ansible-community/molecule/issues/3083 - name: Install molecule and openshift dependencies - run: pip install ansible "molecule<3.3.0" yamllint openshift flake8 jsonpatch + run: pip install ansible "molecule<3.3.0" yamllint openshift flake8 jsonpatch jmespath # The latest release doesn't work with Molecule currently. # See: https://github.com/ansible-community/molecule/issues/2757 diff --git a/molecule/default/tasks/waiter.yml b/molecule/default/tasks/waiter.yml index e2c1da0c81..9fdb8a73c2 100644 --- a/molecule/default/tasks/waiter.yml +++ b/molecule/default/tasks/waiter.yml @@ -374,9 +374,9 @@ namespace: "{{ wait_namespace }}" spec: containers: - - name: crashing-container - image: busybox - command: ['/dummy/dummy-shell', '-c', 'sleep 2000'] + - name: crashing-container + image: busybox + command: ['/dummy/dummy-shell', '-c', 'sleep 2000'] wait: yes wait_timeout: 10 wait_property: @@ -390,7 +390,7 @@ - crash_pod is failed - crash_pod.changed - '"Resource creation timed out" in crash_pod.msg' - + - name: add a valid pod and wait until container is running k8s: definition: @@ -401,9 +401,9 @@ namespace: "{{ wait_namespace }}" spec: containers: - - name: crashing-container - image: busybox - command: ['/bin/sh', '-c', 'sleep 10000'] + - name: crashing-container + image: busybox + command: ['/bin/sh', '-c', 'sleep 10000'] wait: yes wait_timeout: 10 wait_property: @@ -428,16 +428,16 @@ namespace: "{{ wait_namespace }}" spec: containers: - - name: redis-container - image: redis - volumeMounts: - - name: test - mountPath: "/etc/test" - readOnly: true + - name: redis-container + image: redis + volumeMounts: + - name: test + mountPath: "/etc/test" + readOnly: true volumes: - - name: test - configMap: - name: redis-config + - name: test + configMap: + name: redis-config wait: yes wait_timeout: 10 wait_property: diff --git a/plugins/module_utils/jsonpath.py b/plugins/module_utils/jsonpath.py index 411e2289cd..a43106832c 100644 --- a/plugins/module_utils/jsonpath.py +++ b/plugins/module_utils/jsonpath.py @@ -16,7 +16,6 @@ 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 @@ -60,8 +59,6 @@ def _match_value(buf, v): jmespath.functions.REVERSE_TYPES_MAP['string'] = jmespath.functions.REVERSE_TYPES_MAP['string'] + ('AnsibleUnicode', 'AnsibleUnsafeText', ) try: content = jmespath.search(expr, data) - with open("/tmp/play.cont", "w") as f: - f.write("{}".format(content)) if content is None or content == []: return False if value is None or _match_value(content, value): @@ -69,4 +66,4 @@ def _match_value(buf, v): return True return False except Exception as err: - _raise_or_fail(err, msg="JMESPathError failed to extract from JSON document using expr: {}".format(expr)) + _raise_or_fail(err, msg="JMESPathError failed to extract from JSON document using expr: {0}".format(expr)) From 3939c5304b00c1d27556c22f25aa284edb685295 Mon Sep 17 00:00:00 2001 From: abikouo Date: Thu, 20 May 2021 11:45:35 +0200 Subject: [PATCH 08/17] fix --- plugins/module_utils/common.py | 4 ++-- plugins/modules/k8s.py | 2 ++ plugins/modules/k8s_info.py | 4 ++-- plugins/modules/k8s_rollback.py | 1 - 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/plugins/module_utils/common.py b/plugins/module_utils/common.py index db5ddc62e8..f65ae06b8a 100644 --- a/plugins/module_utils/common.py +++ b/plugins/module_utils/common.py @@ -242,7 +242,7 @@ def find_resource(self, kind, api_version, fail=False): 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_property=None): + wait=False, wait_sleep=5, wait_timeout=120, state='present', condition=None, property=None): resource = self.find_resource(kind, api_version) api_found = bool(resource) if not api_found: @@ -298,7 +298,7 @@ def _elapsed(): 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_property=wait_property) + state=state, condition=condition, property=property) if not success: self.fail(msg="Failed to gather information about %s(s) even" " after waiting for %s seconds" % (res.get('kind'), duration)) diff --git a/plugins/modules/k8s.py b/plugins/modules/k8s.py index 7fed71b761..9ee819d2b0 100644 --- a/plugins/modules/k8s.py +++ b/plugins/modules/k8s.py @@ -140,6 +140,7 @@ - "kubernetes >= 12.0.0" - "PyYAML >= 3.11" - "jsonpatch" + - "jmespath" ''' EXAMPLES = r''' @@ -249,6 +250,7 @@ wait_condition: type: Progressing status: Unknown + reason: DeploymentPaused # Wait for this service to have acquired an External IP - name: Create ingress and wait for ip to be assigned diff --git a/plugins/modules/k8s_info.py b/plugins/modules/k8s_info.py index 31f587b855..b99115028f 100644 --- a/plugins/modules/k8s_info.py +++ b/plugins/modules/k8s_info.py @@ -52,7 +52,7 @@ - "python >= 3.6" - "kubernetes >= 12.0.0" - "PyYAML >= 3.11" - - "jsonpath-rw" + - "jmespath" ''' EXAMPLES = r''' @@ -165,7 +165,7 @@ def execute_module(module, k8s_ansible_mixin): wait_sleep=module.params["wait_sleep"], wait_timeout=module.params["wait_timeout"], condition=module.params["wait_condition"], - wait_for=module.params["wait_for"] + property=module.params["wait_property"] ) module.exit_json(changed=False, **facts) diff --git a/plugins/modules/k8s_rollback.py b/plugins/modules/k8s_rollback.py index 2042fde453..f12d3da432 100644 --- a/plugins/modules/k8s_rollback.py +++ b/plugins/modules/k8s_rollback.py @@ -34,7 +34,6 @@ - "python >= 3.6" - "kubernetes >= 12.0.0" - "PyYAML >= 3.11" - - "jsonpath-rw" ''' EXAMPLES = r''' From 930c5516547683218467cf34fd48fc47f6708fe5 Mon Sep 17 00:00:00 2001 From: abikouo <79859644+abikouo@users.noreply.github.com> Date: Thu, 20 May 2021 12:16:57 +0200 Subject: [PATCH 09/17] Update k8s.py --- plugins/modules/k8s.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/plugins/modules/k8s.py b/plugins/modules/k8s.py index 9ee819d2b0..6e61b6dbd0 100644 --- a/plugins/modules/k8s.py +++ b/plugins/modules/k8s.py @@ -260,13 +260,14 @@ wait_property: property: status.loadBalancer.ingress[*].ip -# Wait for containers inside pod to be running -- name: Create Pod and wait for containers for be running +# Wait for container inside a pod to be ready +- name: Create Pod and wait for containers to be ready kubernetes.core.k8s: template: pod.yaml wait: yes wait_property: - property: status.containerStatuses[*].state.running + property: status.containerStatuses[*].ready + value: "true" # Patch existing namespace : add label - name: add label to existing namespace From 483c91ca22c47e4a907029e8dd2f8d1450a3048d Mon Sep 17 00:00:00 2001 From: abikouo Date: Thu, 20 May 2021 12:23:07 +0200 Subject: [PATCH 10/17] attribute should match for all --- plugins/module_utils/jsonpath.py | 2 +- tests/unit/module_utils/test_jsonpath.py | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/plugins/module_utils/jsonpath.py b/plugins/module_utils/jsonpath.py index a43106832c..e4bd7f1353 100644 --- a/plugins/module_utils/jsonpath.py +++ b/plugins/module_utils/jsonpath.py @@ -44,7 +44,7 @@ def _raise_or_fail(err, **kwargs): def _match_value(buf, v): if isinstance(buf, list): # convert all values from bool to str and lowercase them - return v.lower() in [str(i).lower() for i in buf] + return all([str(i).lower() == v.lower() for i in buf]) elif isinstance(buf, str): return v.lower() == content.lower() elif isinstance(buf, bool): diff --git a/tests/unit/module_utils/test_jsonpath.py b/tests/unit/module_utils/test_jsonpath.py index aa7fb3f1be..7aee816866 100644 --- a/tests/unit/module_utils/test_jsonpath.py +++ b/tests/unit/module_utils/test_jsonpath.py @@ -60,6 +60,24 @@ def test_boolean_value(): assert match_json_property(None, data, "containers[*].connected", "TRUE") assert match_json_property(None, data, "containers[0].poweron", "false") + data = { + "containers": [ + {"image": "nginx", "ready": False}, + {"image": "python", "ready": False}, + {"image": "mongo", "ready": True} + ] + } + assert not match_json_property(None, data, "containers[*].ready", "true") + + data = { + "containers": [ + {"image": "nginx", "ready": True}, + {"image": "python", "ready": True}, + {"image": "mongo", "ready": True} + ] + } + assert match_json_property(None, data, "containers[*].ready", "true") + def test_valid_expression(): data = dict(key="ansible", value="unit-test") From a20a1f6f01c8fa726295d5677f859b52cd18eee2 Mon Sep 17 00:00:00 2001 From: abikouo Date: Thu, 20 May 2021 14:30:19 +0200 Subject: [PATCH 11/17] select wait --- molecule/default/converge.yml | 218 ++++----- molecule/default/tasks/waiter.yml | 707 +++++++++++++++--------------- 2 files changed, 464 insertions(+), 461 deletions(-) diff --git a/molecule/default/converge.yml b/molecule/default/converge.yml index a2e7bfb00a..ebfa941eb4 100644 --- a/molecule/default/converge.yml +++ b/molecule/default/converge.yml @@ -20,111 +20,111 @@ assert: that: (pod_list.resources | count) > 5 - - name: Include access_review.yml - include_tasks: - file: tasks/access_review.yml - apply: - tags: [ access_review, k8s ] - tags: - - always - - name: Include append_hash.yml - include_tasks: - file: tasks/append_hash.yml - apply: - tags: [ append_hash, k8s ] - tags: - - always - - name: Include apply.yml - include_tasks: - file: tasks/apply.yml - apply: - tags: [ apply, k8s ] - tags: - - always - - name: Include cluster_info.yml - include_tasks: - file: tasks/cluster_info.yml - apply: - tags: [ cluster_info, k8s ] - tags: - - always - - name: Include crd.yml - include_tasks: - file: tasks/crd.yml - apply: - tags: [ crd, k8s ] - tags: - - always - - name: Include delete.yml - include_tasks: - file: tasks/delete.yml - apply: - tags: [ delete, k8s ] - tags: - - always - - name: Include exec.yml - include_tasks: - file: tasks/exec.yml - apply: - tags: [ exec, k8s ] - tags: - - always - - name: Include full.yml - include_tasks: - file: tasks/full.yml - apply: - tags: [ full, k8s ] - tags: - - always - - name: Include gc.yml - include_tasks: - file: tasks/gc.yml - apply: - tags: [ gc, k8s ] - tags: - - always - - name: Include info.yml - include_tasks: - file: tasks/info.yml - apply: - tags: [ info, k8s ] - tags: - - always - - name: Include lists.yml - include_tasks: - file: tasks/lists.yml - apply: - tags: [ lists, k8s ] - tags: - - always - - name: Include log.yml - include_tasks: - file: tasks/log.yml - apply: - tags: [ log, k8s ] - tags: - - always - - name: Include rollback.yml - include_tasks: - file: tasks/rollback.yml - apply: - tags: [ rollback, k8s ] - tags: - - always - - name: Include scale.yml - include_tasks: - file: tasks/scale.yml - apply: - tags: [ scale, k8s ] - tags: - - always - - name: Include template.yml - include_tasks: - file: tasks/template.yml - apply: - tags: [ template, k8s ] - tags: - - always + # - name: Include access_review.yml + # include_tasks: + # file: tasks/access_review.yml + # apply: + # tags: [ access_review, k8s ] + # tags: + # - always + # - name: Include append_hash.yml + # include_tasks: + # file: tasks/append_hash.yml + # apply: + # tags: [ append_hash, k8s ] + # tags: + # - always + # - name: Include apply.yml + # include_tasks: + # file: tasks/apply.yml + # apply: + # tags: [ apply, k8s ] + # tags: + # - always + # - name: Include cluster_info.yml + # include_tasks: + # file: tasks/cluster_info.yml + # apply: + # tags: [ cluster_info, k8s ] + # tags: + # - always + # - name: Include crd.yml + # include_tasks: + # file: tasks/crd.yml + # apply: + # tags: [ crd, k8s ] + # tags: + # - always + # - name: Include delete.yml + # include_tasks: + # file: tasks/delete.yml + # apply: + # tags: [ delete, k8s ] + # tags: + # - always + # - name: Include exec.yml + # include_tasks: + # file: tasks/exec.yml + # apply: + # tags: [ exec, k8s ] + # tags: + # - always + # - name: Include full.yml + # include_tasks: + # file: tasks/full.yml + # apply: + # tags: [ full, k8s ] + # tags: + # - always + # - name: Include gc.yml + # include_tasks: + # file: tasks/gc.yml + # apply: + # tags: [ gc, k8s ] + # tags: + # - always + # - name: Include info.yml + # include_tasks: + # file: tasks/info.yml + # apply: + # tags: [ info, k8s ] + # tags: + # - always + # - name: Include lists.yml + # include_tasks: + # file: tasks/lists.yml + # apply: + # tags: [ lists, k8s ] + # tags: + # - always + # - name: Include log.yml + # include_tasks: + # file: tasks/log.yml + # apply: + # tags: [ log, k8s ] + # tags: + # - always + # - name: Include rollback.yml + # include_tasks: + # file: tasks/rollback.yml + # apply: + # tags: [ rollback, k8s ] + # tags: + # - always + # - name: Include scale.yml + # include_tasks: + # file: tasks/scale.yml + # apply: + # tags: [ scale, k8s ] + # tags: + # - always + # - name: Include template.yml + # include_tasks: + # file: tasks/template.yml + # apply: + # tags: [ template, k8s ] + # tags: + # - always - name: Include waiter.yml include_tasks: file: tasks/waiter.yml @@ -149,10 +149,10 @@ tags: - always - roles: - - role: helm - tags: - - helm + # roles: + # - role: helm + # tags: + # - helm post_tasks: - name: Ensure namespace exists diff --git a/molecule/default/tasks/waiter.yml b/molecule/default/tasks/waiter.yml index 9fdb8a73c2..5da84dc691 100644 --- a/molecule/default/tasks/waiter.yml +++ b/molecule/default/tasks/waiter.yml @@ -11,358 +11,358 @@ metadata: name: "{{ wait_namespace }}" - - name: Add a simple pod - k8s: - definition: - apiVersion: v1 - kind: Pod - metadata: - name: "{{ k8s_pod_name }}" - namespace: "{{ wait_namespace }}" - spec: "{{ k8s_pod_spec }}" - wait: yes - vars: - k8s_pod_name: wait-pod - k8s_pod_image: alpine:3.8 - k8s_pod_command: - - sleep - - "10000" - register: wait_pod - ignore_errors: yes - - - name: Assert that pod creation succeeded - assert: - that: - - wait_pod is successful - - - name: Add a daemonset - k8s: - definition: - apiVersion: apps/v1 - kind: DaemonSet - metadata: - name: wait-daemonset - namespace: "{{ wait_namespace }}" - spec: - selector: - matchLabels: - app: "{{ k8s_pod_name }}" - template: "{{ k8s_pod_template }}" - wait: yes - wait_sleep: 5 - wait_timeout: 180 - vars: - k8s_pod_name: wait-ds - k8s_pod_image: gcr.io/kuar-demo/kuard-amd64:1 - k8s_pod_command: - - sleep - - "600" - register: ds - - - name: Check that daemonset wait worked - assert: - that: - - ds.result.status.currentNumberScheduled == ds.result.status.desiredNumberScheduled - - - name: Update a daemonset in check_mode - k8s: - definition: - apiVersion: apps/v1 - kind: DaemonSet - metadata: - name: wait-daemonset - namespace: "{{ wait_namespace }}" - spec: - selector: - matchLabels: - app: "{{ k8s_pod_name }}" - updateStrategy: - type: RollingUpdate - template: "{{ k8s_pod_template }}" - wait: yes - wait_sleep: 3 - wait_timeout: 180 - vars: - k8s_pod_name: wait-ds - k8s_pod_image: gcr.io/kuar-demo/kuard-amd64:2 - k8s_pod_command: - - sleep - - "600" - register: update_ds_check_mode - check_mode: yes - - - name: Check that check_mode result contains the changes - assert: - that: - - update_ds_check_mode is changed - - "update_ds_check_mode.result.spec.template.spec.containers[0].image == 'gcr.io/kuar-demo/kuard-amd64:2'" - - - name: Update a daemonset - k8s: - definition: - apiVersion: apps/v1 - kind: DaemonSet - metadata: - name: wait-daemonset - namespace: "{{ wait_namespace }}" - spec: - selector: - matchLabels: - app: "{{ k8s_pod_name }}" - updateStrategy: - type: RollingUpdate - template: "{{ k8s_pod_template }}" - wait: yes - wait_sleep: 3 - wait_timeout: 180 - vars: - k8s_pod_name: wait-ds - k8s_pod_image: gcr.io/kuar-demo/kuard-amd64:3 - k8s_pod_command: - - sleep - - "600" - register: ds - - - name: Get updated pods - k8s_info: - api_version: v1 - kind: Pod - namespace: "{{ wait_namespace }}" - label_selectors: - - app=wait-ds - field_selectors: - - status.phase=Running - register: updated_ds_pods - - - name: Check that daemonset wait worked - assert: - that: - - ds.result.status.currentNumberScheduled == ds.result.status.desiredNumberScheduled - - updated_ds_pods.resources[0].spec.containers[0].image.endswith(":3") - - - name: Add a crashing pod - k8s: - definition: - apiVersion: v1 - kind: Pod - metadata: - name: "{{ k8s_pod_name }}" - namespace: "{{ wait_namespace }}" - spec: "{{ k8s_pod_spec }}" - wait: yes - wait_sleep: 1 - wait_timeout: 30 - vars: - k8s_pod_name: wait-crash-pod - k8s_pod_image: alpine:3.8 - k8s_pod_command: - - /bin/false - register: crash_pod - ignore_errors: yes - - - name: Check that task failed - assert: - that: - - crash_pod is failed - - - name: Use a non-existent image - k8s: - definition: - apiVersion: v1 - kind: Pod - metadata: - name: "{{ k8s_pod_name }}" - namespace: "{{ wait_namespace }}" - spec: "{{ k8s_pod_spec }}" - wait: yes - wait_sleep: 1 - wait_timeout: 30 - vars: - k8s_pod_name: wait-no-image-pod - k8s_pod_image: i_made_this_up:and_this_too - register: no_image_pod - ignore_errors: yes - - - name: Check that task failed - assert: - that: - - no_image_pod is failed - - - name: Add a deployment - k8s: - definition: - apiVersion: apps/v1 - kind: Deployment - metadata: - name: wait-deploy - namespace: "{{ wait_namespace }}" - spec: - replicas: 3 - selector: - matchLabels: - app: "{{ k8s_pod_name }}" - template: "{{ k8s_pod_template }}" - wait: yes - vars: - k8s_pod_name: wait-deploy - k8s_pod_image: gcr.io/kuar-demo/kuard-amd64:1 - k8s_pod_ports: - - containerPort: 8080 - name: http - protocol: TCP - - register: deploy - - - name: Check that deployment wait worked - assert: - that: - - deploy.result.status.availableReplicas == deploy.result.status.replicas - - - name: Update a deployment - k8s: - definition: - apiVersion: apps/v1 - kind: Deployment - metadata: - name: wait-deploy - namespace: "{{ wait_namespace }}" - spec: - replicas: 3 - selector: - matchLabels: - app: "{{ k8s_pod_name }}" - template: "{{ k8s_pod_template }}" - wait: yes - vars: - k8s_pod_name: wait-deploy - k8s_pod_image: gcr.io/kuar-demo/kuard-amd64:2 - k8s_pod_ports: - - containerPort: 8080 - name: http - protocol: TCP - register: update_deploy - - # It looks like the Deployment is updated to have the desired state *before* the pods are terminated - # Wait a couple of seconds to allow the old pods to at least get to Terminating state - - name: Avoid race condition - pause: - seconds: 2 - - - name: Get updated pods - k8s_info: - api_version: v1 - kind: Pod - namespace: "{{ wait_namespace }}" - label_selectors: - - app=wait-deploy - field_selectors: - - status.phase=Running - register: updated_deploy_pods - until: updated_deploy_pods.resources[0].spec.containers[0].image.endswith(':2') - retries: 6 - delay: 5 - - - name: Check that deployment wait worked - assert: - that: - - deploy.result.status.availableReplicas == deploy.result.status.replicas - - - name: Pause a deployment - k8s: - definition: - apiVersion: apps/v1 - kind: Deployment - metadata: - name: wait-deploy - namespace: "{{ wait_namespace }}" - spec: - paused: True - apply: no - wait: yes - wait_condition: - type: Progressing - status: Unknown - reason: DeploymentPaused - register: pause_deploy - - - name: Check that paused deployment wait worked - assert: - that: - - condition.reason == "DeploymentPaused" - - condition.status == "Unknown" - vars: - condition: '{{ pause_deploy.result.status.conditions[1] }}' - - - name: Add a service based on the deployment - k8s: - definition: - apiVersion: v1 - kind: Service - metadata: - name: wait-svc - namespace: "{{ wait_namespace }}" - spec: - selector: - app: "{{ k8s_pod_name }}" - ports: - - port: 8080 - targetPort: 8080 - protocol: TCP - wait: yes - vars: - k8s_pod_name: wait-deploy - register: service - - - name: Assert that waiting for service works - assert: - that: - - service is successful - - - name: Add a crashing deployment - k8s: - definition: - apiVersion: apps/v1 - kind: Deployment - metadata: - name: wait-crash-deploy - namespace: "{{ wait_namespace }}" - spec: - replicas: 3 - selector: - matchLabels: - app: "{{ k8s_pod_name }}" - template: "{{ k8s_pod_template }}" - wait: yes - vars: - k8s_pod_name: wait-crash-deploy - k8s_pod_image: alpine:3.8 - k8s_pod_command: - - /bin/false - register: wait_crash_deploy - ignore_errors: yes - - - name: Check that task failed - assert: - that: - - wait_crash_deploy is failed - - - name: Remove Pod with very short timeout - k8s: - api_version: v1 - kind: Pod - name: wait-pod - namespace: "{{ wait_namespace }}" - state: absent - wait: yes - wait_sleep: 2 - wait_timeout: 5 - ignore_errors: yes - register: short_wait_remove_pod - - - name: Check that task failed - assert: - that: - - short_wait_remove_pod is failed + # - name: Add a simple pod + # k8s: + # definition: + # apiVersion: v1 + # kind: Pod + # metadata: + # name: "{{ k8s_pod_name }}" + # namespace: "{{ wait_namespace }}" + # spec: "{{ k8s_pod_spec }}" + # wait: yes + # vars: + # k8s_pod_name: wait-pod + # k8s_pod_image: alpine:3.8 + # k8s_pod_command: + # - sleep + # - "10000" + # register: wait_pod + # ignore_errors: yes + + # - name: Assert that pod creation succeeded + # assert: + # that: + # - wait_pod is successful + + # - name: Add a daemonset + # k8s: + # definition: + # apiVersion: apps/v1 + # kind: DaemonSet + # metadata: + # name: wait-daemonset + # namespace: "{{ wait_namespace }}" + # spec: + # selector: + # matchLabels: + # app: "{{ k8s_pod_name }}" + # template: "{{ k8s_pod_template }}" + # wait: yes + # wait_sleep: 5 + # wait_timeout: 180 + # vars: + # k8s_pod_name: wait-ds + # k8s_pod_image: gcr.io/kuar-demo/kuard-amd64:1 + # k8s_pod_command: + # - sleep + # - "600" + # register: ds + + # - name: Check that daemonset wait worked + # assert: + # that: + # - ds.result.status.currentNumberScheduled == ds.result.status.desiredNumberScheduled + + # - name: Update a daemonset in check_mode + # k8s: + # definition: + # apiVersion: apps/v1 + # kind: DaemonSet + # metadata: + # name: wait-daemonset + # namespace: "{{ wait_namespace }}" + # spec: + # selector: + # matchLabels: + # app: "{{ k8s_pod_name }}" + # updateStrategy: + # type: RollingUpdate + # template: "{{ k8s_pod_template }}" + # wait: yes + # wait_sleep: 3 + # wait_timeout: 180 + # vars: + # k8s_pod_name: wait-ds + # k8s_pod_image: gcr.io/kuar-demo/kuard-amd64:2 + # k8s_pod_command: + # - sleep + # - "600" + # register: update_ds_check_mode + # check_mode: yes + + # - name: Check that check_mode result contains the changes + # assert: + # that: + # - update_ds_check_mode is changed + # - "update_ds_check_mode.result.spec.template.spec.containers[0].image == 'gcr.io/kuar-demo/kuard-amd64:2'" + + # - name: Update a daemonset + # k8s: + # definition: + # apiVersion: apps/v1 + # kind: DaemonSet + # metadata: + # name: wait-daemonset + # namespace: "{{ wait_namespace }}" + # spec: + # selector: + # matchLabels: + # app: "{{ k8s_pod_name }}" + # updateStrategy: + # type: RollingUpdate + # template: "{{ k8s_pod_template }}" + # wait: yes + # wait_sleep: 3 + # wait_timeout: 180 + # vars: + # k8s_pod_name: wait-ds + # k8s_pod_image: gcr.io/kuar-demo/kuard-amd64:3 + # k8s_pod_command: + # - sleep + # - "600" + # register: ds + + # - name: Get updated pods + # k8s_info: + # api_version: v1 + # kind: Pod + # namespace: "{{ wait_namespace }}" + # label_selectors: + # - app=wait-ds + # field_selectors: + # - status.phase=Running + # register: updated_ds_pods + + # - name: Check that daemonset wait worked + # assert: + # that: + # - ds.result.status.currentNumberScheduled == ds.result.status.desiredNumberScheduled + # - updated_ds_pods.resources[0].spec.containers[0].image.endswith(":3") + + # - name: Add a crashing pod + # k8s: + # definition: + # apiVersion: v1 + # kind: Pod + # metadata: + # name: "{{ k8s_pod_name }}" + # namespace: "{{ wait_namespace }}" + # spec: "{{ k8s_pod_spec }}" + # wait: yes + # wait_sleep: 1 + # wait_timeout: 30 + # vars: + # k8s_pod_name: wait-crash-pod + # k8s_pod_image: alpine:3.8 + # k8s_pod_command: + # - /bin/false + # register: crash_pod + # ignore_errors: yes + + # - name: Check that task failed + # assert: + # that: + # - crash_pod is failed + + # - name: Use a non-existent image + # k8s: + # definition: + # apiVersion: v1 + # kind: Pod + # metadata: + # name: "{{ k8s_pod_name }}" + # namespace: "{{ wait_namespace }}" + # spec: "{{ k8s_pod_spec }}" + # wait: yes + # wait_sleep: 1 + # wait_timeout: 30 + # vars: + # k8s_pod_name: wait-no-image-pod + # k8s_pod_image: i_made_this_up:and_this_too + # register: no_image_pod + # ignore_errors: yes + + # - name: Check that task failed + # assert: + # that: + # - no_image_pod is failed + + # - name: Add a deployment + # k8s: + # definition: + # apiVersion: apps/v1 + # kind: Deployment + # metadata: + # name: wait-deploy + # namespace: "{{ wait_namespace }}" + # spec: + # replicas: 3 + # selector: + # matchLabels: + # app: "{{ k8s_pod_name }}" + # template: "{{ k8s_pod_template }}" + # wait: yes + # vars: + # k8s_pod_name: wait-deploy + # k8s_pod_image: gcr.io/kuar-demo/kuard-amd64:1 + # k8s_pod_ports: + # - containerPort: 8080 + # name: http + # protocol: TCP + + # register: deploy + + # - name: Check that deployment wait worked + # assert: + # that: + # - deploy.result.status.availableReplicas == deploy.result.status.replicas + + # - name: Update a deployment + # k8s: + # definition: + # apiVersion: apps/v1 + # kind: Deployment + # metadata: + # name: wait-deploy + # namespace: "{{ wait_namespace }}" + # spec: + # replicas: 3 + # selector: + # matchLabels: + # app: "{{ k8s_pod_name }}" + # template: "{{ k8s_pod_template }}" + # wait: yes + # vars: + # k8s_pod_name: wait-deploy + # k8s_pod_image: gcr.io/kuar-demo/kuard-amd64:2 + # k8s_pod_ports: + # - containerPort: 8080 + # name: http + # protocol: TCP + # register: update_deploy + + # # It looks like the Deployment is updated to have the desired state *before* the pods are terminated + # # Wait a couple of seconds to allow the old pods to at least get to Terminating state + # - name: Avoid race condition + # pause: + # seconds: 2 + + # - name: Get updated pods + # k8s_info: + # api_version: v1 + # kind: Pod + # namespace: "{{ wait_namespace }}" + # label_selectors: + # - app=wait-deploy + # field_selectors: + # - status.phase=Running + # register: updated_deploy_pods + # until: updated_deploy_pods.resources[0].spec.containers[0].image.endswith(':2') + # retries: 6 + # delay: 5 + + # - name: Check that deployment wait worked + # assert: + # that: + # - deploy.result.status.availableReplicas == deploy.result.status.replicas + + # - name: Pause a deployment + # k8s: + # definition: + # apiVersion: apps/v1 + # kind: Deployment + # metadata: + # name: wait-deploy + # namespace: "{{ wait_namespace }}" + # spec: + # paused: True + # apply: no + # wait: yes + # wait_condition: + # type: Progressing + # status: Unknown + # reason: DeploymentPaused + # register: pause_deploy + + # - name: Check that paused deployment wait worked + # assert: + # that: + # - condition.reason == "DeploymentPaused" + # - condition.status == "Unknown" + # vars: + # condition: '{{ pause_deploy.result.status.conditions[1] }}' + + # - name: Add a service based on the deployment + # k8s: + # definition: + # apiVersion: v1 + # kind: Service + # metadata: + # name: wait-svc + # namespace: "{{ wait_namespace }}" + # spec: + # selector: + # app: "{{ k8s_pod_name }}" + # ports: + # - port: 8080 + # targetPort: 8080 + # protocol: TCP + # wait: yes + # vars: + # k8s_pod_name: wait-deploy + # register: service + + # - name: Assert that waiting for service works + # assert: + # that: + # - service is successful + + # - name: Add a crashing deployment + # k8s: + # definition: + # apiVersion: apps/v1 + # kind: Deployment + # metadata: + # name: wait-crash-deploy + # namespace: "{{ wait_namespace }}" + # spec: + # replicas: 3 + # selector: + # matchLabels: + # app: "{{ k8s_pod_name }}" + # template: "{{ k8s_pod_template }}" + # wait: yes + # vars: + # k8s_pod_name: wait-crash-deploy + # k8s_pod_image: alpine:3.8 + # k8s_pod_command: + # - /bin/false + # register: wait_crash_deploy + # ignore_errors: yes + + # - name: Check that task failed + # assert: + # that: + # - wait_crash_deploy is failed + + # - name: Remove Pod with very short timeout + # k8s: + # api_version: v1 + # kind: Pod + # name: wait-pod + # namespace: "{{ wait_namespace }}" + # state: absent + # wait: yes + # wait_sleep: 2 + # wait_timeout: 5 + # ignore_errors: yes + # register: short_wait_remove_pod + + # - name: Check that task failed + # assert: + # that: + # - short_wait_remove_pod is failed - name: add a simple crashing pod and wait until container is running k8s: @@ -378,6 +378,7 @@ image: busybox command: ['/dummy/dummy-shell', '-c', 'sleep 2000'] wait: yes + wait_sleep: 2 wait_timeout: 10 wait_property: property: status.containerStatuses[*].state.running @@ -405,6 +406,7 @@ image: busybox command: ['/bin/sh', '-c', 'sleep 10000'] wait: yes + wait_sleep: 2 wait_timeout: 10 wait_property: property: status.containerStatuses[*].state.running @@ -439,6 +441,7 @@ configMap: name: redis-config wait: yes + wait_sleep: 2 wait_timeout: 10 wait_property: property: status.containerStatuses[0].ready From 4fa6e4caae18642021e00bd469f400deb6513974 Mon Sep 17 00:00:00 2001 From: abikouo Date: Fri, 21 May 2021 10:25:56 +0200 Subject: [PATCH 12/17] Revert "select wait" This reverts commit a20a1f6f01c8fa726295d5677f859b52cd18eee2. --- molecule/default/converge.yml | 218 ++++----- molecule/default/tasks/waiter.yml | 707 +++++++++++++++--------------- 2 files changed, 461 insertions(+), 464 deletions(-) diff --git a/molecule/default/converge.yml b/molecule/default/converge.yml index ebfa941eb4..a2e7bfb00a 100644 --- a/molecule/default/converge.yml +++ b/molecule/default/converge.yml @@ -20,111 +20,111 @@ assert: that: (pod_list.resources | count) > 5 - # - name: Include access_review.yml - # include_tasks: - # file: tasks/access_review.yml - # apply: - # tags: [ access_review, k8s ] - # tags: - # - always - # - name: Include append_hash.yml - # include_tasks: - # file: tasks/append_hash.yml - # apply: - # tags: [ append_hash, k8s ] - # tags: - # - always - # - name: Include apply.yml - # include_tasks: - # file: tasks/apply.yml - # apply: - # tags: [ apply, k8s ] - # tags: - # - always - # - name: Include cluster_info.yml - # include_tasks: - # file: tasks/cluster_info.yml - # apply: - # tags: [ cluster_info, k8s ] - # tags: - # - always - # - name: Include crd.yml - # include_tasks: - # file: tasks/crd.yml - # apply: - # tags: [ crd, k8s ] - # tags: - # - always - # - name: Include delete.yml - # include_tasks: - # file: tasks/delete.yml - # apply: - # tags: [ delete, k8s ] - # tags: - # - always - # - name: Include exec.yml - # include_tasks: - # file: tasks/exec.yml - # apply: - # tags: [ exec, k8s ] - # tags: - # - always - # - name: Include full.yml - # include_tasks: - # file: tasks/full.yml - # apply: - # tags: [ full, k8s ] - # tags: - # - always - # - name: Include gc.yml - # include_tasks: - # file: tasks/gc.yml - # apply: - # tags: [ gc, k8s ] - # tags: - # - always - # - name: Include info.yml - # include_tasks: - # file: tasks/info.yml - # apply: - # tags: [ info, k8s ] - # tags: - # - always - # - name: Include lists.yml - # include_tasks: - # file: tasks/lists.yml - # apply: - # tags: [ lists, k8s ] - # tags: - # - always - # - name: Include log.yml - # include_tasks: - # file: tasks/log.yml - # apply: - # tags: [ log, k8s ] - # tags: - # - always - # - name: Include rollback.yml - # include_tasks: - # file: tasks/rollback.yml - # apply: - # tags: [ rollback, k8s ] - # tags: - # - always - # - name: Include scale.yml - # include_tasks: - # file: tasks/scale.yml - # apply: - # tags: [ scale, k8s ] - # tags: - # - always - # - name: Include template.yml - # include_tasks: - # file: tasks/template.yml - # apply: - # tags: [ template, k8s ] - # tags: - # - always + - name: Include access_review.yml + include_tasks: + file: tasks/access_review.yml + apply: + tags: [ access_review, k8s ] + tags: + - always + - name: Include append_hash.yml + include_tasks: + file: tasks/append_hash.yml + apply: + tags: [ append_hash, k8s ] + tags: + - always + - name: Include apply.yml + include_tasks: + file: tasks/apply.yml + apply: + tags: [ apply, k8s ] + tags: + - always + - name: Include cluster_info.yml + include_tasks: + file: tasks/cluster_info.yml + apply: + tags: [ cluster_info, k8s ] + tags: + - always + - name: Include crd.yml + include_tasks: + file: tasks/crd.yml + apply: + tags: [ crd, k8s ] + tags: + - always + - name: Include delete.yml + include_tasks: + file: tasks/delete.yml + apply: + tags: [ delete, k8s ] + tags: + - always + - name: Include exec.yml + include_tasks: + file: tasks/exec.yml + apply: + tags: [ exec, k8s ] + tags: + - always + - name: Include full.yml + include_tasks: + file: tasks/full.yml + apply: + tags: [ full, k8s ] + tags: + - always + - name: Include gc.yml + include_tasks: + file: tasks/gc.yml + apply: + tags: [ gc, k8s ] + tags: + - always + - name: Include info.yml + include_tasks: + file: tasks/info.yml + apply: + tags: [ info, k8s ] + tags: + - always + - name: Include lists.yml + include_tasks: + file: tasks/lists.yml + apply: + tags: [ lists, k8s ] + tags: + - always + - name: Include log.yml + include_tasks: + file: tasks/log.yml + apply: + tags: [ log, k8s ] + tags: + - always + - name: Include rollback.yml + include_tasks: + file: tasks/rollback.yml + apply: + tags: [ rollback, k8s ] + tags: + - always + - name: Include scale.yml + include_tasks: + file: tasks/scale.yml + apply: + tags: [ scale, k8s ] + tags: + - always + - name: Include template.yml + include_tasks: + file: tasks/template.yml + apply: + tags: [ template, k8s ] + tags: + - always - name: Include waiter.yml include_tasks: file: tasks/waiter.yml @@ -149,10 +149,10 @@ tags: - always - # roles: - # - role: helm - # tags: - # - helm + roles: + - role: helm + tags: + - helm post_tasks: - name: Ensure namespace exists diff --git a/molecule/default/tasks/waiter.yml b/molecule/default/tasks/waiter.yml index 5da84dc691..9fdb8a73c2 100644 --- a/molecule/default/tasks/waiter.yml +++ b/molecule/default/tasks/waiter.yml @@ -11,358 +11,358 @@ metadata: name: "{{ wait_namespace }}" - # - name: Add a simple pod - # k8s: - # definition: - # apiVersion: v1 - # kind: Pod - # metadata: - # name: "{{ k8s_pod_name }}" - # namespace: "{{ wait_namespace }}" - # spec: "{{ k8s_pod_spec }}" - # wait: yes - # vars: - # k8s_pod_name: wait-pod - # k8s_pod_image: alpine:3.8 - # k8s_pod_command: - # - sleep - # - "10000" - # register: wait_pod - # ignore_errors: yes - - # - name: Assert that pod creation succeeded - # assert: - # that: - # - wait_pod is successful - - # - name: Add a daemonset - # k8s: - # definition: - # apiVersion: apps/v1 - # kind: DaemonSet - # metadata: - # name: wait-daemonset - # namespace: "{{ wait_namespace }}" - # spec: - # selector: - # matchLabels: - # app: "{{ k8s_pod_name }}" - # template: "{{ k8s_pod_template }}" - # wait: yes - # wait_sleep: 5 - # wait_timeout: 180 - # vars: - # k8s_pod_name: wait-ds - # k8s_pod_image: gcr.io/kuar-demo/kuard-amd64:1 - # k8s_pod_command: - # - sleep - # - "600" - # register: ds - - # - name: Check that daemonset wait worked - # assert: - # that: - # - ds.result.status.currentNumberScheduled == ds.result.status.desiredNumberScheduled - - # - name: Update a daemonset in check_mode - # k8s: - # definition: - # apiVersion: apps/v1 - # kind: DaemonSet - # metadata: - # name: wait-daemonset - # namespace: "{{ wait_namespace }}" - # spec: - # selector: - # matchLabels: - # app: "{{ k8s_pod_name }}" - # updateStrategy: - # type: RollingUpdate - # template: "{{ k8s_pod_template }}" - # wait: yes - # wait_sleep: 3 - # wait_timeout: 180 - # vars: - # k8s_pod_name: wait-ds - # k8s_pod_image: gcr.io/kuar-demo/kuard-amd64:2 - # k8s_pod_command: - # - sleep - # - "600" - # register: update_ds_check_mode - # check_mode: yes - - # - name: Check that check_mode result contains the changes - # assert: - # that: - # - update_ds_check_mode is changed - # - "update_ds_check_mode.result.spec.template.spec.containers[0].image == 'gcr.io/kuar-demo/kuard-amd64:2'" - - # - name: Update a daemonset - # k8s: - # definition: - # apiVersion: apps/v1 - # kind: DaemonSet - # metadata: - # name: wait-daemonset - # namespace: "{{ wait_namespace }}" - # spec: - # selector: - # matchLabels: - # app: "{{ k8s_pod_name }}" - # updateStrategy: - # type: RollingUpdate - # template: "{{ k8s_pod_template }}" - # wait: yes - # wait_sleep: 3 - # wait_timeout: 180 - # vars: - # k8s_pod_name: wait-ds - # k8s_pod_image: gcr.io/kuar-demo/kuard-amd64:3 - # k8s_pod_command: - # - sleep - # - "600" - # register: ds - - # - name: Get updated pods - # k8s_info: - # api_version: v1 - # kind: Pod - # namespace: "{{ wait_namespace }}" - # label_selectors: - # - app=wait-ds - # field_selectors: - # - status.phase=Running - # register: updated_ds_pods - - # - name: Check that daemonset wait worked - # assert: - # that: - # - ds.result.status.currentNumberScheduled == ds.result.status.desiredNumberScheduled - # - updated_ds_pods.resources[0].spec.containers[0].image.endswith(":3") - - # - name: Add a crashing pod - # k8s: - # definition: - # apiVersion: v1 - # kind: Pod - # metadata: - # name: "{{ k8s_pod_name }}" - # namespace: "{{ wait_namespace }}" - # spec: "{{ k8s_pod_spec }}" - # wait: yes - # wait_sleep: 1 - # wait_timeout: 30 - # vars: - # k8s_pod_name: wait-crash-pod - # k8s_pod_image: alpine:3.8 - # k8s_pod_command: - # - /bin/false - # register: crash_pod - # ignore_errors: yes - - # - name: Check that task failed - # assert: - # that: - # - crash_pod is failed - - # - name: Use a non-existent image - # k8s: - # definition: - # apiVersion: v1 - # kind: Pod - # metadata: - # name: "{{ k8s_pod_name }}" - # namespace: "{{ wait_namespace }}" - # spec: "{{ k8s_pod_spec }}" - # wait: yes - # wait_sleep: 1 - # wait_timeout: 30 - # vars: - # k8s_pod_name: wait-no-image-pod - # k8s_pod_image: i_made_this_up:and_this_too - # register: no_image_pod - # ignore_errors: yes - - # - name: Check that task failed - # assert: - # that: - # - no_image_pod is failed - - # - name: Add a deployment - # k8s: - # definition: - # apiVersion: apps/v1 - # kind: Deployment - # metadata: - # name: wait-deploy - # namespace: "{{ wait_namespace }}" - # spec: - # replicas: 3 - # selector: - # matchLabels: - # app: "{{ k8s_pod_name }}" - # template: "{{ k8s_pod_template }}" - # wait: yes - # vars: - # k8s_pod_name: wait-deploy - # k8s_pod_image: gcr.io/kuar-demo/kuard-amd64:1 - # k8s_pod_ports: - # - containerPort: 8080 - # name: http - # protocol: TCP - - # register: deploy - - # - name: Check that deployment wait worked - # assert: - # that: - # - deploy.result.status.availableReplicas == deploy.result.status.replicas - - # - name: Update a deployment - # k8s: - # definition: - # apiVersion: apps/v1 - # kind: Deployment - # metadata: - # name: wait-deploy - # namespace: "{{ wait_namespace }}" - # spec: - # replicas: 3 - # selector: - # matchLabels: - # app: "{{ k8s_pod_name }}" - # template: "{{ k8s_pod_template }}" - # wait: yes - # vars: - # k8s_pod_name: wait-deploy - # k8s_pod_image: gcr.io/kuar-demo/kuard-amd64:2 - # k8s_pod_ports: - # - containerPort: 8080 - # name: http - # protocol: TCP - # register: update_deploy - - # # It looks like the Deployment is updated to have the desired state *before* the pods are terminated - # # Wait a couple of seconds to allow the old pods to at least get to Terminating state - # - name: Avoid race condition - # pause: - # seconds: 2 - - # - name: Get updated pods - # k8s_info: - # api_version: v1 - # kind: Pod - # namespace: "{{ wait_namespace }}" - # label_selectors: - # - app=wait-deploy - # field_selectors: - # - status.phase=Running - # register: updated_deploy_pods - # until: updated_deploy_pods.resources[0].spec.containers[0].image.endswith(':2') - # retries: 6 - # delay: 5 - - # - name: Check that deployment wait worked - # assert: - # that: - # - deploy.result.status.availableReplicas == deploy.result.status.replicas - - # - name: Pause a deployment - # k8s: - # definition: - # apiVersion: apps/v1 - # kind: Deployment - # metadata: - # name: wait-deploy - # namespace: "{{ wait_namespace }}" - # spec: - # paused: True - # apply: no - # wait: yes - # wait_condition: - # type: Progressing - # status: Unknown - # reason: DeploymentPaused - # register: pause_deploy - - # - name: Check that paused deployment wait worked - # assert: - # that: - # - condition.reason == "DeploymentPaused" - # - condition.status == "Unknown" - # vars: - # condition: '{{ pause_deploy.result.status.conditions[1] }}' - - # - name: Add a service based on the deployment - # k8s: - # definition: - # apiVersion: v1 - # kind: Service - # metadata: - # name: wait-svc - # namespace: "{{ wait_namespace }}" - # spec: - # selector: - # app: "{{ k8s_pod_name }}" - # ports: - # - port: 8080 - # targetPort: 8080 - # protocol: TCP - # wait: yes - # vars: - # k8s_pod_name: wait-deploy - # register: service - - # - name: Assert that waiting for service works - # assert: - # that: - # - service is successful - - # - name: Add a crashing deployment - # k8s: - # definition: - # apiVersion: apps/v1 - # kind: Deployment - # metadata: - # name: wait-crash-deploy - # namespace: "{{ wait_namespace }}" - # spec: - # replicas: 3 - # selector: - # matchLabels: - # app: "{{ k8s_pod_name }}" - # template: "{{ k8s_pod_template }}" - # wait: yes - # vars: - # k8s_pod_name: wait-crash-deploy - # k8s_pod_image: alpine:3.8 - # k8s_pod_command: - # - /bin/false - # register: wait_crash_deploy - # ignore_errors: yes - - # - name: Check that task failed - # assert: - # that: - # - wait_crash_deploy is failed - - # - name: Remove Pod with very short timeout - # k8s: - # api_version: v1 - # kind: Pod - # name: wait-pod - # namespace: "{{ wait_namespace }}" - # state: absent - # wait: yes - # wait_sleep: 2 - # wait_timeout: 5 - # ignore_errors: yes - # register: short_wait_remove_pod - - # - name: Check that task failed - # assert: - # that: - # - short_wait_remove_pod is failed + - name: Add a simple pod + k8s: + definition: + apiVersion: v1 + kind: Pod + metadata: + name: "{{ k8s_pod_name }}" + namespace: "{{ wait_namespace }}" + spec: "{{ k8s_pod_spec }}" + wait: yes + vars: + k8s_pod_name: wait-pod + k8s_pod_image: alpine:3.8 + k8s_pod_command: + - sleep + - "10000" + register: wait_pod + ignore_errors: yes + + - name: Assert that pod creation succeeded + assert: + that: + - wait_pod is successful + + - name: Add a daemonset + k8s: + definition: + apiVersion: apps/v1 + kind: DaemonSet + metadata: + name: wait-daemonset + namespace: "{{ wait_namespace }}" + spec: + selector: + matchLabels: + app: "{{ k8s_pod_name }}" + template: "{{ k8s_pod_template }}" + wait: yes + wait_sleep: 5 + wait_timeout: 180 + vars: + k8s_pod_name: wait-ds + k8s_pod_image: gcr.io/kuar-demo/kuard-amd64:1 + k8s_pod_command: + - sleep + - "600" + register: ds + + - name: Check that daemonset wait worked + assert: + that: + - ds.result.status.currentNumberScheduled == ds.result.status.desiredNumberScheduled + + - name: Update a daemonset in check_mode + k8s: + definition: + apiVersion: apps/v1 + kind: DaemonSet + metadata: + name: wait-daemonset + namespace: "{{ wait_namespace }}" + spec: + selector: + matchLabels: + app: "{{ k8s_pod_name }}" + updateStrategy: + type: RollingUpdate + template: "{{ k8s_pod_template }}" + wait: yes + wait_sleep: 3 + wait_timeout: 180 + vars: + k8s_pod_name: wait-ds + k8s_pod_image: gcr.io/kuar-demo/kuard-amd64:2 + k8s_pod_command: + - sleep + - "600" + register: update_ds_check_mode + check_mode: yes + + - name: Check that check_mode result contains the changes + assert: + that: + - update_ds_check_mode is changed + - "update_ds_check_mode.result.spec.template.spec.containers[0].image == 'gcr.io/kuar-demo/kuard-amd64:2'" + + - name: Update a daemonset + k8s: + definition: + apiVersion: apps/v1 + kind: DaemonSet + metadata: + name: wait-daemonset + namespace: "{{ wait_namespace }}" + spec: + selector: + matchLabels: + app: "{{ k8s_pod_name }}" + updateStrategy: + type: RollingUpdate + template: "{{ k8s_pod_template }}" + wait: yes + wait_sleep: 3 + wait_timeout: 180 + vars: + k8s_pod_name: wait-ds + k8s_pod_image: gcr.io/kuar-demo/kuard-amd64:3 + k8s_pod_command: + - sleep + - "600" + register: ds + + - name: Get updated pods + k8s_info: + api_version: v1 + kind: Pod + namespace: "{{ wait_namespace }}" + label_selectors: + - app=wait-ds + field_selectors: + - status.phase=Running + register: updated_ds_pods + + - name: Check that daemonset wait worked + assert: + that: + - ds.result.status.currentNumberScheduled == ds.result.status.desiredNumberScheduled + - updated_ds_pods.resources[0].spec.containers[0].image.endswith(":3") + + - name: Add a crashing pod + k8s: + definition: + apiVersion: v1 + kind: Pod + metadata: + name: "{{ k8s_pod_name }}" + namespace: "{{ wait_namespace }}" + spec: "{{ k8s_pod_spec }}" + wait: yes + wait_sleep: 1 + wait_timeout: 30 + vars: + k8s_pod_name: wait-crash-pod + k8s_pod_image: alpine:3.8 + k8s_pod_command: + - /bin/false + register: crash_pod + ignore_errors: yes + + - name: Check that task failed + assert: + that: + - crash_pod is failed + + - name: Use a non-existent image + k8s: + definition: + apiVersion: v1 + kind: Pod + metadata: + name: "{{ k8s_pod_name }}" + namespace: "{{ wait_namespace }}" + spec: "{{ k8s_pod_spec }}" + wait: yes + wait_sleep: 1 + wait_timeout: 30 + vars: + k8s_pod_name: wait-no-image-pod + k8s_pod_image: i_made_this_up:and_this_too + register: no_image_pod + ignore_errors: yes + + - name: Check that task failed + assert: + that: + - no_image_pod is failed + + - name: Add a deployment + k8s: + definition: + apiVersion: apps/v1 + kind: Deployment + metadata: + name: wait-deploy + namespace: "{{ wait_namespace }}" + spec: + replicas: 3 + selector: + matchLabels: + app: "{{ k8s_pod_name }}" + template: "{{ k8s_pod_template }}" + wait: yes + vars: + k8s_pod_name: wait-deploy + k8s_pod_image: gcr.io/kuar-demo/kuard-amd64:1 + k8s_pod_ports: + - containerPort: 8080 + name: http + protocol: TCP + + register: deploy + + - name: Check that deployment wait worked + assert: + that: + - deploy.result.status.availableReplicas == deploy.result.status.replicas + + - name: Update a deployment + k8s: + definition: + apiVersion: apps/v1 + kind: Deployment + metadata: + name: wait-deploy + namespace: "{{ wait_namespace }}" + spec: + replicas: 3 + selector: + matchLabels: + app: "{{ k8s_pod_name }}" + template: "{{ k8s_pod_template }}" + wait: yes + vars: + k8s_pod_name: wait-deploy + k8s_pod_image: gcr.io/kuar-demo/kuard-amd64:2 + k8s_pod_ports: + - containerPort: 8080 + name: http + protocol: TCP + register: update_deploy + + # It looks like the Deployment is updated to have the desired state *before* the pods are terminated + # Wait a couple of seconds to allow the old pods to at least get to Terminating state + - name: Avoid race condition + pause: + seconds: 2 + + - name: Get updated pods + k8s_info: + api_version: v1 + kind: Pod + namespace: "{{ wait_namespace }}" + label_selectors: + - app=wait-deploy + field_selectors: + - status.phase=Running + register: updated_deploy_pods + until: updated_deploy_pods.resources[0].spec.containers[0].image.endswith(':2') + retries: 6 + delay: 5 + + - name: Check that deployment wait worked + assert: + that: + - deploy.result.status.availableReplicas == deploy.result.status.replicas + + - name: Pause a deployment + k8s: + definition: + apiVersion: apps/v1 + kind: Deployment + metadata: + name: wait-deploy + namespace: "{{ wait_namespace }}" + spec: + paused: True + apply: no + wait: yes + wait_condition: + type: Progressing + status: Unknown + reason: DeploymentPaused + register: pause_deploy + + - name: Check that paused deployment wait worked + assert: + that: + - condition.reason == "DeploymentPaused" + - condition.status == "Unknown" + vars: + condition: '{{ pause_deploy.result.status.conditions[1] }}' + + - name: Add a service based on the deployment + k8s: + definition: + apiVersion: v1 + kind: Service + metadata: + name: wait-svc + namespace: "{{ wait_namespace }}" + spec: + selector: + app: "{{ k8s_pod_name }}" + ports: + - port: 8080 + targetPort: 8080 + protocol: TCP + wait: yes + vars: + k8s_pod_name: wait-deploy + register: service + + - name: Assert that waiting for service works + assert: + that: + - service is successful + + - name: Add a crashing deployment + k8s: + definition: + apiVersion: apps/v1 + kind: Deployment + metadata: + name: wait-crash-deploy + namespace: "{{ wait_namespace }}" + spec: + replicas: 3 + selector: + matchLabels: + app: "{{ k8s_pod_name }}" + template: "{{ k8s_pod_template }}" + wait: yes + vars: + k8s_pod_name: wait-crash-deploy + k8s_pod_image: alpine:3.8 + k8s_pod_command: + - /bin/false + register: wait_crash_deploy + ignore_errors: yes + + - name: Check that task failed + assert: + that: + - wait_crash_deploy is failed + + - name: Remove Pod with very short timeout + k8s: + api_version: v1 + kind: Pod + name: wait-pod + namespace: "{{ wait_namespace }}" + state: absent + wait: yes + wait_sleep: 2 + wait_timeout: 5 + ignore_errors: yes + register: short_wait_remove_pod + + - name: Check that task failed + assert: + that: + - short_wait_remove_pod is failed - name: add a simple crashing pod and wait until container is running k8s: @@ -378,7 +378,6 @@ image: busybox command: ['/dummy/dummy-shell', '-c', 'sleep 2000'] wait: yes - wait_sleep: 2 wait_timeout: 10 wait_property: property: status.containerStatuses[*].state.running @@ -406,7 +405,6 @@ image: busybox command: ['/bin/sh', '-c', 'sleep 10000'] wait: yes - wait_sleep: 2 wait_timeout: 10 wait_property: property: status.containerStatuses[*].state.running @@ -441,7 +439,6 @@ configMap: name: redis-config wait: yes - wait_sleep: 2 wait_timeout: 10 wait_property: property: status.containerStatuses[0].ready From c23e3b304ef2f0562eafb2eb2cd38354e027b997 Mon Sep 17 00:00:00 2001 From: abikouo Date: Thu, 27 May 2021 11:50:01 +0200 Subject: [PATCH 13/17] sanity --- plugins/doc_fragments/k8s_wait_options.py | 1 - plugins/modules/k8s.py | 9 +++++++++ tests/unit/module_utils/test_jsonpath.py | 2 -- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/plugins/doc_fragments/k8s_wait_options.py b/plugins/doc_fragments/k8s_wait_options.py index 997db87ade..c1a7aaca5a 100644 --- a/plugins/doc_fragments/k8s_wait_options.py +++ b/plugins/doc_fragments/k8s_wait_options.py @@ -76,7 +76,6 @@ class ModuleDocFragment(object): required: True description: - The property name to wait for. - - This value must be valid json path expression. example: C(containers[*].state.phase), C(spec.containers[0].state) value: type: str description: diff --git a/plugins/modules/k8s.py b/plugins/modules/k8s.py index c20019235a..fe47c71f61 100644 --- a/plugins/modules/k8s.py +++ b/plugins/modules/k8s.py @@ -269,6 +269,15 @@ property: status.containerStatuses[*].ready value: "true" +# Wait for first container inside a pod to be ready +- name: Create Pod and wait for first containers to be ready + kubernetes.core.k8s: + template: pod.yaml + wait: yes + wait_property: + property: status.containerStatuses[0].ready + value: "true" + # Patch existing namespace : add label - name: add label to existing namespace kubernetes.core.k8s: diff --git a/tests/unit/module_utils/test_jsonpath.py b/tests/unit/module_utils/test_jsonpath.py index 54d7aa9398..7dca2a4786 100644 --- a/tests/unit/module_utils/test_jsonpath.py +++ b/tests/unit/module_utils/test_jsonpath.py @@ -18,8 +18,6 @@ from ansible_collections.kubernetes.core.plugins.module_utils.jsonpath_extractor import validate_with_jsonpath -import pytest - def test_property_present(): data = { From 520e925f3999768b50375a2f6ba8f58ddc7a7e57 Mon Sep 17 00:00:00 2001 From: abikouo <79859644+abikouo@users.noreply.github.com> Date: Tue, 1 Jun 2021 10:32:11 +0200 Subject: [PATCH 14/17] Update molecule/default/tasks/waiter.yml Co-authored-by: Abhijeet Kasurde --- molecule/default/tasks/waiter.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/molecule/default/tasks/waiter.yml b/molecule/default/tasks/waiter.yml index 9fdb8a73c2..12920aa22d 100644 --- a/molecule/default/tasks/waiter.yml +++ b/molecule/default/tasks/waiter.yml @@ -388,7 +388,7 @@ assert: that: - crash_pod is failed - - crash_pod.changed + - crash_pod is changed - '"Resource creation timed out" in crash_pod.msg' - name: add a valid pod and wait until container is running From 7891ce82a4cf8ede4e2c2fd0a730d3ad48452ed5 Mon Sep 17 00:00:00 2001 From: abikouo <79859644+abikouo@users.noreply.github.com> Date: Thu, 3 Jun 2021 18:17:37 +0200 Subject: [PATCH 15/17] Update jsonpath_extractor.py --- plugins/module_utils/jsonpath_extractor.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/module_utils/jsonpath_extractor.py b/plugins/module_utils/jsonpath_extractor.py index d1fa051598..5e34583e2f 100644 --- a/plugins/module_utils/jsonpath_extractor.py +++ b/plugins/module_utils/jsonpath_extractor.py @@ -122,10 +122,10 @@ def _match_value(buf, v): if isinstance(buf, list): # convert all values from bool to str and lowercase them return all([str(i).lower() == v.lower() for i in buf]) - elif isinstance(buf, str): - return v.lower() == content.lower() + elif isinstance(buf, str) or isinstance(buf, int) or isinstance(buf, float): + return v.lower() == str(buf).lower() elif isinstance(buf, bool): - return v.lower() == str(content).lower() + return v.lower() == str(buf).lower() else: # unable to test single value against dict return False From 9fcd2498367b2ea38f3949c58b09219a97832626 Mon Sep 17 00:00:00 2001 From: abikouo <79859644+abikouo@users.noreply.github.com> Date: Mon, 14 Jun 2021 11:20:31 +0200 Subject: [PATCH 16/17] Update k8s_wait_options.py --- plugins/doc_fragments/k8s_wait_options.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/doc_fragments/k8s_wait_options.py b/plugins/doc_fragments/k8s_wait_options.py index c1a7aaca5a..7cc74e5543 100644 --- a/plugins/doc_fragments/k8s_wait_options.py +++ b/plugins/doc_fragments/k8s_wait_options.py @@ -69,7 +69,7 @@ class ModuleDocFragment(object): - Specifies a property on the resource to wait for. - Ignored if C(wait) is not set or is set to I(False). type: dict - version_added: '2.0.0' + version_added: '2.1.0' suboptions: property: type: str From 8a98418ddd6567f2f8423abe09665a3e01b103f9 Mon Sep 17 00:00:00 2001 From: abikouo Date: Tue, 15 Jun 2021 14:49:30 +0200 Subject: [PATCH 17/17] Revert "Revert "k8s ability to wait on arbitrary property (#105)" (#133)" This reverts commit 46494a18bde79b6df2691e797da76df7ba5e731b. --- changelogs/fragments/105-wait_property.yaml | 3 + molecule/default/tasks/waiter.yml | 87 ++++++++++++ plugins/doc_fragments/k8s_wait_options.py | 18 +++ plugins/module_utils/args_common.py | 8 ++ plugins/module_utils/common.py | 47 ++++--- plugins/module_utils/jsonpath_extractor.py | 142 ++++++++++++++++++++ plugins/modules/k8s.py | 26 ++++ plugins/modules/k8s_info.py | 1 + tests/unit/module_utils/test_jsonpath.py | 76 +++++++++++ 9 files changed, 393 insertions(+), 15 deletions(-) create mode 100644 changelogs/fragments/105-wait_property.yaml create mode 100644 plugins/module_utils/jsonpath_extractor.py create mode 100644 tests/unit/module_utils/test_jsonpath.py diff --git a/changelogs/fragments/105-wait_property.yaml b/changelogs/fragments/105-wait_property.yaml new file mode 100644 index 0000000000..4ecabd1aa8 --- /dev/null +++ b/changelogs/fragments/105-wait_property.yaml @@ -0,0 +1,3 @@ +--- +minor_changes: + - k8s - add new option ``wait_property`` to support ability to wait on arbitrary property (https://github.com/ansible-collections/kubernetes.core/pull/105). diff --git a/molecule/default/tasks/waiter.yml b/molecule/default/tasks/waiter.yml index 44fc42b3ff..12920aa22d 100644 --- a/molecule/default/tasks/waiter.yml +++ b/molecule/default/tasks/waiter.yml @@ -364,6 +364,93 @@ that: - short_wait_remove_pod is failed + - name: add a simple crashing pod and wait until container is running + k8s: + definition: + apiVersion: v1 + kind: Pod + metadata: + name: pod-crash-0 + namespace: "{{ wait_namespace }}" + spec: + containers: + - name: crashing-container + image: busybox + command: ['/dummy/dummy-shell', '-c', 'sleep 2000'] + wait: yes + wait_timeout: 10 + wait_property: + property: status.containerStatuses[*].state.running + ignore_errors: true + register: crash_pod + + - name: assert that task failed + assert: + that: + - crash_pod is failed + - crash_pod is changed + - '"Resource creation timed out" in crash_pod.msg' + + - name: add a valid pod and wait until container is running + k8s: + definition: + apiVersion: v1 + kind: Pod + metadata: + name: pod-valid-0 + namespace: "{{ wait_namespace }}" + spec: + containers: + - name: crashing-container + image: busybox + command: ['/bin/sh', '-c', 'sleep 10000'] + wait: yes + wait_timeout: 10 + wait_property: + property: status.containerStatuses[*].state.running + ignore_errors: true + register: valid_pod + + - name: assert that task failed + assert: + that: + - valid_pod is successful + - valid_pod.changed + - valid_pod.result.status.containerStatuses[0].state.running is defined + + - name: create pod (waiting for container.ready set to false) + k8s: + definition: + apiVersion: v1 + kind: Pod + metadata: + name: redis-pod + namespace: "{{ wait_namespace }}" + spec: + containers: + - name: redis-container + image: redis + volumeMounts: + - name: test + mountPath: "/etc/test" + readOnly: true + volumes: + - name: test + configMap: + name: redis-config + wait: yes + wait_timeout: 10 + wait_property: + property: status.containerStatuses[0].ready + value: "false" + register: wait_boolean + + - name: assert that pod was created but not running + assert: + that: + - wait_boolean.changed + - wait_boolean.result.status.phase == 'Pending' + always: - name: Remove namespace k8s: diff --git a/plugins/doc_fragments/k8s_wait_options.py b/plugins/doc_fragments/k8s_wait_options.py index 06600564c3..7cc74e5543 100644 --- a/plugins/doc_fragments/k8s_wait_options.py +++ b/plugins/doc_fragments/k8s_wait_options.py @@ -64,4 +64,22 @@ class ModuleDocFragment(object): - 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_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: dict + version_added: '2.1.0' + suboptions: + property: + type: str + required: True + description: + - The property name to wait for. + value: + type: str + description: + - The expected value of the C(property). + - The value is not case-sensitive. + - If this is missing, we will check only that the attribute C(property) is present. ''' diff --git a/plugins/module_utils/args_common.py b/plugins/module_utils/args_common.py index 67c183db74..10171ae9f1 100644 --- a/plugins/module_utils/args_common.py +++ b/plugins/module_utils/args_common.py @@ -70,6 +70,14 @@ def list_dict_str(value): status=dict(default=True, choices=[True, False, "Unknown"]), reason=dict() ) + ), + wait_property=dict( + type='dict', + default=None, + options=dict( + property=dict(required=True), + value=dict() + ) ) ) diff --git a/plugins/module_utils/common.py b/plugins/module_utils/common.py index e3a48cca03..0c094d274e 100644 --- a/plugins/module_utils/common.py +++ b/plugins/module_utils/common.py @@ -29,6 +29,7 @@ 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.jsonpath_extractor import validate_with_jsonpath from ansible.module_utils.basic import missing_required_lib from ansible.module_utils.six import iteritems, string_types @@ -36,6 +37,7 @@ from ansible.module_utils.common.dict_transformations import dict_merge from ansible.module_utils.parsing.convert_bool import boolean + K8S_IMP_ERR = None try: import kubernetes @@ -230,7 +232,7 @@ def find_resource(self, kind, api_version, fail=False): 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=False, wait_sleep=5, wait_timeout=120, state='present', condition=None, property=None): resource = self.find_resource(kind, api_version) api_found = bool(resource) if not api_found: @@ -286,7 +288,7 @@ def _elapsed(): for resource_instance in resource_list: success, res, duration = self.wait(resource, resource_instance, sleep=wait_sleep, timeout=wait_timeout, - state=state, condition=condition) + state=state, condition=condition, property=property) if not success: self.fail(msg="Failed to gather information about %s(s) even" " after waiting for %s seconds" % (res.get('kind'), duration)) @@ -349,7 +351,7 @@ def diff_objects(self, existing, new): def fail(self, msg=None): self.fail_json(msg=msg) - def _wait_for(self, resource, name, namespace, predicate, sleep, timeout, state): + def _wait_for(self, resource, name, namespace, predicates, sleep, timeout, state): start = datetime.now() def _wait_for_elapsed(): @@ -359,7 +361,7 @@ def _wait_for_elapsed(): while _wait_for_elapsed() < timeout: try: response = resource.get(name=name, namespace=namespace) - if predicate(response): + if all([predicate(response) for predicate in predicates]): if response: return True, response.to_dict(), _wait_for_elapsed() return True, {}, _wait_for_elapsed() @@ -371,7 +373,7 @@ def _wait_for_elapsed(): response = response.to_dict() return False, response, _wait_for_elapsed() - def wait(self, resource, definition, sleep, timeout, state='present', condition=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 @@ -422,19 +424,29 @@ def _custom_condition(resource): def _resource_absent(resource): return not resource + def _wait_for_property(resource): + return validate_with_jsonpath(self, resource.to_dict(), property.get('property'), property.get('value', None)) + waiter = dict( Deployment=_deployment_ready, DaemonSet=_daemonset_ready, Pod=_pod_ready ) kind = definition['kind'] - if state == 'present' and not condition: - predicate = waiter.get(kind, lambda x: x) - elif state == 'present' and condition: - predicate = _custom_condition + predicates = [] + if state == 'present': + 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 property: + # json path predicate + predicates.append(_wait_for_property) else: - predicate = _resource_absent - return self._wait_for(resource, definition['metadata']['name'], definition['metadata'].get('namespace'), predicate, sleep, timeout, state) + 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): resource_definition = module.params.get('resource_definition') @@ -577,6 +589,7 @@ def perform_action(self, resource, definition): 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_property = self.params.get('wait_property') def build_error_msg(kind, name, msg): return "%s %s: %s" % (kind, name, msg) @@ -686,7 +699,8 @@ def build_error_msg(kind, name, msg): 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) + success, result['result'], result['duration'] = self.wait(resource, definition, wait_sleep, wait_timeout, + condition=wait_condition, property=wait_property) if existing: existing = existing.to_dict() else: @@ -746,7 +760,8 @@ def build_error_msg(kind, name, msg): 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) + 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: @@ -781,7 +796,8 @@ def build_error_msg(kind, name, msg): 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) + 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' @@ -815,7 +831,8 @@ def build_error_msg(kind, name, msg): 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) + 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' diff --git a/plugins/module_utils/jsonpath_extractor.py b/plugins/module_utils/jsonpath_extractor.py new file mode 100644 index 0000000000..5e34583e2f --- /dev/null +++ b/plugins/module_utils/jsonpath_extractor.py @@ -0,0 +1,142 @@ +# 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.module_utils._text import to_native + + +class JsonPathException(Exception): + """ Error while parsing Json path structure """ + + +def find_element(value, start): + dot_idx = value.find(".", start) + end = dot_idx if dot_idx != -1 else len(value) + arr_idx = value.find("[", start, end) + + if dot_idx == -1 and arr_idx == -1: + # last element of the json path + if value[start] == '[': + raise JsonPathException("unable to find array end string for array starting at index {0} '{1}'".format(start, value[start:])) + return value[start:], None + + elif arr_idx != -1: + if arr_idx == start: + # array element (ex: "[0]" or "[*].ready" or "[*][0].ready" ) + arr_end = value.find("]", arr_idx) + if arr_end == -1: + raise JsonPathException("unable to find array end string for array starting at index {0} '{1}'".format(arr_idx, value[arr_idx:])) + data = value[arr_idx + 1:arr_end] + if data != "*" and not data.isnumeric(): + raise JsonPathException("wrong value specified into array starting at index {0} => '{1}'".format(arr_idx, data)) + return int(data) if data != "*" else -1, arr_end + 1 if arr_end < len(value) - 1 else None + elif arr_idx > start: + # single value found (ex: "containers[0]") + return value[start:arr_idx], arr_idx + + else: # dot_idx != -1 + return value[start:dot_idx], dot_idx + 1 + + +def parse(expr): + result = [] + if expr[0] == ".": + expr = expr[1:] + start = 0 + while start is not None: + elt, next = find_element(expr, start) + if elt == '': + if not isinstance(result[-1], int): + raise JsonPathException("empty element following non array element at index {0} '{1}'".format(start, expr[start:])) + else: + result.append(elt) + start = next + return result + + +def search_json_item(jsonpath_expr, json_doc): + json_idx = 0 + json_item = jsonpath_expr[json_idx] + if isinstance(json_item, int): + if not isinstance(json_doc, list): + # trying to parse list items, but current document is not a list + return None + elements = json_doc + if json_item != -1: + # looking for specific index from the list + if json_item >= len(json_doc): + return None + else: + elements = json_doc[json_item] + + # when we reach the end of the json path + if len(jsonpath_expr) == 1: + return elements + elif json_item != -1 and (isinstance(elements, dict) or isinstance(elements, list)): + return search_json_item(jsonpath_expr[1:], elements) + elif json_item == -1: + result = [] + for elt in elements: + ret = search_json_item(jsonpath_expr[1:], elt) + if ret is not None: + result.append(ret) + return result if result != [] else None + else: + # looking for a specific field into the json document + if not isinstance(json_doc, dict): + return None + if json_item not in json_doc: + return None + if len(jsonpath_expr) == 1: + return json_doc.get(json_item) + else: + return search_json_item(jsonpath_expr[1:], json_doc.get(json_item)) + + +def search(expr, data): + jsonpath_expr = parse(expr) + return search_json_item(jsonpath_expr, data) + + +def validate_with_jsonpath(module, data, expr, value=None): + 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): + if isinstance(buf, list): + # convert all values from bool to str and lowercase them + return all([str(i).lower() == v.lower() for i in buf]) + elif isinstance(buf, str) or isinstance(buf, int) or isinstance(buf, float): + return v.lower() == str(buf).lower() + elif isinstance(buf, bool): + return v.lower() == str(buf).lower() + else: + # unable to test single value against dict + return False + + try: + content = search(expr, data) + if content is None or content == []: + return False + if value is None or _match_value(content, value): + # looking for state present + return True + return False + except Exception as err: + _raise_or_fail(err, msg="Failed to extract path from Json: {0}".format(expr)) diff --git a/plugins/modules/k8s.py b/plugins/modules/k8s.py index 5bcb51cff2..fe47c71f61 100644 --- a/plugins/modules/k8s.py +++ b/plugins/modules/k8s.py @@ -252,6 +252,32 @@ status: Unknown reason: DeploymentPaused +# Wait for this service to have acquired an External IP +- name: Create ingress and wait for ip to be assigned + kubernetes.core.k8s: + template: dash-service.yaml + wait: yes + wait_property: + property: status.loadBalancer.ingress[*].ip + +# Wait for container inside a pod to be ready +- name: Create Pod and wait for containers to be ready + kubernetes.core.k8s: + template: pod.yaml + wait: yes + wait_property: + property: status.containerStatuses[*].ready + value: "true" + +# Wait for first container inside a pod to be ready +- name: Create Pod and wait for first containers to be ready + kubernetes.core.k8s: + template: pod.yaml + wait: yes + wait_property: + property: status.containerStatuses[0].ready + value: "true" + # Patch existing namespace : add label - name: add label to existing namespace kubernetes.core.k8s: diff --git a/plugins/modules/k8s_info.py b/plugins/modules/k8s_info.py index 50059e4124..5871ec3265 100644 --- a/plugins/modules/k8s_info.py +++ b/plugins/modules/k8s_info.py @@ -164,6 +164,7 @@ def execute_module(module, k8s_ansible_mixin): wait_sleep=module.params["wait_sleep"], wait_timeout=module.params["wait_timeout"], condition=module.params["wait_condition"], + property=module.params["wait_property"] ) module.exit_json(changed=False, **facts) diff --git a/tests/unit/module_utils/test_jsonpath.py b/tests/unit/module_utils/test_jsonpath.py new file mode 100644 index 0000000000..7dca2a4786 --- /dev/null +++ b/tests/unit/module_utils/test_jsonpath.py @@ -0,0 +1,76 @@ +# 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_extractor import validate_with_jsonpath + + +def test_property_present(): + data = { + "containers": [ + {"name": "t0", "image": "nginx"}, + {"name": "t1", "image": "python"}, + {"name": "t2", "image": "mongo", "state": "running"} + ] + } + assert validate_with_jsonpath(None, data, "containers[*].state") + assert not validate_with_jsonpath(None, data, "containers[*].status") + + +def test_property_value(): + data = { + "containers": [ + {"name": "t0", "image": "nginx"}, + {"name": "t1", "image": "python"}, + {"name": "t2", "image": "mongo", "state": "running"} + ] + } + assert validate_with_jsonpath(None, data, "containers[*].state", "running") + assert validate_with_jsonpath(None, data, "containers[*].state", "Running") + assert not validate_with_jsonpath(None, data, "containers[*].state", "off") + + +def test_boolean_value(): + data = { + "containers": [ + {"image": "nginx", "poweron": False}, + {"image": "python"}, + {"image": "mongo", "connected": True} + ] + } + assert validate_with_jsonpath(None, data, "containers[*].connected", "true") + assert validate_with_jsonpath(None, data, "containers[*].connected", "True") + assert validate_with_jsonpath(None, data, "containers[*].connected", "TRUE") + assert validate_with_jsonpath(None, data, "containers[0].poweron", "false") + + data = { + "containers": [ + {"image": "nginx", "ready": False}, + {"image": "python", "ready": False}, + {"image": "mongo", "ready": True} + ] + } + assert not validate_with_jsonpath(None, data, "containers[*].ready", "true") + + data = { + "containers": [ + {"image": "nginx", "ready": True}, + {"image": "python", "ready": True}, + {"image": "mongo", "ready": True} + ] + } + assert validate_with_jsonpath(None, data, "containers[*].ready", "true")