Skip to content
This repository has been archived by the owner on Jun 13, 2024. It is now read-only.

Commit

Permalink
k8s_info: Add support for wait (#235)
Browse files Browse the repository at this point in the history
Fixes: #18
  • Loading branch information
Akasurde authored Sep 28, 2020
1 parent 70a4b06 commit f03d2ce
Show file tree
Hide file tree
Showing 7 changed files with 331 additions and 85 deletions.
2 changes: 2 additions & 0 deletions changelogs/fragments/18_k8s_info_wait.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
bugfixes:
- k8s_info - add wait functionality (https://github.com/ansible-collections/community.kubernetes/issues/18).
1 change: 1 addition & 0 deletions molecule/default/converge.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
167 changes: 167 additions & 0 deletions molecule/default/tasks/info.yml
Original file line number Diff line number Diff line change
@@ -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
67 changes: 67 additions & 0 deletions plugins/doc_fragments/k8s_wait_options.py
Original file line number Diff line number Diff line change
@@ -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
'''
91 changes: 71 additions & 20 deletions plugins/module_utils/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,20 @@
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)):
Expand Down Expand Up @@ -158,6 +172,21 @@ def list_dict_str(value):
},
}

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',
Expand Down Expand Up @@ -249,22 +278,46 @@ def find_resource(self, kind, api_version, fail=False):
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):
"""
Expand Down Expand Up @@ -330,8 +383,7 @@ def _wait_for_elapsed():
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':
Expand Down Expand Up @@ -440,21 +492,20 @@ def set_resource_definitions(self):

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 = []
Expand Down
Loading

0 comments on commit f03d2ce

Please sign in to comment.