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