diff --git a/changelogs/fragments/18_k8s_info_wait.yml b/changelogs/fragments/18_k8s_info_wait.yml new file mode 100644 index 00000000..157eb97c --- /dev/null +++ b/changelogs/fragments/18_k8s_info_wait.yml @@ -0,0 +1,2 @@ +bugfixes: +- k8s_info - add wait functionality (https://github.com/ansible-collections/community.kubernetes/issues/18). diff --git a/molecule/default/converge.yml b/molecule/default/converge.yml index f1aa68fc..e4751307 100644 --- a/molecule/default/converge.yml +++ b/molecule/default/converge.yml @@ -27,6 +27,7 @@ - include_tasks: tasks/full.yml - include_tasks: tasks/exec.yml - include_tasks: tasks/log.yml + - include_tasks: tasks/info.yml roles: - helm diff --git a/molecule/default/tasks/info.yml b/molecule/default/tasks/info.yml new file mode 100644 index 00000000..2b7fedaf --- /dev/null +++ b/molecule/default/tasks/info.yml @@ -0,0 +1,167 @@ +--- +- block: + - set_fact: + wait_namespace: wait + k8s_pod_name: pod-info-1 + multi_pod_one: multi-pod-1 + multi_pod_two: multi-pod-2 + + - name: Ensure namespace exists + k8s: + definition: + apiVersion: v1 + kind: Namespace + metadata: + name: "{{ wait_namespace }}" + + - name: Add a simple pod with initContainer + k8s: + definition: + apiVersion: v1 + kind: Pod + metadata: + name: "{{ k8s_pod_name }}" + namespace: "{{ wait_namespace }}" + spec: + initContainers: + - name: init-01 + image: python:3.7-alpine + command: ['sh', '-c', 'sleep 20'] + containers: + - name: utilitypod-01 + image: python:3.7-alpine + command: ['sh', '-c', 'sleep 360'] + + - name: Wait and gather information about new pod + k8s_info: + name: "{{ k8s_pod_name }}" + kind: Pod + namespace: "{{ wait_namespace }}" + wait: yes + wait_sleep: 5 + wait_timeout: 400 + register: wait_info + + - name: Assert that pod creation succeeded + assert: + that: + - wait_info is successful + - not wait_info.changed + - wait_info.resources[0].status.phase == "Running" + + - name: Remove Pod + k8s: + api_version: v1 + kind: Pod + name: "{{ k8s_pod_name }}" + namespace: "{{ wait_namespace }}" + state: absent + wait: yes + ignore_errors: yes + register: short_wait_remove_pod + + - name: Check if pod is removed + assert: + that: + - short_wait_remove_pod is successful + - short_wait_remove_pod.changed + + - name: Create multiple pod with initContainer + k8s: + definition: + apiVersion: v1 + kind: Pod + metadata: + labels: + run: multi-box + name: "{{ multi_pod_one }}" + namespace: "{{ wait_namespace }}" + spec: + initContainers: + - name: init-01 + image: python:3.7-alpine + command: ['sh', '-c', 'sleep 25'] + containers: + - name: multi-pod-01 + image: python:3.7-alpine + command: ['sh', '-c', 'sleep 360'] + + - name: Create another pod with same label as previous pod + k8s: + definition: + apiVersion: v1 + kind: Pod + metadata: + labels: + run: multi-box + name: "{{ multi_pod_two }}" + namespace: "{{ wait_namespace }}" + spec: + initContainers: + - name: init-02 + image: python:3.7-alpine + command: ['sh', '-c', 'sleep 25'] + containers: + - name: multi-pod-02 + image: python:3.7-alpine + command: ['sh', '-c', 'sleep 360'] + + - name: Wait and gather information about new pods + k8s_info: + kind: Pod + namespace: "{{ wait_namespace }}" + wait: yes + wait_sleep: 5 + wait_timeout: 400 + label_selectors: + - run == multi-box + register: wait_info + + - name: Assert that pod creation succeeded + assert: + that: + - wait_info is successful + - not wait_info.changed + - wait_info.resources[0].status.phase == "Running" + - wait_info.resources[1].status.phase == "Running" + + - name: "Remove Pod {{ multi_pod_one }}" + k8s: + api_version: v1 + kind: Pod + name: "{{ multi_pod_one }}" + namespace: "{{ wait_namespace }}" + state: absent + wait: yes + ignore_errors: yes + register: multi_pod_one_remove + + - name: "Check if {{ multi_pod_one }} pod is removed" + assert: + that: + - multi_pod_one_remove is successful + - multi_pod_one_remove.changed + + - name: "Remove Pod {{ multi_pod_two }}" + k8s: + api_version: v1 + kind: Pod + name: "{{ multi_pod_two }}" + namespace: "{{ wait_namespace }}" + state: absent + wait: yes + ignore_errors: yes + register: multi_pod_two_remove + + - name: "Check if {{ multi_pod_two }} pod is removed" + assert: + that: + - multi_pod_two_remove is successful + - multi_pod_two_remove.changed + + always: + - name: Remove namespace + k8s: + kind: Namespace + name: "{{ wait_namespace }}" + state: absent diff --git a/plugins/doc_fragments/k8s_wait_options.py b/plugins/doc_fragments/k8s_wait_options.py new file mode 100644 index 00000000..867901bb --- /dev/null +++ b/plugins/doc_fragments/k8s_wait_options.py @@ -0,0 +1,67 @@ +# -*- coding: utf-8 -*- + +# Copyright: (c) 2020, Red Hat | Ansible +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +# Options for specifying object wait + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + + +class ModuleDocFragment(object): + + DOCUMENTATION = r''' +options: + wait: + description: + - Whether to wait for certain resource kinds to end up in the desired state. + - By default the module exits once Kubernetes has received the request. + - Implemented for C(state=present) for C(Deployment), C(DaemonSet) and C(Pod), and for C(state=absent) for all resource kinds. + - For resource kinds without an implementation, C(wait) returns immediately unless C(wait_condition) is set. + default: no + type: bool + wait_sleep: + description: + - Number of seconds to sleep between checks. + default: 5 + type: int + wait_timeout: + description: + - How long in seconds to wait for the resource to end up in the desired state. + - Ignored if C(wait) is not set. + default: 120 + type: int + wait_condition: + description: + - Specifies a custom condition on the status to wait for. + - Ignored if C(wait) is not set or is set to False. + suboptions: + type: + type: str + description: + - The type of condition to wait for. + - For example, the C(Pod) resource will set the C(Ready) condition (among others). + - Required if you are specifying a C(wait_condition). + - If left empty, the C(wait_condition) field will be ignored. + - The possible types for 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. + status: + type: str + description: + - The value of the status field in your desired condition. + - For example, if a C(Deployment) is paused, the C(Progressing) C(type) will have the C(Unknown) status. + choices: + - True + - False + - Unknown + default: "True" + reason: + type: str + description: + - The value of the reason field in your desired condition + - For example, if a C(Deployment) is paused, The C(Progressing) C(type) will have the C(DeploymentPaused) reason. + - 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 +''' diff --git a/plugins/module_utils/common.py b/plugins/module_utils/common.py index 9503a194..dac92818 100644 --- a/plugins/module_utils/common.py +++ b/plugins/module_utils/common.py @@ -81,6 +81,20 @@ try: except ImportError: from ansible.module_utils.common.dict_transformations import recursive_diff +try: + try: + # >=0.10 + from openshift.dynamic.resource import ResourceInstance + except ImportError: + # <0.10 + from openshift.dynamic.client import ResourceInstance + HAS_K8S_INSTANCE_HELPER = True + k8s_import_exception = None +except ImportError as e: + HAS_K8S_INSTANCE_HELPER = False + k8s_import_exception = e + K8S_IMP_ERR = traceback.format_exc() + def list_dict_str(value): if isinstance(value, (list, dict, string_types)): @@ -158,6 +172,21 @@ AUTH_ARG_SPEC = { }, } +WAIT_ARG_SPEC = dict( + wait=dict(type='bool', default=False), + wait_sleep=dict(type='int', default=5), + wait_timeout=dict(type='int', default=120), + wait_condition=dict( + type='dict', + default=None, + options=dict( + type=dict(), + status=dict(default=True, choices=[True, False, "Unknown"]), + reason=dict() + ) + ) +) + # Map kubernetes-client parameters to ansible parameters AUTH_ARG_MAP = { 'kubeconfig': 'kubeconfig', @@ -249,22 +278,46 @@ class K8sAnsibleMixin(object): if fail: 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): + 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): resource = self.find_resource(kind, api_version) if not resource: return dict(resources=[]) + + if not label_selectors: + label_selectors = [] + if not field_selectors: + field_selectors = [] + try: result = resource.get(name=name, namespace=namespace, label_selector=','.join(label_selectors), - field_selector=','.join(field_selectors)).to_dict() - except openshift.dynamic.exceptions.NotFoundError: + field_selector=','.join(field_selectors)) + if wait: + satisfied_by = [] + if isinstance(result, ResourceInstance): + # We have a list of ResourceInstance + resource_list = result.get('items', []) + if not resource_list: + resource_list = [result] + + for resource_instance in resource_list: + success, res, duration = self.wait(resource, resource_instance, + sleep=wait_sleep, timeout=wait_timeout, + state=state, condition=condition) + if not success: + self.fail(msg="Failed to gather information about %s(s) even" + " after waiting for %s seconds" % (res.get('kind'), duration)) + satisfied_by.append(res) + return dict(resources=satisfied_by) + result = result.to_dict() + except (openshift.dynamic.exceptions.BadRequestError, openshift.dynamic.exceptions.NotFoundError): return dict(resources=[]) if 'items' in result: return dict(resources=result['items']) - else: - return dict(resources=[result]) + return dict(resources=[result]) def remove_aliases(self): """ @@ -330,8 +383,7 @@ class K8sAnsibleMixin(object): if predicate(response): if response: return True, response.to_dict(), _wait_for_elapsed() - else: - return True, {}, _wait_for_elapsed() + return True, {}, _wait_for_elapsed() time.sleep(sleep) except NotFoundError: if state == 'absent': @@ -440,21 +492,20 @@ class K8sAnsibleMixin(object): def check_library_version(self): validate = self.params.get('validate') - if validate: - if LooseVersion(self.openshift_version) < LooseVersion("0.8.0"): - self.fail_json(msg="openshift >= 0.8.0 is required for validate") + if validate and LooseVersion(self.openshift_version) < LooseVersion("0.8.0"): + self.fail_json(msg="openshift >= 0.8.0 is required for validate") self.append_hash = self.params.get('append_hash') - if self.append_hash: - if not HAS_K8S_CONFIG_HASH: - self.fail_json(msg=missing_required_lib("openshift >= 0.7.2", reason="for append_hash"), - exception=K8S_CONFIG_HASH_IMP_ERR) - if self.params['merge_type']: - if LooseVersion(self.openshift_version) < LooseVersion("0.6.2"): - self.fail_json(msg=missing_required_lib("openshift >= 0.6.2", reason="for merge_type")) + if self.append_hash and not HAS_K8S_CONFIG_HASH: + self.fail_json(msg=missing_required_lib("openshift >= 0.7.2", reason="for append_hash"), + exception=K8S_CONFIG_HASH_IMP_ERR) + if self.params['merge_type'] and LooseVersion(self.openshift_version) < LooseVersion("0.6.2"): + self.fail_json(msg=missing_required_lib("openshift >= 0.6.2", reason="for merge_type")) self.apply = self.params.get('apply', False) - if self.apply: - if not HAS_K8S_APPLY: - self.fail_json(msg=missing_required_lib("openshift >= 0.9.2", reason="for apply")) + if self.apply and not HAS_K8S_APPLY: + self.fail_json(msg=missing_required_lib("openshift >= 0.9.2", reason="for apply")) + wait = self.params.get('wait', False) + if wait and not HAS_K8S_INSTANCE_HELPER: + self.fail_json(msg=missing_required_lib("openshift >= 0.4.0", reason="for wait")) def flatten_list_kind(self, list_resource, definitions): flattened = [] diff --git a/plugins/modules/k8s.py b/plugins/modules/k8s.py index 842eb975..df7dd2a8 100644 --- a/plugins/modules/k8s.py +++ b/plugins/modules/k8s.py @@ -33,6 +33,7 @@ extends_documentation_fragment: - community.kubernetes.k8s_name_options - community.kubernetes.k8s_resource_options - community.kubernetes.k8s_auth_options + - community.kubernetes.k8s_wait_options notes: - If your OpenShift Python library is not 0.9.0 or newer and you are trying to @@ -61,53 +62,6 @@ options: - strategic-merge type: list elements: str - wait: - description: - - Whether to wait for certain resource kinds to end up in the desired state. By default the module exits once Kubernetes has - received the request - - Implemented for C(state=present) for C(Deployment), C(DaemonSet) and C(Pod), and for C(state=absent) for all resource kinds. - - For resource kinds without an implementation, C(wait) returns immediately unless C(wait_condition) is set. - default: no - type: bool - wait_sleep: - description: - - Number of seconds to sleep between checks. - default: 5 - type: int - wait_timeout: - description: - - How long in seconds to wait for the resource to end up in the desired state. Ignored if C(wait) is not set. - default: 120 - type: int - wait_condition: - description: - - Specifies a custom condition on the status to wait for. Ignored if C(wait) is not set or is set to False. - suboptions: - type: - type: str - description: - - The type of condition to wait for. For example, the C(Pod) resource will set the C(Ready) condition (among others) - - Required if you are specifying a C(wait_condition). If left empty, the C(wait_condition) field will be ignored. - - The possible types for 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. - status: - type: str - description: - - The value of the status field in your desired condition. - - For example, if a C(Deployment) is paused, the C(Progressing) C(type) will have the C(Unknown) status. - choices: - - True - - False - - Unknown - default: "True" - reason: - type: str - description: - - The value of the reason field in your desired condition - - For example, if a C(Deployment) is paused, The C(Progressing) C(type) will have the C(DeploymentPaused) reason. - - 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 validate: description: - how (if at all) to validate the resource definition against the kubernetes schema. @@ -264,7 +218,7 @@ import copy from ansible.module_utils.basic import AnsibleModule from ansible_collections.community.kubernetes.plugins.module_utils.common import ( - K8sAnsibleMixin, COMMON_ARG_SPEC, NAME_ARG_SPEC, RESOURCE_ARG_SPEC, AUTH_ARG_SPEC) + K8sAnsibleMixin, COMMON_ARG_SPEC, NAME_ARG_SPEC, RESOURCE_ARG_SPEC, AUTH_ARG_SPEC, WAIT_ARG_SPEC) class KubernetesModule(K8sAnsibleMixin): @@ -277,25 +231,14 @@ class KubernetesModule(K8sAnsibleMixin): strict=dict(type='bool', default=True) ) - @property - def condition_spec(self): - return dict( - type=dict(), - status=dict(default=True, choices=[True, False, "Unknown"]), - reason=dict() - ) - @property def argspec(self): argument_spec = copy.deepcopy(COMMON_ARG_SPEC) argument_spec.update(copy.deepcopy(NAME_ARG_SPEC)) argument_spec.update(copy.deepcopy(RESOURCE_ARG_SPEC)) argument_spec.update(copy.deepcopy(AUTH_ARG_SPEC)) + argument_spec.update(copy.deepcopy(WAIT_ARG_SPEC)) argument_spec['merge_type'] = dict(type='list', elements='str', choices=['json', 'merge', 'strategic-merge']) - argument_spec['wait'] = dict(type='bool', default=False) - argument_spec['wait_sleep'] = dict(type='int', default=5) - argument_spec['wait_timeout'] = dict(type='int', default=120) - argument_spec['wait_condition'] = dict(type='dict', default=None, options=self.condition_spec) argument_spec['validate'] = dict(type='dict', default=None, options=self.validate_spec) argument_spec['append_hash'] = dict(type='bool', default=False) argument_spec['apply'] = dict(type='bool', default=False) diff --git a/plugins/modules/k8s_info.py b/plugins/modules/k8s_info.py index 219c4eec..f7a7a0ca 100644 --- a/plugins/modules/k8s_info.py +++ b/plugins/modules/k8s_info.py @@ -46,6 +46,7 @@ options: extends_documentation_fragment: - community.kubernetes.k8s_auth_options - community.kubernetes.k8s_name_options + - community.kubernetes.k8s_wait_options requirements: - "python >= 2.7" @@ -99,6 +100,15 @@ EXAMPLES = r''' community.kubernetes.k8s_info: kind: MyCustomObject api_version: "stable.example.com/v1" + +- name: Wait till the Object is created + community.kubernetes.k8s_info: + kind: Pod + wait: yes + name: pod-not-yet-created + namespace: default + wait_sleep: 10 + wait_timeout: 360 ''' RETURN = r''' @@ -134,7 +144,7 @@ import copy from ansible.module_utils.basic import AnsibleModule from ansible_collections.community.kubernetes.plugins.module_utils.common import ( - K8sAnsibleMixin, AUTH_ARG_SPEC) + K8sAnsibleMixin, AUTH_ARG_SPEC, WAIT_ARG_SPEC) class KubernetesInfoModule(K8sAnsibleMixin): @@ -156,14 +166,19 @@ class KubernetesInfoModule(K8sAnsibleMixin): self.exit_json(changed=False, **self.kubernetes_facts(self.params['kind'], self.params['api_version'], - self.params['name'], - self.params['namespace'], - self.params['label_selectors'], - self.params['field_selectors'])) + name=self.params['name'], + namespace=self.params['namespace'], + label_selectors=self.params['label_selectors'], + field_selectors=self.params['field_selectors'], + wait=self.params['wait'], + wait_sleep=self.params['wait_sleep'], + wait_timeout=self.params['wait_timeout'], + condition=self.params['wait_condition'])) @property def argspec(self): args = copy.deepcopy(AUTH_ARG_SPEC) + args.update(WAIT_ARG_SPEC) args.update( dict( kind=dict(required=True),