diff --git a/lib/ansible/module_utils/k8s/common.py b/lib/ansible/module_utils/k8s/common.py
index 37ec9320220..93d0c3b72c2 100644
--- a/lib/ansible/module_utils/k8s/common.py
+++ b/lib/ansible/module_utils/k8s/common.py
@@ -19,31 +19,17 @@
from __future__ import absolute_import, division, print_function
import os
-import re
import copy
-import json
-from datetime import datetime
from ansible.module_utils.six import iteritems
from ansible.module_utils.basic import AnsibleModule
-from ansible.module_utils.k8s.helper import\
- AnsibleMixin,\
- HAS_STRING_UTILS
-
try:
- from openshift.helper.kubernetes import KubernetesObjectHelper
- from openshift.helper.openshift import OpenShiftObjectHelper
- from openshift.helper.exceptions import KubernetesException
+ import kubernetes
+ from openshift.dynamic import DynamicClient
HAS_K8S_MODULE_HELPER = True
-except ImportError as exc:
- class KubernetesObjectHelper(object):
- pass
-
- class OpenShiftObjectHelper(object):
- pass
-
+except ImportError:
HAS_K8S_MODULE_HELPER = False
try:
@@ -52,104 +38,141 @@ try:
except ImportError:
HAS_YAML = False
+try:
+ import dictdiffer
+ HAS_DICTDIFFER = True
+except ImportError:
+ HAS_DICTDIFFER = False
-def remove_secret_data(obj_dict):
- """ Remove any sensitive data from a K8s dict"""
- if obj_dict.get('data'):
- # Secret data
- obj_dict.pop('data')
- if obj_dict.get('string_data'):
- # The API should not return sting_data in Secrets, but just in case
- obj_dict.pop('string_data')
- if obj_dict['metadata'].get('annotations'):
- # Remove things like 'openshift.io/token-secret' from metadata
- for key in [k for k in obj_dict['metadata']['annotations'] if 'secret' in k]:
- obj_dict['metadata']['annotations'].pop(key)
-
-
-def to_snake(name):
- """ Convert a string from camel to snake """
- if not name:
- return name
-
- def _replace(m):
- m = m.group(0)
- return m[0] + '_' + m[1:]
-
- p = r'[a-z][A-Z]|' \
- r'[A-Z]{2}[a-z]'
- return re.sub(p, _replace, name).lower()
-
-
-class DateTimeEncoder(json.JSONEncoder):
- # When using json.dumps() with K8s object, pass cls=DateTimeEncoder to handle any datetime objects
- def default(self, o):
- if isinstance(o, datetime):
- return o.isoformat()
- return json.JSONEncoder.default(self, o)
-
-
-class KubernetesAnsibleModuleHelper(AnsibleMixin, KubernetesObjectHelper):
+try:
+ import urllib3
+ urllib3.disable_warnings()
+except ImportError:
pass
-
-class KubernetesAnsibleModule(AnsibleModule):
- resource_definition = None
- api_version = None
- kind = None
- helper = None
-
- def __init__(self, *args, **kwargs):
-
- kwargs['argument_spec'] = self.argspec
- AnsibleModule.__init__(self, *args, **kwargs)
-
- if not HAS_K8S_MODULE_HELPER:
- self.fail_json(msg="This module requires the OpenShift Python client. Try `pip install openshift`")
-
- if not HAS_YAML:
- self.fail_json(msg="This module requires PyYAML. Try `pip install PyYAML`")
-
- if not HAS_STRING_UTILS:
- self.fail_json(msg="This module requires Python string utils. Try `pip install python-string-utils`")
+ARG_ATTRIBUTES_BLACKLIST = ('property_path',)
+
+COMMON_ARG_SPEC = {
+ 'state': {
+ 'default': 'present',
+ 'choices': ['present', 'absent'],
+ },
+ 'force': {
+ 'type': 'bool',
+ 'default': False,
+ },
+ 'resource_definition': {
+ 'type': 'dict',
+ 'aliases': ['definition', 'inline']
+ },
+ 'src': {
+ 'type': 'path',
+ },
+ '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,
+ },
+ 'verify_ssl': {
+ 'type': 'bool',
+ },
+ 'ssl_ca_cert': {
+ 'type': 'path',
+ },
+ 'cert_file': {
+ 'type': 'path',
+ },
+ 'key_file': {
+ 'type': 'path',
+ },
+}
+
+
+class K8sAnsibleMixin(object):
+ _argspec_cache = None
@property
def argspec(self):
- raise NotImplementedError()
-
- def get_helper(self, api_version, kind):
- try:
- helper = KubernetesAnsibleModuleHelper(api_version=api_version, kind=kind, debug=False)
- helper.get_model(api_version, kind)
- return helper
- except KubernetesException as exc:
- self.fail_json(msg="Error initializing module helper: {0}".format(exc.message))
-
- def execute_module(self):
- raise NotImplementedError()
-
- def exit_json(self, **return_attributes):
- """ Filter any sensitive data that we don't want logged """
- if return_attributes.get('result') and \
- return_attributes['result'].get('kind') in ('Secret', 'SecretList'):
- if return_attributes['result'].get('data'):
- remove_secret_data(return_attributes['result'])
- elif return_attributes['result'].get('items'):
- for item in return_attributes['result']['items']:
- remove_secret_data(item)
- super(KubernetesAnsibleModule, self).exit_json(**return_attributes)
-
- def authenticate(self):
+ """
+ Introspect the model properties, and return an Ansible module arg_spec dict.
+ :return: dict
+ """
+ if self._argspec_cache:
+ return self._argspec_cache
+ argument_spec = copy.deepcopy(COMMON_ARG_SPEC)
+ argument_spec.update(copy.deepcopy(AUTH_ARG_SPEC))
+ self._argspec_cache = argument_spec
+ return self._argspec_cache
+
+ def get_api_client(self, **auth):
+ auth_args = AUTH_ARG_SPEC.keys()
+
+ auth = auth or getattr(self, 'params', {})
+
+ configuration = kubernetes.client.Configuration()
+ for key, value in iteritems(auth):
+ if key in auth_args and value is not None:
+ if key == 'api_key':
+ setattr(configuration, key, {'authorization': "Bearer {0}".format(value)})
+ else:
+ setattr(configuration, key, value)
+ elif key in auth_args and value is None:
+ env_value = os.getenv('K8S_AUTH_{0}'.format(key.upper()), None)
+ if env_value is not None:
+ setattr(configuration, key, env_value)
+
+ kubernetes.client.Configuration.set_default(configuration)
+
+ if auth.get('username') and auth.get('password') and auth.get('host'):
+ auth_method = 'params'
+ elif auth.get('api_key') and auth.get('host'):
+ auth_method = 'params'
+ elif auth.get('kubeconfig') or auth.get('context'):
+ auth_method = 'file'
+ else:
+ auth_method = 'default'
+
+ # First try to do incluster config, then kubeconfig
+ if auth_method == 'default':
+ try:
+ kubernetes.config.load_incluster_config()
+ return DynamicClient(kubernetes.client.ApiClient())
+ except kubernetes.config.ConfigException:
+ return DynamicClient(self.client_from_kubeconfig(auth.get('kubeconfig'), auth.get('context')))
+
+ if auth_method == 'file':
+ return DynamicClient(self.client_from_kubeconfig(auth.get('kubeconfig'), auth.get('context')))
+
+ if auth_method == 'params':
+ return DynamicClient(kubernetes.client.ApiClient(configuration))
+
+ def client_from_kubeconfig(self, config_file, context):
try:
- auth_options = {}
- auth_args = ('host', 'api_key', 'kubeconfig', 'context', 'username', 'password',
- 'cert_file', 'key_file', 'ssl_ca_cert', 'verify_ssl')
- for key, value in iteritems(self.params):
- if key in auth_args and value is not None:
- auth_options[key] = value
- self.helper.set_client_config(**auth_options)
- except KubernetesException as e:
- self.fail_json(msg='Error loading config', error=str(e))
+ return kubernetes.config.new_client_from_config(config_file, context)
+ except (IOError, kubernetes.config.ConfigException):
+ # If we failed to load the default config file then we'll return
+ # an empty configuration
+ # If one was specified, we will crash
+ if not config_file:
+ return kubernetes.client.ApiClient()
+ raise
def remove_aliases(self):
"""
@@ -161,63 +184,53 @@ class KubernetesAnsibleModule(AnsibleModule):
if alias in self.params:
self.params.pop(alias)
- def load_resource_definition(self, src):
+ def load_resource_definitions(self, src):
""" Load the requested src path """
result = None
path = os.path.normpath(src)
if not os.path.exists(path):
self.fail_json(msg="Error accessing {0}. Does the file exist?".format(path))
try:
- result = yaml.safe_load(open(path, 'r'))
+ with open(path, 'r') as f:
+ result = list(yaml.safe_load_all(f))
except (IOError, yaml.YAMLError) as exc:
self.fail_json(msg="Error loading resource_definition: {0}".format(exc))
return result
- def resource_to_parameters(self, resource):
- """ Converts a resource definition to module parameters """
- parameters = {}
- for key, value in iteritems(resource):
- if key in ('apiVersion', 'kind', 'status'):
- continue
- elif key == 'metadata' and isinstance(value, dict):
- for meta_key, meta_value in iteritems(value):
- if meta_key in ('name', 'namespace', 'labels', 'annotations'):
- parameters[meta_key] = meta_value
- elif key in self.helper.argspec and value is not None:
- parameters[key] = value
- elif isinstance(value, dict):
- self._add_parameter(value, [to_snake(key)], parameters)
- return parameters
-
- def _add_parameter(self, request, path, parameters):
- for key, value in iteritems(request):
- if path:
- param_name = '_'.join(path + [to_snake(key)])
- else:
- param_name = to_snake(key)
- if param_name in self.helper.argspec and value is not None:
- parameters[param_name] = value
- elif isinstance(value, dict):
- continue_path = copy.copy(path) if path else []
- continue_path.append(to_snake(key))
- self._add_parameter(value, continue_path, parameters)
- else:
- self.fail_json(
- msg=("Error parsing resource definition. Encountered {0}, which does not map to a parameter "
- "expected by the OpenShift Python module.".format(param_name))
- )
-
-
-class OpenShiftAnsibleModuleHelper(AnsibleMixin, OpenShiftObjectHelper):
- pass
+ @staticmethod
+ def diff_objects(existing, new):
+ if not HAS_DICTDIFFER:
+ return False, []
+ def get_shared_attrs(o1, o2):
+ shared_attrs = {}
+ for k, v in o2.items():
+ if isinstance(v, dict):
+ shared_attrs[k] = get_shared_attrs(o1.get(k, {}), v)
+ else:
+ shared_attrs[k] = o1.get(k)
+ return shared_attrs
-class OpenShiftAnsibleModuleMixin(object):
+ diffs = list(dictdiffer.diff(new, get_shared_attrs(existing, new)))
+ match = len(diffs) == 0
+ return match, diffs
- def get_helper(self, api_version, kind):
- try:
- helper = OpenShiftAnsibleModuleHelper(api_version=api_version, kind=kind, debug=False)
- helper.get_model(api_version, kind)
- return helper
- except KubernetesException as exc:
- self.fail_json(msg="Error initializing module helper: {0}".format(exc.message))
+
+class KubernetesAnsibleModule(AnsibleModule, K8sAnsibleMixin):
+ resource_definition = None
+ api_version = None
+ kind = None
+
+ def __init__(self, *args, **kwargs):
+
+ kwargs['argument_spec'] = self.argspec
+ AnsibleModule.__init__(self, *args, **kwargs)
+
+ if not HAS_K8S_MODULE_HELPER:
+ self.fail_json(msg="This module requires the OpenShift Python client. Try `pip install openshift`")
+
+ if not HAS_YAML:
+ self.fail_json(msg="This module requires PyYAML. Try `pip install PyYAML`")
+
+ def execute_module(self):
+ raise NotImplementedError()
diff --git a/lib/ansible/module_utils/k8s/helper.py b/lib/ansible/module_utils/k8s/helper.py
deleted file mode 100644
index 438dd9d0dd8..00000000000
--- a/lib/ansible/module_utils/k8s/helper.py
+++ /dev/null
@@ -1,633 +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 .
-
-import base64
-import copy
-
-from ansible.module_utils.six import iteritems, string_types
-from keyword import kwlist
-
-try:
- from openshift.helper import PRIMITIVES
- from openshift.helper.exceptions import KubernetesException
- HAS_K8S_MODULE_HELPER = True
-except ImportError as exc:
- HAS_K8S_MODULE_HELPER = False
-
-# TODO Remove string_utils dependency
-try:
- import string_utils
- HAS_STRING_UTILS = True
-except ImportError:
- HAS_STRING_UTILS = False
-
-
-ARG_ATTRIBUTES_BLACKLIST = ('property_path',)
-PYTHON_KEYWORD_MAPPING = dict(zip(['_{0}'.format(item) for item in kwlist], kwlist))
-PYTHON_KEYWORD_MAPPING.update(dict([reversed(item) for item in iteritems(PYTHON_KEYWORD_MAPPING)]))
-
-COMMON_ARG_SPEC = {
- 'state': {
- 'default': 'present',
- 'choices': ['present', 'absent'],
- },
- 'force': {
- 'type': 'bool',
- 'default': False,
- },
- 'resource_definition': {
- 'type': 'dict',
- 'aliases': ['definition', 'inline']
- },
- 'src': {
- 'type': 'path',
- },
- '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,
- },
- 'verify_ssl': {
- 'type': 'bool',
- },
- 'ssl_ca_cert': {
- 'type': 'path',
- },
- 'cert_file': {
- 'type': 'path',
- },
- 'key_file': {
- 'type': 'path',
- },
-}
-
-OPENSHIFT_ARG_SPEC = {
- 'description': {},
- 'display_name': {},
-}
-
-
-class AnsibleMixin(object):
- _argspec_cache = None
-
- @property
- def argspec(self):
- """
- Introspect the model properties, and return an Ansible module arg_spec dict.
- :return: dict
- """
- if self._argspec_cache:
- return self._argspec_cache
- argument_spec = copy.deepcopy(COMMON_ARG_SPEC)
- argument_spec.update(copy.deepcopy(AUTH_ARG_SPEC))
- argument_spec.update(copy.deepcopy(OPENSHIFT_ARG_SPEC))
- argument_spec.update(self.__transform_properties(self.properties))
- self._argspec_cache = argument_spec
- return self._argspec_cache
-
- def object_from_params(self, module_params, obj=None):
- """
- Update a model object with Ansible module param values. Optionally pass an object
- to update, otherwise a new object will be created.
- :param module_params: dict of key:value pairs
- :param obj: model object to update
- :return: updated model object
- """
- if not obj:
- obj = self.model()
- obj.kind = string_utils.snake_case_to_camel(self.kind, upper_case_first=False)
- obj.api_version = self.api_version.lower()
- for param_name, param_value in iteritems(module_params):
- spec = self.find_arg_spec(param_name)
- if param_value is not None and spec.get('property_path'):
- prop_path = copy.copy(spec['property_path'])
- self.__set_obj_attribute(obj, prop_path, param_value, param_name)
-
- if self.kind.lower() == 'project' and (module_params.get('display_name') or
- module_params.get('description')):
- if not obj.metadata.annotations:
- obj.metadata.annotations = {}
- if module_params.get('display_name'):
- obj.metadata.annotations['openshift.io/display-name'] = module_params['display_name']
- if module_params.get('description'):
- obj.metadata.annotations['openshift.io/description'] = module_params['description']
- elif (self.kind.lower() == 'secret' and getattr(obj, 'string_data', None)
- and hasattr(obj, 'data')):
- if obj.data is None:
- obj.data = {}
-
- # Do a base64 conversion of `string_data` and place it in
- # `data` so that later comparisons to existing objects
- # (if any) do not result in requiring an unnecessary change.
- for key, value in iteritems(obj.string_data):
- obj.data[key] = base64.b64encode(value)
-
- obj.string_data = None
- return obj
-
- def request_body_from_params(self, module_params):
- request = {
- 'kind': self.base_model_name,
- }
- for param_name, param_value in iteritems(module_params):
- spec = self.find_arg_spec(param_name)
- if spec and spec.get('property_path') and param_value is not None:
- self.__add_path_to_dict(request, param_name, param_value, spec['property_path'])
-
- if self.kind.lower() == 'project' and (module_params.get('display_name') or
- module_params.get('description')):
- if not request.get('metadata'):
- request['metadata'] = {}
- if not request['metadata'].get('annotations'):
- request['metadata']['annotations'] = {}
- if module_params.get('display_name'):
- request['metadata']['annotations']['openshift.io/display-name'] = module_params['display_name']
- if module_params.get('description'):
- request['metadata']['annotations']['openshift.io/description'] = module_params['description']
- return request
-
- def find_arg_spec(self, module_param_name):
- """For testing, allow the param_name value to be an alias"""
- if module_param_name in self.argspec:
- return self.argspec[module_param_name]
- result = None
- for key, value in iteritems(self.argspec):
- if value.get('aliases'):
- for alias in value['aliases']:
- if alias == module_param_name:
- result = self.argspec[key]
- break
- if result:
- break
- if not result:
- raise KubernetesException(
- "Error: received unrecognized module parameter {0}".format(module_param_name)
- )
- return result
-
- @staticmethod
- def __convert_params_to_choices(properties):
- def snake_case(name):
- result = string_utils.snake_case_to_camel(name.replace('_params', ''), upper_case_first=True)
- return result[:1].upper() + result[1:]
- choices = {}
- for x in list(properties.keys()):
- if x.endswith('params'):
- choices[x] = snake_case(x)
- return choices
-
- def __add_path_to_dict(self, request_dict, param_name, param_value, path):
- local_path = copy.copy(path)
- spec = self.find_arg_spec(param_name)
- while len(local_path):
- p = string_utils.snake_case_to_camel(local_path.pop(0), upper_case_first=False)
- if len(local_path):
- if request_dict.get(p, None) is None:
- request_dict[p] = {}
- self.__add_path_to_dict(request_dict[p], param_name, param_value, local_path)
- break
- else:
- param_type = spec.get('type', 'str')
- if param_type == 'dict':
- request_dict[p] = self.__dict_keys_to_camel(param_name, param_value)
- elif param_type == 'list':
- request_dict[p] = self.__list_keys_to_camel(param_name, param_value)
- else:
- request_dict[p] = param_value
-
- def __dict_keys_to_camel(self, param_name, param_dict):
- result = {}
- for item, value in iteritems(param_dict):
- key_name = self.__property_name_to_camel(param_name, item)
- if value:
- if isinstance(value, list):
- result[key_name] = self.__list_keys_to_camel(param_name, value)
- elif isinstance(value, dict):
- result[key_name] = self.__dict_keys_to_camel(param_name, value)
- else:
- result[key_name] = value
- return result
-
- @staticmethod
- def __property_name_to_camel(param_name, property_name):
- new_name = property_name
- if 'annotations' not in param_name and 'labels' not in param_name and 'selector' not in param_name:
- camel_name = string_utils.snake_case_to_camel(property_name, upper_case_first=False)
- new_name = camel_name[1:] if camel_name.startswith('_') else camel_name
- return new_name
-
- def __list_keys_to_camel(self, param_name, param_list):
- result = []
- if isinstance(param_list[0], dict):
- for item in param_list:
- result.append(self.__dict_keys_to_camel(param_name, item))
- else:
- result = param_list
- return result
-
- def __set_obj_attribute(self, obj, property_path, param_value, param_name):
- """
- Recursively set object properties
- :param obj: The object on which to set a property value.
- :param property_path: A list of property names in the form of strings.
- :param param_value: The value to set.
- :return: The original object.
- """
- while len(property_path) > 0:
- raw_prop_name = property_path.pop(0)
- prop_name = PYTHON_KEYWORD_MAPPING.get(raw_prop_name, raw_prop_name)
- prop_kind = obj.swagger_types[prop_name]
- if prop_kind in PRIMITIVES:
- try:
- setattr(obj, prop_name, param_value)
- except ValueError as exc:
- msg = str(exc)
- if param_value is None and 'None' in msg:
- pass
- else:
- raise KubernetesException(
- "Error setting {0} to {1}: {2}".format(prop_name, param_value, msg)
- )
- elif prop_kind.startswith('dict('):
- if not getattr(obj, prop_name):
- setattr(obj, prop_name, param_value)
- else:
- self.__compare_dict(getattr(obj, prop_name), param_value, param_name)
- elif prop_kind.startswith('list['):
- if getattr(obj, prop_name) is None:
- setattr(obj, prop_name, [])
- obj_type = prop_kind.replace('list[', '').replace(']', '')
- if obj_type not in PRIMITIVES and obj_type not in ('list', 'dict'):
- self.__compare_obj_list(getattr(obj, prop_name), param_value, obj_type, param_name)
- else:
- self.__compare_list(getattr(obj, prop_name), param_value, param_name)
- else:
- # prop_kind is an object class
- sub_obj = getattr(obj, prop_name)
- if not sub_obj:
- sub_obj = self.model_class_from_name(prop_kind)()
- setattr(obj, prop_name, self.__set_obj_attribute(sub_obj, property_path, param_value, param_name))
- return obj
-
- def __compare_list(self, src_values, request_values, param_name):
- """
- Compare src_values list with request_values list, and append any missing
- request_values to src_values.
- """
- if not request_values:
- return
-
- if not src_values:
- src_values += request_values
-
- if type(src_values[0]).__name__ in PRIMITIVES:
- if set(src_values) >= set(request_values):
- # src_value list includes request_value list
- return
- # append the missing elements from request value
- src_values += list(set(request_values) - set(src_values))
- elif type(src_values[0]).__name__ == 'dict':
- missing = []
- for request_dict in request_values:
- match = False
- for src_dict in src_values:
- if '__cmp__' in dir(src_dict):
- # python < 3
- if src_dict >= request_dict:
- match = True
- break
- elif iteritems(src_dict) == iteritems(request_dict):
- # python >= 3
- match = True
- break
- if not match:
- missing.append(request_dict)
- src_values += missing
- elif type(src_values[0]).__name__ == 'list':
- missing = []
- for request_list in request_values:
- match = False
- for src_list in src_values:
- if set(request_list) >= set(src_list):
- match = True
- break
- if not match:
- missing.append(request_list)
- src_values += missing
- else:
- raise KubernetesException(
- "Evaluating {0}: encountered unimplemented type {1} in "
- "__compare_list()".format(param_name, type(src_values[0]).__name__)
- )
-
- def __compare_dict(self, src_value, request_value, param_name):
- """
- Compare src_value dict with request_value dict, and update src_value with any differences.
- Does not remove items from src_value dict.
- """
- if not request_value:
- return
- for item, value in iteritems(request_value):
- if type(value).__name__ in ('str', 'int', 'bool'):
- src_value[item] = value
- elif type(value).__name__ == 'list':
- self.__compare_list(src_value[item], value, param_name)
- elif type(value).__name__ == 'dict':
- self.__compare_dict(src_value[item], value, param_name)
- else:
- raise KubernetesException(
- "Evaluating {0}: encountered unimplemented type {1} in "
- "__compare_dict()".format(param_name, type(value).__name__)
- )
-
- def __compare_obj_list(self, src_value, request_value, obj_class, param_name):
- """
- Compare a src_value (list of ojects) with a request_value (list of dicts), and update
- src_value with differences. Assumes each object and each dict has a 'name' attributes,
- which can be used for matching. Elements are not removed from the src_value list.
- """
- if not request_value:
- return
-
- model_class = self.model_class_from_name(obj_class)
-
- # Try to determine the unique key for the array
- key_names = [
- 'name',
- 'type'
- ]
- key_name = None
- for key in key_names:
- if hasattr(model_class, key):
- key_name = key
- break
-
- if key_name:
- # If the key doesn't exist in the request values, then ignore it, rather than throwing an error
- for item in request_value:
- if not item.get(key_name):
- key_name = None
- break
-
- if key_name:
- # compare by key field
- for item in request_value:
- if not item.get(key_name):
- # Prevent user from creating something that will be impossible to patch or update later
- raise KubernetesException(
- "Evaluating {0} - expecting parameter {1} to contain a `{2}` attribute "
- "in __compare_obj_list().".format(param_name,
- self.get_base_model_name_snake(obj_class),
- key_name)
- )
- found = False
- for obj in src_value:
- if not obj:
- continue
- if getattr(obj, key_name) == item[key_name]:
- # Assuming both the src_value and the request value include a name property
- found = True
- for key, value in iteritems(item):
- snake_key = self.attribute_to_snake(key)
- item_kind = model_class.swagger_types.get(snake_key)
- if item_kind and item_kind in PRIMITIVES or type(value).__name__ in PRIMITIVES:
- setattr(obj, snake_key, value)
- elif item_kind and item_kind.startswith('list['):
- obj_type = item_kind.replace('list[', '').replace(']', '')
- if getattr(obj, snake_key) is None:
- setattr(obj, snake_key, [])
- if obj_type not in ('str', 'int', 'bool', 'object'):
- self.__compare_obj_list(getattr(obj, snake_key), value, obj_type, param_name)
- else:
- # Straight list comparison
- self.__compare_list(getattr(obj, snake_key), value, param_name)
- elif item_kind and item_kind.startswith('dict('):
- self.__compare_dict(getattr(obj, snake_key), value, param_name)
- elif item_kind and type(value).__name__ == 'dict':
- # object
- param_obj = getattr(obj, snake_key)
- if not param_obj:
- setattr(obj, snake_key, self.model_class_from_name(item_kind)())
- param_obj = getattr(obj, snake_key)
- self.__update_object_properties(param_obj, value)
- else:
- if item_kind:
- raise KubernetesException(
- "Evaluating {0}: encountered unimplemented type {1} in "
- "__compare_obj_list() for model {2}".format(
- param_name,
- item_kind,
- self.get_base_model_name_snake(obj_class))
- )
- else:
- raise KubernetesException(
- "Evaluating {0}: unable to get swagger_type for {1} in "
- "__compare_obj_list() for item {2} in model {3}".format(
- param_name,
- snake_key,
- str(item),
- self.get_base_model_name_snake(obj_class))
- )
- if not found:
- # Requested item not found. Adding.
- obj = self.model_class_from_name(obj_class)(**item)
- src_value.append(obj)
- else:
- # There isn't a key, or we don't know what it is, so check for all properties to match
- for item in request_value:
- found = False
- for obj in src_value:
- match = True
- for item_key, item_value in iteritems(item):
- # TODO: this should probably take the property type into account
- snake_key = self.attribute_to_snake(item_key)
- if getattr(obj, snake_key) != item_value:
- match = False
- break
- if match:
- found = True
- break
- if not found:
- obj = self.model_class_from_name(obj_class)(**item)
- src_value.append(obj)
-
- def __update_object_properties(self, obj, item):
- """ Recursively update an object's properties. Returns a pointer to the object. """
-
- for key, value in iteritems(item):
- snake_key = self.attribute_to_snake(key)
- try:
- kind = obj.swagger_types[snake_key]
- except (AttributeError, KeyError):
- possible_matches = ', '.join(list(obj.swagger_types.keys()))
- class_snake_name = self.get_base_model_name_snake(type(obj).__name__)
- raise KubernetesException(
- "Unable to find '{0}' in {1}. Valid property names include: {2}".format(snake_key,
- class_snake_name,
- possible_matches)
- )
- if kind in PRIMITIVES or kind.startswith('list[') or kind.startswith('dict('):
- self.__set_obj_attribute(obj, [snake_key], value, snake_key)
- else:
- # kind is an object, hopefully
- if not getattr(obj, snake_key):
- setattr(obj, snake_key, self.model_class_from_name(kind)())
- self.__update_object_properties(getattr(obj, snake_key), value)
-
- return obj
-
- def __transform_properties(self, properties, prefix='', path=None, alternate_prefix=''):
- """
- Convert a list of properties to an argument_spec dictionary
-
- :param properties: List of properties from self.properties_from_model_class()
- :param prefix: String to prefix to argument names.
- :param path: List of property names providing the recursive path through the model to the property
- :param alternate_prefix: a more minimal version of prefix
- :return: dict
- """
- primitive_types = list(PRIMITIVES) + ['list', 'dict']
- args = {}
-
- if path is None:
- path = []
-
- def add_meta(prop_name, prop_prefix, prop_alt_prefix):
- """ Adds metadata properties to the argspec """
- # if prop_alt_prefix != prop_prefix:
- # if prop_alt_prefix:
- # args[prop_prefix + prop_name]['aliases'] = [prop_alt_prefix + prop_name]
- # elif prop_prefix:
- # args[prop_prefix + prop_name]['aliases'] = [prop_name]
- prop_paths = copy.copy(path) # copy path from outer scope
- prop_paths.append('metadata')
- prop_paths.append(prop_name)
- args[prop_prefix + prop_name]['property_path'] = prop_paths
-
- for raw_prop, prop_attributes in iteritems(properties):
- prop = PYTHON_KEYWORD_MAPPING.get(raw_prop, raw_prop)
- if prop in ('api_version', 'status', 'kind', 'items') and not prefix:
- # Don't expose these properties
- continue
- elif prop_attributes['immutable']:
- # Property cannot be set by the user
- continue
- elif prop == 'metadata' and prop_attributes['class'].__name__ == 'UnversionedListMeta':
- args['namespace'] = {}
- elif prop == 'metadata' and prop_attributes['class'].__name__ != 'UnversionedListMeta':
- meta_prefix = prefix + '_metadata_' if prefix else ''
- meta_alt_prefix = alternate_prefix + '_metadata_' if alternate_prefix else ''
- if meta_prefix and not meta_alt_prefix:
- meta_alt_prefix = meta_prefix
- if 'labels' in dir(prop_attributes['class']):
- args[meta_prefix + 'labels'] = {
- 'type': 'dict',
- }
- add_meta('labels', meta_prefix, meta_alt_prefix)
- if 'annotations' in dir(prop_attributes['class']):
- args[meta_prefix + 'annotations'] = {
- 'type': 'dict',
- }
- add_meta('annotations', meta_prefix, meta_alt_prefix)
- if 'namespace' in dir(prop_attributes['class']):
- args[meta_prefix + 'namespace'] = {}
- add_meta('namespace', meta_prefix, meta_alt_prefix)
- if 'name' in dir(prop_attributes['class']):
- args[meta_prefix + 'name'] = {}
- add_meta('name', meta_prefix, meta_alt_prefix)
- elif prop_attributes['class'].__name__ not in primitive_types and not prop.endswith('params'):
- # Adds nested properties recursively
-
- label = prop
-
- # Provide a more human-friendly version of the prefix
- alternate_label = label\
- .replace('spec', '')\
- .replace('template', '')\
- .replace('config', '')
-
- p = prefix
- p += '_' + label if p else label
- a = alternate_prefix
- paths = copy.copy(path)
- paths.append(prop)
-
- # if alternate_prefix:
- # # Prevent the last prefix from repeating. In other words, avoid things like 'pod_pod'
- # pieces = alternate_prefix.split('_')
- # alternate_label = alternate_label.replace(pieces[len(pieces) - 1] + '_', '', 1)
- # if alternate_label != self.base_model_name and alternate_label not in a:
- # a += '_' + alternate_label if a else alternate_label
- if prop.endswith('params') and 'type' in properties:
- sub_props = dict()
- sub_props[prop] = {
- 'class': dict,
- 'immutable': False
- }
- args.update(self.__transform_properties(sub_props, prefix=p, path=paths, alternate_prefix=a))
- else:
- sub_props = self.properties_from_model_class(prop_attributes['class'])
- args.update(self.__transform_properties(sub_props, prefix=p, path=paths, alternate_prefix=a))
- else:
- # Adds a primitive property
- arg_prefix = prefix + '_' if prefix else ''
- arg_alt_prefix = alternate_prefix + '_' if alternate_prefix else ''
- paths = copy.copy(path)
- paths.append(prop)
-
- property_type = prop_attributes['class'].__name__
- if property_type == 'object':
- property_type = 'str'
-
- args[arg_prefix + prop] = {
- 'required': False,
- 'type': property_type,
- 'property_path': paths
- }
-
- if prop.endswith('params') and 'type' in properties:
- args[arg_prefix + prop]['type'] = 'dict'
-
- # Use the alternate prefix to construct a human-friendly alias
- if arg_alt_prefix and arg_prefix != arg_alt_prefix:
- args[arg_prefix + prop]['aliases'] = [arg_alt_prefix + prop]
- elif arg_prefix:
- args[arg_prefix + prop]['aliases'] = [prop]
-
- if prop == 'type':
- choices = self.__convert_params_to_choices(properties)
- if len(choices) > 0:
- args[arg_prefix + prop]['choices'] = choices
- return args
diff --git a/lib/ansible/module_utils/k8s/inventory.py b/lib/ansible/module_utils/k8s/inventory.py
index c8c48c39c0b..d8cf8b6654f 100644
--- a/lib/ansible/module_utils/k8s/inventory.py
+++ b/lib/ansible/module_utils/k8s/inventory.py
@@ -18,22 +18,24 @@
from __future__ import absolute_import, division, print_function
-from ansible.module_utils.six import iteritems
+from ansible.module_utils.k8s.common import K8sAnsibleMixin, HAS_K8S_MODULE_HELPER
try:
- from openshift.helper.kubernetes import KubernetesObjectHelper
- from openshift.helper.openshift import OpenShiftObjectHelper
- from openshift.helper.exceptions import KubernetesException
- HAS_K8S_MODULE_HELPER = True
-except ImportError as exc:
- HAS_K8S_MODULE_HELPER = False
+ from ansible.errors import AnsibleError
+except ImportError:
+ AnsibleError = Exception
+
+try:
+ from openshift.dynamic.exceptions import DynamicApiError
+except ImportError:
+ pass
class K8sInventoryException(Exception):
pass
-class K8sInventoryHelper(object):
+class K8sInventoryHelper(K8sAnsibleMixin):
helper = None
transport = 'kubectl'
@@ -56,7 +58,7 @@ class K8sInventoryHelper(object):
self.fetch_objects(connections)
def fetch_objects(self, connections):
- self.helper = self.get_helper('v1', 'namespace_list')
+ client = self.get_api_client()
if connections:
if not isinstance(connections, list):
@@ -65,68 +67,50 @@ class K8sInventoryHelper(object):
for connection in connections:
if not isinstance(connection, dict):
raise K8sInventoryException("Expecting connection to be a dictionary.")
- self.authenticate(connection)
- name = connection.get('name', self.get_default_host_name(self.helper.api_client.host))
+ client = self.get_api_client(**connection)
+ name = connection.get('name', self.get_default_host_name(client.configuration.host))
if connection.get('namespaces'):
namespaces = connections['namespaces']
else:
- namespaces = self.get_available_namespaces()
+ namespaces = self.get_available_namespaces(client)
for namespace in namespaces:
- self.get_pods_for_namespace(name, namespace)
- self.get_services_for_namespace(name, namespace)
+ self.get_pods_for_namespace(client, name, namespace)
+ self.get_services_for_namespace(client, name, namespace)
else:
- name = self.get_default_host_name(self.helper.api_client.host)
- namespaces = self.get_available_namespaces()
+ name = self.get_default_host_name(client.configuration.host)
+ namespaces = self.get_available_namespaces(client)
for namespace in namespaces:
- self.get_pods_for_namespace(name, namespace)
- self.get_services_for_namespace(name, namespace)
-
- def authenticate(self, connection=None):
- auth_options = {}
- if connection:
- auth_args = ('host', 'api_key', 'kubeconfig', 'context', 'username', 'password',
- 'cert_file', 'key_file', 'ssl_ca_cert', 'verify_ssl')
- for key, value in iteritems(connection):
- if key in auth_args and value is not None:
- auth_options[key] = value
- try:
- self.helper.set_client_config(**auth_options)
- except KubernetesException as exc:
- raise K8sInventoryException('Error connecting to the API: {0}'.format(exc.message))
+ self.get_pods_for_namespace(client, name, namespace)
+ self.get_services_for_namespace(client, name, namespace)
@staticmethod
def get_default_host_name(host):
return host.replace('https://', '').replace('http://', '').replace('.', '-').replace(':', '_')
- def get_helper(self, api_version, kind):
- try:
- helper = KubernetesObjectHelper(api_version=api_version, kind=kind, debug=False)
- helper.get_model(api_version, kind)
- return helper
- except KubernetesException as exc:
- raise K8sInventoryException('Error initializing object helper: {0}'.format(exc.message))
-
- def get_available_namespaces(self):
+ def get_available_namespaces(self, client):
+ v1_namespace = client.resources.get(api_version='v1', kind='Namespace')
try:
- obj = self.helper.get_object()
- except KubernetesObjectHelper as exc:
+ obj = v1_namespace.get()
+ except DynamicApiError as exc:
raise K8sInventoryException('Error fetching Namespace list: {0}'.format(exc.message))
return [namespace.metadata.name for namespace in obj.items]
- def get_pods_for_namespace(self, name, namespace):
- self.helper.set_model('v1', 'pod_list')
+ def get_pods_for_namespace(self, client, name, namespace):
+ v1_pod = client.resources.get(api_version='v1', kind='Pod')
try:
- obj = self.helper.get_object(namespace=namespace)
- except KubernetesException as exc:
+ obj = v1_pod.get(namespace=namespace)
+ except DynamicApiError as exc:
raise K8sInventoryException('Error fetching Pod list: {0}'.format(exc.message))
- namespace_pod_group = '{0}_pods'.format(namespace)
+ namespace_group = 'namespace_{0}'.format(namespace)
+ namespace_pods_group = '{0}_pods'.format(namespace_group)
self.inventory.add_group(name)
- self.inventory.add_group(namespace)
- self.inventory.add_child(name, namespace)
- self.inventory.add_group(namespace_pod_group)
- self.inventory.add_child(namespace, namespace_pod_group)
+ self.inventory.add_group(namespace_group)
+ self.inventory.add_child(name, namespace_group)
+ self.inventory.add_group(namespace_pods_group)
+ self.inventory.add_child(namespace_group, namespace_pods_group)
+
for pod in obj.items:
pod_name = pod.metadata.name
pod_groups = []
@@ -136,17 +120,17 @@ class K8sInventoryHelper(object):
if pod.metadata.labels:
pod_labels = pod.metadata.labels
# create a group for each label_value
- for key, value in iteritems(pod.metadata.labels):
- group_name = '{0}_{1}'.format(key, value)
+ for key, value in pod.metadata.labels:
+ group_name = 'label_{0}_{1}'.format(key, value)
if group_name not in pod_groups:
pod_groups.append(group_name)
self.inventory.add_group(group_name)
- for container in pod.status.container_statuses:
+ for container in pod.status.containerStatuses:
# add each pod_container to the namespace group, and to each label_value group
container_name = '{0}_{1}'.format(pod.metadata.name, container.name)
self.inventory.add_host(container_name)
- self.inventory.add_child(namespace_pod_group, container_name)
+ self.inventory.add_child(namespace_pods_group, container_name)
if pod_groups:
for group in pod_groups:
self.inventory.add_child(group, container_name)
@@ -155,14 +139,14 @@ class K8sInventoryHelper(object):
self.inventory.set_variable(container_name, 'object_type', 'pod')
self.inventory.set_variable(container_name, 'labels', pod_labels)
self.inventory.set_variable(container_name, 'annotations', pod_annotations)
- self.inventory.set_variable(container_name, 'cluster_name', pod.metadata.cluster_name)
- self.inventory.set_variable(container_name, 'pod_node_name', pod.spec.node_name)
- self.inventory.set_variable(container_name, 'pod_name', pod.spec.node_name)
- self.inventory.set_variable(container_name, 'pod_host_ip', pod.status.host_ip)
+ self.inventory.set_variable(container_name, 'cluster_name', pod.metadata.clusterName)
+ self.inventory.set_variable(container_name, 'pod_node_name', pod.spec.nodeName)
+ self.inventory.set_variable(container_name, 'pod_name', pod.spec.name)
+ self.inventory.set_variable(container_name, 'pod_host_ip', pod.status.hostIP)
self.inventory.set_variable(container_name, 'pod_phase', pod.status.phase)
- self.inventory.set_variable(container_name, 'pod_ip', pod.status.pod_ip)
- self.inventory.set_variable(container_name, 'pod_self_link', pod.metadata.self_link)
- self.inventory.set_variable(container_name, 'pod_resource_version', pod.metadata.resource_version)
+ self.inventory.set_variable(container_name, 'pod_ip', pod.status.podIP)
+ self.inventory.set_variable(container_name, 'pod_self_link', pod.metadata.selfLink)
+ self.inventory.set_variable(container_name, 'pod_resource_version', pod.metadata.resourceVersion)
self.inventory.set_variable(container_name, 'pod_uid', pod.metadata.uid)
self.inventory.set_variable(container_name, 'container_name', container.image)
self.inventory.set_variable(container_name, 'container_image', container.image)
@@ -179,20 +163,22 @@ class K8sInventoryHelper(object):
self.inventory.set_variable(container_name, 'ansible_{0}_container'.format(self.transport),
container.name)
- def get_services_for_namespace(self, name, namespace):
- self.helper.set_model('v1', 'service_list')
+ def get_services_for_namespace(self, client, name, namespace):
+ v1_service = client.resources.get(api_version='v1', kind='Service')
try:
- obj = self.helper.get_object(namespace=namespace)
- except KubernetesException as exc:
+ obj = v1_service.get(namespace=namespace)
+ except DynamicApiError as exc:
raise K8sInventoryException('Error fetching Service list: {0}'.format(exc.message))
- namespace_service_group = '{0}_services'.format(namespace)
+ namespace_group = 'namespace_{0}'.format(namespace)
+ namespace_services_group = '{0}_services'.format(namespace_group)
self.inventory.add_group(name)
- self.inventory.add_group(namespace)
- self.inventory.add_child(name, namespace)
- self.inventory.add_group(namespace_service_group)
- self.inventory.add_child(namespace, namespace_service_group)
+ self.inventory.add_group(namespace_group)
+ self.inventory.add_child(name, namespace_group)
+ self.inventory.add_group(namespace_services_group)
+ self.inventory.add_child(namespace_group, namespace_services_group)
+
for service in obj.items:
service_name = service.metadata.name
service_labels = {} if not service.metadata.labels else service.metadata.labels
@@ -202,51 +188,54 @@ class K8sInventoryHelper(object):
if service.metadata.labels:
# create a group for each label_value
- for key, value in iteritems(service.metadata.labels):
- group_name = '{0}_{1}'.format(key, value)
+ for key, value in service.metadata.labels:
+ group_name = 'label_{0}_{1}'.format(key, value)
self.inventory.add_group(group_name)
self.inventory.add_child(group_name, service_name)
- self.inventory.add_child(namespace_service_group, service_name)
+ try:
+ self.inventory.add_child(namespace_services_group, service_name)
+ except AnsibleError as e:
+ raise
ports = [{'name': port.name,
'port': port.port,
'protocol': port.protocol,
- 'targetPort': port.target_port,
- 'nodePort': port.node_port} for port in service.spec.ports]
+ 'targetPort': port.targetPort,
+ 'nodePort': port.nodePort} for port in service.spec.ports or []]
# add hostvars
self.inventory.set_variable(service_name, 'object_type', 'service')
self.inventory.set_variable(service_name, 'labels', service_labels)
self.inventory.set_variable(service_name, 'annotations', service_annotations)
- self.inventory.set_variable(service_name, 'cluster_name', service.metadata.cluster_name)
+ self.inventory.set_variable(service_name, 'cluster_name', service.metadata.clusterName)
self.inventory.set_variable(service_name, 'ports', ports)
self.inventory.set_variable(service_name, 'type', service.spec.type)
- self.inventory.set_variable(service_name, 'self_link', service.metadata.self_link)
- self.inventory.set_variable(service_name, 'resource_version', service.metadata.resource_version)
+ self.inventory.set_variable(service_name, 'self_link', service.metadata.selfLink)
+ self.inventory.set_variable(service_name, 'resource_version', service.metadata.resourceVersion)
self.inventory.set_variable(service_name, 'uid', service.metadata.uid)
- if service.spec.external_traffic_policy:
+ if service.spec.externalTrafficPolicy:
self.inventory.set_variable(service_name, 'external_traffic_policy',
- service.spec.external_traffic_policy)
- if hasattr(service.spec, 'external_ips') and service.spec.external_ips:
- self.inventory.set_variable(service_name, 'external_ips', service.spec.external_ips)
+ service.spec.externalTrafficPolicy)
+ if service.spec.externalIPs:
+ self.inventory.set_variable(service_name, 'external_ips', service.spec.externalIPs)
- if service.spec.external_name:
- self.inventory.set_variable(service_name, 'external_name', service.spec.external_name)
+ if service.spec.externalName:
+ self.inventory.set_variable(service_name, 'external_name', service.spec.externalName)
- if service.spec.health_check_node_port:
+ if service.spec.healthCheckNodePort:
self.inventory.set_variable(service_name, 'health_check_node_port',
- service.spec.health_check_node_port)
- if service.spec.load_balancer_ip:
+ service.spec.healthCheckNodePort)
+ if service.spec.loadBalancerIP:
self.inventory.set_variable(service_name, 'load_balancer_ip',
- service.spec.load_balancer_ip)
+ service.spec.loadBalancerIP)
if service.spec.selector:
self.inventory.set_variable(service_name, 'selector', service.spec.selector)
- if hasattr(service.status.load_balancer, 'ingress') and service.status.load_balancer.ingress:
+ if hasattr(service.status.loadBalancer, 'ingress') and service.status.loadBalancer.ingress:
load_balancer = [{'hostname': ingress.hostname,
- 'ip': ingress.ip} for ingress in service.status.load_balancer.ingress]
+ 'ip': ingress.ip} for ingress in service.status.loadBalancer.ingress]
self.inventory.set_variable(service_name, 'load_balancer', load_balancer)
@@ -256,46 +245,39 @@ class OpenShiftInventoryHelper(K8sInventoryHelper):
def fetch_objects(self, connections):
super(OpenShiftInventoryHelper, self).fetch_objects(connections)
- self.helper = self.get_helper('v1', 'namespace_list')
+ client = self.get_api_client()
if connections:
for connection in connections:
- self.authenticate(connection)
- name = connection.get('name', self.get_default_host_name(self.helper.api_client.host))
+ 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()
+ namespaces = self.get_available_namespaces(client)
for namespace in namespaces:
- self.get_routes_for_namespace(name, namespace)
+ self.get_routes_for_namespace(client, name, namespace)
else:
- name = self.get_default_host_name(self.helper.api_client.host)
- namespaces = self.get_available_namespaces()
+ name = self.get_default_host_name(client.configuration.host)
+ namespaces = self.get_available_namespaces(client)
for namespace in namespaces:
- self.get_routes_for_namespace(name, namespace)
+ self.get_routes_for_namespace(client, name, namespace)
- def get_helper(self, api_version, kind):
- try:
- helper = OpenShiftObjectHelper(api_version=api_version, kind=kind, debug=False)
- helper.get_model(api_version, kind)
- return helper
- except KubernetesException as exc:
- raise K8sInventoryException('Error initializing object helper: {0}'.format(exc.message))
-
- def get_routes_for_namespace(self, name, namespace):
- self.helper.set_model('v1', 'route_list')
+ def get_routes_for_namespace(self, client, name, namespace):
+ v1_route = client.resources.get(api_version='v1', kind='Route')
try:
- obj = self.helper.get_object(namespace=namespace)
- except KubernetesException as exc:
+ obj = v1_route.get(namespace=namespace)
+ except DynamicApiError as exc:
raise K8sInventoryException('Error fetching Routes list: {0}'.format(exc.message))
- namespace_routes_group = '{0}_routes'.format(namespace)
+ namespace_group = 'namespace_{0}'.format(namespace)
+ namespace_routes_group = '{0}_routes'.format(namespace_group)
self.inventory.add_group(name)
- self.inventory.add_group(namespace)
- self.inventory.add_child(name, namespace)
+ 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, namespace_routes_group)
+ self.inventory.add_child(namespace_group, namespace_routes_group)
for route in obj.items:
route_name = route.metadata.name
route_labels = {} if not route.metadata.labels else route.metadata.labels
@@ -305,8 +287,8 @@ class OpenShiftInventoryHelper(K8sInventoryHelper):
if route.metadata.labels:
# create a group for each label_value
- for key, value in iteritems(route.metadata.labels):
- group_name = '{0}_{1}'.format(key, 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)
@@ -315,10 +297,10 @@ class OpenShiftInventoryHelper(K8sInventoryHelper):
# 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.cluster_name)
+ 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.self_link)
- self.inventory.set_variable(route_name, 'resource_version', route.metadata.resource_version)
+ 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:
@@ -327,5 +309,5 @@ class OpenShiftInventoryHelper(K8sInventoryHelper):
if route.spec.path:
self.inventory.set_variable(route_name, 'path', route.spec.path)
- if hasattr(route.spec.port, 'target_port') and route.spec.port.target_port:
+ if hasattr(route.spec.port, 'targetPort') and route.spec.port.targetPort:
self.inventory.set_variable(route_name, 'port', route.spec.port)
diff --git a/lib/ansible/module_utils/k8s/lookup.py b/lib/ansible/module_utils/k8s/lookup.py
deleted file mode 100644
index a05f3ca8120..00000000000
--- a/lib/ansible/module_utils/k8s/lookup.py
+++ /dev/null
@@ -1,208 +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
-
-import json
-import os
-
-from ansible.module_utils.k8s.common import OpenShiftAnsibleModuleMixin, DateTimeEncoder, remove_secret_data, to_snake
-from ansible.module_utils.k8s.helper import AUTH_ARG_SPEC
-
-try:
- from openshift.helper.kubernetes import KubernetesObjectHelper
- from openshift.helper.exceptions import KubernetesException
- HAS_K8S_MODULE_HELPER = True
-except ImportError as exc:
- HAS_K8S_MODULE_HELPER = False
-
-try:
- import yaml
- HAS_YAML = True
-except ImportError:
- HAS_YAML = False
-
-
-class KubernetesLookup(object):
-
- def __init__(self):
-
- if not HAS_K8S_MODULE_HELPER:
- raise Exception(
- "Requires the OpenShift Python client. Try `pip install openshift`"
- )
-
- if not HAS_YAML:
- raise Exception(
- "Requires PyYAML. Try `pip install PyYAML`"
- )
-
- self.kind = None
- self.name = None
- self.namespace = None
- self.api_version = None
- self.label_selector = None
- self.field_selector = None
- self.include_uninitialized = None
- self.resource_definition = None
- self.helper = None
- self.connection = {}
-
- def run(self, terms, variables=None, **kwargs):
- self.kind = kwargs.get('kind')
- self.name = kwargs.get('resource_name')
- self.namespace = kwargs.get('namespace')
- self.api_version = kwargs.get('api_version', 'v1')
- self.label_selector = kwargs.get('label_selector')
- self.field_selector = kwargs.get('field_selector')
- self.include_uninitialized = kwargs.get('include_uninitialized', False)
-
- resource_definition = kwargs.get('resource_definition')
- src = kwargs.get('src')
- if src:
- resource_definition = self.load_resource_definition(src)
- if resource_definition:
- self.params_from_resource_definition(resource_definition)
-
- if not self.kind:
- raise Exception(
- "Error: no Kind specified. Use the 'kind' parameter, or provide an object YAML configuration "
- "using the 'resource_definition' parameter."
- )
-
- self.kind = to_snake(self.kind)
- self.helper = self.get_helper(self.api_version, self.kind)
-
- auth_args = ('host', 'api_key', 'kubeconfig', 'context', 'username', 'password',
- 'cert_file', 'key_file', 'ssl_ca_cert', 'verify_ssl')
-
- for arg in AUTH_ARG_SPEC:
- if arg in auth_args and kwargs.get(arg) is not None:
- self.connection[arg] = kwargs.get(arg)
-
- try:
- self.helper.set_client_config(**self.connection)
- except Exception as exc:
- raise Exception(
- "Client authentication failed: {0}".format(exc.message)
- )
-
- if self.name:
- return self.get_object()
-
- return self.list_objects()
-
- def get_helper(self, api_version, kind):
- try:
- helper = KubernetesObjectHelper(api_version=api_version, kind=kind, debug=False)
- helper.get_model(api_version, kind)
- return helper
- except KubernetesException as exc:
- raise Exception("Error initializing helper: {0}".format(exc.message))
-
- def load_resource_definition(self, src):
- """ Load the requested src path """
- path = os.path.normpath(src)
- if not os.path.exists(path):
- raise Exception("Error accessing {0}. Does the file exist?".format(path))
- try:
- result = yaml.safe_load(open(path, 'r'))
- except (IOError, yaml.YAMLError) as exc:
- raise Exception("Error loading resource_definition: {0}".format(exc))
- return result
-
- def params_from_resource_definition(self, defn):
- if defn.get('apiVersion'):
- self.api_version = defn['apiVersion']
- if defn.get('kind'):
- self.kind = defn['kind']
- if defn.get('metadata', {}).get('name'):
- self.name = defn['metadata']['name']
- if defn.get('metadata', {}).get('namespace'):
- self.namespace = defn['metadata']['namespace']
-
- def get_object(self):
- """ Fetch a named object """
- try:
- result = self.helper.get_object(self.name, self.namespace)
- except KubernetesException as exc:
- raise Exception('Failed to retrieve requested object: {0}'.format(exc.message))
- response = []
- if result is not None:
- # Convert Datetime objects to ISO format
- result_json = json.loads(json.dumps(result.to_dict(), cls=DateTimeEncoder))
- if self.kind == 'secret':
- remove_secret_data(result_json)
- response.append(result_json)
-
- return response
-
- def list_objects(self):
- """ Query for a set of objects """
- if self.namespace:
- method_name = 'list_namespaced_{0}'.format(self.kind)
- try:
- method = self.helper.lookup_method(method_name=method_name)
- except KubernetesException:
- raise Exception(
- "Failed to find method {0} for API {1}".format(method_name, self.api_version)
- )
- else:
- method_name = 'list_{0}_for_all_namespaces'.format(self.kind)
- try:
- method = self.helper.lookup_method(method_name=method_name)
- except KubernetesException:
- method_name = 'list_{0}'.format(self.kind)
- try:
- method = self.helper.lookup_method(method_name=method_name)
- except KubernetesException:
- raise Exception(
- "Failed to find method for API {0} and Kind {1}".format(self.api_version, self.kind)
- )
-
- params = {}
- if self.field_selector:
- params['field_selector'] = self.field_selector
- if self.label_selector:
- params['label_selector'] = self.label_selector
- params['include_uninitialized'] = self.include_uninitialized
-
- if self.namespace:
- try:
- result = method(self.namespace, **params)
- except KubernetesException as exc:
- raise Exception(exc.message)
- else:
- try:
- result = method(**params)
- except KubernetesException as exc:
- raise Exception(exc.message)
-
- response = []
- if result is not None:
- # Convert Datetime objects to ISO format
- result_json = json.loads(json.dumps(result.to_dict(), cls=DateTimeEncoder))
- response = result_json.get('items', [])
- if self.kind == 'secret':
- for item in response:
- remove_secret_data(item)
- return response
-
-
-class OpenShiftLookup(OpenShiftAnsibleModuleMixin, KubernetesLookup):
- pass
diff --git a/lib/ansible/module_utils/k8s/raw.py b/lib/ansible/module_utils/k8s/raw.py
index 7b7a68ca373..c3e1ccb88f2 100644
--- a/lib/ansible/module_utils/k8s/raw.py
+++ b/lib/ansible/module_utils/k8s/raw.py
@@ -18,13 +18,12 @@
from __future__ import absolute_import, division, print_function
-import copy
-from ansible.module_utils.k8s.helper import COMMON_ARG_SPEC, AUTH_ARG_SPEC, OPENSHIFT_ARG_SPEC
-from ansible.module_utils.k8s.common import KubernetesAnsibleModule, OpenShiftAnsibleModuleMixin, to_snake
+from ansible.module_utils.k8s.common import KubernetesAnsibleModule
+
try:
- from openshift.helper.exceptions import KubernetesException
+ from openshift.dynamic.exceptions import DynamicApiError, NotFoundError, ConflictError
except ImportError:
# Exception handled in common
pass
@@ -33,6 +32,8 @@ except ImportError:
class KubernetesRawModule(KubernetesAnsibleModule):
def __init__(self, *args, **kwargs):
+ self.client = None
+
mutually_exclusive = [
('resource_definition', 'src'),
]
@@ -42,170 +43,140 @@ class KubernetesRawModule(KubernetesAnsibleModule):
supports_check_mode=True,
**kwargs)
- self.kind = self.params.pop('kind')
- self.api_version = self.params.pop('api_version')
- self.resource_definition = self.params.pop('resource_definition')
- self.src = self.params.pop('src')
- if self.src:
- self.resource_definition = self.load_resource_definition(self.src)
-
- if self.resource_definition:
- self.api_version = self.resource_definition.get('apiVersion')
- self.kind = self.resource_definition.get('kind')
-
- self.api_version = self.api_version.lower()
- self.kind = to_snake(self.kind)
-
- if not self.api_version:
- self.fail_json(
- msg=("Error: no api_version specified. Use the api_version parameter, or provide it as part of a ",
- "resource_definition.")
- )
- if not self.kind:
- self.fail_json(
- msg="Error: no kind specified. Use the kind parameter, or provide it as part of a resource_definition"
- )
-
- self.helper = self.get_helper(self.api_version, self.kind)
-
- @property
- def argspec(self):
- argspec = copy.deepcopy(COMMON_ARG_SPEC)
- argspec.update(copy.deepcopy(AUTH_ARG_SPEC))
- return argspec
+ kind = self.params.pop('kind')
+ api_version = self.params.pop('api_version')
+ name = self.params.pop('name')
+ namespace = self.params.pop('namespace')
+ resource_definition = self.params.pop('resource_definition')
+ if resource_definition:
+ self.resource_definitions = [resource_definition]
+ src = self.params.pop('src')
+ if src:
+ self.resource_definitions = self.load_resource_definitions(src)
+
+ if not resource_definition and not src:
+ self.resource_definitions = [{
+ 'kind': kind,
+ 'apiVersion': api_version,
+ 'metadata': {
+ 'name': name,
+ 'namespace': namespace
+ }
+ }]
def execute_module(self):
- if self.resource_definition:
- resource_params = self.resource_to_parameters(self.resource_definition)
- self.params.update(resource_params)
-
- self.authenticate()
-
+ changed = False
+ results = []
+ self.client = self.get_api_client()
+ for definition in self.resource_definitions:
+ kind = definition.get('kind')
+ search_kind = kind
+ if kind.lower().endswith('list'):
+ search_kind = kind[:-4]
+ api_version = definition.get('apiVersion')
+ try:
+ resource = self.client.resources.get(kind=search_kind, api_version=api_version)
+ except Exception as e:
+ self.fail_json(msg='Failed to find resource {0}.{1}: {2}'.format(
+ api_version, search_kind, e
+ ))
+ result = self.perform_action(resource, definition)
+ 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 perform_action(self, resource, definition):
+ result = {'changed': False, 'result': {}}
state = self.params.pop('state', None)
force = self.params.pop('force', False)
- name = self.params.get('name')
- namespace = self.params.get('namespace')
+ name = definition.get('metadata', {}).get('name')
+ namespace = definition.get('metadata', {}).get('namespace')
existing = None
self.remove_aliases()
- return_attributes = dict(changed=False, result=dict())
-
- if self.helper.base_model_name_snake.endswith('list'):
- k8s_obj = self._read(name, namespace)
- return_attributes['result'] = k8s_obj.to_dict()
- self.exit_json(**return_attributes)
+ if definition['kind'].endswith('list'):
+ result['result'] = resource.get(namespace=namespace).to_dict()
+ result['changed'] = False
+ result['method'] = 'get'
+ return result
try:
- existing = self.helper.get_object(name, namespace)
- except KubernetesException as exc:
- self.fail_json(msg='Failed to retrieve requested object: {0}'.format(exc.message),
- error=exc.value.get('status'))
+ existing = resource.get(name=name, namespace=namespace)
+ except NotFoundError:
+ pass
+ 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)
if state == 'absent':
+ result['method'] = "delete"
if not existing:
# The object already does not exist
- self.exit_json(**return_attributes)
+ return result
else:
# Delete the object
if not self.check_mode:
try:
- self.helper.delete_object(name, namespace)
- except KubernetesException as exc:
- self.fail_json(msg="Failed to delete object: {0}".format(exc.message),
- error=exc.value.get('status'))
- return_attributes['changed'] = True
- self.exit_json(**return_attributes)
+ k8s_obj = resource.delete(name, namespace=namespace)
+ 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)
+ result['changed'] = True
+ return result
else:
if not existing:
- k8s_obj = self._create(namespace)
- return_attributes['result'] = k8s_obj.to_dict()
- return_attributes['changed'] = True
- self.exit_json(**return_attributes)
+ if not self.check_mode:
+ try:
+ k8s_obj = resource.create(definition, namespace=namespace)
+ 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
+ result['result'] = k8s_obj.to_dict()
+ result['changed'] = True
+ result['method'] = 'create'
+ return result
if existing and force:
- k8s_obj = None
- request_body = self.helper.request_body_from_params(self.params)
if not self.check_mode:
try:
- k8s_obj = self.helper.replace_object(name, namespace, body=request_body)
- except KubernetesException as exc:
- self.fail_json(msg="Failed to replace object: {0}".format(exc.message),
- error=exc.value.get('status'))
- return_attributes['result'] = k8s_obj.to_dict()
- return_attributes['changed'] = True
- self.exit_json(**return_attributes)
-
- # Check if existing object should be patched
- k8s_obj = copy.deepcopy(existing)
- try:
- self.helper.object_from_params(self.params, obj=k8s_obj)
- except KubernetesException as exc:
- self.fail_json(msg="Failed to patch object: {0}".format(exc.message))
- match, diff = self.helper.objects_match(self.helper.fix_serialization(existing), k8s_obj)
+ k8s_obj = resource.replace(definition, name=name, namespace=namespace)
+ result['result'] = k8s_obj.to_dict()
+ except DynamicApiError as exc:
+ self.fail_json(msg="Failed to replace object: {0}".format(exc.body),
+ error=exc.status, status=exc.status, reason=exc.reason)
+ result['changed'] = True
+ result['method'] = 'replace'
+ return result
+
+ match, diffs = self.diff_objects(existing.to_dict(), definition)
+
if match:
- return_attributes['result'] = existing.to_dict()
- self.exit_json(**return_attributes)
+ result['result'] = existing.to_dict()
+ return result
# Differences exist between the existing obj and requested params
if not self.check_mode:
try:
- k8s_obj = self.helper.patch_object(name, namespace, k8s_obj)
- except KubernetesException as exc:
- self.fail_json(msg="Failed to patch object: {0}".format(exc.message))
- return_attributes['result'] = k8s_obj.to_dict()
- return_attributes['changed'] = True
- self.exit_json(**return_attributes)
-
- def _create(self, namespace):
- request_body = None
- k8s_obj = None
- try:
- request_body = self.helper.request_body_from_params(self.params)
- except KubernetesException as exc:
- self.fail_json(msg="Failed to create object: {0}".format(exc.message))
- if not self.check_mode:
- try:
- k8s_obj = self.helper.create_object(namespace, body=request_body)
- except KubernetesException as exc:
- self.fail_json(msg="Failed to create object: {0}".format(exc.message),
- error=exc.value.get('status'))
- return k8s_obj
-
- def _read(self, name, namespace):
- k8s_obj = None
- try:
- k8s_obj = self.helper.get_object(name, namespace)
- except KubernetesException as exc:
- self.fail_json(msg='Failed to retrieve requested object',
- error=exc.value.get('status'))
- return k8s_obj
-
-
-class OpenShiftRawModule(OpenShiftAnsibleModuleMixin, KubernetesRawModule):
-
- @property
- def argspec(self):
- args = super(OpenShiftRawModule, self).argspec
- args.update(copy.deepcopy(OPENSHIFT_ARG_SPEC))
- return args
-
- def _create(self, namespace):
- if self.kind.lower() == 'project':
- return self._create_project()
- return KubernetesRawModule._create(self, namespace)
-
- def _create_project(self):
- new_obj = None
- k8s_obj = None
- try:
- new_obj = self.helper.object_from_params(self.params)
- except KubernetesException as exc:
- self.fail_json(msg="Failed to create object: {0}".format(exc.message))
- try:
- k8s_obj = self.helper.create_project(metadata=new_obj.metadata,
- display_name=self.params.get('display_name'),
- description=self.params.get('description'))
- except KubernetesException as exc:
- self.fail_json(msg='Failed to retrieve requested object',
- error=exc.value.get('status'))
- return k8s_obj
+ k8s_obj = resource.patch(definition, name=name, namespace=namespace)
+ result['result'] = k8s_obj.to_dict()
+ except DynamicApiError as exc:
+ self.fail_json(msg="Failed to patch object: {0}".format(exc.body),
+ error=exc.status, status=exc.status, reason=exc.reason)
+ result['changed'] = True
+ result['method'] = 'patch'
+ result['diff'] = diffs
+ return result
diff --git a/lib/ansible/module_utils/k8s/scale.py b/lib/ansible/module_utils/k8s/scale.py
index 3441bb89fc8..d0d6cc223c8 100644
--- a/lib/ansible/module_utils/k8s/scale.py
+++ b/lib/ansible/module_utils/k8s/scale.py
@@ -22,13 +22,12 @@ import copy
import math
import time
-from ansible.module_utils.six import iteritems
-from ansible.module_utils.k8s.common import OpenShiftAnsibleModuleMixin
from ansible.module_utils.k8s.raw import KubernetesRawModule
-from ansible.module_utils.k8s.helper import AUTH_ARG_SPEC, COMMON_ARG_SPEC
+from ansible.module_utils.k8s.common import AUTH_ARG_SPEC, COMMON_ARG_SPEC
try:
from openshift import watch
+ from openshift.dynamic.client import ResourceInstance
from openshift.helper.exceptions import KubernetesException
except ImportError as exc:
class KubernetesException(Exception):
@@ -47,14 +46,12 @@ SCALE_ARG_SPEC = {
class KubernetesAnsibleScaleModule(KubernetesRawModule):
def execute_module(self):
- if self.resource_definition:
- resource_params = self.resource_to_parameters(self.resource_definition)
- self.params.update(resource_params)
+ definition = self.resource_definitions[0]
- self.authenticate()
+ self.client = self.get_api_client()
- name = self.params.get('name')
- namespace = self.params.get('namespace')
+ name = definition['metadata']['name']
+ namespace = definition['metadata'].get('namespace')
current_replicas = self.params.get('current_replicas')
replicas = self.params.get('replicas')
resource_version = self.params.get('resource_version')
@@ -65,8 +62,10 @@ class KubernetesAnsibleScaleModule(KubernetesRawModule):
existing_count = None
return_attributes = dict(changed=False, result=dict())
+ resource = self.client.resources.get(api_version=definition['apiVersion'], kind=definition['kind'])
+
try:
- existing = self.helper.get_object(name, namespace)
+ existing = resource.get(name=name, namespace=namespace)
return_attributes['result'] = existing.to_dict()
except KubernetesException as exc:
self.fail_json(msg='Failed to retrieve requested object: {0}'.format(exc.message),
@@ -80,7 +79,7 @@ class KubernetesAnsibleScaleModule(KubernetesRawModule):
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.resource_version:
+ 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:
@@ -91,25 +90,13 @@ class KubernetesAnsibleScaleModule(KubernetesRawModule):
if not self.check_mode:
if self.kind == 'job':
existing.spec.parallelism = replicas
- k8s_obj = self.helper.patch_object(name, namespace, existing)
+ k8s_obj = resource.patch(existing.to_dict())
else:
- k8s_obj = self.scale(existing, replicas, wait, wait_time)
+ k8s_obj = self.scale(resource, existing, replicas, wait, wait_time)
return_attributes['result'] = k8s_obj.to_dict()
self.exit_json(**return_attributes)
- def resource_to_parameters(self, resource):
- """ Converts a resource definition to module parameters """
- parameters = {}
- for key, value in iteritems(resource):
- if key in ('apiVersion', 'kind', 'status'):
- continue
- elif key == 'metadata' and isinstance(value, dict):
- for meta_key, meta_value in iteritems(value):
- if meta_key in ('name', 'namespace', 'resourceVersion'):
- parameters[meta_key] = meta_value
- return parameters
-
@property
def argspec(self):
args = copy.deepcopy(COMMON_ARG_SPEC)
@@ -119,91 +106,67 @@ class KubernetesAnsibleScaleModule(KubernetesRawModule):
args.update(SCALE_ARG_SPEC)
return args
- def scale(self, existing_object, replicas, wait, wait_time):
+ def scale(self, resource, existing_object, replicas, wait, wait_time):
name = existing_object.metadata.name
namespace = existing_object.metadata.namespace
- method_name = 'patch_namespaced_{0}_scale'.format(self.kind)
- method = None
- model = None
- try:
- method = self.helper.lookup_method(method_name=method_name)
- except KubernetesException:
+ if not hasattr(resource, 'scale'):
self.fail_json(
- msg="Failed to get method {0}. Is 'scale' a valid operation for {1}?".format(method_name, self.kind)
+ msg="Cannot perform scale on resource of kind {0}".format(resource.kind)
)
- try:
- model = self.helper.get_model(self.api_version, 'scale')
- except KubernetesException:
- self.fail_json(
- msg="Failed to fetch the 'Scale' model for API version {0}. Are you using the correct "
- "API?".format(self.api_version)
- )
-
- scale_obj = model()
- scale_obj.kind = 'scale'
- scale_obj.api_version = self.api_version.lower()
- scale_obj.metadata = self.helper.get_model(
- self.api_version,
- self.helper.get_base_model_name(scale_obj.swagger_types['metadata'])
- )()
- scale_obj.metadata.name = name
- scale_obj.metadata.namespace = namespace
- scale_obj.spec = self.helper.get_model(
- self.api_version,
- self.helper.get_base_model_name(scale_obj.swagger_types['spec'])
- )()
- scale_obj.spec.replicas = replicas
+ scale_obj = {'metadata': {'name': name, 'namespace': namespace}, 'spec': {'replicas': replicas}}
return_obj = None
stream = None
if wait:
- w, stream = self._create_stream(namespace, wait_time)
+ w, stream = self._create_stream(resource, namespace, wait_time)
try:
- method(name, namespace, scale_obj)
+ resource.scale.patch(body=scale_obj)
except Exception as exc:
self.fail_json(
msg="Scale request failed: {0}".format(exc.message)
)
if wait and stream is not None:
- return_obj = self._read_stream(w, stream, name, replicas)
+ return_obj = self._read_stream(resource, w, stream, name, replicas)
if not return_obj:
return_obj = self._wait_for_response(name, namespace)
return return_obj
- def _create_stream(self, namespace, wait_time):
+ def _create_stream(self, resource, namespace, wait_time):
""" Create a stream of events for the object """
w = None
stream = None
try:
- list_method = self.helper.lookup_method('list', namespace)
w = watch.Watch()
- w._api_client = self.helper.api_client
+ w._api_client = self.client.client
if namespace:
- stream = w.stream(list_method, namespace, timeout_seconds=wait_time)
+ stream = w.stream(resource.get, serialize=False, namespace=namespace, timeout_seconds=wait_time)
else:
- stream = w.stream(list_method, timeout_seconds=wait_time)
+ stream = w.stream(resource.get, serialize=False, namespace=namespace, timeout_seconds=wait_time)
except KubernetesException:
pass
- except Exception:
- raise
return w, stream
- def _read_stream(self, watcher, stream, name, replicas):
+ def _read_stream(self, resource, watcher, stream, name, replicas):
""" Wait for ready_replicas to equal the requested number of replicas. """
return_obj = None
try:
for event in stream:
if event.get('object'):
- obj = event['object']
+ obj = ResourceInstance(resource, event['object'])
if obj.metadata.name == name and hasattr(obj, 'status'):
- if hasattr(obj.status, 'ready_replicas') and obj.status.ready_replicas == replicas:
+ if replicas == 0:
+ if not hasattr(obj.status, 'readyReplicas') or not obj.status.readyReplicas:
+ return_obj = obj
+ watcher.stop()
+ break
+ if hasattr(obj.status, 'readyReplicas') and obj.status.readyReplicas == replicas:
return_obj = obj
watcher.stop()
break
@@ -212,27 +175,23 @@ class KubernetesAnsibleScaleModule(KubernetesRawModule):
if not return_obj:
self.fail_json(msg="Error fetching the patched object. Try a higher wait_timeout value.")
- if return_obj.status.ready_replicas is None:
+ if replicas and return_obj.status.readyReplicas is None:
self.fail_json(msg="Failed to fetch the number of ready replicas. Try a higher wait_timeout value.")
- if return_obj.status.ready_replicas != replicas:
+ if replicas and return_obj.status.readyReplicas != replicas:
self.fail_json(msg="Number of ready replicas is {0}. Failed to reach {1} ready replicas within "
"the wait_timeout period.".format(return_obj.status.ready_replicas, replicas))
return return_obj
- def _wait_for_response(self, name, namespace):
+ def _wait_for_response(self, resource, name, namespace):
""" Wait for an API response """
tries = 0
half = math.ceil(20 / 2)
obj = None
while tries <= half:
- obj = self.helper.get_object(name, namespace)
+ obj = resource.get(name=name, namespace=namespace)
if obj:
break
tries += 2
time.sleep(2)
return obj
-
-
-class OpenShiftAnsibleScaleModule(OpenShiftAnsibleModuleMixin, KubernetesAnsibleScaleModule):
- pass
diff --git a/lib/ansible/modules/clustering/k8s/_k8s_raw.py b/lib/ansible/modules/clustering/k8s/_k8s_raw.py
new file mode 120000
index 00000000000..b0ee072f962
--- /dev/null
+++ b/lib/ansible/modules/clustering/k8s/_k8s_raw.py
@@ -0,0 +1 @@
+k8s.py
\ No newline at end of file
diff --git a/lib/ansible/modules/clustering/k8s/k8s_raw.py b/lib/ansible/modules/clustering/k8s/k8s.py
similarity index 96%
rename from lib/ansible/modules/clustering/k8s/k8s_raw.py
rename to lib/ansible/modules/clustering/k8s/k8s.py
index ff54c678066..57d0408dceb 100644
--- a/lib/ansible/modules/clustering/k8s/k8s_raw.py
+++ b/lib/ansible/modules/clustering/k8s/k8s.py
@@ -15,13 +15,15 @@ ANSIBLE_METADATA = {'metadata_version': '1.1',
DOCUMENTATION = '''
-module: k8s_raw
+module: k8s
short_description: Manage Kubernetes (K8s) objects
-version_added: "2.5"
+version_added: "2.6"
-author: "Chris Houseknecht (@chouseknecht)"
+author:
+ - "Chris Houseknecht (@chouseknecht)"
+ - "Fabian von Feilitzsch (@fabianvf)"
description:
- Use the OpenShift Python client to perform CRUD operations on K8s objects.
@@ -39,7 +41,7 @@ extends_documentation_fragment:
requirements:
- "python >= 2.7"
- - "openshift == 0.4.3"
+ - "openshift >= 0.6"
- "PyYAML >= 3.11"
'''
diff --git a/lib/ansible/modules/clustering/k8s/k8s_scale.py b/lib/ansible/modules/clustering/k8s/k8s_scale.py
index 5fb3dec414b..f438aaa0e4c 100644
--- a/lib/ansible/modules/clustering/k8s/k8s_scale.py
+++ b/lib/ansible/modules/clustering/k8s/k8s_scale.py
@@ -21,7 +21,9 @@ short_description: Set a new size for a Deployment, ReplicaSet, Replication Cont
version_added: "2.5"
-author: "Chris Houseknecht (@chouseknecht)"
+author:
+ - "Chris Houseknecht (@chouseknecht)"
+ - "Fabian von Feilitzsch (@fabianvf)"
description:
- Similar to the kubectl scale command. Use to set the number of replicas for a Deployment, ReplicatSet,
@@ -35,7 +37,7 @@ extends_documentation_fragment:
requirements:
- "python >= 2.7"
- - "openshift == 0.4.3"
+ - "openshift >= 0.6"
- "PyYAML >= 3.11"
'''
diff --git a/lib/ansible/modules/clustering/openshift/_openshift_raw.py b/lib/ansible/modules/clustering/openshift/_openshift_raw.py
new file mode 120000
index 00000000000..53f9af49a89
--- /dev/null
+++ b/lib/ansible/modules/clustering/openshift/_openshift_raw.py
@@ -0,0 +1 @@
+../k8s/k8s.py
\ No newline at end of file
diff --git a/lib/ansible/modules/clustering/openshift/_openshift_scale.py b/lib/ansible/modules/clustering/openshift/_openshift_scale.py
new file mode 120000
index 00000000000..bfa5417ea0e
--- /dev/null
+++ b/lib/ansible/modules/clustering/openshift/_openshift_scale.py
@@ -0,0 +1 @@
+../k8s/k8s_scale.py
\ No newline at end of file
diff --git a/lib/ansible/modules/clustering/openshift/openshift_raw.py b/lib/ansible/modules/clustering/openshift/openshift_raw.py
deleted file mode 100644
index bf39917d111..00000000000
--- a/lib/ansible/modules/clustering/openshift/openshift_raw.py
+++ /dev/null
@@ -1,204 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-
-# (c) 2018, Chris Houseknecht <@chouseknecht>
-# 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
-
-
-ANSIBLE_METADATA = {'metadata_version': '1.1',
- 'status': ['preview'],
- 'supported_by': 'community'}
-
-DOCUMENTATION = '''
-
-module: openshift_raw
-
-short_description: Manage OpenShift objects
-
-version_added: "2.5"
-
-author: "Chris Houseknecht (@chouseknecht)"
-
-description:
- - Use the OpenShift Python client to perform CRUD operations on OpenShift objects.
- - Pass the object definition from a source file or inline. See examples for reading
- files and using Jinja templates.
- - Access to the full range of K8s and OpenShift APIs.
- - Authenticate using either a config file, certificates, password or token.
- - Supports check mode.
-
-extends_documentation_fragment:
- - k8s_state_options
- - k8s_name_options
- - k8s_resource_options
- - k8s_auth_options
-
-options:
- description:
- description:
- - Use only when creating a project, otherwise ignored. Adds a description to the project
- metadata.
- display_name:
- description:
- - Use only when creating a project, otherwise ignored. Adds a display name to the project
- metadata.
-
-requirements:
- - "python >= 2.7"
- - "openshift == 0.4.3"
- - "PyYAML >= 3.11"
-'''
-
-EXAMPLES = '''
-- name: Create a project
- openshift_raw:
- api_version: v1
- kind: Project
- name: testing
- description: Testing
- display_name: "This is a test project."
- state: present
-
-- name: Create a Persistent Volume Claim from an inline definition
- openshift_raw:
- state: present
- definition:
- apiVersion: v1
- kind: PersistentVolumeClaim
- metadata:
- name: elastic-volume
- namespace: testing
- spec:
- resources:
- requests:
- storage: 5Gi
- accessModes:
- - ReadWriteOnce
-
-- name: Create a Deployment from an inline definition
- openshift_raw:
- state: present
- definition:
- 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
-
-- name: Remove an existing Deployment
- openshift_raw:
- api_version: v1
- kind: DeploymentConfig
- name: elastic
- namespace: testing
- state: absent
-
-- name: Create a Secret
- openshift_raw:
- definition:
- apiVersion: v1
- kind: Secret
- metadata:
- name: mysecret
- namespace: testing
- type: Opaque
- data:
- username: "{{ 'admin' | b64encode }}"
- password: "{{ 'foobard' | b64encode }}"
-
-- name: Retrieve a Secret
- openshift_raw:
- api: v1
- kind: Secret
- name: mysecret
- namespace: testing
- register: mysecret
-
-# Passing the object definition from a file
-
-- name: Create a Deployment by reading the definition from a local file
- openshift_raw:
- state: present
- src: /testing/deployment.yml
-
-- name: Read definition file from the Ansible controller file system
- openshift_raw:
- state: present
- definition: "{{ lookup('file', '/testing/deployment.yml') | from_yaml }}"
-
-- name: Read definition file from the Ansible controller file system after Jinja templating
- openshift_raw:
- state: present
- definition: "{{ lookup('template', '/testing/deployment.yml') | from_yaml }}"
-'''
-
-RETURN = '''
-result:
- description:
- - The created, patched, or otherwise present object. Will be empty in the case of a deletion.
- returned: success
- type: complex
- contains:
- api_version:
- description: The versioned schema of this representation of an object.
- returned: success
- type: str
- kind:
- description: Represents the REST resource this object represents.
- returned: success
- type: str
- metadata:
- description: Standard object metadata. Includes name, namespace, annotations, labels, etc.
- returned: success
- type: complex
- spec:
- description: Specific attributes of the object. Will vary based on the I(api_version) and I(kind).
- returned: success
- type: complex
- status:
- description: Current status details for the object.
- returned: success
- type: complex
- items:
- description: Returned only when the I(kind) is a List type resource. Contains a set of objects.
- returned: when resource is a List
- type: list
-'''
-
-from ansible.module_utils.k8s.raw import OpenShiftRawModule
-
-
-def main():
- OpenShiftRawModule().execute_module()
-
-
-if __name__ == '__main__':
- main()
diff --git a/lib/ansible/modules/clustering/openshift/openshift_scale.py b/lib/ansible/modules/clustering/openshift/openshift_scale.py
deleted file mode 100644
index e8e3dd26d95..00000000000
--- a/lib/ansible/modules/clustering/openshift/openshift_scale.py
+++ /dev/null
@@ -1,127 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-
-# (c) 2018, Chris Houseknecht <@chouseknecht>
-# 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
-
-ANSIBLE_METADATA = {'metadata_version': '1.1',
- 'status': ['preview'],
- 'supported_by': 'community'}
-
-DOCUMENTATION = '''
-
-module: openshift_scale
-
-short_description: Set a new size for a Deployment Config, Deployment, Replica Set, Replication Controller, or Job.
-
-version_added: "2.5"
-
-author: "Chris Houseknecht (@chouseknecht)"
-
-description:
- - Similar to the oc scale command. Use to set the number of replicas for a Deployment Config, Deployment,
- ReplicatSet, or Replication Controller, or the parallelism attribute of a Job. Supports check mode.
-
-extends_documentation_fragment:
- - k8s_name_options
- - k8s_auth_options
- - k8s_resource_options
- - k8s_scale_options
-
-requirements:
- - "python >= 2.7"
- - "openshift == 0.4.3"
- - "PyYAML >= 3.11"
-'''
-
-EXAMPLES = '''
-- name: Scale deployment config up, and extend timeout
- openshift_scale:
- api_version: v1
- kind: DeploymentConfig
- name: elastic
- namespace: myproject
- replicas: 3
- wait_timeout: 60
-
-- name: Scale deployment config down when current replicas match
- openshift_scale:
- api_version: v1
- kind: DeploymentConfig
- name: elastic
- namespace: myproject
- current_replicas: 3
- replicas: 2
-
-- name: Increase job parallelism
- openshift_scale:
- api_version: batch/v1
- kind: job
- name: pi-with-timeout
- namespace: testing
- replicas: 2
-
-# Match object using local file or inline definition
-
-- name: Scale deployment based on a file from the local filesystem
- openshift_scale:
- src: /myproject/elastic_deployment.yml
- replicas: 3
- wait: no
-
-- name: Scale deployment based on a template output
- openshift_scale:
- resource_definition: "{{ lookup('template', '/myproject/elastic_deployment.yml') | from_yaml }}"
- replicas: 3
- wait: no
-
-- name: Scale deployment based on a file from the Ansible controller filesystem
- openshift_scale:
- resource_definition: "{{ lookup('file', '/myproject/elastic_deployment.yml') | from_yaml }}"
- replicas: 3
- wait: no
-'''
-
-RETURN = '''
-result:
- description:
- - If a change was made, will return the patched object, otherwise returns the existing object.
- returned: success
- type: complex
- contains:
- api_version:
- description: The versioned schema of this representation of an object.
- returned: success
- type: str
- kind:
- description: Represents the REST resource this object represents.
- returned: success
- type: str
- metadata:
- description: Standard object metadata. Includes name, namespace, annotations, labels, etc.
- returned: success
- type: complex
- spec:
- description: Specific attributes of the object. Will vary based on the I(api_version) and I(kind).
- returned: success
- type: complex
- status:
- description: Current status details for the object.
- returned: success
- type: complex
-'''
-
-from ansible.module_utils.k8s.scale import OpenShiftAnsibleScaleModule
-
-
-def main():
- OpenShiftAnsibleScaleModule().execute_module()
-
-
-if __name__ == '__main__':
- main()
diff --git a/lib/ansible/plugins/inventory/k8s.py b/lib/ansible/plugins/inventory/k8s.py
index b5089288885..9ce2c4c2a1e 100644
--- a/lib/ansible/plugins/inventory/k8s.py
+++ b/lib/ansible/plugins/inventory/k8s.py
@@ -9,6 +9,7 @@ DOCUMENTATION = '''
plugin_type: inventory
authors:
- Chris Houseknecht <@chouseknecht>
+ - Fabian von Feilitzsch <@fabianvf>
short_description: Kubernetes (K8s) inventory source
@@ -76,7 +77,7 @@ DOCUMENTATION = '''
requirements:
- "python >= 2.7"
- - "openshift == 0.4.1"
+ - "openshift >= 0.6"
- "PyYAML >= 3.11"
'''
diff --git a/lib/ansible/plugins/inventory/openshift.py b/lib/ansible/plugins/inventory/openshift.py
index 17b8f347ede..6b1dabe5b86 100644
--- a/lib/ansible/plugins/inventory/openshift.py
+++ b/lib/ansible/plugins/inventory/openshift.py
@@ -77,7 +77,7 @@ DOCUMENTATION = '''
requirements:
- "python >= 2.7"
- - "openshift == 0.4.1"
+ - "openshift >= 0.6"
- "PyYAML >= 3.11"
'''
diff --git a/lib/ansible/plugins/lookup/_openshift.py b/lib/ansible/plugins/lookup/_openshift.py
new file mode 120000
index 00000000000..b0ee072f962
--- /dev/null
+++ b/lib/ansible/plugins/lookup/_openshift.py
@@ -0,0 +1 @@
+k8s.py
\ No newline at end of file
diff --git a/lib/ansible/plugins/lookup/k8s.py b/lib/ansible/plugins/lookup/k8s.py
index 5b7c6069482..8e7abe64b03 100644
--- a/lib/ansible/plugins/lookup/k8s.py
+++ b/lib/ansible/plugins/lookup/k8s.py
@@ -29,11 +29,15 @@ DOCUMENTATION = """
description:
- Uses the OpenShift Python client to fetch a specific object by name, all matching objects within a
- namespace, or all matching objects for all namespaces.
+ namespace, or all matching objects for all namespaces, as well as information about the cluster.
- Provides access the full range of K8s APIs.
- Enables authentication via config file, certificates, password or token.
options:
+ cluster_info:
+ description:
+ - Use to specify the type of cluster information you are attempting to retrieve. Will take priority
+ over all the other options.
api_version:
description:
- Use to specify the API version. If I(resource definition) is provided, the I(apiVersion) from the
@@ -115,7 +119,7 @@ DOCUMENTATION = """
requirements:
- "python >= 2.7"
- - "openshift == 0.4.1"
+ - "openshift >= 0.6"
- "PyYAML >= 3.11"
notes:
@@ -189,7 +193,95 @@ RETURN = """
"""
from ansible.plugins.lookup import LookupBase
-from ansible.module_utils.k8s.lookup import KubernetesLookup
+
+import os
+
+from ansible.module_utils.six import iteritems
+from ansible.module_utils.k8s.common import K8sAnsibleMixin
+
+try:
+ from openshift.dynamic import DynamicClient
+ from openshift.dynamic.exceptions import NotFoundError
+ HAS_K8S_MODULE_HELPER = True
+except ImportError as exc:
+ HAS_K8S_MODULE_HELPER = False
+
+try:
+ import yaml
+ HAS_YAML = True
+except ImportError:
+ HAS_YAML = False
+
+
+class KubernetesLookup(K8sAnsibleMixin):
+
+ def __init__(self):
+
+ if not HAS_K8S_MODULE_HELPER:
+ raise Exception(
+ "Requires the OpenShift Python client. Try `pip install openshift`"
+ )
+
+ if not HAS_YAML:
+ raise Exception(
+ "Requires PyYAML. Try `pip install PyYAML`"
+ )
+
+ self.kind = None
+ self.name = None
+ self.namespace = None
+ self.api_version = None
+ self.label_selector = None
+ self.field_selector = None
+ self.include_uninitialized = None
+ self.resource_definition = None
+ self.helper = None
+ self.connection = {}
+
+ def run(self, terms, variables=None, **kwargs):
+ self.params = kwargs
+ self.client = self.get_api_client()
+
+ cluster_info = kwargs.get('cluster_info')
+ if cluster_info == 'version':
+ return [self.client.version]
+ if cluster_info == 'api_groups':
+ return [self.client.resources.api_groups]
+
+ self.kind = kwargs.get('kind')
+ self.name = kwargs.get('resource_name')
+ self.namespace = kwargs.get('namespace')
+ self.api_version = kwargs.get('api_version', 'v1')
+ self.label_selector = kwargs.get('label_selector')
+ self.field_selector = kwargs.get('field_selector')
+ self.include_uninitialized = kwargs.get('include_uninitialized', False)
+
+ resource_definition = kwargs.get('resource_definition')
+ src = kwargs.get('src')
+ if src:
+ resource_definition = self.load_resource_definitions(src)[0]
+ if resource_definition:
+ self.kind = resource_definition.get('kind', self.kind)
+ self.api_version = resource_definition.get('apiVersion', self.api_version)
+ self.name = resource_definition.get('metadata', {}).get('name', self.name)
+ self.namespace = resource_definition.get('metadata', {}).get('namespace', self.namespace)
+
+ if not self.kind:
+ raise Exception(
+ "Error: no Kind specified. Use the 'kind' parameter, or provide an object YAML configuration "
+ "using the 'resource_definition' parameter."
+ )
+
+ resource = self.client.resources.get(kind=self.kind, api_version=self.api_version)
+ try:
+ k8s_obj = resource.get(name=self.name, namespace=self.namespace, label_selector=self.label_selector, field_selector=self.field_selector)
+ except NotFoundError:
+ return []
+
+ if self.name:
+ return [k8s_obj.to_dict()]
+
+ return k8s_obj.to_dict().get('items')
class LookupModule(LookupBase):
diff --git a/lib/ansible/plugins/lookup/openshift.py b/lib/ansible/plugins/lookup/openshift.py
deleted file mode 100644
index fffeca777bc..00000000000
--- a/lib/ansible/plugins/lookup/openshift.py
+++ /dev/null
@@ -1,197 +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
-
-DOCUMENTATION = """
- lookup: openshift
-
- version_added: "2.5"
-
- short_description: Query the OpenShift API
-
- description:
- - Uses the OpenShift Python client to fetch a specific object by name, all matching objects within a
- namespace, or all matching objects for all namespaces.
- - Provides access the full range of K8s APIs.
- - Enables authentication via config file, certificates, password or token.
-
- options:
- api_version:
- description:
- - Use to specify the API version. If I(resource definition) is provided, the I(apiVersion) from the
- I(resource_definition) will override this option.
- default: v1
- kind:
- description:
- - Use to specify an object model. If I(resource definition) is provided, the I(kind) from a
- I(resource_definition) will override this option.
- required: true
- resource_name:
- description:
- - Fetch a specific object by name. If I(resource definition) is provided, the I(metadata.name) value
- from the I(resource_definition) will override this option.
- namespace:
- description:
- - Limit the objects returned to a specific namespace. If I(resource definition) is provided, the
- I(metadata.namespace) value from the I(resource_definition) will override this option.
- label_selector:
- description:
- - Additional labels to include in the query. Ignored when I(resource_name) is provided.
- field_selector:
- description:
- - Specific fields on which to query. Ignored when I(resource_name) is provided.
- resource_definition:
- description:
- - "Provide a YAML configuration for an object. NOTE: I(kind), I(api_version), I(resource_name), I(namespace),
- and I(resource_version) will be overwritten by corresponding values found in the provided
- I(resource_definition)."
- src:
- description:
- - "Provide a path to a file containing a valid YAML definition of an object dated. Mutually
- exclusive with I(resource_definition). NOTE: I(kind), I(api_version), I(resource_name), and I(namespace)
- will be overwritten by corresponding values found in the configuration read in from the I(src) file."
- - Reads from the local file system. To read from the Ansible controller's file system, use the file lookup
- plugin or template lookup plugin, combined with the from_yaml filter, and pass the result to
- I(resource_definition). See Examples below.
- 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.
- 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.
- 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.
- cert_file:
- description:
- - Path to a certificate used to authenticate with the API. Can also be specified via K8S_AUTH_CERT_FILE
- environment variable.
- key_file:
- description:
- - Path to a key file used to authenticate with the API. Can also be specified via K8S_AUTH_HOST environment
- variable.
- ssl_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.
- verify_ssl:
- 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
-
- requirements:
- - "python >= 2.7"
- - "openshift == 0.4.1"
- - "PyYAML >= 3.11"
-
- notes:
- - "The OpenShift Python client wraps the K8s Python client, providing full access to
- all of the APIS and models available on both platforms. For API version details and
- additional information visit https://github.com/openshift/openshift-restclient-python"
-"""
-
-EXAMPLES = """
-- name: Fetch a list of projects
- set_fact:
- projects: "{{ lookup('openshift', api_version='v1', kind='Project') }}"
-
-- name: Fetch all deployments
- set_fact:
- deployments: "{{ lookup('openshift', kind='DeploymentConfig', namespace='testing') }}"
-
-- name: Fetch all deployments in a namespace
- set_fact:
- deployments: "{{ lookup('openshift', kind='DeploymentConfig', namespace='testing') }}"
-
-- name: Fetch a specific deployment by name
- set_fact:
- deployments: "{{ lookup('openshift', kind='DeploymentConfig', namespace='testing', resource_name='elastic') }}"
-
-- name: Fetch with label selector
- set_fact:
- service: "{{ lookup('openshift', kind='Service', label_selector='app=galaxy') }}"
-
-# Use parameters from a YAML config
-
-- name: Load config from the Ansible controller filesystem
- set_fact:
- config: "{{ lookup('file', 'service.yml') | from_yaml }}"
-
-- name: Using the config (loaded from a file in prior task), fetch the latest version of the object
- set_fact:
- service: "{{ lookup('openshift', resource_definition=config) }}"
-
-- name: Use a config from the local filesystem
- set_fact:
- service: "{{ lookup('openshift', src='service.yml') }}"
-"""
-
-RETURN = """
- _list:
- description:
- - One or more object definitions returned from the API.
- type: complex
- contains:
- api_version:
- description: The versioned schema of this representation of an object.
- returned: success
- type: str
- kind:
- description: Represents the REST resource this object represents.
- returned: success
- type: str
- metadata:
- description: Standard object metadata. Includes name, namespace, annotations, labels, etc.
- returned: success
- type: complex
- spec:
- description: Specific attributes of the object. Will vary based on the I(api_version) and I(kind).
- returned: success
- type: complex
- status:
- description: Current status details for the object.
- returned: success
- type: complex
-"""
-
-from ansible.plugins.lookup import LookupBase
-from ansible.module_utils.k8s.lookup import OpenShiftLookup
-
-
-class LookupModule(LookupBase):
- def run(self, terms, variables=None, **kwargs):
- return OpenShiftLookup().run(terms, variables=variables, **kwargs)