diff --git a/plugins/inventory/k8s.py b/plugins/inventory/k8s.py index ede54375..f98d9d21 100644 --- a/plugins/inventory/k8s.py +++ b/plugins/inventory/k8s.py @@ -117,7 +117,7 @@ connections: import json from ansible.errors import AnsibleError -from ansible_collections.community.kubernetes.plugins.module_utils.common import K8sAnsibleMixin, HAS_K8S_MODULE_HELPER, k8s_import_exception +from ansible_collections.community.kubernetes.plugins.module_utils.common import K8sAnsibleMixin, HAS_K8S_MODULE_HELPER, k8s_import_exception, get_api_client from ansible.plugins.inventory import BaseInventoryPlugin, Constructable, Cacheable try: @@ -180,7 +180,7 @@ class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable, K8sAnsibleM for connection in connections: if not isinstance(connection, dict): raise K8sInventoryException("Expecting connection to be a dictionary.") - client = self.get_api_client(**connection) + client = get_api_client(**connection) name = connection.get('name', self.get_default_host_name(client.configuration.host)) if connection.get('namespaces'): namespaces = connection['namespaces'] @@ -190,7 +190,7 @@ class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable, K8sAnsibleM self.get_pods_for_namespace(client, name, namespace) self.get_services_for_namespace(client, name, namespace) else: - client = self.get_api_client() + client = get_api_client() name = self.get_default_host_name(client.configuration.host) namespaces = self.get_available_namespaces(client) for namespace in namespaces: diff --git a/plugins/lookup/k8s.py b/plugins/lookup/k8s.py index 68849053..fc4558cf 100644 --- a/plugins/lookup/k8s.py +++ b/plugins/lookup/k8s.py @@ -198,7 +198,7 @@ from ansible.errors import AnsibleError from ansible.module_utils.common._collections_compat import KeysView from ansible.plugins.lookup import LookupBase -from ansible_collections.community.kubernetes.plugins.module_utils.common import K8sAnsibleMixin +from ansible_collections.community.kubernetes.plugins.module_utils.common import K8sAnsibleMixin, get_api_client try: @@ -235,7 +235,7 @@ class KubernetesLookup(K8sAnsibleMixin): def run(self, terms, variables=None, **kwargs): self.params = kwargs - self.client = self.get_api_client() + self.client = get_api_client() cluster_info = kwargs.get('cluster_info') if cluster_info == 'version': diff --git a/plugins/module_utils/ansiblemodule.py b/plugins/module_utils/ansiblemodule.py new file mode 100644 index 00000000..a647b163 --- /dev/null +++ b/plugins/module_utils/ansiblemodule.py @@ -0,0 +1,6 @@ +from __future__ import (absolute_import, division, print_function) + +__metaclass__ = type + + +from ansible.module_utils.basic import AnsibleModule # noqa: F401 diff --git a/plugins/module_utils/args_common.py b/plugins/module_utils/args_common.py new file mode 100644 index 00000000..fadaf44e --- /dev/null +++ b/plugins/module_utils/args_common.py @@ -0,0 +1,133 @@ +from __future__ import (absolute_import, division, print_function) + +from ansible.module_utils.six import string_types + +__metaclass__ = type + + +def list_dict_str(value): + if isinstance(value, (list, dict, string_types)): + return value + raise TypeError + + +AUTH_ARG_SPEC = { + 'kubeconfig': { + 'type': 'path', + }, + 'context': {}, + 'host': {}, + 'api_key': { + 'no_log': True, + }, + 'username': {}, + 'password': { + 'no_log': True, + }, + 'validate_certs': { + 'type': 'bool', + 'aliases': ['verify_ssl'], + }, + 'ca_cert': { + 'type': 'path', + 'aliases': ['ssl_ca_cert'], + }, + 'client_cert': { + 'type': 'path', + 'aliases': ['cert_file'], + }, + 'client_key': { + 'type': 'path', + 'aliases': ['key_file'], + }, + 'proxy': { + 'type': 'str', + }, + 'persist_config': { + 'type': 'bool', + }, +} + +WAIT_ARG_SPEC = dict( + wait=dict(type='bool', default=False), + wait_sleep=dict(type='int', default=5), + wait_timeout=dict(type='int', default=120), + wait_condition=dict( + type='dict', + default=None, + options=dict( + type=dict(), + status=dict(default=True, choices=[True, False, "Unknown"]), + reason=dict() + ) + ) +) + +# Map kubernetes-client parameters to ansible parameters +AUTH_ARG_MAP = { + 'kubeconfig': 'kubeconfig', + 'context': 'context', + 'host': 'host', + 'api_key': 'api_key', + 'username': 'username', + 'password': 'password', + 'verify_ssl': 'validate_certs', + 'ssl_ca_cert': 'ca_cert', + 'cert_file': 'client_cert', + 'key_file': 'client_key', + 'proxy': 'proxy', + 'persist_config': 'persist_config', +} + +NAME_ARG_SPEC = { + 'kind': {}, + 'name': {}, + 'namespace': {}, + 'api_version': { + 'default': 'v1', + 'aliases': ['api', 'version'], + }, +} + +COMMON_ARG_SPEC = { + 'state': { + 'default': 'present', + 'choices': ['present', 'absent'], + }, + 'force': { + 'type': 'bool', + 'default': False, + }, +} + +RESOURCE_ARG_SPEC = { + 'resource_definition': { + 'type': list_dict_str, + 'aliases': ['definition', 'inline'] + }, + 'src': { + 'type': 'path', + }, +} + +ARG_ATTRIBUTES_BLACKLIST = ('property_path',) + +DELETE_OPTS_ARG_SPEC = { + 'propagationPolicy': { + 'choices': ['Foreground', 'Background', 'Orphan'], + }, + 'gracePeriodSeconds': { + 'type': 'int', + }, + 'preconditions': { + 'type': 'dict', + 'options': { + 'resourceVersion': { + 'type': 'str', + }, + 'uid': { + 'type': 'str', + } + } + } +} diff --git a/plugins/module_utils/common.py b/plugins/module_utils/common.py index 3c44f5c9..114ba777 100644 --- a/plugins/module_utils/common.py +++ b/plugins/module_utils/common.py @@ -26,6 +26,7 @@ import sys from datetime import datetime from distutils.version import LooseVersion +from ansible_collections.community.kubernetes.plugins.module_utils.args_common import (AUTH_ARG_MAP, AUTH_ARG_SPEC) from ansible.module_utils.basic import AnsibleModule, missing_required_lib from ansible.module_utils.six import iteritems, string_types @@ -99,201 +100,107 @@ except ImportError as e: K8S_IMP_ERR = traceback.format_exc() -def list_dict_str(value): - if isinstance(value, (list, dict, string_types)): - return value - raise TypeError +def configuration_digest(configuration): + import hashlib + m = hashlib.sha256() + for k in AUTH_ARG_MAP: + if not hasattr(configuration, k): + v = None + else: + v = getattr(configuration, k) + if v and k in ["ssl_ca_cert", "cert_file", "key_file"]: + with open(str(v), "r") as fd: + content = fd.read() + m.update(content.encode()) + else: + m.update(str(v).encode()) + digest = m.hexdigest() + return digest -ARG_ATTRIBUTES_BLACKLIST = ('property_path',) +def get_api_client(module=None): + auth = {} -COMMON_ARG_SPEC = { - 'state': { - 'default': 'present', - 'choices': ['present', 'absent'], - }, - 'force': { - 'type': 'bool', - 'default': False, - }, -} + def _raise_or_fail(exc, msg): + if module: + module.fail_json(msg % to_native(exc)) + else: + raise exc -RESOURCE_ARG_SPEC = { - 'resource_definition': { - 'type': list_dict_str, - 'aliases': ['definition', 'inline'] - }, - 'src': { - 'type': 'path', - }, -} + # If authorization variables aren't defined, look for them in environment variables + for true_name, arg_name in AUTH_ARG_MAP.items(): + if module and module.params.get(arg_name): + auth[true_name] = module.params.get(arg_name) + else: + env_value = os.getenv('K8S_AUTH_{0}'.format(arg_name.upper()), None) or os.getenv('K8S_AUTH_{0}'.format(true_name.upper()), None) + if env_value is not None: + if AUTH_ARG_SPEC[arg_name].get('type') == 'bool': + env_value = env_value.lower() not in ['0', 'false', 'no'] + auth[true_name] = env_value -NAME_ARG_SPEC = { - 'kind': {}, - 'name': {}, - 'namespace': {}, - 'api_version': { - 'default': 'v1', - 'aliases': ['api', 'version'], - }, -} + def auth_set(*names): + return all([auth.get(name) for name in names]) -AUTH_ARG_SPEC = { - 'kubeconfig': { - 'type': 'path', - }, - 'context': {}, - 'host': {}, - 'api_key': { - 'no_log': True, - }, - 'username': {}, - 'password': { - 'no_log': True, - }, - 'validate_certs': { - 'type': 'bool', - 'aliases': ['verify_ssl'], - }, - 'ca_cert': { - 'type': 'path', - 'aliases': ['ssl_ca_cert'], - }, - 'client_cert': { - 'type': 'path', - 'aliases': ['cert_file'], - }, - 'client_key': { - 'type': 'path', - 'aliases': ['key_file'], - }, - 'proxy': { - 'type': 'str', - }, - 'persist_config': { - 'type': 'bool', - }, -} + if auth_set('username', 'password', 'host') or auth_set('api_key', 'host'): + # We have enough in the parameters to authenticate, no need to load incluster or kubeconfig + pass + elif auth_set('kubeconfig') or auth_set('context'): + try: + kubernetes.config.load_kube_config(auth.get('kubeconfig'), auth.get('context'), persist_config=auth.get('persist_config')) + except Exception as err: + _raise_or_fail(err, 'Failed to load kubeconfig due to %s') -WAIT_ARG_SPEC = dict( - wait=dict(type='bool', default=False), - wait_sleep=dict(type='int', default=5), - wait_timeout=dict(type='int', default=120), - wait_condition=dict( - type='dict', - default=None, - options=dict( - type=dict(), - status=dict(type='str', default="True", choices=["True", "False", "Unknown"]), - reason=dict() - ) - ) -) + else: + # First try to do incluster config, then kubeconfig + try: + kubernetes.config.load_incluster_config() + except kubernetes.config.ConfigException: + try: + kubernetes.config.load_kube_config(auth.get('kubeconfig'), auth.get('context'), persist_config=auth.get('persist_config')) + except Exception as err: + _raise_or_fail(err, 'Failed to load kubeconfig due to %s') -DELETE_OPTS_ARG_SPEC = { - 'propagationPolicy': { - 'choices': ['Foreground', 'Background', 'Orphan'], - }, - 'gracePeriodSeconds': { - 'type': 'int', - }, - 'preconditions': { - 'type': 'dict', - 'options': { - 'resourceVersion': { - 'type': 'str', - }, - 'uid': { - 'type': 'str', - } - } - } -} + # Override any values in the default configuration with Ansible parameters + # As of kubernetes-client v12.0.0, get_default_copy() is required here + try: + configuration = kubernetes.client.Configuration().get_default_copy() + except AttributeError: + configuration = kubernetes.client.Configuration() + + for key, value in iteritems(auth): + if key in AUTH_ARG_MAP.keys() and value is not None: + if key == 'api_key': + setattr(configuration, key, {'authorization': "Bearer {0}".format(value)}) + else: + setattr(configuration, key, value) + + digest = configuration_digest(configuration) + if digest in get_api_client._pool: + client = get_api_client._pool[digest] + return client + + try: + client = DynamicClient(kubernetes.client.ApiClient(configuration)) + except Exception as err: + _raise_or_fail(err, 'Failed to get client due to %s') + + get_api_client._pool[digest] = client + return client -# Map kubernetes-client parameters to ansible parameters -AUTH_ARG_MAP = { - 'kubeconfig': 'kubeconfig', - 'context': 'context', - 'host': 'host', - 'api_key': 'api_key', - 'username': 'username', - 'password': 'password', - 'verify_ssl': 'validate_certs', - 'ssl_ca_cert': 'ca_cert', - 'cert_file': 'client_cert', - 'key_file': 'client_key', - 'proxy': 'proxy', - 'persist_config': 'persist_config', -} +get_api_client._pool = {} class K8sAnsibleMixin(object): - def __init__(self, *args, **kwargs): + def __init__(self, module, *args, **kwargs): if not HAS_K8S_MODULE_HELPER: - self.fail_json(msg=missing_required_lib('openshift'), exception=K8S_IMP_ERR, - error=to_native(k8s_import_exception)) + module.fail_json(msg=missing_required_lib('openshift'), exception=K8S_IMP_ERR, + error=to_native(k8s_import_exception)) self.openshift_version = openshift.__version__ if not HAS_YAML: - self.fail_json(msg=missing_required_lib("PyYAML"), exception=YAML_IMP_ERR) - - def get_api_client(self, **auth_params): - auth_params = auth_params or getattr(self, 'params', {}) - auth = {} - - # If authorization variables aren't defined, look for them in environment variables - for true_name, arg_name in AUTH_ARG_MAP.items(): - if auth_params.get(arg_name) is None: - env_value = os.getenv('K8S_AUTH_{0}'.format(arg_name.upper()), None) or os.getenv('K8S_AUTH_{0}'.format(true_name.upper()), None) - if env_value is not None: - if AUTH_ARG_SPEC[arg_name].get('type') == 'bool': - env_value = env_value.lower() not in ['0', 'false', 'no'] - auth[true_name] = env_value - else: - auth[true_name] = auth_params[arg_name] - - def auth_set(*names): - return all([auth.get(name) for name in names]) - - if auth_set('username', 'password', 'host') or auth_set('api_key', 'host'): - # We have enough in the parameters to authenticate, no need to load incluster or kubeconfig - pass - elif auth_set('kubeconfig') or auth_set('context'): - try: - kubernetes.config.load_kube_config(auth.get('kubeconfig'), auth.get('context'), persist_config=auth.get('persist_config')) - except Exception as err: - self.fail(msg='Failed to load kubeconfig due to %s' % to_native(err)) - else: - # First try to do incluster config, then kubeconfig - try: - kubernetes.config.load_incluster_config() - except kubernetes.config.ConfigException: - try: - kubernetes.config.load_kube_config(auth.get('kubeconfig'), auth.get('context'), persist_config=auth.get('persist_config')) - except Exception as err: - self.fail(msg='Failed to load kubeconfig due to %s' % to_native(err)) - - # Override any values in the default configuration with Ansible parameters - # As of kubernetes-client v12.0.0, get_default_copy() is required here - try: - configuration = kubernetes.client.Configuration().get_default_copy() - except AttributeError: - configuration = kubernetes.client.Configuration() - - for key, value in iteritems(auth): - if key in AUTH_ARG_MAP.keys() and value is not None: - if key == 'api_key': - setattr(configuration, key, {'authorization': "Bearer {0}".format(value)}) - else: - setattr(configuration, key, value) - - kubernetes.client.Configuration.set_default(configuration) - try: - return DynamicClient(kubernetes.client.ApiClient(configuration)) - except Exception as err: - self.fail(msg='Failed to get client due to %s' % to_native(err)) + module.fail_json(msg=missing_required_lib("PyYAML"), exception=YAML_IMP_ERR) def find_resource(self, kind, api_version, fail=False): for attribute in ['kind', 'name', 'singular_name']: @@ -513,8 +420,8 @@ class K8sAnsibleMixin(object): predicate = _resource_absent return self._wait_for(resource, definition['metadata']['name'], definition['metadata'].get('namespace'), predicate, sleep, timeout, state) - def set_resource_definitions(self): - resource_definition = self.params.get('resource_definition') + def set_resource_definitions(self, module): + resource_definition = module.params.get('resource_definition') self.resource_definitions = [] @@ -529,7 +436,7 @@ class K8sAnsibleMixin(object): else: self.resource_definitions = [resource_definition] - src = self.params.get('src') + src = module.params.get('src') if src: self.resource_definitions = self.load_resource_definitions(src) try: @@ -539,12 +446,12 @@ class K8sAnsibleMixin(object): if not resource_definition and not src: implicit_definition = dict( - kind=self.kind, - apiVersion=self.api_version, - metadata=dict(name=self.name) + kind=module.params['kind'], + apiVersion=module.params['api_version'], + metadata=dict(name=module.params['name']) ) - if self.namespace: - implicit_definition['metadata']['namespace'] = self.namespace + if module.params.get('namespace'): + implicit_definition['metadata']['namespace'] = module.params.get('namespace') self.resource_definitions = [implicit_definition] def check_library_version(self): @@ -577,7 +484,7 @@ class K8sAnsibleMixin(object): changed = False results = [] try: - self.client = self.get_api_client() + self.client = get_api_client() # Hopefully the kubernetes client will provide its own exception class one day except (urllib3.exceptions.RequestError) as e: self.fail_json(msg="Couldn't connect to Kubernetes: %s" % str(e)) diff --git a/plugins/module_utils/scale.py b/plugins/module_utils/scale.py deleted file mode 100644 index 55bab010..00000000 --- a/plugins/module_utils/scale.py +++ /dev/null @@ -1,166 +0,0 @@ -# -# Copyright 2018 Red Hat | Ansible -# -# This file is part of Ansible -# -# Ansible is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Ansible is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Ansible. If not, see . - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - -import copy - -from ansible.module_utils.basic import AnsibleModule -from ansible_collections.community.kubernetes.plugins.module_utils.common import ( - AUTH_ARG_SPEC, RESOURCE_ARG_SPEC, NAME_ARG_SPEC, K8sAnsibleMixin) - -try: - from openshift.dynamic.exceptions import NotFoundError -except ImportError: - pass - - -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}, -} - - -class KubernetesAnsibleScaleModule(K8sAnsibleMixin): - - def __init__(self, k8s_kind=None, *args, **kwargs): - self.client = None - self.warnings = [] - - mutually_exclusive = [ - ('resource_definition', 'src'), - ] - - module = AnsibleModule( - argument_spec=self.argspec, - mutually_exclusive=mutually_exclusive, - supports_check_mode=True, - ) - - self.module = module - self.params = self.module.params - self.check_mode = self.module.check_mode - self.fail_json = self.module.fail_json - self.fail = self.module.fail_json - self.exit_json = self.module.exit_json - super(KubernetesAnsibleScaleModule, self).__init__() - - self.kind = k8s_kind or self.params.get('kind') - self.api_version = self.params.get('api_version') - self.name = self.params.get('name') - self.namespace = self.params.get('namespace') - self.set_resource_definitions() - - def execute_module(self): - definition = self.resource_definitions[0] - - self.client = self.get_api_client() - - name = definition['metadata']['name'] - namespace = definition['metadata'].get('namespace') - api_version = definition['apiVersion'] - kind = definition['kind'] - current_replicas = self.params.get('current_replicas') - replicas = self.params.get('replicas') - resource_version = self.params.get('resource_version') - - wait = self.params.get('wait') - wait_time = self.params.get('wait_timeout') - existing = None - existing_count = None - return_attributes = dict(changed=False, result=dict(), diff=dict()) - if wait: - return_attributes['duration'] = 0 - - resource = self.find_resource(kind, api_version, fail=True) - - try: - existing = resource.get(name=name, namespace=namespace) - return_attributes['result'] = existing.to_dict() - except NotFoundError as exc: - self.fail_json(msg='Failed to retrieve requested object: {0}'.format(exc), - error=exc.value.get('status')) - - if self.kind == 'job': - existing_count = existing.spec.parallelism - elif hasattr(existing.spec, 'replicas'): - existing_count = existing.spec.replicas - - if existing_count is None: - self.fail_json(msg='Failed to retrieve the available count for the requested object.') - - if resource_version and resource_version != existing.metadata.resourceVersion: - self.exit_json(**return_attributes) - - if current_replicas is not None and existing_count != current_replicas: - self.exit_json(**return_attributes) - - if existing_count != replicas: - return_attributes['changed'] = True - if not self.check_mode: - if self.kind == 'job': - existing.spec.parallelism = replicas - return_attributes['result'] = resource.patch(existing.to_dict()).to_dict() - else: - return_attributes = self.scale(resource, existing, replicas, wait, wait_time) - - self.exit_json(**return_attributes) - - @property - def argspec(self): - args = copy.deepcopy(SCALE_ARG_SPEC) - args.update(RESOURCE_ARG_SPEC) - args.update(NAME_ARG_SPEC) - args.update(AUTH_ARG_SPEC) - return args - - def scale(self, resource, existing_object, replicas, wait, wait_time): - name = existing_object.metadata.name - namespace = existing_object.metadata.namespace - kind = existing_object.kind - - if not hasattr(resource, 'scale'): - self.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) - - try: - resource.scale.patch(body=scale_obj) - except Exception as exc: - self.fail_json(msg="Scale request failed: {0}".format(exc)) - - k8s_obj = resource.get(name=name, namespace=namespace).to_dict() - match, diffs = self.diff_objects(existing.to_dict(), k8s_obj) - result = dict() - result['result'] = k8s_obj - result['changed'] = not match - result['diff'] = diffs - - if wait: - success, result['result'], result['duration'] = self.wait(resource, scale_obj, 5, wait_time) - if not success: - self.fail_json(msg="Resource scaling timed out", **result) - return result diff --git a/plugins/modules/k8s.py b/plugins/modules/k8s.py index 6d0afdc4..7dd7a67b 100644 --- a/plugins/modules/k8s.py +++ b/plugins/modules/k8s.py @@ -263,74 +263,68 @@ result: import copy -from ansible.module_utils.basic import AnsibleModule -from ansible_collections.community.kubernetes.plugins.module_utils.common import ( - K8sAnsibleMixin, COMMON_ARG_SPEC, NAME_ARG_SPEC, RESOURCE_ARG_SPEC, AUTH_ARG_SPEC, - WAIT_ARG_SPEC, DELETE_OPTS_ARG_SPEC) +from ansible_collections.community.kubernetes.plugins.module_utils.ansiblemodule import AnsibleModule +from ansible_collections.community.kubernetes.plugins.module_utils.args_common import ( + AUTH_ARG_SPEC, WAIT_ARG_SPEC, NAME_ARG_SPEC, COMMON_ARG_SPEC, RESOURCE_ARG_SPEC, DELETE_OPTS_ARG_SPEC) -class KubernetesModule(K8sAnsibleMixin): +def validate_spec(): + return dict( + fail_on_error=dict(type='bool'), + version=dict(), + strict=dict(type='bool', default=True) + ) - @property - def validate_spec(self): - return dict( - fail_on_error=dict(type='bool'), - version=dict(), - strict=dict(type='bool', default=True) - ) - @property - def argspec(self): - argument_spec = copy.deepcopy(COMMON_ARG_SPEC) - argument_spec.update(copy.deepcopy(NAME_ARG_SPEC)) - argument_spec.update(copy.deepcopy(RESOURCE_ARG_SPEC)) - argument_spec.update(copy.deepcopy(AUTH_ARG_SPEC)) - argument_spec.update(copy.deepcopy(WAIT_ARG_SPEC)) - argument_spec['merge_type'] = dict(type='list', elements='str', choices=['json', 'merge', 'strategic-merge']) - argument_spec['validate'] = dict(type='dict', default=None, options=self.validate_spec) - argument_spec['append_hash'] = dict(type='bool', default=False) - argument_spec['apply'] = dict(type='bool', default=False) - argument_spec['template'] = dict(type='raw', default=None) - argument_spec['delete_options'] = dict(type='dict', default=None, options=copy.deepcopy(DELETE_OPTS_ARG_SPEC)) - return argument_spec +def argspec(): + argument_spec = copy.deepcopy(COMMON_ARG_SPEC) + argument_spec.update(copy.deepcopy(NAME_ARG_SPEC)) + argument_spec.update(copy.deepcopy(RESOURCE_ARG_SPEC)) + argument_spec.update(copy.deepcopy(AUTH_ARG_SPEC)) + argument_spec.update(copy.deepcopy(WAIT_ARG_SPEC)) + argument_spec['merge_type'] = dict(type='list', elements='str', choices=['json', 'merge', 'strategic-merge']) + argument_spec['validate'] = dict(type='dict', default=None, options=validate_spec()) + argument_spec['append_hash'] = dict(type='bool', default=False) + argument_spec['apply'] = dict(type='bool', default=False) + argument_spec['template'] = dict(type='raw', default=None) + argument_spec['delete_options'] = dict(type='dict', default=None, options=copy.deepcopy(DELETE_OPTS_ARG_SPEC)) + return argument_spec - def __init__(self, k8s_kind=None, *args, **kwargs): - mutually_exclusive = [ - ('resource_definition', 'src'), - ('merge_type', 'apply'), - ('template', 'resource_definition'), - ('template', 'src'), - ] - module = AnsibleModule( - argument_spec=self.argspec, - mutually_exclusive=mutually_exclusive, - supports_check_mode=True, - ) +def execute_module(module, k8s_ansible_mixin): + k8s_ansible_mixin.module = module + k8s_ansible_mixin.argspec = module.argument_spec + k8s_ansible_mixin.check_mode = k8s_ansible_mixin.module.check_mode + k8s_ansible_mixin.params = k8s_ansible_mixin.module.params + k8s_ansible_mixin.fail_json = k8s_ansible_mixin.module.fail_json + k8s_ansible_mixin.fail = k8s_ansible_mixin.module.fail_json + k8s_ansible_mixin.exit_json = k8s_ansible_mixin.module.exit_json + k8s_ansible_mixin.warnings = [] - self.module = module - self.check_mode = self.module.check_mode - self.params = self.module.params - self.fail_json = self.module.fail_json - self.fail = self.module.fail_json - self.exit_json = self.module.exit_json + k8s_ansible_mixin.kind = k8s_ansible_mixin.params.get('kind') + k8s_ansible_mixin.api_version = k8s_ansible_mixin.params.get('api_version') + k8s_ansible_mixin.name = k8s_ansible_mixin.params.get('name') + k8s_ansible_mixin.namespace = k8s_ansible_mixin.params.get('namespace') - super(KubernetesModule, self).__init__(*args, **kwargs) - - self.client = None - self.warnings = [] - - self.kind = k8s_kind or self.params.get('kind') - self.api_version = self.params.get('api_version') - self.name = self.params.get('name') - self.namespace = self.params.get('namespace') - - self.check_library_version() - self.set_resource_definitions() + k8s_ansible_mixin.check_library_version() + k8s_ansible_mixin.set_resource_definitions(module) + k8s_ansible_mixin.execute_module() def main(): - KubernetesModule().execute_module() + mutually_exclusive = [ + ('resource_definition', 'src'), + ('merge_type', 'apply'), + ('template', 'resource_definition'), + ('template', 'src'), + ] + module = AnsibleModule(argument_spec=argspec(), mutually_exclusive=mutually_exclusive, supports_check_mode=True) + from ansible_collections.community.kubernetes.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__': diff --git a/plugins/modules/k8s_cluster_info.py b/plugins/modules/k8s_cluster_info.py index e01009d6..40e9e09e 100644 --- a/plugins/modules/k8s_cluster_info.py +++ b/plugins/modules/k8s_cluster_info.py @@ -159,88 +159,60 @@ apis: import copy -import traceback -from ansible.module_utils.basic import AnsibleModule, missing_required_lib +from ansible_collections.community.kubernetes.plugins.module_utils.ansiblemodule import AnsibleModule from ansible.module_utils.parsing.convert_bool import boolean -from ansible_collections.community.kubernetes.plugins.module_utils.common import K8sAnsibleMixin, AUTH_ARG_SPEC - -try: - try: - from openshift import __version__ as version - # >=0.10 - from openshift.dynamic.resource import ResourceList - except ImportError: - # <0.10 - from openshift.dynamic.client import ResourceList - HAS_K8S_INSTANCE_HELPER = True - k8s_import_exception = None -except ImportError: - HAS_K8S_INSTANCE_HELPER = False - k8s_import_exception = traceback.format_exc() +from ansible_collections.community.kubernetes.plugins.module_utils.args_common import (AUTH_ARG_SPEC) -class KubernetesInfoModule(K8sAnsibleMixin): - - def __init__(self): - module = AnsibleModule( - argument_spec=self.argspec, - supports_check_mode=True, - ) - self.module = module - self.params = self.module.params - - if not HAS_K8S_INSTANCE_HELPER: - self.module.fail_json(msg=missing_required_lib("openshift >= 0.6.2", reason="for merge_type"), - exception=k8s_import_exception) - - super(KubernetesInfoModule, self).__init__() - - def execute_module(self): - self.client = self.get_api_client() - invalidate_cache = boolean(self.module.params.get('invalidate_cache', True), strict=False) - if invalidate_cache: - self.client.resources.invalidate_cache() - results = {} - for resource in list(self.client.resources): - resource = resource[0] - if isinstance(resource, ResourceList): - continue - results[resource.group] = { - 'api_version': resource.group_version, - 'categories': resource.categories if resource.categories else [], - 'kind': resource.kind, - 'name': resource.name, - 'namespaced': resource.namespaced, - 'preferred': resource.preferred, - 'short_names': resource.short_names if resource.short_names else [], - 'singular_name': resource.singular_name, - } - configuration = self.client.configuration - connection = { - 'cert_file': configuration.cert_file, - 'host': configuration.host, - 'password': configuration.password, - 'proxy': configuration.proxy, - 'ssl_ca_cert': configuration.ssl_ca_cert, - 'username': configuration.username, - 'verify_ssl': configuration.verify_ssl, +def execute_module(module, client): + invalidate_cache = boolean(module.params.get('invalidate_cache', True), strict=False) + if invalidate_cache: + client.resources.invalidate_cache() + results = {} + from openshift.dynamic.resource import ResourceList + for resource in list(client.resources): + resource = resource[0] + if isinstance(resource, ResourceList): + continue + results[resource.group] = { + 'api_version': resource.group_version, + 'categories': resource.categories if resource.categories else [], + 'kind': resource.kind, + 'name': resource.name, + 'namespaced': resource.namespaced, + 'preferred': resource.preferred, + 'short_names': resource.short_names if resource.short_names else [], + 'singular_name': resource.singular_name, } - version_info = { - 'client': version, - 'server': self.client.version, - } - self.module.exit_json(changed=False, apis=results, connection=connection, version=version_info) + configuration = client.configuration + connection = { + 'cert_file': configuration.cert_file, + 'host': configuration.host, + 'password': configuration.password, + 'proxy': configuration.proxy, + 'ssl_ca_cert': configuration.ssl_ca_cert, + 'username': configuration.username, + 'verify_ssl': configuration.verify_ssl, + } + from openshift import __version__ as version + version_info = { + 'client': version, + 'server': client.version, + } + module.exit_json(changed=False, apis=results, connection=connection, version=version_info) - @property - def argspec(self): - spec = copy.deepcopy(AUTH_ARG_SPEC) - spec['invalidate_cache'] = dict(type='bool', default=True) - return spec + +def argspec(): + spec = copy.deepcopy(AUTH_ARG_SPEC) + spec['invalidate_cache'] = dict(type='bool', default=True) + return spec def main(): - KubernetesInfoModule().execute_module() + module = AnsibleModule(argument_spec=argspec(), supports_check_mode=True) + from ansible_collections.community.kubernetes.plugins.module_utils.common import get_api_client + execute_module(module, client=get_api_client(module=module)) if __name__ == '__main__': diff --git a/plugins/modules/k8s_exec.py b/plugins/modules/k8s_exec.py index a2542b25..55e60849 100644 --- a/plugins/modules/k8s_exec.py +++ b/plugins/modules/k8s_exec.py @@ -118,10 +118,10 @@ except ImportError: # ImportError are managed by the common module already. pass -from ansible.module_utils.basic import AnsibleModule +from ansible_collections.community.kubernetes.plugins.module_utils.ansiblemodule import AnsibleModule from ansible.module_utils._text import to_native from ansible_collections.community.kubernetes.plugins.module_utils.common import ( - K8sAnsibleMixin, AUTH_ARG_SPEC + AUTH_ARG_SPEC ) try: @@ -132,75 +132,72 @@ except ImportError: pass -class KubernetesExecCommand(K8sAnsibleMixin): +def argspec(): + spec = copy.deepcopy(AUTH_ARG_SPEC) + spec['namespace'] = dict(type='str', required=True) + spec['pod'] = dict(type='str', required=True) + spec['container'] = dict(type='str') + spec['command'] = dict(type='str', required=True) + return spec - def __init__(self): - module = AnsibleModule( - argument_spec=self.argspec, - supports_check_mode=True, - ) - self.module = module - self.params = self.module.params - self.fail_json = self.module.fail_json - super(KubernetesExecCommand, self).__init__() - @property - def argspec(self): - spec = copy.deepcopy(AUTH_ARG_SPEC) - spec['namespace'] = dict(type='str', required=True) - spec['pod'] = dict(type='str', required=True) - spec['container'] = dict(type='str') - spec['command'] = dict(type='str', required=True) - return spec +def execute_module(module, k8s_ansible_mixin): - def execute_module(self): - # Load kubernetes.client.Configuration - self.get_api_client() - api = core_v1_api.CoreV1Api() + # Load kubernetes.client.Configuration + api = core_v1_api.CoreV1Api() - # hack because passing the container as None breaks things - optional_kwargs = {} - if self.params.get('container'): - optional_kwargs['container'] = self.params['container'] - try: - resp = stream( - api.connect_get_namespaced_pod_exec, - self.params["pod"], - self.params["namespace"], - command=shlex.split(self.params["command"]), - stdout=True, - stderr=True, - stdin=False, - tty=False, - _preload_content=False, **optional_kwargs) - except Exception as e: - self.module.fail_json(msg="Failed to execute on pod %s" - " due to : %s" % (self.params.get('pod'), to_native(e))) - stdout, stderr, rc = [], [], 0 - while resp.is_open(): - resp.update(timeout=1) - if resp.peek_stdout(): - stdout.append(resp.read_stdout()) - if resp.peek_stderr(): - stderr.append(resp.read_stderr()) - err = resp.read_channel(3) - err = yaml.safe_load(err) - if err['status'] == 'Success': - rc = 0 - else: - rc = int(err['details']['causes'][0]['message']) + # hack because passing the container as None breaks things + optional_kwargs = {} + if module.params.get('container'): + optional_kwargs['container'] = module.params['container'] + try: + resp = stream( + api.connect_get_namespaced_pod_exec, + module.params["pod"], + module.params["namespace"], + command=shlex.split(module.params["command"]), + stdout=True, + stderr=True, + stdin=False, + tty=False, + _preload_content=False, **optional_kwargs) + except Exception as e: + module.fail_json(msg="Failed to execute on pod %s" + " due to : %s" % (module.params.get('pod'), to_native(e))) + stdout, stderr, rc = [], [], 0 + while resp.is_open(): + resp.update(timeout=1) + if resp.peek_stdout(): + stdout.append(resp.read_stdout()) + if resp.peek_stderr(): + stderr.append(resp.read_stderr()) + err = resp.read_channel(3) + err = yaml.safe_load(err) + if err['status'] == 'Success': + rc = 0 + else: + rc = int(err['details']['causes'][0]['message']) - self.module.exit_json( - # Some command might change environment, but ultimately failing at end - changed=True, - stdout="".join(stdout), - stderr="".join(stderr), - return_code=rc - ) + module.exit_json( + # Some command might change environment, but ultimately failing at end + changed=True, + stdout="".join(stdout), + stderr="".join(stderr), + return_code=rc + ) def main(): - KubernetesExecCommand().execute_module() + module = AnsibleModule( + argument_spec=argspec(), + supports_check_mode=True, + ) + from ansible_collections.community.kubernetes.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__': diff --git a/plugins/modules/k8s_info.py b/plugins/modules/k8s_info.py index 07536e2e..0119d61e 100644 --- a/plugins/modules/k8s_info.py +++ b/plugins/modules/k8s_info.py @@ -148,58 +148,50 @@ resources: import copy -from ansible.module_utils.basic import AnsibleModule -from ansible_collections.community.kubernetes.plugins.module_utils.common import ( - K8sAnsibleMixin, AUTH_ARG_SPEC, WAIT_ARG_SPEC) +from ansible_collections.community.kubernetes.plugins.module_utils.ansiblemodule import AnsibleModule +from ansible_collections.community.kubernetes.plugins.module_utils.args_common import (AUTH_ARG_SPEC, WAIT_ARG_SPEC) -class KubernetesInfoModule(K8sAnsibleMixin): +def execute_module(module, k8s_ansible_mixin): + facts = k8s_ansible_mixin.kubernetes_facts( + module.params["kind"], + module.params["api_version"], + name=module.params["name"], + namespace=module.params["namespace"], + label_selectors=module.params["label_selectors"], + field_selectors=module.params["field_selectors"], + wait=module.params["wait"], + wait_sleep=module.params["wait_sleep"], + wait_timeout=module.params["wait_timeout"], + condition=module.params["wait_condition"], + ) + module.exit_json(changed=False, **facts) - def __init__(self, *args, **kwargs): - module = AnsibleModule( - argument_spec=self.argspec, - supports_check_mode=True, + +def argspec(): + args = copy.deepcopy(AUTH_ARG_SPEC) + args.update(WAIT_ARG_SPEC) + args.update( + dict( + kind=dict(required=True), + api_version=dict(default='v1', aliases=['api', 'version']), + name=dict(), + namespace=dict(), + label_selectors=dict(type='list', elements='str', default=[]), + field_selectors=dict(type='list', elements='str', default=[]), ) - self.module = module - self.params = self.module.params - self.fail_json = self.module.fail_json - self.exit_json = self.module.exit_json - super(KubernetesInfoModule, self).__init__() - - def execute_module(self): - self.client = self.get_api_client() - - self.exit_json(changed=False, - **self.kubernetes_facts(self.params['kind'], - self.params['api_version'], - name=self.params['name'], - namespace=self.params['namespace'], - label_selectors=self.params['label_selectors'], - field_selectors=self.params['field_selectors'], - wait=self.params['wait'], - wait_sleep=self.params['wait_sleep'], - wait_timeout=self.params['wait_timeout'], - condition=self.params['wait_condition'])) - - @property - def argspec(self): - args = copy.deepcopy(AUTH_ARG_SPEC) - args.update(WAIT_ARG_SPEC) - args.update( - dict( - kind=dict(required=True), - api_version=dict(default='v1', aliases=['api', 'version']), - name=dict(), - namespace=dict(), - label_selectors=dict(type='list', elements='str', default=[]), - field_selectors=dict(type='list', elements='str', default=[]), - ) - ) - return args + ) + return args def main(): - KubernetesInfoModule().execute_module() + module = AnsibleModule(argument_spec=argspec(), supports_check_mode=True) + from ansible_collections.community.kubernetes.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__': diff --git a/plugins/modules/k8s_log.py b/plugins/modules/k8s_log.py index e7b75711..50b568b4 100644 --- a/plugins/modules/k8s_log.py +++ b/plugins/modules/k8s_log.py @@ -111,116 +111,101 @@ log_lines: import copy -from ansible.module_utils.basic import AnsibleModule +from ansible_collections.community.kubernetes.plugins.module_utils.ansiblemodule import AnsibleModule from ansible.module_utils.six import PY2 -from ansible_collections.community.kubernetes.plugins.module_utils.common import ( - K8sAnsibleMixin, AUTH_ARG_SPEC, NAME_ARG_SPEC) +from ansible_collections.community.kubernetes.plugins.module_utils.args_common import (AUTH_ARG_SPEC, NAME_ARG_SPEC) -class KubernetesLogModule(K8sAnsibleMixin): - - def __init__(self): - module = AnsibleModule( - argument_spec=self.argspec, - supports_check_mode=True, +def argspec(): + args = copy.deepcopy(AUTH_ARG_SPEC) + args.update(NAME_ARG_SPEC) + args.update( + dict( + kind=dict(type='str', default='Pod'), + container=dict(), + label_selectors=dict(type='list', elements='str', default=[]), ) - self.module = module - self.params = self.module.params - self.fail_json = self.module.fail_json - self.fail = self.module.fail_json - self.exit_json = self.module.exit_json - super(KubernetesLogModule, self).__init__() + ) + return args - @property - def argspec(self): - args = copy.deepcopy(AUTH_ARG_SPEC) - args.update(NAME_ARG_SPEC) - args.update( - dict( - kind=dict(type='str', default='Pod'), - container=dict(), - label_selectors=dict(type='list', elements='str', default=[]), - ) - ) - return args - def execute_module(self): - name = self.params.get('name') - namespace = self.params.get('namespace') - label_selector = ','.join(self.params.get('label_selectors', {})) - if name and label_selector: - self.fail(msg='Only one of name or label_selectors can be provided') +def execute_module(module, k8s_ansible_mixin): + name = module.params.get('name') + namespace = module.params.get('namespace') + label_selector = ','.join(module.params.get('label_selectors', {})) + if name and label_selector: + module.fail(msg='Only one of name or label_selectors can be provided') - self.client = self.get_api_client() - resource = self.find_resource(self.params['kind'], self.params['api_version'], fail=True) - v1_pods = self.find_resource('Pod', 'v1', fail=True) + resource = k8s_ansible_mixin.find_resource(module.params['kind'], module.params['api_version'], fail=True) + v1_pods = k8s_ansible_mixin.find_resource('Pod', 'v1', fail=True) - if 'log' not in resource.subresources: - if not name: - self.fail(msg='name must be provided for resources that do not support the log subresource') - instance = resource.get(name=name, namespace=namespace) - label_selector = ','.join(self.extract_selectors(instance)) - resource = v1_pods + if 'log' not in resource.subresources: + if not name: + module.fail(msg='name must be provided for resources that do not support the log subresource') + instance = resource.get(name=name, namespace=namespace) + label_selector = ','.join(extract_selectors(module, instance)) + resource = v1_pods - if label_selector: - instances = v1_pods.get(namespace=namespace, label_selector=label_selector) - if not instances.items: - self.fail(msg='No pods in namespace {0} matched selector {1}'.format(namespace, label_selector)) - # This matches the behavior of kubectl when logging pods via a selector - name = instances.items[0].metadata.name - resource = v1_pods + if label_selector: + instances = v1_pods.get(namespace=namespace, label_selector=label_selector) + if not instances.items: + module.fail(msg='No pods in namespace {0} matched selector {1}'.format(namespace, label_selector)) + # This matches the behavior of kubectl when logging pods via a selector + name = instances.items[0].metadata.name + resource = v1_pods - kwargs = {} - if self.params.get('container'): - kwargs['query_params'] = dict(container=self.params['container']) + kwargs = {} + if module.params.get('container'): + kwargs['query_params'] = dict(container=module.params['container']) - log = serialize_log(resource.log.get( - name=name, - namespace=namespace, - serialize=False, - **kwargs - )) + log = serialize_log(resource.log.get( + name=name, + namespace=namespace, + serialize=False, + **kwargs + )) - self.exit_json(changed=False, log=log, log_lines=log.split('\n')) + module.exit_json(changed=False, log=log, log_lines=log.split('\n')) - def extract_selectors(self, instance): - # Parses selectors on an object based on the specifications documented here: - # https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#label-selectors - selectors = [] - if not instance.spec.selector: - self.fail(msg='{0} {1} does not support the log subresource directly, and no Pod selector was found on the object'.format( - '/'.join(instance.group, instance.apiVersion), instance.kind)) - if not (instance.spec.selector.matchLabels or instance.spec.selector.matchExpressions): - # A few resources (like DeploymentConfigs) just use a simple key:value style instead of supporting expressions - for k, v in dict(instance.spec.selector).items(): - selectors.append('{0}={1}'.format(k, v)) - return selectors - - if instance.spec.selector.matchLabels: - for k, v in dict(instance.spec.selector.matchLabels).items(): - selectors.append('{0}={1}'.format(k, v)) - - if instance.spec.selector.matchExpressions: - for expression in instance.spec.selector.matchExpressions: - operator = expression.operator - - if operator == 'Exists': - selectors.append(expression.key) - elif operator == 'DoesNotExist': - selectors.append('!{0}'.format(expression.key)) - elif operator in ['In', 'NotIn']: - selectors.append('{key} {operator} {values}'.format( - key=expression.key, - operator=operator.lower(), - values='({0})'.format(', '.join(expression.values)) - )) - else: - self.fail(msg='The k8s_log module does not support the {0} matchExpression operator'.format(operator.lower())) +def extract_selectors(module, instance): + # Parses selectors on an object based on the specifications documented here: + # https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#label-selectors + selectors = [] + if not instance.spec.selector: + module.fail(msg='{0} {1} does not support the log subresource directly, and no Pod selector was found on the object'.format( + '/'.join(instance.group, instance.apiVersion), instance.kind)) + if not (instance.spec.selector.matchLabels or instance.spec.selector.matchExpressions): + # A few resources (like DeploymentConfigs) just use a simple key:value style instead of supporting expressions + for k, v in dict(instance.spec.selector).items(): + selectors.append('{0}={1}'.format(k, v)) return selectors + if instance.spec.selector.matchLabels: + for k, v in dict(instance.spec.selector.matchLabels).items(): + selectors.append('{0}={1}'.format(k, v)) + + if instance.spec.selector.matchExpressions: + for expression in instance.spec.selector.matchExpressions: + operator = expression.operator + + if operator == 'Exists': + selectors.append(expression.key) + elif operator == 'DoesNotExist': + selectors.append('!{0}'.format(expression.key)) + elif operator in ['In', 'NotIn']: + selectors.append('{key} {operator} {values}'.format( + key=expression.key, + operator=operator.lower(), + values='({0})'.format(', '.join(expression.values)) + )) + else: + module.fail(msg='The k8s_log module does not support the {0} matchExpression operator'.format(operator.lower())) + + return selectors + def serialize_log(response): if PY2: @@ -229,7 +214,13 @@ def serialize_log(response): def main(): - KubernetesLogModule().execute_module() + module = AnsibleModule(argument_spec=argspec(), supports_check_mode=True) + from ansible_collections.community.kubernetes.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__': diff --git a/plugins/modules/k8s_rollback.py b/plugins/modules/k8s_rollback.py index 7ccd4153..20465b44 100644 --- a/plugins/modules/k8s_rollback.py +++ b/plugins/modules/k8s_rollback.py @@ -78,127 +78,117 @@ rollback_info: import copy -from ansible.module_utils.basic import AnsibleModule -from ansible_collections.community.kubernetes.plugins.module_utils.common import ( - K8sAnsibleMixin, AUTH_ARG_SPEC, NAME_ARG_SPEC) +from ansible_collections.community.kubernetes.plugins.module_utils.ansiblemodule import AnsibleModule +from ansible_collections.community.kubernetes.plugins.module_utils.args_common import ( + AUTH_ARG_SPEC, NAME_ARG_SPEC) -class KubernetesRollbackModule(K8sAnsibleMixin): +def get_managed_resource(module): + managed_resource = {} - def __init__(self): - module = AnsibleModule( - argument_spec=self.argspec, - supports_check_mode=True, + kind = module.params['kind'] + if kind == "DaemonSet": + managed_resource['kind'] = "ControllerRevision" + managed_resource['api_version'] = "apps/v1" + elif kind == "Deployment": + managed_resource['kind'] = "ReplicaSet" + managed_resource['api_version'] = "apps/v1" + else: + module.fail(msg="Cannot perform rollback on resource of kind {0}".format(kind)) + return managed_resource + + +def execute_module(module, k8s_ansible_mixin): + results = [] + + resources = k8s_ansible_mixin.kubernetes_facts( + module.params['kind'], + module.params['api_version'], + module.params['name'], + module.params['namespace'], + module.params['label_selectors'], + module.params['field_selectors']) + + for resource in resources['resources']: + result = perform_action(module, k8s_ansible_mixin, resource) + results.append(result) + + module.exit_json(**{ + 'changed': True, + 'rollback_info': results + }) + + +def perform_action(module, k8s_ansible_mixin, resource): + if module.params['kind'] == "DaemonSet": + current_revision = resource['metadata']['generation'] + elif module.params['kind'] == "Deployment": + current_revision = resource['metadata']['annotations']['deployment.kubernetes.io/revision'] + + managed_resource = get_managed_resource(module) + managed_resources = k8s_ansible_mixin.kubernetes_facts( + managed_resource['kind'], + managed_resource['api_version'], + '', + module.params['namespace'], + resource['spec'] + ['selector'] + ['matchLabels'], + '') + + prev_managed_resource = get_previous_revision(managed_resources['resources'], + current_revision) + + if module.params['kind'] == "Deployment": + del prev_managed_resource['spec']['template']['metadata']['labels']['pod-template-hash'] + + resource_patch = [{ + "op": "replace", + "path": "/spec/template", + "value": prev_managed_resource['spec']['template'] + }, { + "op": "replace", + "path": "/metadata/annotations", + "value": { + "deployment.kubernetes.io/revision": prev_managed_resource['metadata']['annotations']['deployment.kubernetes.io/revision'] + } + }] + + api_target = 'deployments' + content_type = 'application/json-patch+json' + elif module.params['kind'] == "DaemonSet": + resource_patch = prev_managed_resource["data"] + + api_target = 'daemonsets' + content_type = 'application/strategic-merge-patch+json' + + rollback = k8s_ansible_mixin.client.request( + "PATCH", + "/apis/{0}/namespaces/{1}/{2}/{3}" + .format(module.params['api_version'], + module.params['namespace'], + api_target, + module.params['name']), + body=resource_patch, + content_type=content_type) + + result = {'changed': True} + result['method'] = 'patch' + result['body'] = resource_patch + result['resources'] = rollback.to_dict() + return result + + +def argspec(): + args = copy.deepcopy(AUTH_ARG_SPEC) + args.update(NAME_ARG_SPEC) + args.update( + dict( + label_selectors=dict(type='list', elements='str', default=[]), + field_selectors=dict(type='list', elements='str', default=[]), ) - self.module = module - self.params = self.module.params - self.fail_json = self.module.fail_json - self.fail = self.module.fail_json - self.exit_json = self.module.exit_json - super(KubernetesRollbackModule, self).__init__() - - self.kind = self.params['kind'] - self.api_version = self.params['api_version'] - self.name = self.params['name'] - self.namespace = self.params['namespace'] - self.managed_resource = {} - - if self.kind == "DaemonSet": - self.managed_resource['kind'] = "ControllerRevision" - self.managed_resource['api_version'] = "apps/v1" - elif self.kind == "Deployment": - self.managed_resource['kind'] = "ReplicaSet" - self.managed_resource['api_version'] = "apps/v1" - else: - self.fail(msg="Cannot perform rollback on resource of kind {0}".format(self.kind)) - - def execute_module(self): - results = [] - self.client = self.get_api_client() - - resources = self.kubernetes_facts(self.kind, - self.api_version, - self.name, - self.namespace, - self.params['label_selectors'], - self.params['field_selectors']) - - for resource in resources['resources']: - result = self.perform_action(resource) - results.append(result) - - self.exit_json(**{ - 'changed': True, - 'rollback_info': results - }) - - def perform_action(self, resource): - if self.kind == "DaemonSet": - current_revision = resource['metadata']['generation'] - elif self.kind == "Deployment": - current_revision = resource['metadata']['annotations']['deployment.kubernetes.io/revision'] - - managed_resources = self.kubernetes_facts(self.managed_resource['kind'], - self.managed_resource['api_version'], - '', - self.namespace, - resource['spec'] - ['selector'] - ['matchLabels'], - '') - - prev_managed_resource = get_previous_revision(managed_resources['resources'], - current_revision) - - if self.kind == "Deployment": - del prev_managed_resource['spec']['template']['metadata']['labels']['pod-template-hash'] - - resource_patch = [{ - "op": "replace", - "path": "/spec/template", - "value": prev_managed_resource['spec']['template'] - }, { - "op": "replace", - "path": "/metadata/annotations", - "value": { - "deployment.kubernetes.io/revision": prev_managed_resource['metadata']['annotations']['deployment.kubernetes.io/revision'] - } - }] - - api_target = 'deployments' - content_type = 'application/json-patch+json' - elif self.kind == "DaemonSet": - resource_patch = prev_managed_resource["data"] - - api_target = 'daemonsets' - content_type = 'application/strategic-merge-patch+json' - - rollback = self.client.request("PATCH", - "/apis/{0}/namespaces/{1}/{2}/{3}" - .format(self.api_version, - self.namespace, - api_target, - self.name), - body=resource_patch, - content_type=content_type) - - result = {'changed': True} - result['method'] = 'patch' - result['body'] = resource_patch - result['resources'] = rollback.to_dict() - return result - - @property - def argspec(self): - args = copy.deepcopy(AUTH_ARG_SPEC) - args.update(NAME_ARG_SPEC) - args.update( - dict( - label_selectors=dict(type='list', elements='str', default=[]), - field_selectors=dict(type='list', elements='str', default=[]), - ) - ) - return args + ) + return args def get_previous_revision(all_resources, current_revision): @@ -217,7 +207,12 @@ def get_previous_revision(all_resources, current_revision): def main(): - KubernetesRollbackModule().execute_module() + module = AnsibleModule(argument_spec=argspec(), supports_check_mode=True) + from ansible_collections.community.kubernetes.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__': diff --git a/plugins/modules/k8s_scale.py b/plugins/modules/k8s_scale.py index 9e63366a..c15bd5e0 100644 --- a/plugins/modules/k8s_scale.py +++ b/plugins/modules/k8s_scale.py @@ -118,11 +118,129 @@ result: sample: 48 ''' -from ansible_collections.community.kubernetes.plugins.module_utils.scale import KubernetesAnsibleScaleModule +import copy + +from ansible_collections.community.kubernetes.plugins.module_utils.ansiblemodule import AnsibleModule +from ansible_collections.community.kubernetes.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}, +} + + +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') + + wait = module.params.get('wait') + wait_time = module.params.get('wait_timeout') + existing = None + existing_count = None + return_attributes = dict(changed=False, result=dict(), diff=dict()) + if wait: + return_attributes['duration'] = 0 + + resource = k8s_ansible_mixin.find_resource(kind, api_version, fail=True) + + from ansible_collections.community.kubernetes.plugins.module_utils.common import NotFoundError + + try: + existing = resource.get(name=name, namespace=namespace) + return_attributes['result'] = existing.to_dict() + except NotFoundError as exc: + module.fail_json(msg='Failed to retrieve requested object: {0}'.format(exc), + error=exc.value.get('status')) + + if module.params['kind'] == 'job': + existing_count = existing.spec.parallelism + elif hasattr(existing.spec, 'replicas'): + existing_count = existing.spec.replicas + + if existing_count is None: + module.fail_json(msg='Failed to retrieve the available count for the requested object.') + + if resource_version and resource_version != existing.metadata.resourceVersion: + module.exit_json(**return_attributes) + + if current_replicas is not None and existing_count != current_replicas: + module.exit_json(**return_attributes) + + if existing_count != replicas: + return_attributes['changed'] = True + if not module.check_mode: + if module.params['kind'] == 'job': + existing.spec.parallelism = replicas + return_attributes['result'] = resource.patch(existing.to_dict()).to_dict() + else: + return_attributes = scale(module, k8s_ansible_mixin, resource, existing, replicas, wait, wait_time) + + module.exit_json(**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) + return args + + +def scale(module, k8s_ansible_mixin, resource, existing_object, replicas, wait, wait_time): + 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) + + 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() + match, diffs = k8s_ansible_mixin.diff_objects(existing.to_dict(), k8s_obj) + result = dict() + result['result'] = k8s_obj + result['changed'] = not match + result['diff'] = diffs + + if wait: + success, result['result'], result['duration'] = k8s_ansible_mixin.wait(resource, scale_obj, 5, wait_time) + if not success: + module.fail_json(msg="Resource scaling timed out", **result) + return result def main(): - KubernetesAnsibleScaleModule().execute_module() + module = AnsibleModule(argument_spec=argspec(), supports_check_mode=True) + from ansible_collections.community.kubernetes.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__': diff --git a/plugins/modules/k8s_service.py b/plugins/modules/k8s_service.py index ad2ed240..945df2d4 100644 --- a/plugins/modules/k8s_service.py +++ b/plugins/modules/k8s_service.py @@ -145,14 +145,12 @@ result: ''' import copy -import traceback from collections import defaultdict -from ansible.module_utils.basic import AnsibleModule -from ansible_collections.community.kubernetes.plugins.module_utils.common import ( - K8sAnsibleMixin, AUTH_ARG_SPEC, COMMON_ARG_SPEC, RESOURCE_ARG_SPEC) - +from ansible_collections.community.kubernetes.plugins.module_utils.ansiblemodule import AnsibleModule +from ansible_collections.community.kubernetes.plugins.module_utils.args_common import ( + AUTH_ARG_SPEC, COMMON_ARG_SPEC, RESOURCE_ARG_SPEC) SERVICE_ARG_SPEC = { 'apply': { @@ -173,100 +171,69 @@ SERVICE_ARG_SPEC = { } -class KubernetesService(K8sAnsibleMixin): - def __init__(self, *args, **kwargs): - mutually_exclusive = [ - ('resource_definition', 'src'), - ('merge_type', 'apply'), - ] - - module = AnsibleModule( - argument_spec=self.argspec, - mutually_exclusive=mutually_exclusive, - supports_check_mode=True, - ) - - self.module = module - self.check_mode = self.module.check_mode - self.params = self.module.params - self.fail_json = self.module.fail_json - self.fail = self.module.fail_json - self.exit_json = self.module.exit_json - - super(KubernetesService, self).__init__(*args, **kwargs) - - self.client = None - self.warnings = [] - - self.kind = self.params.get('kind') - self.api_version = self.params.get('api_version') - self.name = self.params.get('name') - self.namespace = self.params.get('namespace') - - self.check_library_version() - self.set_resource_definitions() - - @staticmethod - def merge_dicts(x, y): - for k in set(x.keys()).union(y.keys()): - if k in x and k in y: - if isinstance(x[k], dict) and isinstance(y[k], dict): - yield (k, dict(KubernetesService.merge_dicts(x[k], y[k]))) - else: - yield (k, y[k]) - elif k in x: - yield (k, x[k]) +def merge_dicts(x, y): + for k in set(x.keys()).union(y.keys()): + if k in x and k in y: + if isinstance(x[k], dict) and isinstance(y[k], dict): + yield (k, dict(merge_dicts(x[k], y[k]))) else: yield (k, y[k]) + elif k in x: + yield (k, x[k]) + else: + yield (k, y[k]) - @property - def argspec(self): - """ argspec property builder """ - argument_spec = copy.deepcopy(AUTH_ARG_SPEC) - argument_spec.update(COMMON_ARG_SPEC) - argument_spec.update(RESOURCE_ARG_SPEC) - argument_spec.update(SERVICE_ARG_SPEC) - return argument_spec - def execute_module(self): - """ Module execution """ - self.client = self.get_api_client() +def argspec(): + """ argspec property builder """ + argument_spec = copy.deepcopy(AUTH_ARG_SPEC) + argument_spec.update(COMMON_ARG_SPEC) + argument_spec.update(RESOURCE_ARG_SPEC) + argument_spec.update(SERVICE_ARG_SPEC) + return argument_spec - api_version = 'v1' - selector = self.params.get('selector') - service_type = self.params.get('type') - ports = self.params.get('ports') - definition = defaultdict(defaultdict) +def execute_module(module, k8s_ansible_mixin): + """ Module execution """ + k8s_ansible_mixin.set_resource_definitions(module) - definition['kind'] = 'Service' - definition['apiVersion'] = api_version + api_version = 'v1' + selector = module.params.get('selector') + service_type = module.params.get('type') + ports = module.params.get('ports') - def_spec = definition['spec'] - def_spec['type'] = service_type - def_spec['ports'] = ports - def_spec['selector'] = selector + definition = defaultdict(defaultdict) - def_meta = definition['metadata'] - def_meta['name'] = self.params.get('name') - def_meta['namespace'] = self.params.get('namespace') + definition['kind'] = 'Service' + definition['apiVersion'] = api_version - # 'resource_definition:' has lower priority than module parameters - definition = dict(self.merge_dicts(self.resource_definitions[0], definition)) + def_spec = definition['spec'] + def_spec['type'] = service_type + def_spec['ports'] = ports + def_spec['selector'] = selector - resource = self.find_resource('Service', api_version, fail=True) - definition = self.set_defaults(resource, definition) - result = self.perform_action(resource, definition) + def_meta = definition['metadata'] + def_meta['name'] = module.params.get('name') + def_meta['namespace'] = module.params.get('namespace') - self.exit_json(**result) + # 'resource_definition:' has lower priority than module parameters + definition = dict(merge_dicts(k8s_ansible_mixin.resource_definitions[0], definition)) + + resource = k8s_ansible_mixin.find_resource('Service', api_version, fail=True) + definition = k8s_ansible_mixin.set_defaults(resource, definition) + result = k8s_ansible_mixin.perform_action(resource, definition) + + module.exit_json(**result) def main(): - module = KubernetesService() - try: - module.execute_module() - except Exception as e: - module.fail_json(msg=str(e), exception=traceback.format_exc()) + module = AnsibleModule(argument_spec=argspec(), supports_check_mode=True) + from ansible_collections.community.kubernetes.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__':