1#
2#  Copyright 2018 Red Hat | Ansible
3#
4# This file is part of Ansible
5#
6# Ansible is free software: you can redistribute it and/or modify
7# it under the terms of the GNU General Public License as published by
8# the Free Software Foundation, either version 3 of the License, or
9# (at your option) any later version.
10#
11# Ansible is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14# GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License
17# along with Ansible.  If not, see <http://www.gnu.org/licenses/>.
18
19from __future__ import absolute_import, division, print_function
20__metaclass__ = type
21
22import copy
23
24from ansible.module_utils.basic import AnsibleModule
25from ansible_collections.community.kubernetes.plugins.module_utils.common import (
26    AUTH_ARG_SPEC, RESOURCE_ARG_SPEC, NAME_ARG_SPEC, K8sAnsibleMixin)
27
28try:
29    from openshift.dynamic.exceptions import NotFoundError
30except ImportError:
31    pass
32
33
34SCALE_ARG_SPEC = {
35    'replicas': {'type': 'int', 'required': True},
36    'current_replicas': {'type': 'int'},
37    'resource_version': {},
38    'wait': {'type': 'bool', 'default': True},
39    'wait_timeout': {'type': 'int', 'default': 20},
40}
41
42
43class KubernetesAnsibleScaleModule(K8sAnsibleMixin):
44
45    def __init__(self, k8s_kind=None, *args, **kwargs):
46        self.client = None
47        self.warnings = []
48
49        mutually_exclusive = [
50            ('resource_definition', 'src'),
51        ]
52
53        module = AnsibleModule(
54            argument_spec=self.argspec,
55            mutually_exclusive=mutually_exclusive,
56            supports_check_mode=True,
57        )
58
59        self.module = module
60        self.params = self.module.params
61        self.check_mode = self.module.check_mode
62        self.fail_json = self.module.fail_json
63        self.fail = self.module.fail_json
64        self.exit_json = self.module.exit_json
65        super(KubernetesAnsibleScaleModule, self).__init__()
66
67        self.kind = k8s_kind or self.params.get('kind')
68        self.api_version = self.params.get('api_version')
69        self.name = self.params.get('name')
70        self.namespace = self.params.get('namespace')
71        self.set_resource_definitions()
72
73    def execute_module(self):
74        definition = self.resource_definitions[0]
75
76        self.client = self.get_api_client()
77
78        name = definition['metadata']['name']
79        namespace = definition['metadata'].get('namespace')
80        api_version = definition['apiVersion']
81        kind = definition['kind']
82        current_replicas = self.params.get('current_replicas')
83        replicas = self.params.get('replicas')
84        resource_version = self.params.get('resource_version')
85
86        wait = self.params.get('wait')
87        wait_time = self.params.get('wait_timeout')
88        existing = None
89        existing_count = None
90        return_attributes = dict(changed=False, result=dict(), diff=dict())
91        if wait:
92            return_attributes['duration'] = 0
93
94        resource = self.find_resource(kind, api_version, fail=True)
95
96        try:
97            existing = resource.get(name=name, namespace=namespace)
98            return_attributes['result'] = existing.to_dict()
99        except NotFoundError as exc:
100            self.fail_json(msg='Failed to retrieve requested object: {0}'.format(exc),
101                           error=exc.value.get('status'))
102
103        if self.kind == 'job':
104            existing_count = existing.spec.parallelism
105        elif hasattr(existing.spec, 'replicas'):
106            existing_count = existing.spec.replicas
107
108        if existing_count is None:
109            self.fail_json(msg='Failed to retrieve the available count for the requested object.')
110
111        if resource_version and resource_version != existing.metadata.resourceVersion:
112            self.exit_json(**return_attributes)
113
114        if current_replicas is not None and existing_count != current_replicas:
115            self.exit_json(**return_attributes)
116
117        if existing_count != replicas:
118            return_attributes['changed'] = True
119            if not self.check_mode:
120                if self.kind == 'job':
121                    existing.spec.parallelism = replicas
122                    return_attributes['result'] = resource.patch(existing.to_dict()).to_dict()
123                else:
124                    return_attributes = self.scale(resource, existing, replicas, wait, wait_time)
125
126        self.exit_json(**return_attributes)
127
128    @property
129    def argspec(self):
130        args = copy.deepcopy(SCALE_ARG_SPEC)
131        args.update(RESOURCE_ARG_SPEC)
132        args.update(NAME_ARG_SPEC)
133        args.update(AUTH_ARG_SPEC)
134        return args
135
136    def scale(self, resource, existing_object, replicas, wait, wait_time):
137        name = existing_object.metadata.name
138        namespace = existing_object.metadata.namespace
139        kind = existing_object.kind
140
141        if not hasattr(resource, 'scale'):
142            self.fail_json(
143                msg="Cannot perform scale on resource of kind {0}".format(resource.kind)
144            )
145
146        scale_obj = {'kind': kind, 'metadata': {'name': name, 'namespace': namespace}, 'spec': {'replicas': replicas}}
147
148        existing = resource.get(name=name, namespace=namespace)
149
150        try:
151            resource.scale.patch(body=scale_obj)
152        except Exception as exc:
153            self.fail_json(msg="Scale request failed: {0}".format(exc))
154
155        k8s_obj = resource.get(name=name, namespace=namespace).to_dict()
156        match, diffs = self.diff_objects(existing.to_dict(), k8s_obj)
157        result = dict()
158        result['result'] = k8s_obj
159        result['changed'] = not match
160        result['diff'] = diffs
161
162        if wait:
163            success, result['result'], result['duration'] = self.wait(resource, scale_obj, 5, wait_time)
164            if not success:
165                self.fail_json(msg="Resource scaling timed out", **result)
166        return result
167