Skip to content
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

k8s - add label_selectors options #158

Merged
merged 16 commits into from Jul 29, 2021
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions changelogs/fragments/157-k8s-add-support-label_selectors.yml
@@ -0,0 +1,3 @@
---
minor_changes:
- k8s - add support for label_selectors options (https://github.com/ansible-collections/kubernetes.core/issues/43).
abikouo marked this conversation as resolved.
Show resolved Hide resolved
3 changes: 3 additions & 0 deletions changelogs/fragments/158-k8s-add-support-label_selectors.yml
@@ -0,0 +1,3 @@
---
minor_changes:
- k8s - add support for label_selectors options (https://github.com/ansible-collections/kubernetes.core/issues/43).
8 changes: 8 additions & 0 deletions molecule/default/converge.yml
Expand Up @@ -164,6 +164,14 @@
tags:
- always

- name: Include label_selectors.yml
include_tasks:
file: tasks/label_selectors.yml
apply:
tags: [ label_selectors, k8s ]
tags:
- always

roles:
- role: helm
tags:
Expand Down
243 changes: 243 additions & 0 deletions molecule/default/tasks/label_selectors.yml
@@ -0,0 +1,243 @@
---
- block:
- set_fact:
selector_namespace: "selector"
selector_pod_delete: "pod-selector-delete"
selector_pod_apply: "pod-selector-apply"
selector_pod_create:
- "pod-selector-apply-00"
- "pod-selector-apply-01"

- name: Ensure namespace selector
k8s:
kind: namespace
name: '{{ selector_namespace }}'

# Resource deletion using label selector
- name: Create simple pod
k8s:
namespace: '{{ selector_namespace }}'
definition:
apiVersion: v1
kind: Pod
metadata:
name: '{{ selector_pod_delete }}'
labels:
ansible.dev/team: "cloud"
ansible.release/version: upstream
ansible.dev/test: "true"
spec:
containers:
- name: c0
image: busybox
command:
- /bin/sh
- -c
- while true;do date;sleep 5; done

- name: Delete all resource using selector
k8s:
state: absent
kind: Pod
namespace: '{{ selector_namespace }}'
label_selectors:
- ansible.dev/team=cloud
wait: yes
wait_timeout: 180

- name: Ensure resources have been deleted
k8s_info:
kind: Pod
namespace: '{{ selector_namespace }}'
label_selectors:
- ansible.dev/team=cloud
register: result

- assert:
that:
- result.resources == []

# Resource creation using label selector
- name: Create simple pod using label_selectors option (no resource matching label)
k8s:
namespace: '{{ selector_namespace }}'
label_selectors:
- ansible.pod/created=true
definition: |
---
apiVersion: v1
kind: Pod
metadata:
name: '{{ selector_pod_create[0] }}'
labels:
ansible.pod/created: "false"
spec:
containers:
- name: c0
image: busybox
command:
- /bin/sh
- -c
- while true;do date;sleep 5; done
---
apiVersion: v1
kind: Pod
metadata:
name: '{{ selector_pod_create[1] }}'
labels:
ansible.pod/created: "false"
spec:
containers:
- name: c0
image: busybox
command:
- /bin/sh
- -c
- while true;do date;sleep 5; done
register: result

- assert:
that:
- result is not changed

- name: Create simple pod using label_selectors option
k8s:
namespace: '{{ selector_namespace }}'
label_selectors:
- ansible.pod/created=true
definition: |
---
apiVersion: v1
kind: Pod
metadata:
name: '{{ selector_pod_create[0] }}'
labels:
ansible.pod/created: "false"
spec:
containers:
- name: c0
image: busybox
command:
- /bin/sh
- -c
- while true;do date;sleep 5; done
---
apiVersion: v1
kind: Pod
metadata:
name: '{{ selector_pod_create[1] }}'
labels:
ansible.pod/created: "true"
spec:
containers:
- name: c0
image: busybox
command:
- /bin/sh
- -c
- while true;do date;sleep 5; done
register: result

- assert:
that:
- result is changed

- name: list pod created
k8s_info:
namespace: '{{ selector_namespace }}'
kind: Pod
register: pod_created

- name: Validate that pod with matching label was created
assert:
that:
- selector_pod_create[0] not in pods_created
- selector_pod_create[1] in pods_created
vars:
pods_created: '{{ pod_created.resources | map(attribute="metadata.name") | list }}'

# Resource update using apply
- name: Create simple pod using apply
k8s:
namespace: '{{ selector_namespace }}'
apply: yes
definition:
apiVersion: v1
kind: Pod
metadata:
name: '{{ selector_pod_apply }}'
labels:
ansible.dev/test: "false"
spec:
containers:
- name: c0
image: busybox:1.31.0
command:
- /bin/sh
- -c
- while true;do date;sleep 5; done

- name: Apply new pod definition using label_selectors (no match)
k8s:
namespace: '{{ selector_namespace }}'
apply: yes
definition:
apiVersion: v1
kind: Pod
metadata:
name: '{{ selector_pod_apply }}'
labels:
ansible.dev/test: "false"
spec:
containers:
- name: c0
image: busybox:1.33.0
command:
- /bin/sh
- -c
- while true;do date;sleep 5; done
label_selectors:
- ansible.dev/test=true
register: result

- name: check task output
assert:
that:
- result is not changed
- '"filtered by label_selectors" in result.warning'

- name: Apply new pod definition using label_selectors
k8s:
namespace: '{{ selector_namespace }}'
apply: yes
definition:
apiVersion: v1
kind: Pod
metadata:
name: '{{ selector_pod_apply }}'
labels:
ansible.dev/test: "false"
spec:
containers:
- name: c0
image: busybox:1.33.0
command:
- /bin/sh
- -c
- while true;do date;sleep 5; done
label_selectors:
- ansible.dev/test=false
register: result

- name: check task output
assert:
that:
- result is changed

always:
- name: Ensure namespace is deleted
k8s:
kind: Namespace
name: '{{ selector_namespace }}'
state: absent
ignore_errors: true
44 changes: 33 additions & 11 deletions plugins/module_utils/common.py
Expand Up @@ -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.selector import LabelSelectorFilter

from ansible.module_utils.basic import missing_required_lib
from ansible.module_utils.six import iteritems, string_types
Expand Down Expand Up @@ -349,7 +350,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, predicate, sleep, timeout, state, label_selectors):
start = datetime.now()

def _wait_for_elapsed():
Expand All @@ -358,7 +359,10 @@ def _wait_for_elapsed():
response = None
while _wait_for_elapsed() < timeout:
try:
response = resource.get(name=name, namespace=namespace)
params = dict(name=name, namespace=namespace)
if label_selectors:
params['label_selector'] = ','.join(label_selectors)
response = resource.get(**params)
if predicate(response):
if response:
return True, response.to_dict(), _wait_for_elapsed()
Expand All @@ -371,7 +375,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, label_selectors=None):

def _deployment_ready(deployment):
# FIXME: frustratingly bool(deployment.status) is True even if status is empty
Expand Down Expand Up @@ -420,21 +424,21 @@ def _custom_condition(resource):
return False

def _resource_absent(resource):
return not resource
return not resource or (resource.kind.endswith('List') and resource.items == [])

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
if state == 'present':
predicate = waiter.get(kind, lambda x: x) if not condition else _custom_condition
else:
predicate = _resource_absent
return self._wait_for(resource, definition['metadata']['name'], definition['metadata'].get('namespace'), predicate, sleep, timeout, state)
name = definition['metadata']['name']
namespace = definition['metadata'].get('namespace')
return self._wait_for(resource, name, namespace, predicate, sleep, timeout, state, label_selectors)

def set_resource_definitions(self, module):
resource_definition = module.params.get('resource_definition')
Expand Down Expand Up @@ -575,6 +579,7 @@ def perform_action(self, resource, definition):
wait_timeout = self.params.get('wait_timeout')
wait_condition = None
continue_on_error = self.params.get('continue_on_error')
label_selectors = self.params.get('label_selectors')
if self.params.get('wait_condition') and self.params['wait_condition'].get('type'):
wait_condition = self.params['wait_condition']

Expand All @@ -591,6 +596,8 @@ def build_error_msg(kind, name, msg):
params = dict(name=name)
if namespace:
params['namespace'] = namespace
if label_selectors:
params['label_selector'] = ','.join(label_selectors)
existing = resource.get(**params)
except (NotFoundError, MethodNotAllowedError):
# Remove traceback so that it doesn't show up in later failures
Expand Down Expand Up @@ -625,8 +632,15 @@ def build_error_msg(kind, name, msg):

if state == 'absent':
result['method'] = "delete"
if not existing:

def _empty_resource_list():
if existing and existing.kind.endswith('List'):
return existing.items == []
return False

if not existing or _empty_resource_list():
# The object already does not exist
result['result']['warning'] = 'No resources found'
abikouo marked this conversation as resolved.
Show resolved Hide resolved
return result
else:
# Delete the object
Expand All @@ -651,7 +665,7 @@ def build_error_msg(kind, name, msg):
else:
self.fail_json(msg=build_error_msg(definition['kind'], origin_name, msg), error=exc.status, status=exc.status, reason=exc.reason)
if wait:
success, resource, duration = self.wait(resource, definition, wait_sleep, wait_timeout, 'absent')
success, resource, duration = self.wait(resource, definition, wait_sleep, wait_timeout, 'absent', label_selectors=label_selectors)
result['duration'] = duration
if not success:
msg = "Resource deletion timed out"
Expand All @@ -663,6 +677,14 @@ def build_error_msg(kind, name, msg):
return result

else:
if label_selectors:
filter_selector = LabelSelectorFilter(label_selectors)
if not filter_selector.isMatching(definition):
result['changed'] = False
del result['result']
abikouo marked this conversation as resolved.
Show resolved Hide resolved
result['warning'] = "resource 'kind={kind},name={name},namespace={namespace}' filtered by label_selectors.".format(
abikouo marked this conversation as resolved.
Show resolved Hide resolved
kind=definition['kind'], name=origin_name, namespace=namespace)
return result
if apply:
if self.check_mode:
ignored, patch = apply_object(resource, _encode_stringdata(definition))
Expand Down