From 4b682666f199185e9b79ea5c40b238f9001feaee Mon Sep 17 00:00:00 2001 From: abikouo <79859644+abikouo@users.noreply.github.com> Date: Thu, 29 Jul 2021 11:56:34 +0200 Subject: [PATCH] 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 Reviewed-by: None --- .../158-k8s-add-support-label_selectors.yml | 3 + molecule/default/converge.yml | 8 + molecule/default/tasks/label_selectors.yml | 657 ++++++++++++++++++ plugins/module_utils/common.py | 42 +- plugins/module_utils/selector.py | 71 ++ plugins/modules/k8s.py | 7 + tests/sanity/ignore-2.10.txt | 6 +- tests/sanity/ignore-2.11.txt | 6 +- tests/sanity/ignore-2.12.txt | 4 +- tests/sanity/ignore-2.9.txt | 6 +- tests/unit/module_utils/test_selector.py | 187 +++++ 11 files changed, 982 insertions(+), 15 deletions(-) create mode 100644 changelogs/fragments/158-k8s-add-support-label_selectors.yml create mode 100644 molecule/default/tasks/label_selectors.yml create mode 100644 plugins/module_utils/selector.py create mode 100644 tests/unit/module_utils/test_selector.py diff --git a/changelogs/fragments/158-k8s-add-support-label_selectors.yml b/changelogs/fragments/158-k8s-add-support-label_selectors.yml new file mode 100644 index 0000000000..a4bd473d9f --- /dev/null +++ b/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). diff --git a/molecule/default/converge.yml b/molecule/default/converge.yml index bc5b38c8fd..108e9c82f5 100644 --- a/molecule/default/converge.yml +++ b/molecule/default/converge.yml @@ -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 + - name: Include diff.yml include_tasks: file: tasks/diff.yml diff --git a/molecule/default/tasks/label_selectors.yml b/molecule/default/tasks/label_selectors.yml new file mode 100644 index 0000000000..3d7e55919f --- /dev/null +++ b/molecule/default/tasks/label_selectors.yml @@ -0,0 +1,657 @@ +--- +- 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" + - "pod-selector-apply-02" + - "pod-selector-apply-03" + + - name: Ensure namespace selector + k8s: + kind: namespace + name: '{{ selector_namespace }}' + + # Resource deletion using label selector (equality-based requirement) + - name: Create simple pod + k8s: + namespace: '{{ selector_namespace }}' + definition: + apiVersion: v1 + kind: Pod + metadata: + name: '{{ selector_pod_delete }}-00' + 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 deletion using label selector (set-based requirement) + - name: Create simple pod + k8s: + namespace: '{{ selector_namespace }}' + definition: + apiVersion: v1 + kind: Pod + metadata: + name: '{{ selector_pod_delete }}-01' + labels: + environment: production + spec: + containers: + - name: c0 + image: alpine:3.14.0 + 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: + - environment in (test, qa) + wait: yes + wait_timeout: 180 + register: result + + - name: check that no resources were deleted + assert: + that: + - result is not changed + + - name: Ensure resources have not been deleted + k8s_info: + kind: Pod + namespace: '{{ selector_namespace }}' + label_selectors: + - environment in (production) + register: result + + - assert: + that: + - result.resources | list | length > 0 + + - name: Delete all resource using selector + k8s: + state: absent + kind: Pod + namespace: '{{ selector_namespace }}' + label_selectors: + - environment in (production) + wait: yes + wait_timeout: 180 + register: result + + - name: check result is changed + assert: + that: + - result is changed + + - name: Ensure resources have not been deleted + k8s_info: + kind: Pod + namespace: '{{ selector_namespace }}' + label_selectors: + - environment in (production) + register: result + + - assert: + that: + - result.resources | list | length == 0 + + # Resource creation using label selector + - name: Create simple pod using label_selectors option (equality-based requirement) + k8s: + namespace: '{{ selector_namespace }}' + label_selectors: + - container.image=fedora + definition: | + --- + apiVersion: v1 + kind: Pod + metadata: + name: '{{ selector_pod_create[0] }}' + labels: + container.image: busybox + 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: + container.image: alpine + spec: + containers: + - name: c0 + image: alpine + command: + - /bin/sh + - -c + - while true;do date;sleep 5; done + --- + apiVersion: v1 + kind: Pod + metadata: + name: '{{ selector_pod_create[2] }}' + labels: + container.image: python + release: dev + spec: + containers: + - name: c0 + image: python + command: + - /bin/sh + - -c + - while true;do date;sleep 5; done + --- + apiVersion: v1 + kind: Pod + metadata: + name: '{{ selector_pod_create[3] }}' + labels: + container.image: python + release: dev + spec: + containers: + - name: c0 + image: python + 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: + - container.image==alpine + definition: | + --- + apiVersion: v1 + kind: Pod + metadata: + name: '{{ selector_pod_create[0] }}' + labels: + container.image: busybox + 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: + container.image: alpine + spec: + containers: + - name: c0 + image: alpine + command: + - /bin/sh + - -c + - while true;do date;sleep 5; done + --- + apiVersion: v1 + kind: Pod + metadata: + name: '{{ selector_pod_create[2] }}' + labels: + container.image: python + environment: test + spec: + containers: + - name: c0 + image: python + command: + - /bin/sh + - -c + - while true;do date;sleep 5; done + --- + apiVersion: v1 + kind: Pod + metadata: + name: '{{ selector_pod_create[3] }}' + labels: + container.image: python + environment: production + spec: + containers: + - name: c0 + image: python + 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 + label_selectors: + - container.image + register: pod_created + + - name: Validate that pod with matching label was created + assert: + that: + - pods_created | length == 1 + - selector_pod_create[1] in pods_created + vars: + pods_created: '{{ pod_created.resources | map(attribute="metadata.name") | list }}' + + - name: Create simple pod using label_selectors option (set-based requirement) + k8s: + namespace: '{{ selector_namespace }}' + label_selectors: + - "!environment" + definition: | + --- + apiVersion: v1 + kind: Pod + metadata: + name: '{{ selector_pod_create[0] }}' + labels: + container.image: busybox + 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: + container.image: alpine + spec: + containers: + - name: c0 + image: alpine + command: + - /bin/sh + - -c + - while true;do date;sleep 5; done + --- + apiVersion: v1 + kind: Pod + metadata: + name: '{{ selector_pod_create[2] }}' + labels: + container.image: python + environment: test + spec: + containers: + - name: c0 + image: python + command: + - /bin/sh + - -c + - while true;do date;sleep 5; done + --- + apiVersion: v1 + kind: Pod + metadata: + name: '{{ selector_pod_create[3] }}' + labels: + container.image: python + environment: production + spec: + containers: + - name: c0 + image: python + 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 + label_selectors: + - container.image + register: pod_created + + - name: Validate that pod with matching label was created + assert: + that: + - pods_created | length == 2 + - selector_pod_create[0] in pods_created + vars: + pods_created: '{{ pod_created.resources | map(attribute="metadata.name") | list }}' + + - name: Create simple pod using label_selectors option (set-based requirement) + k8s: + namespace: '{{ selector_namespace }}' + label_selectors: + - environment in (test) + definition: | + --- + apiVersion: v1 + kind: Pod + metadata: + name: '{{ selector_pod_create[0] }}' + labels: + container.image: busybox + 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: + container.image: alpine + spec: + containers: + - name: c0 + image: alpine + command: + - /bin/sh + - -c + - while true;do date;sleep 5; done + --- + apiVersion: v1 + kind: Pod + metadata: + name: '{{ selector_pod_create[2] }}' + labels: + container.image: python + environment: test + spec: + containers: + - name: c0 + image: python + command: + - /bin/sh + - -c + - while true;do date;sleep 5; done + --- + apiVersion: v1 + kind: Pod + metadata: + name: '{{ selector_pod_create[3] }}' + labels: + container.image: python + environment: production + spec: + containers: + - name: c0 + image: python + 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 + label_selectors: + - container.image + register: pod_created + + - name: Validate that pod with matching label was created + assert: + that: + - pods_created | length == 3 + - selector_pod_create[2] in pods_created + vars: + pods_created: '{{ pod_created.resources | map(attribute="metadata.name") | list }}' + + - name: Create simple pod using label_selectors option (set-based requirement) + k8s: + namespace: '{{ selector_namespace }}' + label_selectors: + - environment notin (test) + definition: | + --- + apiVersion: v1 + kind: Pod + metadata: + name: '{{ selector_pod_create[0] }}' + labels: + container.image: busybox + 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: + container.image: alpine + spec: + containers: + - name: c0 + image: alpine + command: + - /bin/sh + - -c + - while true;do date;sleep 5; done + --- + apiVersion: v1 + kind: Pod + metadata: + name: '{{ selector_pod_create[2] }}' + labels: + container.image: python + environment: test + spec: + containers: + - name: c0 + image: python + command: + - /bin/sh + - -c + - while true;do date;sleep 5; done + --- + apiVersion: v1 + kind: Pod + metadata: + name: '{{ selector_pod_create[3] }}' + labels: + container.image: python + environment: production + spec: + containers: + - name: c0 + image: python + 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 + label_selectors: + - container.image + register: pod_created + + - name: Validate that pod with matching label was created + assert: + that: + - pods_created | length == 4 + - selector_pod_create[3] 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: + environment: test + 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: + environment: test + spec: + containers: + - name: c0 + image: busybox:1.33.0 + command: + - /bin/sh + - -c + - while true;do date;sleep 5; done + label_selectors: + - environment=qa + register: result + + - name: check task output + assert: + that: + - result is not changed + - '"filtered by label_selectors" in result.msg' + + - 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: + environment: test + spec: + containers: + - name: c0 + image: busybox:1.33.0 + command: + - /bin/sh + - -c + - while true;do date;sleep 5; done + label_selectors: + - environment!=qa + 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 diff --git a/plugins/module_utils/common.py b/plugins/module_utils/common.py index 70c6fb19e0..cb090eff37 100644 --- a/plugins/module_utils/common.py +++ b/plugins/module_utils/common.py @@ -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 @@ -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(): @@ -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() @@ -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 @@ -420,7 +424,7 @@ 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, @@ -428,13 +432,13 @@ def _resource_absent(resource): 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 @@ 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'] @@ -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 @@ -625,7 +632,13 @@ 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 return result else: @@ -651,7 +664,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" @@ -663,6 +676,13 @@ 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 + 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)) diff --git a/plugins/module_utils/selector.py b/plugins/module_utils/selector.py new file mode 100644 index 0000000000..5401f6a3af --- /dev/null +++ b/plugins/module_utils/selector.py @@ -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]) diff --git a/plugins/modules/k8s.py b/plugins/modules/k8s.py index 5bcb51cff2..d2fb02a56d 100644 --- a/plugins/modules/k8s.py +++ b/plugins/modules/k8s.py @@ -135,6 +135,12 @@ type: bool default: False version_added: 2.0.0 + label_selectors: + description: + - Selector (label query) to filter on. + type: list + elements: str + version_added: 2.2.0 requirements: - "python >= 3.6" @@ -335,6 +341,7 @@ def argspec(): argument_spec['continue_on_error'] = dict(type='bool', default=False) argument_spec['state'] = dict(default='present', choices=['present', 'absent', 'patched']) argument_spec['force'] = dict(type='bool', default=False) + argument_spec['label_selectors'] = dict(type='list', elements='str') return argument_spec diff --git a/tests/sanity/ignore-2.10.txt b/tests/sanity/ignore-2.10.txt index d0c179e569..79e4388009 100644 --- a/tests/sanity/ignore-2.10.txt +++ b/tests/sanity/ignore-2.10.txt @@ -240,4 +240,8 @@ plugins/modules/k8s_cp.py import-2.6!skip plugins/modules/k8s_cp.py import-2.7!skip molecule/default/roles/k8scopy/library/kubectl_file_compare.py compile-2.6!skip molecule/default/roles/k8scopy/library/kubectl_file_compare.py compile-2.7!skip -molecule/default/roles/k8scopy/library/kubectl_file_compare.py compile-3.5!skip \ No newline at end of file +molecule/default/roles/k8scopy/library/kubectl_file_compare.py compile-3.5!skip +tests/unit/module_utils/test_selector.py future-import-boilerplate!skip +tests/unit/module_utils/test_selector.py metaclass-boilerplate!skip +plugins/module_utils/selector.py future-import-boilerplate!skip +plugins/module_utils/selector.py metaclass-boilerplate!skip \ No newline at end of file diff --git a/tests/sanity/ignore-2.11.txt b/tests/sanity/ignore-2.11.txt index d0c179e569..79e4388009 100644 --- a/tests/sanity/ignore-2.11.txt +++ b/tests/sanity/ignore-2.11.txt @@ -240,4 +240,8 @@ plugins/modules/k8s_cp.py import-2.6!skip plugins/modules/k8s_cp.py import-2.7!skip molecule/default/roles/k8scopy/library/kubectl_file_compare.py compile-2.6!skip molecule/default/roles/k8scopy/library/kubectl_file_compare.py compile-2.7!skip -molecule/default/roles/k8scopy/library/kubectl_file_compare.py compile-3.5!skip \ No newline at end of file +molecule/default/roles/k8scopy/library/kubectl_file_compare.py compile-3.5!skip +tests/unit/module_utils/test_selector.py future-import-boilerplate!skip +tests/unit/module_utils/test_selector.py metaclass-boilerplate!skip +plugins/module_utils/selector.py future-import-boilerplate!skip +plugins/module_utils/selector.py metaclass-boilerplate!skip \ No newline at end of file diff --git a/tests/sanity/ignore-2.12.txt b/tests/sanity/ignore-2.12.txt index 3ef6897e05..f9aee54e04 100644 --- a/tests/sanity/ignore-2.12.txt +++ b/tests/sanity/ignore-2.12.txt @@ -238,4 +238,6 @@ tests/sanity/refresh_ignore_files shebang!skip plugins/modules/k8s_cp.py compile-2.6!skip plugins/modules/k8s_cp.py compile-2.7!skip plugins/modules/k8s_cp.py import-2.6!skip -plugins/modules/k8s_cp.py import-2.7!skip \ No newline at end of file +plugins/modules/k8s_cp.py import-2.7!skip +plugins/module_utils/selector.py future-import-boilerplate!skip +plugins/module_utils/selector.py metaclass-boilerplate!skip \ No newline at end of file diff --git a/tests/sanity/ignore-2.9.txt b/tests/sanity/ignore-2.9.txt index 5057968943..0b23ebe1e4 100644 --- a/tests/sanity/ignore-2.9.txt +++ b/tests/sanity/ignore-2.9.txt @@ -234,4 +234,8 @@ plugins/modules/k8s_cp.py import-2.6!skip plugins/modules/k8s_cp.py import-2.7!skip molecule/default/roles/k8scopy/library/kubectl_file_compare.py compile-2.6!skip molecule/default/roles/k8scopy/library/kubectl_file_compare.py compile-2.7!skip -molecule/default/roles/k8scopy/library/kubectl_file_compare.py compile-3.5!skip \ No newline at end of file +molecule/default/roles/k8scopy/library/kubectl_file_compare.py compile-3.5!skip +tests/unit/module_utils/test_selector.py future-import-boilerplate!skip +tests/unit/module_utils/test_selector.py metaclass-boilerplate!skip +plugins/module_utils/selector.py future-import-boilerplate!skip +plugins/module_utils/selector.py metaclass-boilerplate!skip \ No newline at end of file diff --git a/tests/unit/module_utils/test_selector.py b/tests/unit/module_utils/test_selector.py new file mode 100644 index 0000000000..9f14b768e3 --- /dev/null +++ b/tests/unit/module_utils/test_selector.py @@ -0,0 +1,187 @@ +# 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. + +from ansible_collections.kubernetes.core.plugins.module_utils.selector import LabelSelectorFilter, Selector + +prod_definition = { + 'apiVersion': 'v1', + 'kind': 'Pod', + 'metadata': { + 'name': 'test', + 'labels': { + 'environment': 'production', + 'app': 'nginx', + } + }, + 'spec': { + 'containers': [ + {'name': 'nginx', 'image': 'nginx:1.14.2', 'command': ['/bin/sh', '-c', 'sleep 10']} + ] + } +} + +no_label_definition = { + 'apiVersion': 'v1', + 'kind': 'Pod', + 'metadata': { + 'name': 'test', + 'labels': {} + }, + 'spec': { + 'containers': [ + {'name': 'nginx', 'image': 'nginx:1.14.2', 'command': ['/bin/sh', '-c', 'sleep 10']} + ] + } +} + +test_definition = { + 'apiVersion': 'v1', + 'kind': 'Pod', + 'metadata': { + 'name': 'test', + 'labels': { + 'environment': 'test', + 'app': 'nginx', + } + }, + 'spec': { + 'containers': [ + {'name': 'nginx', 'image': 'nginx:1.15.2', 'command': ['/bin/sh', '-c', 'sleep 10']} + ] + } +} + + +def test_selector_parser(): + f_selector = "environment==true" + sel = Selector(f_selector) + assert sel._operator == "in" and sel._data == ["true"] and sel._key == "environment" + f_selector = "environment=true" + sel = Selector(f_selector) + assert sel._operator == "in" and sel._data == ["true"] and sel._key == "environment" + f_selector = " environment == true " + sel = Selector(f_selector) + assert sel._operator == "in" and sel._data == ["true"] and sel._key == "environment" + f_selector = "environment!=false" + sel = Selector(f_selector) + assert sel._operator == "notin" and sel._data == ["false"] and sel._key == "environment" + f_selector = "environment notin (true, false)" + sel = Selector(f_selector) + assert sel._operator == "notin" and "true" in sel._data and "false" in sel._data and sel._key == "environment" + f_selector = "environment in (true, false)" + sel = Selector(f_selector) + assert sel._operator == "in" and "true" in sel._data and "false" in sel._data and sel._key == "environment" + f_selector = "environmentin(true, false)" + sel = Selector(f_selector) + assert not sel._operator and not sel._data and sel._key == f_selector + f_selector = "environment notin (true, false" + sel = Selector(f_selector) + assert not sel._operator and not sel._data and sel._key == f_selector + f_selector = "!environment" + sel = Selector(f_selector) + assert sel._operator == "!" and not sel._data and sel._key == "environment" + f_selector = "! environment " + sel = Selector(f_selector) + assert sel._operator == "!" and not sel._data and sel._key == "environment" + + +def test_label_selector_without_operator(): + label_selector = ['environment', 'app'] + assert LabelSelectorFilter(label_selector).isMatching(prod_definition) + assert not LabelSelectorFilter(label_selector).isMatching(no_label_definition) + assert LabelSelectorFilter(label_selector).isMatching(test_definition) + + +def test_label_selector_equal_operator(): + label_selector = ['environment==test'] + assert not LabelSelectorFilter(label_selector).isMatching(prod_definition) + assert not LabelSelectorFilter(label_selector).isMatching(no_label_definition) + assert LabelSelectorFilter(label_selector).isMatching(test_definition) + label_selector = ['environment=production'] + assert LabelSelectorFilter(label_selector).isMatching(prod_definition) + assert not LabelSelectorFilter(label_selector).isMatching(no_label_definition) + assert not LabelSelectorFilter(label_selector).isMatching(test_definition) + label_selector = ['environment=production', 'app==mongodb'] + assert not LabelSelectorFilter(label_selector).isMatching(prod_definition) + assert not LabelSelectorFilter(label_selector).isMatching(no_label_definition) + assert not LabelSelectorFilter(label_selector).isMatching(test_definition) + label_selector = ['environment=production', 'app==nginx'] + assert LabelSelectorFilter(label_selector).isMatching(prod_definition) + assert not LabelSelectorFilter(label_selector).isMatching(no_label_definition) + assert not LabelSelectorFilter(label_selector).isMatching(test_definition) + label_selector = ['environment', 'app==nginx'] + assert LabelSelectorFilter(label_selector).isMatching(prod_definition) + assert not LabelSelectorFilter(label_selector).isMatching(no_label_definition) + assert LabelSelectorFilter(label_selector).isMatching(test_definition) + + +def test_label_selector_notequal_operator(): + label_selector = ['environment!=test'] + assert LabelSelectorFilter(label_selector).isMatching(prod_definition) + assert LabelSelectorFilter(label_selector).isMatching(no_label_definition) + assert not LabelSelectorFilter(label_selector).isMatching(test_definition) + label_selector = ['environment!=production'] + assert not LabelSelectorFilter(label_selector).isMatching(prod_definition) + assert LabelSelectorFilter(label_selector).isMatching(no_label_definition) + assert LabelSelectorFilter(label_selector).isMatching(test_definition) + label_selector = ['environment=production', 'app!=mongodb'] + assert LabelSelectorFilter(label_selector).isMatching(prod_definition) + assert not LabelSelectorFilter(label_selector).isMatching(no_label_definition) + assert not LabelSelectorFilter(label_selector).isMatching(test_definition) + label_selector = ['environment=production', 'app!=nginx'] + assert not LabelSelectorFilter(label_selector).isMatching(prod_definition) + assert not LabelSelectorFilter(label_selector).isMatching(no_label_definition) + assert not LabelSelectorFilter(label_selector).isMatching(test_definition) + label_selector = ['environment', 'app!=nginx'] + assert not LabelSelectorFilter(label_selector).isMatching(prod_definition) + assert not LabelSelectorFilter(label_selector).isMatching(no_label_definition) + assert not LabelSelectorFilter(label_selector).isMatching(test_definition) + + +def test_label_selector_conflicting_definition(): + label_selector = ['environment==test', 'environment!=test'] + assert not LabelSelectorFilter(label_selector).isMatching(prod_definition) + assert not LabelSelectorFilter(label_selector).isMatching(no_label_definition) + assert not LabelSelectorFilter(label_selector).isMatching(test_definition) + label_selector = ['environment==test', 'environment==production'] + assert not LabelSelectorFilter(label_selector).isMatching(prod_definition) + assert not LabelSelectorFilter(label_selector).isMatching(no_label_definition) + assert not LabelSelectorFilter(label_selector).isMatching(test_definition) + + +def test_set_based_requirement(): + label_selector = ['environment in (production)'] + assert LabelSelectorFilter(label_selector).isMatching(prod_definition) + assert not LabelSelectorFilter(label_selector).isMatching(no_label_definition) + assert not LabelSelectorFilter(label_selector).isMatching(test_definition) + label_selector = ['environment in (production, test)'] + assert LabelSelectorFilter(label_selector).isMatching(prod_definition) + assert not LabelSelectorFilter(label_selector).isMatching(no_label_definition) + assert LabelSelectorFilter(label_selector).isMatching(test_definition) + label_selector = ['environment notin (production)'] + assert not LabelSelectorFilter(label_selector).isMatching(prod_definition) + assert LabelSelectorFilter(label_selector).isMatching(no_label_definition) + assert LabelSelectorFilter(label_selector).isMatching(test_definition) + label_selector = ['environment notin (production, test)'] + assert not LabelSelectorFilter(label_selector).isMatching(prod_definition) + assert LabelSelectorFilter(label_selector).isMatching(no_label_definition) + assert not LabelSelectorFilter(label_selector).isMatching(test_definition) + label_selector = ['environment'] + assert LabelSelectorFilter(label_selector).isMatching(prod_definition) + assert not LabelSelectorFilter(label_selector).isMatching(no_label_definition) + assert LabelSelectorFilter(label_selector).isMatching(test_definition) + label_selector = ['!environment'] + assert not LabelSelectorFilter(label_selector).isMatching(prod_definition) + assert LabelSelectorFilter(label_selector).isMatching(no_label_definition) + assert not LabelSelectorFilter(label_selector).isMatching(test_definition)