Files
kubernetes.core/plugins/modules/k8s_scale.py
Mike Graves 7c71436f3b Upgrade black version (#424)
Upgrade black version

SUMMARY

Move off of beta version of black and pin to current calendar year
version.
The only manual changes here are to tox.ini. Everything else is from running the new version of black.

ISSUE TYPE

COMPONENT NAME

ADDITIONAL INFORMATION

Reviewed-by: Abhijeet Kasurde <None>
2022-03-30 15:02:46 +00:00

401 lines
12 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
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
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,
RESOURCE_ARG_SPEC,
NAME_ARG_SPEC,
)
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(
module,
k8s_ansible_mixin,
):
k8s_ansible_mixin.set_resource_definitions(module)
definition = k8s_ansible_mixin.resource_definitions[0]
name = definition["metadata"]["name"]
namespace = definition["metadata"].get("namespace")
api_version = definition["apiVersion"]
kind = definition["kind"]
current_replicas = module.params.get("current_replicas")
replicas = module.params.get("replicas")
resource_version = module.params.get("resource_version")
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 = k8s_ansible_mixin.find_resource(kind, api_version, fail=True)
from ansible_collections.kubernetes.core.plugins.module_utils.common import (
NotFoundError,
)
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 exc:
module.fail_json(
msg="Failed to retrieve requested object: {0}".format(exc),
error=exc.value.get("status"),
)
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 module.params["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 module.params["kind"].lower() == "job":
existing.spec.parallelism = replicas
result = {"changed": True}
if module.check_mode:
result["result"] = existing.to_dict()
else:
result["result"] = resource.patch(existing.to_dict()).to_dict()
else:
result = scale(
module,
k8s_ansible_mixin,
resource,
existing,
replicas,
wait,
wait_time,
wait_sleep,
)
changed = changed or result["changed"]
else:
name = existing.metadata.name
namespace = existing.metadata.namespace
existing = resource.get(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(
module,
k8s_ansible_mixin,
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"):
module.fail_json(
msg="Cannot perform scale on resource of kind {0}".format(resource.kind)
)
scale_obj = {
"kind": kind,
"metadata": {"name": name, "namespace": namespace},
"spec": {"replicas": replicas},
}
existing = resource.get(name=name, namespace=namespace)
result = dict()
if module.check_mode:
k8s_obj = copy.deepcopy(existing.to_dict())
k8s_obj["spec"]["replicas"] = replicas
match, diffs = k8s_ansible_mixin.diff_objects(existing.to_dict(), k8s_obj)
if wait:
result["duration"] = 0
result["result"] = k8s_obj
else:
try:
resource.scale.patch(body=scale_obj)
except Exception as exc:
module.fail_json(msg="Scale request failed: {0}".format(exc))
k8s_obj = resource.get(name=name, namespace=namespace).to_dict()
result["result"] = k8s_obj
if wait and not module.check_mode:
success, result["result"], result["duration"] = k8s_ansible_mixin.wait(
resource, scale_obj, wait_sleep, wait_time
)
if not success:
module.fail_json(msg="Resource scaling timed out", **result)
match, diffs = k8s_ansible_mixin.diff_objects(existing.to_dict(), k8s_obj)
result["changed"] = not match
if module._diff:
result["diff"] = diffs
return result
def main():
mutually_exclusive = [
("resource_definition", "src"),
]
module = AnsibleModule(
argument_spec=argspec(),
mutually_exclusive=mutually_exclusive,
supports_check_mode=True,
)
from ansible_collections.kubernetes.core.plugins.module_utils.common import (
K8sAnsibleMixin,
get_api_client,
)
k8s_ansible_mixin = K8sAnsibleMixin(module)
k8s_ansible_mixin.client = get_api_client(module=module)
execute_module(module, k8s_ansible_mixin)
if __name__ == "__main__":
main()