K8s_taint new module (#264)

K8s_taint new module

SUMMARY

k8s_taint - new module to apply/remove taints to/from nodes.

ISSUE TYPE


New Module Pull Request

COMPONENT NAME

k8s_taint

Reviewed-by: Mike Graves <mgraves@redhat.com>
Reviewed-by: Alina Buzachis <None>
Reviewed-by: None <None>
Reviewed-by: None <None>
This commit is contained in:
Alina Buzachis
2021-12-09 19:16:55 +01:00
committed by GitHub
parent 79699ba429
commit e77c8f1449
8 changed files with 807 additions and 5 deletions

View File

@@ -10,3 +10,7 @@
- name: Include drain.yml
include_tasks:
file: tasks/drain.yml
- name: Include taint.yml
include_tasks:
file: tasks/taint.yml

View File

@@ -0,0 +1,454 @@
---
- block:
- set_fact:
namespace: "namespace-taint"
pod_name_1: "pod-1-taint"
taint_patch_1:
- effect: NoExecute
key: "key1"
value: "value1"
taint_patch_1_update:
- effect: NoExecute
key: "key1"
value: "value_updated"
taint_patch_2:
- effect: NoSchedule
key: "key2"
value: "value2"
- effect: NoExecute
key: "key2"
taint_patch_3:
- effect: NoSchedule
key: "key3"
- effect: NoExecute
key: "key1"
- name: List cluster nodes
kubernetes.core.k8s_info:
kind: node
register: _result
- name: Select a node to taint
set_fact:
node_to_taint: "{{ _result.resources[0].metadata.name }}"
- name: Create namespace
kubernetes.core.k8s:
kind: Namespace
name: "{{ namespace }}"
- name: Create Pod
kubernetes.core.k8s:
namespace: '{{ namespace }}'
wait: yes
definition:
apiVersion: v1
kind: Pod
metadata:
name: "{{ pod_name_1 }}"
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchFields:
- key: metadata.name
operator: In
values:
- '{{ node_to_taint }}'
containers:
- name: c0
image: busybox
command:
- /bin/sh
- -c
- while true; do date;sleep 5; done
terminationGracePeriodSeconds: 10
register: _result
- name: Assert that pod is running on the node
assert:
that:
- _result.result.status.phase == 'Running'
- _result.result.spec.nodeName == "{{ node_to_taint }}"
- name: Taint node (check_mode)
kubernetes.core.k8s_taint:
name: "{{ node_to_taint }}"
taints: "{{ taint_patch_1 }}"
check_mode: true
register: _result
- name: Assert that node has been tainted (check_mode)
assert:
that:
- _result.changed
- name: Taint node
kubernetes.core.k8s_taint:
name: "{{ node_to_taint }}"
taints: "{{ taint_patch_1 }}"
register: _result
- name: Assert that node has been tainted
assert:
that:
- _result.changed
- "{{ item['effect'] == taint_patch_1[0]['effect'] }}"
- "{{ item['key'] == taint_patch_1[0]['key'] }}"
loop: "{{ _result.result.spec.taints }}"
- name: Taint node (idempotency) - (check_mode)
kubernetes.core.k8s_taint:
name: "{{ node_to_taint }}"
taints: "{{ taint_patch_1 }}"
check_mode: true
register: _result
- name: Assert that node has been tainted (idempotency - no change) - (check_mode)
assert:
that:
- not _result.changed
- name: Taint node (idempotency)
kubernetes.core.k8s_taint:
name: "{{ node_to_taint }}"
taints: "{{ taint_patch_1 }}"
register: _result
- name: Assert that node has been tainted (idempotency - no change)
assert:
that:
- not _result.changed
- name: Pause for 30 seconds
pause:
seconds: 30
- name: Get Pods
kubernetes.core.k8s_info:
kind: Pod
namespace: "{{ namespace }}"
register: _result
- name: Assert that Pod has been evicted
assert:
that:
- _result.resources | list | length == 0
- name: Taint node with replace=true (check_mode)
kubernetes.core.k8s_taint:
name: "{{ node_to_taint }}"
taints: "{{ taint_patch_1}}"
replace: true
check_mode: true
register: _result
- name: Assert that node has been tainted (replace=true)
assert:
that:
- not _result.changed
- name: Taint node with replace=true
kubernetes.core.k8s_taint:
name: "{{ node_to_taint }}"
taints: "{{ taint_patch_1}}"
replace: true
register: _result
- name: Assert that node has been tainted (replace=true)
assert:
that:
- not _result.changed
- name: Taint again node with replace=true (check_mode)
kubernetes.core.k8s_taint:
name: "{{ node_to_taint }}"
taints: "{{ taint_patch_1 }}"
replace: true
check_mode: true
register: _result
- name: Assert that node has been tainted (replace=true) - (check_mode)
assert:
that:
- not _result.changed
- name: Taint again node with replace=true
kubernetes.core.k8s_taint:
name: "{{ node_to_taint }}"
taints: "{{ taint_patch_1 }}"
replace: true
register: _result
- name: Assert that node has been tainted (replace=true)
assert:
that:
- not _result.changed
- name: Update node taints
kubernetes.core.k8s_taint:
name: "{{ node_to_taint }}"
taints: "{{ taint_patch_1_update }}"
register: _result
- name: Update node taints
assert:
that:
- _result.changed
- all_taints | selectattr('key', 'equalto', search_key) | selectattr('effect', 'equalto', search_effect) | selectattr('value', 'equalto', search_value) | list | count > 0
vars:
search_key: "{{ item.key}}"
search_effect: "{{ item.effect }}"
search_value: "{{ item.value }}"
all_taints: "{{ taint_patch_1_update }}"
with_items: "{{ _result.result.spec.taints }}"
- name: Update node taints (idempotence)
kubernetes.core.k8s_taint:
name: "{{ node_to_taint }}"
taints: "{{ taint_patch_1_update }}"
register: _result
- name: Update node taints (idempotence)
assert:
that:
- not _result.changed
- name: Add other taints to node (check_mode)
kubernetes.core.k8s_taint:
name: "{{ node_to_taint }}"
taints: "{{ taint_patch_2 }}"
check_mode: true
register: _result
- name: Assert that other taints has been added (check_mode)
assert:
that:
- _result.changed
- name: Add other taints to node
kubernetes.core.k8s_taint:
name: "{{ node_to_taint }}"
taints: "{{ taint_patch_2 }}"
register: _result
- name: Assert that other taints has been added
assert:
that:
- _result.changed
- all_taints | selectattr('key', 'equalto', search_key) | selectattr('effect', 'equalto', search_effect) | list | count > 0
vars:
search_key: "{{ item.key}}"
search_effect: "{{ item.effect }}"
all_taints: "{{ taint_patch_1 }} + {{ taint_patch_2 }}"
with_items: "{{ _result.result.spec.taints }}"
- name: Remove taints from node (check_mode)
kubernetes.core.k8s_taint:
state: absent
name: "{{ node_to_taint }}"
taints: "{{ taint_patch_1 }}"
check_mode: true
register: _result
- name: Assert that taint has been removed (check_mode)
assert:
that:
- _result.changed
- name: Remove taint from node
kubernetes.core.k8s_taint:
state: absent
name: "{{ node_to_taint }}"
taints: "{{ taint_patch_1 }}"
register: _result
- name: Assert that taint has been removed
assert:
that:
- _result.changed
- name: Get node taints
kubernetes.core.k8s_info:
kind: node
name: "{{ node_to_taint }}"
register: _result
- name: Assert that taint has been removed
assert:
that:
- all_taints | selectattr('key', 'equalto', search_key) | selectattr('effect', 'equalto', search_effect) | list | count > 0
vars:
search_key: "{{ item.key}}"
search_effect: "{{ item.effect }}"
all_taints: "{{ taint_patch_2 }}"
with_items: "{{ _result.resources[0].spec.taints }}"
- name: Remove taint from node (idempotency)
kubernetes.core.k8s_taint:
state: absent
name: "{{ node_to_taint }}"
taints: "{{ taint_patch_1 }}"
register: _result
- name: Assert that taint has been removed (idempotency)
assert:
that:
- not _result.changed
- name: Remove nonexistent taint from node
kubernetes.core.k8s_taint:
state: absent
name: "{{ node_to_taint }}"
taints: "{{ taint_patch_3 }}"
register: _result
- name: Assert taint has been removed
assert:
that:
- not _result.changed
- name: Re-add taint to node
kubernetes.core.k8s_taint:
name: "{{ node_to_taint }}"
taints: "{{ taint_patch_1 }}"
register: _result
- name: Assert that taint has been added
assert:
that:
- _result.changed
- all_taints | selectattr('key', 'equalto', search_key) | selectattr('effect', 'equalto', search_effect) | list | count > 0
vars:
search_key: "{{ item.key}}"
search_effect: "{{ item.effect }}"
all_taints: "{{ taint_patch_1 }} + {{ taint_patch_2 }}"
with_items: "{{ _result.result.spec.taints }}"
- name: Add other taints and update
kubernetes.core.k8s_taint:
name: "{{ node_to_taint }}"
taints: "{{ taint_patch_3 }}"
register: _result
- name: Assert that taints have been added and updated
assert:
that:
- _result.changed
- all_taints | selectattr('key', 'equalto', search_key) | selectattr('effect', 'equalto', search_effect) | list | count > 0
vars:
search_key: "{{ item.key}}"
search_effect: "{{ item.effect }}"
all_taints: "{{ taint_patch_3 }} + {{ taint_patch_2 }}"
with_items: "{{ _result.result.spec.taints }}"
- name: Remove taint using key:effect
kubernetes.core.k8s_taint:
state: absent
name: "{{ node_to_taint }}"
taints:
- key: "key2"
effect: "NoSchedule"
register: _result
- name: Assert that taint using key:effect has been removed
assert:
that:
- _result.changed
- name: Get node taints
kubernetes.core.k8s_info:
kind: node
name: "{{ node_to_taint }}"
register: _result
- set_fact:
left_taint_patch_2: [ "{{ taint_patch_2[1] }}" ]
- name: Assert that taint using key:effect has been removed
assert:
that:
- all_taints | selectattr('key', 'equalto', search_key) | selectattr('effect', 'equalto', search_effect) | list | count > 0
vars:
search_key: "{{ item.key}}"
search_effect: "{{ item.effect }}"
all_taints: "{{ taint_patch_3 }} + {{ left_taint_patch_2 }}"
with_items: "{{ _result.resources[0].spec.taints }}"
- name: Remove taint using key
kubernetes.core.k8s_taint:
state: absent
name: "{{ node_to_taint }}"
taints:
- key: "key3"
register: _result
- name: Assert that taint using key has been removed
assert:
that:
- _result.changed
- set_fact:
left_taint_patch_3: [ "{{ taint_patch_3[1] }}" ]
- name: Get node taints
kubernetes.core.k8s_info:
kind: node
name: "{{ node_to_taint }}"
register: _result
- name: Assert that taint using key has been removed
assert:
that:
- all_taints | selectattr('key', 'equalto', search_key) | selectattr('effect', 'equalto', search_effect) | list | count > 0
vars:
search_key: "{{ item.key}}"
search_effect: "{{ item.effect }}"
all_taints: "{{ left_taint_patch_2 }} + {{ left_taint_patch_3 }}"
with_items: "{{ _result.resources[0].spec.taints }}"
- name: Remove taints (including non existing ones)
kubernetes.core.k8s_taint:
state: absent
name: "{{ node_to_taint }}"
taints:
- key: "key1"
- key: "key2"
- key: "key3"
- name: Get node taints
kubernetes.core.k8s_info:
kind: node
name: "{{ node_to_taint }}"
register: _result
- name: Assert that taints have been removed
assert:
that:
- _result.resources | selectattr('spec.taints', 'undefined')
always:
- name: Delete Pods
kubernetes.core.k8s:
state: absent
kind: Pod
name: "{{ pod_name_1 }}"
ignore_errors: true
- name: Delete namespace
kubernetes.core.k8s:
state: absent
kind: Namespace
name: "{{ namespace }}"
ignore_errors: true
- name: Remove taints
kubernetes.core.k8s_taint:
state: absent
name: "{{ node_to_taint }}"
taints:
- key: "key1"
- key: "key2"
- key: "key3"
ignore_errors: true

View File

@@ -0,0 +1,310 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Alina Buzachis <@alinabuzachis>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = r"""
module: k8s_taint
short_description: Taint a node in a Kubernetes/OpenShift cluster
version_added: "2.3.0"
author: Alina Buzachis (@alinabuzachis)
description:
- Taint allows a node to refuse Pod to be scheduled unless that Pod has a matching toleration.
- Untaint will remove taints from nodes as needed.
extends_documentation_fragment:
- kubernetes.core.k8s_auth_options
options:
state:
description:
- Determines whether to add or remove taints.
type: str
default: present
choices: [ present, absent ]
name:
description:
- The name of the node.
required: true
type: str
taints:
description:
- List containing the taints.
type: list
required: true
elements: dict
suboptions:
key:
description:
- The taint key to be applied to a node.
type: str
value:
description:
- The taint value corresponding to the taint key.
type: str
effect:
description:
- The effect of the taint on Pods that do not tolerate the taint.
- Required when I(state=present).
type: str
choices: [ NoSchedule, NoExecute, PreferNoSchedule ]
replace:
description:
- If C(true), allow taints to be replaced.
required: false
default: false
type: bool
requirements:
- python >= 3.6
- kubernetes >= 12.0.0
"""
EXAMPLES = r"""
- name: Taint node "foo"
kubernetes.core.k8s_taint:
state: present
name: foo
taints:
- effect: NoExecute
key: "key1"
- name: Taint node "foo"
kubernetes.core.k8s_taint:
state: present
name: foo
taints:
- effect: NoExecute
key: "key1"
value: "value1"
- effect: NoSchedule
key: "key1"
value: "value1"
- name: Remove taint from "foo".
kubernetes.core.k8s_taint:
state: absent
name: foo
taints:
- effect: NoExecute
key: "key1"
value: "value1"
"""
RETURN = r"""
result:
description:
- The tainted Node object. Will be empty in the case of a deletion.
returned: success
type: complex
contains:
api_version:
description: The versioned schema of this representation of an object.
returned: success
type: str
kind:
description: Represents the REST resource this object represents.
returned: success
type: str
metadata:
description: Standard object metadata. Includes name, namespace, annotations, labels, etc.
returned: success
type: complex
spec:
description: Specific attributes of the object. Will vary based on the I(api_version) and I(kind).
returned: success
type: complex
status:
description: Current status details for the object.
returned: success
type: complex
"""
import copy
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils._text import to_native
from ansible_collections.kubernetes.core.plugins.module_utils.common import (
K8sAnsibleMixin,
get_api_client,
)
from ansible_collections.kubernetes.core.plugins.module_utils.args_common import (
AUTH_ARG_SPEC,
)
try:
from kubernetes.client.api import core_v1_api
from kubernetes.dynamic.exceptions import ApiException
except ImportError:
# ImportError are managed by the common module already.
pass
def _equal_dicts(a, b):
keys = ["key", "effect"]
if "effect" not in set(a).intersection(b):
keys.remove("effect")
return all((a[x] == b[x] for x in keys))
def _get_difference(a, b):
return [
a_item for a_item in a if not any(_equal_dicts(a_item, b_item) for b_item in b)
]
def _get_intersection(a, b):
return [a_item for a_item in a if any(_equal_dicts(a_item, b_item) for b_item in b)]
def _update_exists(a, b):
return any(
(
any(
_equal_dicts(a_item, b_item)
and a_item.get("value") != b_item.get("value")
for b_item in b
)
for a_item in a
)
)
def argspec():
argument_spec = copy.deepcopy(AUTH_ARG_SPEC)
argument_spec.update(
dict(
state=dict(type="str", choices=["present", "absent"], default="present"),
name=dict(type="str", required=True),
taints=dict(type="list", required=True, elements="dict"),
replace=dict(type="bool", default=False),
)
)
return argument_spec
class K8sTaintAnsible:
def __init__(self, module):
self.module = module
self.k8s_ansible_mixin = K8sAnsibleMixin(module=self.module)
self.k8s_ansible_mixin.client = get_api_client(module=self.module)
self.k8s_ansible_mixin.module = self.module
self.k8s_ansible_mixin.argspec = self.module.argument_spec
self.k8s_ansible_mixin.check_mode = self.module.check_mode
self.k8s_ansible_mixin.params = self.module.params
self.k8s_ansible_mixin.fail_json = self.module.fail_json
self.k8s_ansible_mixin.fail = self.module.fail_json
self.k8s_ansible_mixin.exit_json = self.module.exit_json
self.k8s_ansible_mixin.warn = self.module.warn
self.k8s_ansible_mixin.warnings = []
self.api_instance = core_v1_api.CoreV1Api(self.k8s_ansible_mixin.client.client)
self.k8s_ansible_mixin.check_library_version()
self.changed = False
def get_node(self, name):
try:
node = self.api_instance.read_node(name=name)
except ApiException as exc:
if exc.reason == "Not Found":
self.module.fail_json(msg="Node '{0}' has not been found.".format(name))
self.module.fail_json(
msg="Failed to retrieve node '{0}' due to: {1}".format(
name, exc.reason
),
status=exc.status,
)
except Exception as exc:
self.module.fail_json(
msg="Failed to retrieve node '{0}' due to: {1}".format(
name, to_native(exc)
)
)
return node
def patch_node(self, taints):
body = {"spec": {"taints": taints}}
try:
result = self.api_instance.patch_node(
name=self.module.params.get("name"), body=body
)
except Exception as exc:
self.module.fail_json(
msg="Failed to patch node due to: {0}".format(to_native(exc))
)
return result.to_dict()
def execute_module(self):
result = {"result": {}}
state = self.module.params.get("state")
taints = self.module.params.get("taints")
name = self.module.params.get("name")
node = self.get_node(name)
existing_taints = node.spec.to_dict().get("taints") or []
diff = _get_difference(taints, existing_taints)
if state == "present":
if diff:
# There are new taints to be added
self.changed = True
if self.module.check_mode:
self.module.exit_json(changed=self.changed, **result)
if self.module.params.get("replace"):
# Patch with the new taints
result["result"] = self.patch_node(taints=taints)
self.module.exit_json(changed=self.changed, **result)
result["result"] = self.patch_node(
taints=[*_get_difference(existing_taints, taints), *taints]
)
else:
# No new taints to be added, but maybe there is something to be updated
if _update_exists(existing_taints, taints):
self.changed = True
if self.module.check_mode:
self.module.exit_json(changed=self.changed, **result)
result["result"] = self.patch_node(
taints=[*_get_difference(existing_taints, taints), *taints]
)
else:
result["result"] = node.to_dict()
elif state == "absent":
# Nothing to be removed
if not existing_taints:
result["result"] = node.to_dict()
if not diff:
self.changed = True
if self.module.check_mode:
self.module.exit_json(changed=self.changed, **result)
self.patch_node(taints=_get_difference(existing_taints, taints))
else:
if _get_intersection(existing_taints, taints):
self.changed = True
if self.module.check_mode:
self.module.exit_json(changed=self.changed, **result)
self.patch_node(taints=_get_difference(existing_taints, taints))
else:
self.module.exit_json(changed=self.changed, **result)
self.module.exit_json(changed=self.changed, **result)
def main():
module = AnsibleModule(argument_spec=argspec(), supports_check_mode=True,)
k8s_taint = K8sTaintAnsible(module)
k8s_taint.execute_module()
if __name__ == "__main__":
main()

View File

@@ -249,4 +249,11 @@ plugins/lookup/kustomize.py future-import-boilerplate!skip
plugins/lookup/kustomize.py metaclass-boilerplate!skip
molecule/default/roles/helm/library/helm_test_version.py metaclass-boilerplate!skip
molecule/default/roles/helm/library/helm_test_version.py future-import-boilerplate!skip
molecule/default/roles/helm/library/helm_test_version.py shebang
molecule/default/roles/helm/library/helm_test_version.py shebang
plugins/modules/k8s_taint.py compile-2.7!skip
plugins/modules/k8s_taint.py compile-3.5!skip
plugins/modules/k8s_taint.py future-import-boilerplate!skip
plugins/modules/k8s_taint.py import-2.7!skip
plugins/modules/k8s_taint.py import-3.5!skip
plugins/modules/k8s_taint.py metaclass-boilerplate!skip
plugins/modules/k8s_taint.py validate-modules:return-syntax-error

View File

@@ -249,4 +249,11 @@ plugins/lookup/kustomize.py future-import-boilerplate!skip
plugins/lookup/kustomize.py metaclass-boilerplate!skip
molecule/default/roles/helm/library/helm_test_version.py metaclass-boilerplate!skip
molecule/default/roles/helm/library/helm_test_version.py future-import-boilerplate!skip
molecule/default/roles/helm/library/helm_test_version.py shebang
molecule/default/roles/helm/library/helm_test_version.py shebang
plugins/modules/k8s_taint.py compile-2.7!skip
plugins/modules/k8s_taint.py compile-3.5!skip
plugins/modules/k8s_taint.py future-import-boilerplate!skip
plugins/modules/k8s_taint.py import-2.7!skip
plugins/modules/k8s_taint.py import-3.5!skip
plugins/modules/k8s_taint.py metaclass-boilerplate!skip
plugins/modules/k8s_taint.py validate-modules:return-syntax-error

View File

@@ -241,4 +241,11 @@ plugins/modules/k8s_cp.py import-2.6!skip
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
molecule/default/roles/helm/library/helm_test_version.py shebang
molecule/default/roles/helm/library/helm_test_version.py shebang
plugins/modules/k8s_taint.py compile-2.7!skip
plugins/modules/k8s_taint.py compile-3.5!skip
plugins/modules/k8s_taint.py future-import-boilerplate!skip
plugins/modules/k8s_taint.py import-2.7!skip
plugins/modules/k8s_taint.py import-3.5!skip
plugins/modules/k8s_taint.py metaclass-boilerplate!skip
plugins/modules/k8s_taint.py validate-modules:return-syntax-error

View File

@@ -187,4 +187,11 @@ plugins/modules/k8s_cp.py compile-2.7!skip
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
molecule/default/roles/helm/library/helm_test_version.py shebang
molecule/default/roles/helm/library/helm_test_version.py shebang
plugins/modules/k8s_taint.py compile-2.7!skip
plugins/modules/k8s_taint.py compile-3.5!skip
plugins/modules/k8s_taint.py future-import-boilerplate!skip
plugins/modules/k8s_taint.py import-2.7!skip
plugins/modules/k8s_taint.py import-3.5!skip
plugins/modules/k8s_taint.py metaclass-boilerplate!skip
plugins/modules/k8s_taint.py validate-modules:return-syntax-error

View File

@@ -243,4 +243,10 @@ plugins/lookup/kustomize.py future-import-boilerplate!skip
plugins/lookup/kustomize.py metaclass-boilerplate!skip
molecule/default/roles/helm/library/helm_test_version.py metaclass-boilerplate!skip
molecule/default/roles/helm/library/helm_test_version.py future-import-boilerplate!skip
molecule/default/roles/helm/library/helm_test_version.py shebang
molecule/default/roles/helm/library/helm_test_version.py shebang
plugins/modules/k8s_taint.py compile-2.7!skip
plugins/modules/k8s_taint.py compile-3.5!skip
plugins/modules/k8s_taint.py future-import-boilerplate!skip
plugins/modules/k8s_taint.py import-2.7!skip
plugins/modules/k8s_taint.py import-3.5!skip
plugins/modules/k8s_taint.py metaclass-boilerplate!skip