Skip to content

k8s ability to wait on arbitrary property #134

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 27 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
4a9eca5
missing implementation of jsonpath library
abikouo May 3, 2021
06c99d6
merge with main branch
abikouo May 17, 2021
ce037ce
not tested
abikouo May 18, 2021
b453f79
sanity
abikouo May 18, 2021
ac97df6
merge with main
abikouo May 19, 2021
dd821f0
save
abikouo May 19, 2021
9e8b1da
merge with main
abikouo May 19, 2021
e29e35d
Merge branch 'main' of https://github.com/ansible-collections/kuberne…
abikouo May 20, 2021
0a28aa0
updates
abikouo May 20, 2021
4db9724
Update args_common.py
abikouo May 20, 2021
2691b0f
lint validation
abikouo May 20, 2021
3939c53
fix
abikouo May 20, 2021
930c551
Update k8s.py
abikouo May 20, 2021
483c91c
attribute should match for all
abikouo May 20, 2021
a20a1f6
select wait
abikouo May 20, 2021
4fa6e4c
Revert "select wait"
abikouo May 21, 2021
538d938
Merge branch 'main' of https://github.com/ansible-collections/kuberne…
abikouo May 21, 2021
9092d6e
replace jmespath with builtin library
abikouo May 27, 2021
c23e3b3
sanity
abikouo May 27, 2021
520e925
Update molecule/default/tasks/waiter.yml
abikouo Jun 1, 2021
68f34aa
Merge branch 'main' of https://github.com/ansible-collections/kuberne…
abikouo Jun 1, 2021
8865ece
Merge branch 'wait_arbitrary' of https://github.com/abikouo/kubernete…
abikouo Jun 1, 2021
7891ce8
Update jsonpath_extractor.py
abikouo Jun 3, 2021
9fcd249
Update k8s_wait_options.py
abikouo Jun 14, 2021
710a7c3
Merge branch 'wait_arbitrary' into wait_arbit_2
abikouo Jun 15, 2021
cb0d965
Merge branch 'main' of https://github.com/ansible-collections/kuberne…
abikouo Jun 15, 2021
8a98418
Revert "Revert "k8s ability to wait on arbitrary property (#105)" (#1…
abikouo Jun 15, 2021
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
3 changes: 3 additions & 0 deletions changelogs/fragments/105-wait_property.yaml
Original file line number Diff line number Diff line change
@@ -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).
87 changes: 87 additions & 0 deletions molecule/default/tasks/waiter.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
18 changes: 18 additions & 0 deletions plugins/doc_fragments/k8s_wait_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
'''
8 changes: 8 additions & 0 deletions plugins/module_utils/args_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
)
)
)

Expand Down
47 changes: 32 additions & 15 deletions plugins/module_utils/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,15 @@

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
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


K8S_IMP_ERR = None
try:
import kubernetes
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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():
Expand All @@ -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()
Expand All @@ -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
Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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'
Expand Down Expand Up @@ -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'
Expand Down
Loading