diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4e490c60..2807a59c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -84,8 +84,10 @@ jobs: with: python-version: ${{ matrix.python_version }} + # The 3.3.0 release of molecule introduced a breaking change. See + # https://github.com/ansible-community/molecule/issues/3083 - name: Install molecule and openshift dependencies - run: pip install ansible molecule yamllint openshift flake8 + run: pip install ansible "molecule<3.3.0" yamllint openshift flake8 # The latest release doesn't work with Molecule currently. # See: https://github.com/ansible-community/molecule/issues/2757 @@ -177,7 +179,7 @@ jobs: python-version: ${{ matrix.python_version }} - name: Install molecule and openshift dependencies - run: pip install "ansible>=2.9.0,<2.10.0" molecule yamllint openshift flake8 + run: pip install "ansible>=2.9.0,<2.10.0" "molecule<3.3.0" yamllint openshift flake8 - name: Create default collection path symlink run: | diff --git a/changelogs/fragments/379-remove-kubernetesrawmodule.yaml b/changelogs/fragments/379-remove-kubernetesrawmodule.yaml new file mode 100644 index 00000000..680f544a --- /dev/null +++ b/changelogs/fragments/379-remove-kubernetesrawmodule.yaml @@ -0,0 +1,2 @@ +minor_changes: + - remove the deprecated ``KubernetesRawModule`` class (https://github.com/ansible-collections/community.kubernetes/issues/232). diff --git a/changelogs/fragments/387-fix-helm-ignoring-context.yaml b/changelogs/fragments/387-fix-helm-ignoring-context.yaml new file mode 100644 index 00000000..8e696b06 --- /dev/null +++ b/changelogs/fragments/387-fix-helm-ignoring-context.yaml @@ -0,0 +1,2 @@ +bugfixes: + - helm - fix helm ignoring the kubeconfig context when passed through the ``context`` param or the ``K8S_AUTH_CONTEXT`` environment variable (https://github.com/ansible-collections/community.kubernetes/issues/385). diff --git a/changelogs/fragments/k8s_inventory.yml b/changelogs/fragments/k8s_inventory.yml new file mode 100644 index 00000000..7800c288 --- /dev/null +++ b/changelogs/fragments/k8s_inventory.yml @@ -0,0 +1,2 @@ +bugfixes: +- k8s - fix get_api_client API in k8s inventory plugin (https://github.com/ansible-collections/community.kubernetes/pull/395). diff --git a/meta/runtime.yml b/meta/runtime.yml index a0329447..9f7fee5d 100644 --- a/meta/runtime.yml +++ b/meta/runtime.yml @@ -43,6 +43,9 @@ plugin_routing: redirect: community.kubernetes.k8s_info k8s_service: redirect: community.kubernetes.k8s_info + inventory: + openshift: + redirect: community.okd.openshift modules: k8s_auth: redirect: community.okd.k8s_auth diff --git a/molecule/default/roles/helm/tasks/tests_chart.yml b/molecule/default/roles/helm/tasks/tests_chart.yml index e0644048..2199991c 100644 --- a/molecule/default/roles/helm/tasks/tests_chart.yml +++ b/molecule/default/roles/helm/tasks/tests_chart.yml @@ -348,6 +348,24 @@ that: result.stat.exists + - name: Release using non-existent context + helm: + binary_path: "{{ helm_binary }}" + name: test + chart_ref: "{{ chart_source }}" + chart_version: "{{ chart_source_version | default(omit) }}" + namespace: "{{ helm_namespace }}" + create_namespace: true + context: does-not-exist + ignore_errors: yes + register: result + + - name: Assert that release fails with non-existent context + assert: + that: + - result is failed + - "'context \"does-not-exist\" does not exist' in result.stderr" + always: - name: Clean up temp dir file: diff --git a/plugins/inventory/k8s.py b/plugins/inventory/k8s.py index ede54375..cc106fce 100644 --- a/plugins/inventory/k8s.py +++ b/plugins/inventory/k8s.py @@ -23,7 +23,7 @@ plugin: description: token that ensures this is a source file for the 'k8s' plugin. required: True - choices: ['k8s'] + choices: ['community.kubernetes.k8s', 'k8s'] connections: description: - Optional list of cluster connection settings. If no connections are provided, the default @@ -117,7 +117,7 @@ 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 @@ def fetch_objects(self, connections): 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 @@ def fetch_objects(self, connections): 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/inventory/openshift.py b/plugins/inventory/openshift.py deleted file mode 100644 index f6c393bd..00000000 --- a/plugins/inventory/openshift.py +++ /dev/null @@ -1,202 +0,0 @@ -# Copyright (c) 2018 Ansible Project -# 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 = ''' - name: openshift - plugin_type: inventory - author: - - Chris Houseknecht <@chouseknecht> - - short_description: OpenShift inventory source - - description: - - Fetch containers, services and routes for one or more clusters - - Groups by cluster name, namespace, namespace_services, namespace_pods, namespace_routes, and labels - - Uses openshift.(yml|yaml) YAML configuration file to set parameter values. - - options: - plugin: - description: token that ensures this is a source file for the 'openshift' plugin. - required: True - choices: ['openshift'] - connections: - description: - - Optional list of cluster connection settings. If no connections are provided, the default - I(~/.kube/config) and active context will be used, and objects will be returned for all namespaces - the active user is authorized to access. - suboptions: - name: - description: - - Optional name to assign to the cluster. If not provided, a name is constructed from the server - and port. - kubeconfig: - description: - - Path to an existing Kubernetes config file. If not provided, and no other connection - options are provided, the OpenShift client will attempt to load the default - configuration file from I(~/.kube/config.json). Can also be specified via K8S_AUTH_KUBECONFIG - environment variable. - context: - description: - - The name of a context found in the config file. Can also be specified via K8S_AUTH_CONTEXT environment - variable. - host: - description: - - Provide a URL for accessing the API. Can also be specified via K8S_AUTH_HOST environment variable. - api_key: - description: - - Token used to authenticate with the API. Can also be specified via K8S_AUTH_API_KEY environment - variable. - username: - description: - - Provide a username for authenticating with the API. Can also be specified via K8S_AUTH_USERNAME - environment variable. - password: - description: - - Provide a password for authenticating with the API. Can also be specified via K8S_AUTH_PASSWORD - environment variable. - client_cert: - description: - - Path to a certificate used to authenticate with the API. Can also be specified via K8S_AUTH_CERT_FILE - environment variable. - aliases: [ cert_file ] - client_key: - description: - - Path to a key file used to authenticate with the API. Can also be specified via K8S_AUTH_KEY_FILE - environment variable. - aliases: [ key_file ] - ca_cert: - description: - - Path to a CA certificate used to authenticate with the API. Can also be specified via - K8S_AUTH_SSL_CA_CERT environment variable. - aliases: [ ssl_ca_cert ] - validate_certs: - description: - - "Whether or not to verify the API server's SSL certificates. Can also be specified via - K8S_AUTH_VERIFY_SSL environment variable." - type: bool - aliases: [ verify_ssl ] - namespaces: - description: - - List of namespaces. If not specified, will fetch all containers for all namespaces user is authorized - to access. - - requirements: - - "python >= 2.7" - - "openshift >= 0.6" - - "PyYAML >= 3.11" -''' - -EXAMPLES = ''' -# File must be named openshift.yaml or openshift.yml - -# Authenticate with token, and return all pods and services for all namespaces -plugin: community.kubernetes.openshift -connections: - - host: https://192.168.64.4:8443 - api_key: xxxxxxxxxxxxxxxx - verify_ssl: false - -# Use default config (~/.kube/config) file and active context, and return objects for a specific namespace -plugin: community.kubernetes.openshift -connections: - - namespaces: - - testing - -# Use a custom config file, and a specific context. -plugin: community.kubernetes.openshift -connections: - - kubeconfig: /path/to/config - context: 'awx/192-168-64-4:8443/developer' -''' - -from ansible_collections.community.kubernetes.plugins.inventory.k8s import K8sInventoryException, InventoryModule as K8sInventoryModule, format_dynamic_api_exc - -try: - from openshift.dynamic.exceptions import DynamicApiError -except ImportError: - pass - - -class InventoryModule(K8sInventoryModule): - NAME = 'community.kubernetes.openshift' - - transport = 'oc' - - def fetch_objects(self, connections): - super(InventoryModule, self).fetch_objects(connections) - - if connections: - if not isinstance(connections, list): - raise K8sInventoryException("Expecting connections to be a list.") - - for connection in connections: - client = self.get_api_client(**connection) - name = connection.get('name', self.get_default_host_name(client.configuration.host)) - if connection.get('namespaces'): - namespaces = connection['namespaces'] - else: - namespaces = self.get_available_namespaces(client) - for namespace in namespaces: - self.get_routes_for_namespace(client, name, namespace) - else: - client = self.get_api_client() - name = self.get_default_host_name(client.configuration.host) - namespaces = self.get_available_namespaces(client) - for namespace in namespaces: - self.get_routes_for_namespace(client, name, namespace) - - def get_routes_for_namespace(self, client, name, namespace): - v1_route = client.resources.get(api_version='v1', kind='Route') - try: - obj = v1_route.get(namespace=namespace) - except DynamicApiError as exc: - self.display.debug(exc) - raise K8sInventoryException('Error fetching Routes list: %s' % format_dynamic_api_exc(exc)) - - namespace_group = 'namespace_{0}'.format(namespace) - namespace_routes_group = '{0}_routes'.format(namespace_group) - - self.inventory.add_group(name) - self.inventory.add_group(namespace_group) - self.inventory.add_child(name, namespace_group) - self.inventory.add_group(namespace_routes_group) - self.inventory.add_child(namespace_group, namespace_routes_group) - for route in obj.items: - route_name = route.metadata.name - route_annotations = {} if not route.metadata.annotations else dict(route.metadata.annotations) - - self.inventory.add_host(route_name) - - if route.metadata.labels: - # create a group for each label_value - for key, value in route.metadata.labels: - group_name = 'label_{0}_{1}'.format(key, value) - self.inventory.add_group(group_name) - self.inventory.add_child(group_name, route_name) - route_labels = dict(route.metadata.labels) - else: - route_labels = {} - - self.inventory.add_child(namespace_routes_group, route_name) - - # add hostvars - self.inventory.set_variable(route_name, 'labels', route_labels) - self.inventory.set_variable(route_name, 'annotations', route_annotations) - self.inventory.set_variable(route_name, 'cluster_name', route.metadata.clusterName) - self.inventory.set_variable(route_name, 'object_type', 'route') - self.inventory.set_variable(route_name, 'self_link', route.metadata.selfLink) - self.inventory.set_variable(route_name, 'resource_version', route.metadata.resourceVersion) - self.inventory.set_variable(route_name, 'uid', route.metadata.uid) - - if route.spec.host: - self.inventory.set_variable(route_name, 'host', route.spec.host) - - if route.spec.path: - self.inventory.set_variable(route_name, 'path', route.spec.path) - - if hasattr(route.spec.port, 'targetPort') and route.spec.port.targetPort: - self.inventory.set_variable(route_name, 'port', dict(route.spec.port)) 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.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 @@ def fail(self, msg=None): 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..40e9564e 100644 --- a/plugins/module_utils/common.py +++ b/plugins/module_utils/common.py @@ -26,6 +26,7 @@ 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,109 @@ K8S_IMP_ERR = traceback.format_exc() -def list_dict_str(value): - if isinstance(value, (list, dict, string_types)): - return value - raise TypeError - - -ARG_ATTRIBUTES_BLACKLIST = ('property_path',) - -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', - }, -} - -NAME_ARG_SPEC = { - 'kind': {}, - 'name': {}, - 'namespace': {}, - 'api_version': { - 'default': 'v1', - 'aliases': ['api', 'version'], - }, -} - -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(type='str', default="True", choices=["True", "False", "Unknown"]), - reason=dict() - ) - ) -) - -DELETE_OPTS_ARG_SPEC = { - 'propagationPolicy': { - 'choices': ['Foreground', 'Background', 'Orphan'], - }, - 'gracePeriodSeconds': { - 'type': 'int', - }, - 'preconditions': { - 'type': 'dict', - 'options': { - 'resourceVersion': { - 'type': 'str', - }, - 'uid': { - 'type': 'str', - } - } - } -} - - -# 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', -} - - -class K8sAnsibleMixin(object): +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 - def __init__(self, *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)) - 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 get_api_client(module=None, **kwargs): + auth = {} - def auth_set(*names): - return all([auth.get(name) for name in names]) + def _raise_or_fail(exc, msg): + if module: + module.fail_json(msg % to_native(exc)) + else: + raise exc + + # 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) + elif arg_name in kwargs and kwargs.get(arg_name) is not None: + auth[true_name] = kwargs.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 + + 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: + _raise_or_fail(err, 'Failed to load kubeconfig due to %s') - 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'): + 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)) - 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)) + _raise_or_fail(err, 'Failed to load kubeconfig due to %s') - # 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() + # 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) - 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 - 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)) + 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 + + +get_api_client._pool = {} + + +class K8sAnsibleMixin(object): + + def __init__(self, module, *args, **kwargs): + if not HAS_K8S_MODULE_HELPER: + 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: + 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 +422,8 @@ def _resource_absent(resource): 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 +438,7 @@ def set_resource_definitions(self): 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 +448,12 @@ def set_resource_definitions(self): 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 +486,7 @@ def execute_module(self): 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/helm.py b/plugins/module_utils/helm.py index e5a7e901..021a74da 100644 --- a/plugins/module_utils/helm.py +++ b/plugins/module_utils/helm.py @@ -28,8 +28,8 @@ def prepare_helm_environ_update(module): environ_update = {} file_to_cleam_up = None kubeconfig_path = module.params.get('kubeconfig') - if module.params.get('kube_context') is not None: - environ_update["HELM_KUBECONTEXT"] = module.params.get('kube_context') + if module.params.get('context') is not None: + environ_update["HELM_KUBECONTEXT"] = module.params.get('context') if module.params.get('release_namespace'): environ_update["HELM_NAMESPACE"] = module.params.get('release_namespace') if module.params.get("api_key"): diff --git a/plugins/module_utils/raw.py b/plugins/module_utils/raw.py deleted file mode 100644 index a353f1cb..00000000 --- a/plugins/module_utils/raw.py +++ /dev/null @@ -1,97 +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 ( - K8sAnsibleMixin, AUTH_ARG_SPEC, COMMON_ARG_SPEC, RESOURCE_ARG_SPEC, NAME_ARG_SPEC) - - -class KubernetesRawModule(K8sAnsibleMixin): - # NOTE: This class KubernetesRawModule is deprecated in favor of - # class K8sAnsibleMixin and will be removed 2.0.0 release. - # Please use K8sAnsibleMixin instead. - @property - def validate_spec(self): - return dict( - fail_on_error=dict(type='bool'), - version=dict(), - strict=dict(type='bool', default=True) - ) - - @property - def condition_spec(self): - return dict( - type=dict(), - status=dict(default=True, choices=[True, False, "Unknown"]), - reason=dict() - ) - - @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['merge_type'] = dict(type='list', elements='str', choices=['json', 'merge', 'strategic-merge']) - argument_spec['wait'] = dict(type='bool', default=False) - argument_spec['wait_sleep'] = dict(type='int', default=5) - argument_spec['wait_timeout'] = dict(type='int', default=120) - argument_spec['wait_condition'] = dict(type='dict', default=None, options=self.condition_spec) - 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) - return argument_spec - - def __init__(self, k8s_kind=None, *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 - - self.module.warn("class KubernetesRawModule is deprecated" - " and will be removed in 2.0.0. Please use K8sAnsibleMixin instead.") - super(KubernetesRawModule, 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() 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..4baefa14 100644 --- a/plugins/modules/k8s.py +++ b/plugins/modules/k8s.py @@ -221,6 +221,23 @@ community.kubernetes.k8s: state: present src: ~/metrics-server.yaml + +# Wait for a Deployment to pause before continuing +- name: Pause a Deployment. + community.kubernetes.k8s: + definition: + apiVersion: apps/v1 + kind: Deployment + metadata: + name: example + namespace: testing + spec: + paused: True + wait: yes + wait_condition: + type: Progressing + status: Unknown + reason: DeploymentPaused ''' RETURN = r''' @@ -263,74 +280,68 @@ 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) - - -class KubernetesModule(K8sAnsibleMixin): - - @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 __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, - ) - - 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(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() +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) + + +def validate_spec(): + return dict( + fail_on_error=dict(type='bool'), + version=dict(), + strict=dict(type='bool', default=True) + ) + + +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 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 = [] + + 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') + + 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 a80a6d62..f15fce40 100644 --- a/plugins/modules/k8s_cluster_info.py +++ b/plugins/modules/k8s_cluster_info.py @@ -144,89 +144,60 @@ 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 from collections import defaultdict - -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() - - -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 = defaultdict(dict) - for resource in list(self.client.resources): - resource = resource[0] - if isinstance(resource, ResourceList): - continue - key=resource.group_version if resource.group == '' else '/'.join([ resource.group,resource.group_version.split('/')[-1] ]) - results[key][resource.kind] = { - 'categories': resource.categories if resource.categories else [], - '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, - } - version_info = { - 'client': version, - 'server': self.client.version, +from ansible_collections.community.kubernetes.plugins.module_utils.args_common import (AUTH_ARG_SPEC) + + +def execute_module(module, client): + invalidate_cache = boolean(module.params.get('invalidate_cache', True), strict=False) + if invalidate_cache: + client.resources.invalidate_cache() + results = defaultdict(dict) + from openshift.dynamic.resource import ResourceList + for resource in list(client.resources): + resource = resource[0] + if isinstance(resource, ResourceList): + continue + key=resource.group_version if resource.group == '' else '/'.join([ resource.group,resource.group_version.split('/')[-1] ]) + results[key][resource.kind] = { + 'categories': resource.categories if resource.categories else [], + 'name': resource.name, + 'namespaced': resource.namespaced, + 'preferred': resource.preferred, + 'short_names': resource.short_names if resource.short_names else [], + 'singular_name': resource.singular_name, } - self.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 + 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) + + +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 @@ # 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 @@ pass -class KubernetesExecCommand(K8sAnsibleMixin): - - 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(self): - # Load kubernetes.client.Configuration - self.get_api_client() - 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']) - - 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 - ) +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 execute_module(module, k8s_ansible_mixin): + + # Load kubernetes.client.Configuration + api = core_v1_api.CoreV1Api() + + # 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']) + + 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 @@ 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) - - -class KubernetesInfoModule(K8sAnsibleMixin): - - def __init__(self, *args, **kwargs): - module = AnsibleModule( - argument_spec=self.argspec, - supports_check_mode=True, +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) + + +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 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 @@ 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, - ) - 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__() - - @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=[]), - ) +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=[]), ) - 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') - - 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) - - 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 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 - - kwargs = {} - if self.params.get('container'): - kwargs['query_params'] = dict(container=self.params['container']) - - 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')) - - 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())) - + ) + return args + + +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') + + 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: + 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: + 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 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 + )) + + module.exit_json(changed=False, log=log, log_lines=log.split('\n')) + + +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 @@ 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) - - -class KubernetesRollbackModule(K8sAnsibleMixin): - - def __init__(self): - module = AnsibleModule( - argument_spec=self.argspec, - supports_check_mode=True, +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) + + +def get_managed_resource(module): + managed_resource = {} + + 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 @@ 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 @@ ''' 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 @@ } -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 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 - def execute_module(self): - """ Module execution """ - self.client = self.get_api_client() - api_version = 'v1' - selector = self.params.get('selector') - service_type = self.params.get('type') - ports = self.params.get('ports') +def execute_module(module, k8s_ansible_mixin): + """ Module execution """ + k8s_ansible_mixin.set_resource_definitions(module) - definition = defaultdict(defaultdict) + api_version = 'v1' + selector = module.params.get('selector') + service_type = module.params.get('type') + ports = module.params.get('ports') - definition['kind'] = 'Service' - definition['apiVersion'] = api_version + definition = defaultdict(defaultdict) - def_spec = definition['spec'] - def_spec['type'] = service_type - def_spec['ports'] = ports - def_spec['selector'] = selector + definition['kind'] = 'Service' + definition['apiVersion'] = api_version - def_meta = definition['metadata'] - def_meta['name'] = self.params.get('name') - def_meta['namespace'] = self.params.get('namespace') + def_spec = definition['spec'] + def_spec['type'] = service_type + def_spec['ports'] = ports + def_spec['selector'] = selector - # 'resource_definition:' has lower priority than module parameters - definition = dict(self.merge_dicts(self.resource_definitions[0], definition)) + def_meta = definition['metadata'] + def_meta['name'] = module.params.get('name') + def_meta['namespace'] = module.params.get('namespace') - resource = self.find_resource('Service', api_version, fail=True) - definition = self.set_defaults(resource, definition) - result = self.perform_action(resource, definition) + # 'resource_definition:' has lower priority than module parameters + definition = dict(merge_dicts(k8s_ansible_mixin.resource_definitions[0], definition)) - self.exit_json(**result) + 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__': diff --git a/tests/integration/targets/kubernetes/defaults/main.yml b/tests/integration/targets/kubernetes/defaults/main.yml deleted file mode 100644 index e46ca26f..00000000 --- a/tests/integration/targets/kubernetes/defaults/main.yml +++ /dev/null @@ -1,2 +0,0 @@ ---- -k8s_openshift: true diff --git a/tests/integration/targets/kubernetes/tasks/main.yml b/tests/integration/targets/kubernetes/tasks/main.yml index de85850d..fb8be1a1 100644 --- a/tests/integration/targets/kubernetes/tasks/main.yml +++ b/tests/integration/targets/kubernetes/tasks/main.yml @@ -77,11 +77,3 @@ path: "{{ virtualenv }}" state: absent no_log: yes - -# Test openshift - -- debug: - var: k8s_openshift - -- include: openshift.yml - when: k8s_openshift | bool diff --git a/tests/integration/targets/kubernetes/tasks/openshift.yml b/tests/integration/targets/kubernetes/tasks/openshift.yml deleted file mode 100644 index af0f51a7..00000000 --- a/tests/integration/targets/kubernetes/tasks/openshift.yml +++ /dev/null @@ -1,62 +0,0 @@ ---- -# OpenShift Resources -- name: Create a project - k8s: - name: testing - kind: Project - api_version: v1 - apply: no - register: output - -- name: show output - debug: - var: output - -- name: Create deployment config - k8s: - state: present - inline: &dc - apiVersion: v1 - kind: DeploymentConfig - metadata: - name: elastic - labels: - app: galaxy - service: elastic - namespace: testing - spec: - template: - metadata: - labels: - app: galaxy - service: elastic - spec: - containers: - - name: elastic - volumeMounts: - - mountPath: /usr/share/elasticsearch/data - name: elastic-volume - command: ['elasticsearch'] - image: 'ansible/galaxy-elasticsearch:2.4.6' - volumes: - - name: elastic-volume - persistentVolumeClaim: - claimName: elastic-volume - replicas: 1 - strategy: - type: Rolling - register: output - -- name: Show output - debug: - var: output - -- name: Create deployment config again - k8s: - state: present - inline: *dc - register: output - -- name: DC creation should be idempotent - assert: - that: not output.changed