mirror of
https://github.com/ansible-collections/kubernetes.core.git
synced 2026-03-26 21:33:02 +00:00
* Cleanup gha * test by removing matrix excludes * Rename sanity tests * trigger integration tests * Fix ansible-lint workflow * Fix concurrency * Add ansible-lint config * Add ansible-lint config * Fix integration and lint issues * integration wf * fix yamllint issues * fix yamllint issues * update readme and add ignore-2.16.txt * fix ansible-doc * Add version * Use /dev/random to generate random data The GHA environment has difficultly generating entropy. Trying to read from /dev/urandom just blocks forever. We don't care if the random data is cryptographically secure; it's just garbage data for the test. Read from /dev/random, instead. This is only used during the k8s_copy test target. This also removes the custom test module that was being used to generate the files. It's not worth maintaining this for two task that can be replaced with some simple command/shell tasks. * Fix saniry errors * test github_action fix * Address review comments * Remove default types * review comments * isort fixes * remove tags * Add setuptools to venv * Test gh changes * update changelog * update ignore-2.16 * Fix indentation in inventory plugin example * Update .github/workflows/integration-tests.yaml * Update integration-tests.yaml --------- Co-authored-by: Mike Graves <mgraves@redhat.com> Co-authored-by: Bikouo Aubin <79859644+abikouo@users.noreply.github.com>
313 lines
9.3 KiB
Python
313 lines
9.3 KiB
Python
#!/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._text import to_native
|
|
from ansible_collections.kubernetes.core.plugins.module_utils.ansiblemodule import (
|
|
AnsibleModule,
|
|
)
|
|
from ansible_collections.kubernetes.core.plugins.module_utils.args_common import (
|
|
AUTH_ARG_SPEC,
|
|
)
|
|
from ansible_collections.kubernetes.core.plugins.module_utils.k8s.client import (
|
|
get_api_client,
|
|
)
|
|
from ansible_collections.kubernetes.core.plugins.module_utils.k8s.core import (
|
|
AnsibleK8SModule,
|
|
)
|
|
from ansible_collections.kubernetes.core.plugins.module_utils.k8s.exceptions import (
|
|
CoreException,
|
|
)
|
|
|
|
try:
|
|
from kubernetes.client.api import core_v1_api
|
|
from kubernetes.client.exceptions import ApiException
|
|
except ImportError:
|
|
# ImportErrors are handled during module setup
|
|
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, client):
|
|
self.module = module
|
|
self.api_instance = core_v1_api.CoreV1Api(client.client)
|
|
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 = AnsibleK8SModule(
|
|
module_class=AnsibleModule,
|
|
argument_spec=argspec(),
|
|
supports_check_mode=True,
|
|
)
|
|
try:
|
|
client = get_api_client(module)
|
|
k8s_taint = K8sTaintAnsible(module, client.client)
|
|
k8s_taint.execute_module()
|
|
except CoreException as e:
|
|
module.fail_from_exception(e)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|