k8s - add label_selectors options (#158)

k8s - add label_selectors options

SUMMARY
k8s now support label_selectors options same as k8s_info

Resolves #43

ISSUE TYPE


Feature Pull Request


COMPONENT NAME

k8s

Reviewed-by: Mike Graves <mgraves@redhat.com>
Reviewed-by: None <None>
This commit is contained in:
abikouo
2021-07-29 11:56:34 +02:00
committed by GitHub
parent abd2abb33e
commit 4b682666f1
11 changed files with 982 additions and 15 deletions

View File

@@ -29,6 +29,7 @@ from distutils.version import LooseVersion
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
@@ -349,7 +350,7 @@ class K8sAnsibleMixin(object):
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():
@@ -358,7 +359,10 @@ class K8sAnsibleMixin(object):
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()
@@ -371,7 +375,7 @@ class K8sAnsibleMixin(object):
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
@@ -420,7 +424,7 @@ class K8sAnsibleMixin(object):
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,
@@ -428,13 +432,13 @@ class K8sAnsibleMixin(object):
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')
@@ -575,6 +579,7 @@ class K8sAnsibleMixin(object):
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']
@@ -591,6 +596,8 @@ class K8sAnsibleMixin(object):
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
@@ -625,7 +632,13 @@ class K8sAnsibleMixin(object):
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
return result
else:
@@ -651,7 +664,7 @@ class K8sAnsibleMixin(object):
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"
@@ -663,6 +676,13 @@ class K8sAnsibleMixin(object):
return result
else:
if label_selectors:
filter_selector = LabelSelectorFilter(label_selectors)
if not filter_selector.isMatching(definition):
result['changed'] = False
result['msg'] = "resource 'kind={kind},name={name},namespace={namespace}' filtered by label_selectors.".format(
kind=definition['kind'], name=origin_name, namespace=namespace)
return result
if apply:
if self.check_mode:
ignored, patch = apply_object(resource, _encode_stringdata(definition))

View File

@@ -0,0 +1,71 @@
# Copyright [2021] [Red Hat, Inc.]
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import re
class Selector(object):
equality_based_operators = ('==', '!=', '=')
def __init__(self, data):
self._operator = None
self._data = None
if not self.parse_set_based_requirement(data):
no_whitespace_data = data.replace(" ", "")
for op in self.equality_based_operators:
idx = no_whitespace_data.find(op)
if idx != -1:
self._operator = "in" if op == '==' or op == '=' else "notin"
self._key = no_whitespace_data[0:idx]
self._data = [no_whitespace_data[idx + len(op):]]
break
def parse_set_based_requirement(self, data):
m = re.match(r'( *)([a-z0-9A-Z][a-z0-9A-Z\._-]*[a-z0-9A-Z])( +)(notin|in)( +)\((.*)\)( *)', data)
if m:
self._set_based_requirement = True
self._key = m.group(2)
self._operator = m.group(4)
self._data = [x.replace(' ', '') for x in m.group(6).split(',') if x != '']
return True
elif all([x not in data for x in self.equality_based_operators]):
self._key = data.rstrip(" ").lstrip(" ")
if self._key.startswith("!"):
self._key = self._key[1:].lstrip(" ")
self._operator = "!"
return True
return False
def isMatch(self, labels):
if self._operator == "in":
return self._key in labels and labels.get(self._key) in self._data
elif self._operator == "notin":
return self._key not in labels or labels.get(self._key) not in self._data
else:
return self._key not in labels if self._operator == "!" else self._key in labels
class LabelSelectorFilter(object):
def __init__(self, label_selectors):
self.selectors = [Selector(data) for data in label_selectors]
def isMatching(self, definition):
if "metadata" not in definition or "labels" not in definition['metadata']:
return False
labels = definition['metadata']['labels']
if not isinstance(labels, dict):
return None
return all([sel.isMatch(labels) for sel in self.selectors])