mirror of
https://github.com/ansible-collections/kubernetes.core.git
synced 2026-03-26 21:33:02 +00:00
Port changes from main to refactored branch Depends-on: ansible/ansible-zuul-jobs#1563 SUMMARY This PR contains several commits that complete the rebase of the 2.x-refactor branch onto main. Most of the changes here had to be manually backported after rebasing as the original changes were to code that will be deprecated. In addition, rather than trying to manually sort out conflicts and changes to the sanity ignores, I rewrote the refresh_ignore_files script to fully automate the management of ignore files. Previously, these files were both manually edited and auto-generated. This should no longer be the case, and these files should never be manually edited going forward. For the purposes of reviewing and history, I kept all changes in separate commits tied to the original commit being backported. ISSUE TYPE COMPONENT NAME ADDITIONAL INFORMATION Reviewed-by: Jill R <None>
420 lines
13 KiB
Python
420 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
|
|
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,
|
|
RESOURCE_ARG_SPEC,
|
|
NAME_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.service import (
|
|
diff_objects,
|
|
)
|
|
from ansible_collections.kubernetes.core.plugins.module_utils.k8s.waiter import (
|
|
get_waiter,
|
|
)
|
|
from ansible_collections.kubernetes.core.plugins.module_utils.k8s.resource import (
|
|
create_definitions,
|
|
)
|
|
|
|
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 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"] = 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,
|
|
)
|
|
|
|
client = get_api_client(module=module)
|
|
execute_module(client, module)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|