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

k8s_info: Add support for wait #235

Merged
merged 2 commits into from
Sep 28, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Akasurde marked this conversation as resolved.
Show resolved Hide resolved
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,
Akasurde marked this conversation as resolved.
Show resolved Hide resolved
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