From 308956b313628d05dc3d3090f846deb4c325d3ee Mon Sep 17 00:00:00 2001 From: Abhijeet Kasurde Date: Sat, 19 Sep 2020 09:54:40 +0530 Subject: [PATCH] Replace KubernetesRawModule class (#231) * Migrate from KubernetesRawModule to K8sAnsibleMixin * Modified k8s.py * Modified k8s_service.py Signed-off-by: Abhijeet Kasurde --- plugins/module_utils/common.py | 339 ++++++++++++++++++++++++++++++- plugins/module_utils/raw.py | 361 ++------------------------------- plugins/modules/k8s.py | 75 ++++++- plugins/modules/k8s_service.py | 38 +++- 4 files changed, 457 insertions(+), 356 deletions(-) diff --git a/plugins/module_utils/common.py b/plugins/module_utils/common.py index 58bb0855..9503a194 100644 --- a/plugins/module_utils/common.py +++ b/plugins/module_utils/common.py @@ -18,22 +18,27 @@ from __future__ import absolute_import, division, print_function __metaclass__ = type -from datetime import datetime import time import os import traceback +import sys +from datetime import datetime +from distutils.version import LooseVersion from ansible.module_utils.basic import AnsibleModule, missing_required_lib from ansible.module_utils.six import iteritems, string_types from ansible.module_utils._text import to_native +from ansible.module_utils.common.dict_transformations import dict_merge K8S_IMP_ERR = None try: import kubernetes import openshift from openshift.dynamic import DynamicClient - from openshift.dynamic.exceptions import ResourceNotFoundError, ResourceNotUniqueError, NotFoundError + from openshift.dynamic.exceptions import ( + ResourceNotFoundError, ResourceNotUniqueError, NotFoundError, DynamicApiError, + ConflictError, ForbiddenError) HAS_K8S_MODULE_HELPER = True k8s_import_exception = None except ImportError as e: @@ -49,6 +54,22 @@ except ImportError: YAML_IMP_ERR = traceback.format_exc() HAS_YAML = False +K8S_CONFIG_HASH_IMP_ERR = None +try: + from openshift.helper.hashes import generate_hash + from openshift.dynamic.exceptions import KubernetesValidateMissing + HAS_K8S_CONFIG_HASH = True +except ImportError: + K8S_CONFIG_HASH_IMP_ERR = traceback.format_exc() + HAS_K8S_CONFIG_HASH = False + +HAS_K8S_APPLY = None +try: + from openshift.dynamic.apply import apply_object + HAS_K8S_APPLY = True +except ImportError: + HAS_K8S_APPLY = False + try: import urllib3 urllib3.disable_warnings() @@ -417,6 +438,320 @@ class K8sAnsibleMixin(object): implicit_definition['metadata']['namespace'] = self.namespace self.resource_definitions = [implicit_definition] + def check_library_version(self): + validate = self.params.get('validate') + if validate: + if LooseVersion(self.openshift_version) < LooseVersion("0.8.0"): + self.fail_json(msg="openshift >= 0.8.0 is required for validate") + self.append_hash = self.params.get('append_hash') + if self.append_hash: + if not HAS_K8S_CONFIG_HASH: + self.fail_json(msg=missing_required_lib("openshift >= 0.7.2", reason="for append_hash"), + exception=K8S_CONFIG_HASH_IMP_ERR) + if self.params['merge_type']: + if LooseVersion(self.openshift_version) < LooseVersion("0.6.2"): + self.fail_json(msg=missing_required_lib("openshift >= 0.6.2", reason="for merge_type")) + self.apply = self.params.get('apply', False) + if self.apply: + if not HAS_K8S_APPLY: + self.fail_json(msg=missing_required_lib("openshift >= 0.9.2", reason="for apply")) + + def flatten_list_kind(self, list_resource, definitions): + flattened = [] + parent_api_version = list_resource.group_version if list_resource else None + parent_kind = list_resource.kind[:-4] if list_resource else None + for definition in definitions.get('items', []): + resource = self.find_resource(definition.get('kind', parent_kind), definition.get('apiVersion', parent_api_version), fail=True) + flattened.append((resource, self.set_defaults(resource, definition))) + return flattened + + def execute_module(self): + changed = False + results = [] + try: + self.client = self.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)) + + flattened_definitions = [] + for definition in self.resource_definitions: + if definition is None: + continue + kind = definition.get('kind', self.kind) + api_version = definition.get('apiVersion', self.api_version) + if kind and kind.endswith('List'): + resource = self.find_resource(kind, api_version, fail=False) + flattened_definitions.extend(self.flatten_list_kind(resource, definition)) + else: + resource = self.find_resource(kind, api_version, fail=True) + flattened_definitions.append((resource, definition)) + + for (resource, definition) in flattened_definitions: + kind = definition.get('kind', self.kind) + api_version = definition.get('apiVersion', self.api_version) + definition = self.set_defaults(resource, definition) + self.warnings = [] + if self.params['validate'] is not None: + self.warnings = self.validate(definition) + result = self.perform_action(resource, definition) + result['warnings'] = self.warnings + changed = changed or result['changed'] + results.append(result) + + if len(results) == 1: + self.exit_json(**results[0]) + + self.exit_json(**{ + 'changed': changed, + 'result': { + 'results': results + } + }) + + def validate(self, resource): + def _prepend_resource_info(resource, msg): + return "%s %s: %s" % (resource['kind'], resource['metadata']['name'], msg) + + try: + warnings, errors = self.client.validate(resource, self.params['validate'].get('version'), self.params['validate'].get('strict')) + except KubernetesValidateMissing: + self.fail_json(msg="kubernetes-validate python library is required to validate resources") + + if errors and self.params['validate']['fail_on_error']: + self.fail_json(msg="\n".join([_prepend_resource_info(resource, error) for error in errors])) + else: + return [_prepend_resource_info(resource, msg) for msg in warnings + errors] + + def set_defaults(self, resource, definition): + definition['kind'] = resource.kind + definition['apiVersion'] = resource.group_version + metadata = definition.get('metadata', {}) + if self.name and not metadata.get('name'): + metadata['name'] = self.name + if resource.namespaced and self.namespace and not metadata.get('namespace'): + metadata['namespace'] = self.namespace + definition['metadata'] = metadata + return definition + + def perform_action(self, resource, definition): + result = {'changed': False, 'result': {}} + state = self.params.get('state', None) + force = self.params.get('force', False) + name = definition['metadata'].get('name') + namespace = definition['metadata'].get('namespace') + existing = None + wait = self.params.get('wait') + wait_sleep = self.params.get('wait_sleep') + wait_timeout = self.params.get('wait_timeout') + wait_condition = None + if self.params.get('wait_condition') and self.params['wait_condition'].get('type'): + wait_condition = self.params['wait_condition'] + + self.remove_aliases() + + try: + # ignore append_hash for resources other than ConfigMap and Secret + if self.append_hash and definition['kind'] in ['ConfigMap', 'Secret']: + name = '%s-%s' % (name, generate_hash(definition)) + definition['metadata']['name'] = name + params = dict(name=name) + if namespace: + params['namespace'] = namespace + existing = resource.get(**params) + except NotFoundError: + # Remove traceback so that it doesn't show up in later failures + try: + sys.exc_clear() + except AttributeError: + # no sys.exc_clear on python3 + pass + except ForbiddenError as exc: + if definition['kind'] in ['Project', 'ProjectRequest'] and state != 'absent': + return self.create_project_request(definition) + self.fail_json(msg='Failed to retrieve requested object: {0}'.format(exc.body), + error=exc.status, status=exc.status, reason=exc.reason) + except DynamicApiError as exc: + self.fail_json(msg='Failed to retrieve requested object: {0}'.format(exc.body), + error=exc.status, status=exc.status, reason=exc.reason) + except Exception as exc: + self.fail_json(msg='Failed to retrieve requested object: {0}'.format(to_native(exc)), + error='', status='', reason='') + + if state == 'absent': + result['method'] = "delete" + if not existing: + # The object already does not exist + return result + else: + # Delete the object + result['changed'] = True + if not self.check_mode: + try: + k8s_obj = resource.delete(**params) + result['result'] = k8s_obj.to_dict() + except DynamicApiError as exc: + self.fail_json(msg="Failed to delete object: {0}".format(exc.body), + error=exc.status, status=exc.status, reason=exc.reason) + if wait: + success, resource, duration = self.wait(resource, definition, wait_sleep, wait_timeout, 'absent') + result['duration'] = duration + if not success: + self.fail_json(msg="Resource deletion timed out", **result) + return result + else: + if self.apply: + if self.check_mode: + ignored, patch = apply_object(resource, definition) + if existing: + k8s_obj = dict_merge(existing.to_dict(), patch) + else: + k8s_obj = patch + else: + try: + k8s_obj = resource.apply(definition, namespace=namespace).to_dict() + except DynamicApiError as exc: + msg = "Failed to apply object: {0}".format(exc.body) + if self.warnings: + msg += "\n" + "\n ".join(self.warnings) + self.fail_json(msg=msg, error=exc.status, status=exc.status, reason=exc.reason) + success = True + result['result'] = k8s_obj + if wait and not self.check_mode: + success, result['result'], result['duration'] = self.wait(resource, definition, wait_sleep, wait_timeout, condition=wait_condition) + if existing: + existing = existing.to_dict() + else: + existing = {} + match, diffs = self.diff_objects(existing, result['result']) + result['changed'] = not match + result['diff'] = diffs + result['method'] = 'apply' + if not success: + self.fail_json(msg="Resource apply timed out", **result) + return result + + if not existing: + if self.check_mode: + k8s_obj = definition + else: + try: + k8s_obj = resource.create(definition, namespace=namespace).to_dict() + except ConflictError: + # Some resources, like ProjectRequests, can't be created multiple times, + # because the resources that they create don't match their kind + # In this case we'll mark it as unchanged and warn the user + self.warn("{0} was not found, but creating it returned a 409 Conflict error. This can happen \ + if the resource you are creating does not directly create a resource of the same kind.".format(name)) + return result + except DynamicApiError as exc: + msg = "Failed to create object: {0}".format(exc.body) + if self.warnings: + msg += "\n" + "\n ".join(self.warnings) + self.fail_json(msg=msg, error=exc.status, status=exc.status, reason=exc.reason) + success = True + result['result'] = k8s_obj + if wait and not self.check_mode: + success, result['result'], result['duration'] = self.wait(resource, definition, wait_sleep, wait_timeout, condition=wait_condition) + result['changed'] = True + result['method'] = 'create' + if not success: + self.fail_json(msg="Resource creation timed out", **result) + return result + + match = False + diffs = [] + + if existing and force: + if self.check_mode: + k8s_obj = definition + else: + try: + k8s_obj = resource.replace(definition, name=name, namespace=namespace, append_hash=self.append_hash).to_dict() + except DynamicApiError as exc: + msg = "Failed to replace object: {0}".format(exc.body) + if self.warnings: + msg += "\n" + "\n ".join(self.warnings) + self.fail_json(msg=msg, error=exc.status, status=exc.status, reason=exc.reason) + match, diffs = self.diff_objects(existing.to_dict(), k8s_obj) + success = True + result['result'] = k8s_obj + if wait and not self.check_mode: + success, result['result'], result['duration'] = self.wait(resource, definition, wait_sleep, wait_timeout, condition=wait_condition) + match, diffs = self.diff_objects(existing.to_dict(), result['result']) + result['changed'] = not match + result['method'] = 'replace' + result['diff'] = diffs + if not success: + self.fail_json(msg="Resource replacement timed out", **result) + return result + + # Differences exist between the existing obj and requested params + if self.check_mode: + k8s_obj = dict_merge(existing.to_dict(), definition) + else: + if LooseVersion(self.openshift_version) < LooseVersion("0.6.2"): + k8s_obj, error = self.patch_resource(resource, definition, existing, name, + namespace) + else: + for merge_type in self.params['merge_type'] or ['strategic-merge', 'merge']: + k8s_obj, error = self.patch_resource(resource, definition, existing, name, + namespace, merge_type=merge_type) + if not error: + break + if error: + self.fail_json(**error) + + success = True + result['result'] = k8s_obj + if wait and not self.check_mode: + success, result['result'], result['duration'] = self.wait(resource, definition, wait_sleep, wait_timeout, condition=wait_condition) + match, diffs = self.diff_objects(existing.to_dict(), result['result']) + result['changed'] = not match + result['method'] = 'patch' + result['diff'] = diffs + + if not success: + self.fail_json(msg="Resource update timed out", **result) + return result + + def patch_resource(self, resource, definition, existing, name, namespace, merge_type=None): + try: + params = dict(name=name, namespace=namespace) + if merge_type: + params['content_type'] = 'application/{0}-patch+json'.format(merge_type) + k8s_obj = resource.patch(definition, **params).to_dict() + match, diffs = self.diff_objects(existing.to_dict(), k8s_obj) + error = {} + return k8s_obj, {} + except DynamicApiError as exc: + msg = "Failed to patch object: {0}".format(exc.body) + if self.warnings: + msg += "\n" + "\n ".join(self.warnings) + error = dict(msg=msg, error=exc.status, status=exc.status, reason=exc.reason, warnings=self.warnings) + return None, error + except Exception as exc: + msg = "Failed to patch object: {0}".format(exc) + if self.warnings: + msg += "\n" + "\n ".join(self.warnings) + error = dict(msg=msg, error=to_native(exc), status='', reason='', warnings=self.warnings) + return None, error + + def create_project_request(self, definition): + definition['kind'] = 'ProjectRequest' + result = {'changed': False, 'result': {}} + resource = self.find_resource('ProjectRequest', definition['apiVersion'], fail=True) + if not self.check_mode: + try: + k8s_obj = resource.create(definition) + result['result'] = k8s_obj.to_dict() + except DynamicApiError as exc: + self.fail_json(msg="Failed to create object: {0}".format(exc.body), + error=exc.status, status=exc.status, reason=exc.reason) + result['changed'] = True + result['method'] = 'create' + return result + class KubernetesAnsibleModule(AnsibleModule, K8sAnsibleMixin): # NOTE: This class KubernetesAnsibleModule is deprecated in favor of diff --git a/plugins/module_utils/raw.py b/plugins/module_utils/raw.py index bbe2b2d1..a353f1cb 100644 --- a/plugins/module_utils/raw.py +++ b/plugins/module_utils/raw.py @@ -20,43 +20,16 @@ from __future__ import absolute_import, division, print_function __metaclass__ = type import copy -from distutils.version import LooseVersion -import sys -import traceback -from ansible.module_utils.basic import missing_required_lib, AnsibleModule -from ansible.module_utils._text import to_native -from ansible.module_utils.common.dict_transformations import dict_merge +from ansible.module_utils.basic import AnsibleModule from ansible_collections.community.kubernetes.plugins.module_utils.common import ( - AUTH_ARG_SPEC, COMMON_ARG_SPEC, RESOURCE_ARG_SPEC, NAME_ARG_SPEC, K8sAnsibleMixin) - - -try: - from openshift.dynamic.exceptions import DynamicApiError, NotFoundError, ConflictError, ForbiddenError, KubernetesValidateMissing - import urllib3 -except ImportError: - # Exceptions handled in common - pass - - -K8S_CONFIG_HASH_IMP_ERR = None -try: - from openshift.helper.hashes import generate_hash - HAS_K8S_CONFIG_HASH = True -except ImportError: - K8S_CONFIG_HASH_IMP_ERR = traceback.format_exc() - HAS_K8S_CONFIG_HASH = False - -HAS_K8S_APPLY = None -try: - from openshift.dynamic.apply import apply_object - HAS_K8S_APPLY = True -except ImportError: - HAS_K8S_APPLY = False + 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( @@ -90,9 +63,6 @@ class KubernetesRawModule(K8sAnsibleMixin): return argument_spec def __init__(self, k8s_kind=None, *args, **kwargs): - self.client = None - self.warnings = [] - mutually_exclusive = [ ('resource_definition', 'src'), ('merge_type', 'apply'), @@ -111,322 +81,17 @@ class KubernetesRawModule(K8sAnsibleMixin): self.fail = self.module.fail_json self.exit_json = self.module.exit_json - super(KubernetesRawModule, self).__init__() + 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') - validate = self.params.get('validate') - if validate: - if LooseVersion(self.openshift_version) < LooseVersion("0.8.0"): - self.fail_json(msg="openshift >= 0.8.0 is required for validate") - self.append_hash = self.params.get('append_hash') - if self.append_hash: - if not HAS_K8S_CONFIG_HASH: - self.fail_json(msg=missing_required_lib("openshift >= 0.7.2", reason="for append_hash"), - exception=K8S_CONFIG_HASH_IMP_ERR) - if self.params['merge_type']: - if LooseVersion(self.openshift_version) < LooseVersion("0.6.2"): - self.fail_json(msg=missing_required_lib("openshift >= 0.6.2", reason="for merge_type")) - self.apply = self.params.get('apply', False) - if self.apply: - if not HAS_K8S_APPLY: - self.fail_json(msg=missing_required_lib("openshift >= 0.9.2", reason="for apply")) + + self.check_library_version() self.set_resource_definitions() - - def flatten_list_kind(self, list_resource, definitions): - flattened = [] - parent_api_version = list_resource.group_version if list_resource else None - parent_kind = list_resource.kind[:-4] if list_resource else None - for definition in definitions.get('items', []): - resource = self.find_resource(definition.get('kind', parent_kind), definition.get('apiVersion', parent_api_version), fail=True) - flattened.append((resource, self.set_defaults(resource, definition))) - return flattened - - def execute_module(self): - changed = False - results = [] - try: - self.client = self.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)) - - flattened_definitions = [] - for definition in self.resource_definitions: - if definition is None: - continue - kind = definition.get('kind', self.kind) - api_version = definition.get('apiVersion', self.api_version) - if kind and kind.endswith('List'): - resource = self.find_resource(kind, api_version, fail=False) - flattened_definitions.extend(self.flatten_list_kind(resource, definition)) - else: - resource = self.find_resource(kind, api_version, fail=True) - flattened_definitions.append((resource, definition)) - - for (resource, definition) in flattened_definitions: - kind = definition.get('kind', self.kind) - api_version = definition.get('apiVersion', self.api_version) - definition = self.set_defaults(resource, definition) - self.warnings = [] - if self.params['validate'] is not None: - self.warnings = self.validate(definition) - result = self.perform_action(resource, definition) - result['warnings'] = self.warnings - changed = changed or result['changed'] - results.append(result) - - if len(results) == 1: - self.exit_json(**results[0]) - - self.exit_json(**{ - 'changed': changed, - 'result': { - 'results': results - } - }) - - def validate(self, resource): - def _prepend_resource_info(resource, msg): - return "%s %s: %s" % (resource['kind'], resource['metadata']['name'], msg) - - try: - warnings, errors = self.client.validate(resource, self.params['validate'].get('version'), self.params['validate'].get('strict')) - except KubernetesValidateMissing: - self.fail_json(msg="kubernetes-validate python library is required to validate resources") - - if errors and self.params['validate']['fail_on_error']: - self.fail_json(msg="\n".join([_prepend_resource_info(resource, error) for error in errors])) - else: - return [_prepend_resource_info(resource, msg) for msg in warnings + errors] - - def set_defaults(self, resource, definition): - definition['kind'] = resource.kind - definition['apiVersion'] = resource.group_version - metadata = definition.get('metadata', {}) - if self.name and not metadata.get('name'): - metadata['name'] = self.name - if resource.namespaced and self.namespace and not metadata.get('namespace'): - metadata['namespace'] = self.namespace - definition['metadata'] = metadata - return definition - - def perform_action(self, resource, definition): - result = {'changed': False, 'result': {}} - state = self.params.get('state', None) - force = self.params.get('force', False) - name = definition['metadata'].get('name') - namespace = definition['metadata'].get('namespace') - existing = None - wait = self.params.get('wait') - wait_sleep = self.params.get('wait_sleep') - wait_timeout = self.params.get('wait_timeout') - wait_condition = None - if self.params.get('wait_condition') and self.params['wait_condition'].get('type'): - wait_condition = self.params['wait_condition'] - - self.remove_aliases() - - try: - # ignore append_hash for resources other than ConfigMap and Secret - if self.append_hash and definition['kind'] in ['ConfigMap', 'Secret']: - name = '%s-%s' % (name, generate_hash(definition)) - definition['metadata']['name'] = name - params = dict(name=name) - if namespace: - params['namespace'] = namespace - existing = resource.get(**params) - except NotFoundError: - # Remove traceback so that it doesn't show up in later failures - try: - sys.exc_clear() - except AttributeError: - # no sys.exc_clear on python3 - pass - except ForbiddenError as exc: - if definition['kind'] in ['Project', 'ProjectRequest'] and state != 'absent': - return self.create_project_request(definition) - self.fail_json(msg='Failed to retrieve requested object: {0}'.format(exc.body), - error=exc.status, status=exc.status, reason=exc.reason) - except DynamicApiError as exc: - self.fail_json(msg='Failed to retrieve requested object: {0}'.format(exc.body), - error=exc.status, status=exc.status, reason=exc.reason) - except Exception as exc: - self.fail_json(msg='Failed to retrieve requested object: {0}'.format(to_native(exc)), - error='', status='', reason='') - - if state == 'absent': - result['method'] = "delete" - if not existing: - # The object already does not exist - return result - else: - # Delete the object - result['changed'] = True - if not self.check_mode: - try: - k8s_obj = resource.delete(**params) - result['result'] = k8s_obj.to_dict() - except DynamicApiError as exc: - self.fail_json(msg="Failed to delete object: {0}".format(exc.body), - error=exc.status, status=exc.status, reason=exc.reason) - if wait: - success, resource, duration = self.wait(resource, definition, wait_sleep, wait_timeout, 'absent') - result['duration'] = duration - if not success: - self.fail_json(msg="Resource deletion timed out", **result) - return result - else: - if self.apply: - if self.check_mode: - ignored, patch = apply_object(resource, definition) - if existing: - k8s_obj = dict_merge(existing.to_dict(), patch) - else: - k8s_obj = patch - else: - try: - k8s_obj = resource.apply(definition, namespace=namespace).to_dict() - except DynamicApiError as exc: - msg = "Failed to apply object: {0}".format(exc.body) - if self.warnings: - msg += "\n" + "\n ".join(self.warnings) - self.fail_json(msg=msg, error=exc.status, status=exc.status, reason=exc.reason) - success = True - result['result'] = k8s_obj - if wait and not self.check_mode: - success, result['result'], result['duration'] = self.wait(resource, definition, wait_sleep, wait_timeout, condition=wait_condition) - if existing: - existing = existing.to_dict() - else: - existing = {} - match, diffs = self.diff_objects(existing, result['result']) - result['changed'] = not match - result['diff'] = diffs - result['method'] = 'apply' - if not success: - self.fail_json(msg="Resource apply timed out", **result) - return result - - if not existing: - if self.check_mode: - k8s_obj = definition - else: - try: - k8s_obj = resource.create(definition, namespace=namespace).to_dict() - except ConflictError: - # Some resources, like ProjectRequests, can't be created multiple times, - # because the resources that they create don't match their kind - # In this case we'll mark it as unchanged and warn the user - self.warn("{0} was not found, but creating it returned a 409 Conflict error. This can happen \ - if the resource you are creating does not directly create a resource of the same kind.".format(name)) - return result - except DynamicApiError as exc: - msg = "Failed to create object: {0}".format(exc.body) - if self.warnings: - msg += "\n" + "\n ".join(self.warnings) - self.fail_json(msg=msg, error=exc.status, status=exc.status, reason=exc.reason) - success = True - result['result'] = k8s_obj - if wait and not self.check_mode: - success, result['result'], result['duration'] = self.wait(resource, definition, wait_sleep, wait_timeout, condition=wait_condition) - result['changed'] = True - result['method'] = 'create' - if not success: - self.fail_json(msg="Resource creation timed out", **result) - return result - - match = False - diffs = [] - - if existing and force: - if self.check_mode: - k8s_obj = definition - else: - try: - k8s_obj = resource.replace(definition, name=name, namespace=namespace, append_hash=self.append_hash).to_dict() - except DynamicApiError as exc: - msg = "Failed to replace object: {0}".format(exc.body) - if self.warnings: - msg += "\n" + "\n ".join(self.warnings) - self.fail_json(msg=msg, error=exc.status, status=exc.status, reason=exc.reason) - match, diffs = self.diff_objects(existing.to_dict(), k8s_obj) - success = True - result['result'] = k8s_obj - if wait and not self.check_mode: - success, result['result'], result['duration'] = self.wait(resource, definition, wait_sleep, wait_timeout, condition=wait_condition) - match, diffs = self.diff_objects(existing.to_dict(), result['result']) - result['changed'] = not match - result['method'] = 'replace' - result['diff'] = diffs - if not success: - self.fail_json(msg="Resource replacement timed out", **result) - return result - - # Differences exist between the existing obj and requested params - if self.check_mode: - k8s_obj = dict_merge(existing.to_dict(), definition) - else: - if LooseVersion(self.openshift_version) < LooseVersion("0.6.2"): - k8s_obj, error = self.patch_resource(resource, definition, existing, name, - namespace) - else: - for merge_type in self.params['merge_type'] or ['strategic-merge', 'merge']: - k8s_obj, error = self.patch_resource(resource, definition, existing, name, - namespace, merge_type=merge_type) - if not error: - break - if error: - self.fail_json(**error) - - success = True - result['result'] = k8s_obj - if wait and not self.check_mode: - success, result['result'], result['duration'] = self.wait(resource, definition, wait_sleep, wait_timeout, condition=wait_condition) - match, diffs = self.diff_objects(existing.to_dict(), result['result']) - result['changed'] = not match - result['method'] = 'patch' - result['diff'] = diffs - - if not success: - self.fail_json(msg="Resource update timed out", **result) - return result - - def patch_resource(self, resource, definition, existing, name, namespace, merge_type=None): - try: - params = dict(name=name, namespace=namespace) - if merge_type: - params['content_type'] = 'application/{0}-patch+json'.format(merge_type) - k8s_obj = resource.patch(definition, **params).to_dict() - match, diffs = self.diff_objects(existing.to_dict(), k8s_obj) - error = {} - return k8s_obj, {} - except DynamicApiError as exc: - msg = "Failed to patch object: {0}".format(exc.body) - if self.warnings: - msg += "\n" + "\n ".join(self.warnings) - error = dict(msg=msg, error=exc.status, status=exc.status, reason=exc.reason, warnings=self.warnings) - return None, error - except Exception as exc: - msg = "Failed to patch object: {0}".format(exc) - if self.warnings: - msg += "\n" + "\n ".join(self.warnings) - error = dict(msg=msg, error=to_native(exc), status='', reason='', warnings=self.warnings) - return None, error - - def create_project_request(self, definition): - definition['kind'] = 'ProjectRequest' - result = {'changed': False, 'result': {}} - resource = self.find_resource('ProjectRequest', definition['apiVersion'], fail=True) - if not self.check_mode: - try: - k8s_obj = resource.create(definition) - result['result'] = k8s_obj.to_dict() - except DynamicApiError as exc: - self.fail_json(msg="Failed to create object: {0}".format(exc.body), - error=exc.status, status=exc.status, reason=exc.reason) - result['changed'] = True - result['method'] = 'create' - return result diff --git a/plugins/modules/k8s.py b/plugins/modules/k8s.py index 23cf2f45..842eb975 100644 --- a/plugins/modules/k8s.py +++ b/plugins/modules/k8s.py @@ -260,11 +260,82 @@ result: sample: 48 ''' -from ansible_collections.community.kubernetes.plugins.module_utils.raw import KubernetesRawModule +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) + + +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 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, *args, k8s_kind=None, **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(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() def main(): - KubernetesRawModule().execute_module() + KubernetesModule().execute_module() if __name__ == '__main__': diff --git a/plugins/modules/k8s_service.py b/plugins/modules/k8s_service.py index 932825f7..0485d710 100644 --- a/plugins/modules/k8s_service.py +++ b/plugins/modules/k8s_service.py @@ -148,8 +148,9 @@ import traceback from collections import defaultdict -from ansible_collections.community.kubernetes.plugins.module_utils.common import AUTH_ARG_SPEC, COMMON_ARG_SPEC, RESOURCE_ARG_SPEC -from ansible_collections.community.kubernetes.plugins.module_utils.raw import KubernetesRawModule +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) SERVICE_ARG_SPEC = { @@ -171,9 +172,38 @@ SERVICE_ARG_SPEC = { } -class KubernetesService(KubernetesRawModule): +class KubernetesService(K8sAnsibleMixin): def __init__(self, *args, **kwargs): - super(KubernetesService, self).__init__(*args, k8s_kind='Service', **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):