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>
422 lines
13 KiB
Python
422 lines
13 KiB
Python
#!/usr/bin/python
|
|
# -*- coding: utf-8 -*-
|
|
|
|
# (c) 2018, Chris Houseknecht <@chouseknecht>
|
|
# 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_scale
|
|
|
|
short_description: Set a new size for a Deployment, ReplicaSet, Replication Controller, or Job.
|
|
|
|
author:
|
|
- "Chris Houseknecht (@chouseknecht)"
|
|
- "Fabian von Feilitzsch (@fabianvf)"
|
|
|
|
description:
|
|
- Similar to the kubectl scale command. Use to set the number of replicas for a Deployment, ReplicaSet,
|
|
or Replication Controller, or the parallelism attribute of a Job. Supports check mode.
|
|
- C(wait) parameter is not supported for Jobs.
|
|
|
|
extends_documentation_fragment:
|
|
- kubernetes.core.k8s_name_options
|
|
- kubernetes.core.k8s_auth_options
|
|
- kubernetes.core.k8s_resource_options
|
|
- kubernetes.core.k8s_scale_options
|
|
|
|
options:
|
|
label_selectors:
|
|
description: List of label selectors to use to filter results.
|
|
type: list
|
|
elements: str
|
|
default: []
|
|
version_added: 2.0.0
|
|
continue_on_error:
|
|
description:
|
|
- Whether to continue on errors when multiple resources are defined.
|
|
type: bool
|
|
default: False
|
|
version_added: 2.0.0
|
|
|
|
requirements:
|
|
- "python >= 3.6"
|
|
- "kubernetes >= 12.0.0"
|
|
- "PyYAML >= 3.11"
|
|
"""
|
|
|
|
EXAMPLES = r"""
|
|
- name: Scale deployment up, and extend timeout
|
|
kubernetes.core.k8s_scale:
|
|
api_version: v1
|
|
kind: Deployment
|
|
name: elastic
|
|
namespace: myproject
|
|
replicas: 3
|
|
wait_timeout: 60
|
|
|
|
- name: Scale deployment down when current replicas match
|
|
kubernetes.core.k8s_scale:
|
|
api_version: v1
|
|
kind: Deployment
|
|
name: elastic
|
|
namespace: myproject
|
|
current_replicas: 3
|
|
replicas: 2
|
|
|
|
- name: Increase job parallelism
|
|
kubernetes.core.k8s_scale:
|
|
api_version: batch/v1
|
|
kind: job
|
|
name: pi-with-timeout
|
|
namespace: testing
|
|
replicas: 2
|
|
|
|
# Match object using local file or inline definition
|
|
|
|
- name: Scale deployment based on a file from the local filesystem
|
|
kubernetes.core.k8s_scale:
|
|
src: /myproject/elastic_deployment.yml
|
|
replicas: 3
|
|
wait: no
|
|
|
|
- name: Scale deployment based on a template output
|
|
kubernetes.core.k8s_scale:
|
|
resource_definition: "{{ lookup('template', '/myproject/elastic_deployment.yml') | from_yaml }}"
|
|
replicas: 3
|
|
wait: no
|
|
|
|
- name: Scale deployment based on a file from the Ansible controller filesystem
|
|
kubernetes.core.k8s_scale:
|
|
resource_definition: "{{ lookup('file', '/myproject/elastic_deployment.yml') | from_yaml }}"
|
|
replicas: 3
|
|
wait: no
|
|
|
|
- name: Scale deployment using label selectors (continue operation in case error occured on one resource)
|
|
kubernetes.core.k8s_scale:
|
|
replicas: 3
|
|
kind: Deployment
|
|
namespace: test
|
|
label_selectors:
|
|
- app=test
|
|
continue_on_error: true
|
|
"""
|
|
|
|
RETURN = r"""
|
|
result:
|
|
description:
|
|
- If a change was made, will return the patched object, otherwise returns the existing object.
|
|
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
|
|
duration:
|
|
description: elapsed time of task in seconds
|
|
returned: when C(wait) is true
|
|
type: int
|
|
sample: 48
|
|
"""
|
|
|
|
import copy
|
|
|
|
try:
|
|
from kubernetes.dynamic.exceptions import NotFoundError
|
|
except ImportError:
|
|
# Handled in module setup
|
|
pass
|
|
|
|
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,
|
|
NAME_ARG_SPEC,
|
|
RESOURCE_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,
|
|
ResourceTimeout,
|
|
)
|
|
from ansible_collections.kubernetes.core.plugins.module_utils.k8s.resource import (
|
|
create_definitions,
|
|
)
|
|
from ansible_collections.kubernetes.core.plugins.module_utils.k8s.service import (
|
|
diff_objects,
|
|
)
|
|
from ansible_collections.kubernetes.core.plugins.module_utils.k8s.waiter import (
|
|
get_waiter,
|
|
)
|
|
|
|
SCALE_ARG_SPEC = {
|
|
"replicas": {"type": "int", "required": True},
|
|
"current_replicas": {"type": "int"},
|
|
"resource_version": {},
|
|
"wait": {"type": "bool", "default": True},
|
|
"wait_timeout": {"type": "int", "default": 20},
|
|
"wait_sleep": {"type": "int", "default": 5},
|
|
}
|
|
|
|
|
|
def execute_module(client, module):
|
|
current_replicas = module.params.get("current_replicas")
|
|
replicas = module.params.get("replicas")
|
|
resource_version = module.params.get("resource_version")
|
|
definitions = create_definitions(module.params)
|
|
definition = definitions[0]
|
|
name = definition["metadata"].get("name")
|
|
namespace = definition["metadata"].get("namespace")
|
|
api_version = definition["apiVersion"]
|
|
kind = definition["kind"]
|
|
label_selectors = module.params.get("label_selectors")
|
|
if not label_selectors:
|
|
label_selectors = []
|
|
continue_on_error = module.params.get("continue_on_error")
|
|
wait = module.params.get("wait")
|
|
wait_time = module.params.get("wait_timeout")
|
|
wait_sleep = module.params.get("wait_sleep")
|
|
existing = None
|
|
existing_count = None
|
|
return_attributes = dict(result=dict())
|
|
if module._diff:
|
|
return_attributes["diff"] = dict()
|
|
if wait:
|
|
return_attributes["duration"] = 0
|
|
|
|
resource = client.resource(kind, api_version)
|
|
multiple_scale = False
|
|
try:
|
|
existing = resource.get(
|
|
name=name, namespace=namespace, label_selector=",".join(label_selectors)
|
|
)
|
|
if existing.kind.endswith("List"):
|
|
existing_items = existing.items
|
|
multiple_scale = len(existing_items) > 1
|
|
else:
|
|
existing_items = [existing]
|
|
except NotFoundError as e:
|
|
reason = e.body if hasattr(e, "body") else e
|
|
msg = "Failed to retrieve requested object: {0}".format(reason)
|
|
raise CoreException(msg) from e
|
|
|
|
if multiple_scale:
|
|
# when scaling multiple resource, the 'result' is changed to 'results' and is a list
|
|
return_attributes = {"results": []}
|
|
changed = False
|
|
|
|
def _continue_or_fail(error):
|
|
if multiple_scale and continue_on_error:
|
|
if "errors" not in return_attributes:
|
|
return_attributes["errors"] = []
|
|
return_attributes["errors"].append({"error": error, "failed": True})
|
|
else:
|
|
module.fail_json(msg=error, **return_attributes)
|
|
|
|
def _continue_or_exit(warn):
|
|
if multiple_scale:
|
|
return_attributes["results"].append({"warning": warn, "changed": False})
|
|
else:
|
|
module.exit_json(warning=warn, **return_attributes)
|
|
|
|
for existing in existing_items:
|
|
if kind.lower() == "job":
|
|
existing_count = existing.spec.parallelism
|
|
elif hasattr(existing.spec, "replicas"):
|
|
existing_count = existing.spec.replicas
|
|
|
|
if existing_count is None:
|
|
error = "Failed to retrieve the available count for object kind={0} name={1} namespace={2}.".format(
|
|
existing.kind, existing.metadata.name, existing.metadata.namespace
|
|
)
|
|
_continue_or_fail(error)
|
|
continue
|
|
|
|
if resource_version and resource_version != existing.metadata.resourceVersion:
|
|
warn = "expected resource version {0} does not match with actual {1} for object kind={2} name={3} namespace={4}.".format(
|
|
resource_version,
|
|
existing.metadata.resourceVersion,
|
|
existing.kind,
|
|
existing.metadata.name,
|
|
existing.metadata.namespace,
|
|
)
|
|
_continue_or_exit(warn)
|
|
continue
|
|
|
|
if current_replicas is not None and existing_count != current_replicas:
|
|
warn = "current replicas {0} does not match with actual {1} for object kind={2} name={3} namespace={4}.".format(
|
|
current_replicas,
|
|
existing_count,
|
|
existing.kind,
|
|
existing.metadata.name,
|
|
existing.metadata.namespace,
|
|
)
|
|
_continue_or_exit(warn)
|
|
continue
|
|
|
|
if existing_count != replicas:
|
|
if kind.lower() == "job":
|
|
existing.spec.parallelism = replicas
|
|
result = {"changed": True}
|
|
if module.check_mode:
|
|
result["result"] = existing.to_dict()
|
|
else:
|
|
result["result"] = client.patch(
|
|
resource, existing.to_dict()
|
|
).to_dict()
|
|
else:
|
|
try:
|
|
result = scale(
|
|
client,
|
|
module,
|
|
resource,
|
|
existing,
|
|
replicas,
|
|
wait,
|
|
wait_time,
|
|
wait_sleep,
|
|
)
|
|
except CoreException as e:
|
|
module.fail_json(msg=to_native(e))
|
|
changed = changed or result["changed"]
|
|
else:
|
|
name = existing.metadata.name
|
|
namespace = existing.metadata.namespace
|
|
existing = client.get(resource, name=name, namespace=namespace)
|
|
result = {"changed": False, "result": existing.to_dict()}
|
|
if module._diff:
|
|
result["diff"] = {}
|
|
if wait:
|
|
result["duration"] = 0
|
|
# append result to the return attribute
|
|
if multiple_scale:
|
|
return_attributes["results"].append(result)
|
|
else:
|
|
module.exit_json(**result)
|
|
|
|
module.exit_json(changed=changed, **return_attributes)
|
|
|
|
|
|
def argspec():
|
|
args = copy.deepcopy(SCALE_ARG_SPEC)
|
|
args.update(RESOURCE_ARG_SPEC)
|
|
args.update(NAME_ARG_SPEC)
|
|
args.update(AUTH_ARG_SPEC)
|
|
args.update({"label_selectors": {"type": "list", "elements": "str", "default": []}})
|
|
args.update(({"continue_on_error": {"type": "bool", "default": False}}))
|
|
return args
|
|
|
|
|
|
def scale(
|
|
client,
|
|
module,
|
|
resource,
|
|
existing_object,
|
|
replicas,
|
|
wait,
|
|
wait_time,
|
|
wait_sleep,
|
|
):
|
|
name = existing_object.metadata.name
|
|
namespace = existing_object.metadata.namespace
|
|
kind = existing_object.kind
|
|
|
|
if not hasattr(resource, "scale"):
|
|
raise CoreException(
|
|
"Cannot perform scale on resource of kind {0}".format(resource.kind)
|
|
)
|
|
|
|
scale_obj = {
|
|
"kind": kind,
|
|
"metadata": {"name": name, "namespace": namespace},
|
|
"spec": {"replicas": replicas},
|
|
}
|
|
|
|
existing = client.get(resource, name=name, namespace=namespace)
|
|
|
|
result = dict()
|
|
if module.check_mode:
|
|
k8s_obj = copy.deepcopy(existing.to_dict())
|
|
k8s_obj["spec"]["replicas"] = replicas
|
|
if wait:
|
|
result["duration"] = 0
|
|
result["result"] = k8s_obj
|
|
else:
|
|
try:
|
|
resource.scale.patch(body=scale_obj)
|
|
except Exception as e:
|
|
reason = e.body if hasattr(e, "body") else e
|
|
msg = "Scale request failed: {0}".format(reason)
|
|
raise CoreException(msg) from e
|
|
|
|
k8s_obj = client.get(resource, name=name, namespace=namespace).to_dict()
|
|
result["result"] = k8s_obj
|
|
if wait:
|
|
waiter = get_waiter(client, resource)
|
|
success, result["result"], result["duration"] = waiter.wait(
|
|
timeout=wait_time,
|
|
sleep=wait_sleep,
|
|
name=name,
|
|
namespace=namespace,
|
|
)
|
|
if not success:
|
|
raise ResourceTimeout("Resource scaling timed out", result)
|
|
|
|
match, diffs = diff_objects(existing.to_dict(), result["result"])
|
|
result["changed"] = not match
|
|
if module._diff:
|
|
result["diff"] = diffs
|
|
|
|
return result
|
|
|
|
|
|
def main():
|
|
mutually_exclusive = [
|
|
("resource_definition", "src"),
|
|
]
|
|
module = AnsibleK8SModule(
|
|
module_class=AnsibleModule,
|
|
argument_spec=argspec(),
|
|
mutually_exclusive=mutually_exclusive,
|
|
supports_check_mode=True,
|
|
)
|
|
|
|
try:
|
|
client = get_api_client(module=module)
|
|
execute_module(client, module)
|
|
except CoreException as e:
|
|
module.fail_from_exception(e)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|