From 78023e79d75ca98cf76c2ff24fae517c4a2d8da5 Mon Sep 17 00:00:00 2001
From: Adam Miller <admiller@redhat.com>
Date: Sat, 26 May 2018 00:30:16 -0500
Subject: [PATCH] Revert "Move k8s modules to dynamic backend (#39632)"
 (#40743)

This reverts commit 4373b155a58dc8268e6a54ca9d32000450b39fdb.
---
 lib/ansible/module_utils/k8s/common.py        | 343 +++++-----
 lib/ansible/module_utils/k8s/helper.py        | 633 ++++++++++++++++++
 lib/ansible/module_utils/k8s/inventory.py     | 224 ++++---
 lib/ansible/module_utils/k8s/lookup.py        | 208 ++++++
 lib/ansible/module_utils/k8s/raw.py           | 249 ++++---
 lib/ansible/module_utils/k8s/scale.py         | 111 ++-
 .../modules/clustering/k8s/_k8s_raw.py        |   1 -
 .../clustering/k8s/{k8s.py => k8s_raw.py}     |  10 +-
 .../modules/clustering/k8s/k8s_scale.py       |   6 +-
 .../clustering/openshift/_openshift_raw.py    |   1 -
 .../clustering/openshift/_openshift_scale.py  |   1 -
 .../clustering/openshift/openshift_raw.py     | 204 ++++++
 .../clustering/openshift/openshift_scale.py   | 127 ++++
 lib/ansible/plugins/inventory/k8s.py          |   3 +-
 lib/ansible/plugins/inventory/openshift.py    |   2 +-
 lib/ansible/plugins/lookup/_openshift.py      |   1 -
 lib/ansible/plugins/lookup/k8s.py             |  98 +--
 lib/ansible/plugins/lookup/openshift.py       | 197 ++++++
 18 files changed, 1881 insertions(+), 538 deletions(-)
 create mode 100644 lib/ansible/module_utils/k8s/helper.py
 create mode 100644 lib/ansible/module_utils/k8s/lookup.py
 delete mode 120000 lib/ansible/modules/clustering/k8s/_k8s_raw.py
 rename lib/ansible/modules/clustering/k8s/{k8s.py => k8s_raw.py} (96%)
 delete mode 120000 lib/ansible/modules/clustering/openshift/_openshift_raw.py
 delete mode 120000 lib/ansible/modules/clustering/openshift/_openshift_scale.py
 create mode 100644 lib/ansible/modules/clustering/openshift/openshift_raw.py
 create mode 100644 lib/ansible/modules/clustering/openshift/openshift_scale.py
 delete mode 120000 lib/ansible/plugins/lookup/_openshift.py
 create mode 100644 lib/ansible/plugins/lookup/openshift.py

diff --git a/lib/ansible/module_utils/k8s/common.py b/lib/ansible/module_utils/k8s/common.py
index 93d0c3b72c2..37ec9320220 100644
--- a/lib/ansible/module_utils/k8s/common.py
+++ b/lib/ansible/module_utils/k8s/common.py
@@ -19,17 +19,31 @@
 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:
-    import kubernetes
-    from openshift.dynamic import DynamicClient
+    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:
+except ImportError as exc:
+    class KubernetesObjectHelper(object):
+        pass
+
+    class OpenShiftObjectHelper(object):
+        pass
+
     HAS_K8S_MODULE_HELPER = False
 
 try:
@@ -38,188 +52,52 @@ try:
 except ImportError:
     HAS_YAML = False
 
-try:
-    import dictdiffer
-    HAS_DICTDIFFER = True
-except ImportError:
-    HAS_DICTDIFFER = False
 
-try:
-    import urllib3
-    urllib3.disable_warnings()
-except ImportError:
+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):
     pass
 
-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):
-        """
-        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:
-            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):
-        """
-        The helper doesn't know what to do with aliased keys
-        """
-        for k, v in iteritems(self.argspec):
-            if 'aliases' in v:
-                for alias in v['aliases']:
-                    if alias in self.params:
-                        self.params.pop(alias)
-
-    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:
-            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
-
-    @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
-
-        diffs = list(dictdiffer.diff(new, get_shared_attrs(existing, new)))
-        match = len(diffs) == 0
-        return match, diffs
-
-
-class KubernetesAnsibleModule(AnsibleModule, K8sAnsibleMixin):
+class KubernetesAnsibleModule(AnsibleModule):
     resource_definition = None
     api_version = None
     kind = None
+    helper = None
 
     def __init__(self, *args, **kwargs):
 
@@ -232,5 +110,114 @@ class KubernetesAnsibleModule(AnsibleModule, K8sAnsibleMixin):
         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`")
+
+    @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):
+        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))
+
+    def remove_aliases(self):
+        """
+        The helper doesn't know what to do with aliased keys
+        """
+        for k, v in iteritems(self.argspec):
+            if 'aliases' in v:
+                for alias in v['aliases']:
+                    if alias in self.params:
+                        self.params.pop(alias)
+
+    def load_resource_definition(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'))
+        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
+
+
+class OpenShiftAnsibleModuleMixin(object):
+
+    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))
diff --git a/lib/ansible/module_utils/k8s/helper.py b/lib/ansible/module_utils/k8s/helper.py
new file mode 100644
index 00000000000..438dd9d0dd8
--- /dev/null
+++ b/lib/ansible/module_utils/k8s/helper.py
@@ -0,0 +1,633 @@
+#
+#  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 <http://www.gnu.org/licenses/>.
+
+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 d8cf8b6654f..c8c48c39c0b 100644
--- a/lib/ansible/module_utils/k8s/inventory.py
+++ b/lib/ansible/module_utils/k8s/inventory.py
@@ -18,24 +18,22 @@
 
 from __future__ import absolute_import, division, print_function
 
-from ansible.module_utils.k8s.common import K8sAnsibleMixin, HAS_K8S_MODULE_HELPER
+from ansible.module_utils.six import iteritems
 
 try:
-    from ansible.errors import AnsibleError
-except ImportError:
-    AnsibleError = Exception
-
-try:
-    from openshift.dynamic.exceptions import DynamicApiError
-except ImportError:
-    pass
+    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
 
 
 class K8sInventoryException(Exception):
     pass
 
 
-class K8sInventoryHelper(K8sAnsibleMixin):
+class K8sInventoryHelper(object):
     helper = None
     transport = 'kubectl'
 
@@ -58,7 +56,7 @@ class K8sInventoryHelper(K8sAnsibleMixin):
             self.fetch_objects(connections)
 
     def fetch_objects(self, connections):
-        client = self.get_api_client()
+        self.helper = self.get_helper('v1', 'namespace_list')
 
         if connections:
             if not isinstance(connections, list):
@@ -67,50 +65,68 @@ class K8sInventoryHelper(K8sAnsibleMixin):
             for connection in connections:
                 if not isinstance(connection, dict):
                     raise K8sInventoryException("Expecting connection to be a dictionary.")
-                client = self.get_api_client(**connection)
-                name = connection.get('name', self.get_default_host_name(client.configuration.host))
+                self.authenticate(connection)
+                name = connection.get('name', self.get_default_host_name(self.helper.api_client.host))
                 if connection.get('namespaces'):
                     namespaces = connections['namespaces']
                 else:
-                    namespaces = self.get_available_namespaces(client)
+                    namespaces = self.get_available_namespaces()
                 for namespace in namespaces:
-                    self.get_pods_for_namespace(client, name, namespace)
-                    self.get_services_for_namespace(client, name, namespace)
+                    self.get_pods_for_namespace(name, namespace)
+                    self.get_services_for_namespace(name, namespace)
         else:
-            name = self.get_default_host_name(client.configuration.host)
-            namespaces = self.get_available_namespaces(client)
+            name = self.get_default_host_name(self.helper.api_client.host)
+            namespaces = self.get_available_namespaces()
             for namespace in namespaces:
-                self.get_pods_for_namespace(client, name, namespace)
-                self.get_services_for_namespace(client, name, namespace)
+                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))
 
     @staticmethod
     def get_default_host_name(host):
         return host.replace('https://', '').replace('http://', '').replace('.', '-').replace(':', '_')
 
-    def get_available_namespaces(self, client):
-        v1_namespace = client.resources.get(api_version='v1', kind='Namespace')
+    def get_helper(self, api_version, kind):
         try:
-            obj = v1_namespace.get()
-        except DynamicApiError as exc:
+            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):
+        try:
+            obj = self.helper.get_object()
+        except KubernetesObjectHelper 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, client, name, namespace):
-        v1_pod = client.resources.get(api_version='v1', kind='Pod')
+    def get_pods_for_namespace(self, name, namespace):
+        self.helper.set_model('v1', 'pod_list')
         try:
-            obj = v1_pod.get(namespace=namespace)
-        except DynamicApiError as exc:
+            obj = self.helper.get_object(namespace=namespace)
+        except KubernetesException as exc:
             raise K8sInventoryException('Error fetching Pod list: {0}'.format(exc.message))
 
-        namespace_group = 'namespace_{0}'.format(namespace)
-        namespace_pods_group = '{0}_pods'.format(namespace_group)
+        namespace_pod_group = '{0}_pods'.format(namespace)
 
         self.inventory.add_group(name)
-        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)
-
+        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)
         for pod in obj.items:
             pod_name = pod.metadata.name
             pod_groups = []
@@ -120,17 +136,17 @@ class K8sInventoryHelper(K8sAnsibleMixin):
             if pod.metadata.labels:
                 pod_labels = pod.metadata.labels
                 # create a group for each label_value
-                for key, value in pod.metadata.labels:
-                    group_name = 'label_{0}_{1}'.format(key, value)
+                for key, value in iteritems(pod.metadata.labels):
+                    group_name = '{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.containerStatuses:
+            for container in pod.status.container_statuses:
                 # 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_pods_group, container_name)
+                self.inventory.add_child(namespace_pod_group, container_name)
                 if pod_groups:
                     for group in pod_groups:
                         self.inventory.add_child(group, container_name)
@@ -139,14 +155,14 @@ class K8sInventoryHelper(K8sAnsibleMixin):
                 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.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, '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, 'pod_phase', pod.status.phase)
-                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_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_uid', pod.metadata.uid)
                 self.inventory.set_variable(container_name, 'container_name', container.image)
                 self.inventory.set_variable(container_name, 'container_image', container.image)
@@ -163,22 +179,20 @@ class K8sInventoryHelper(K8sAnsibleMixin):
                 self.inventory.set_variable(container_name, 'ansible_{0}_container'.format(self.transport),
                                             container.name)
 
-    def get_services_for_namespace(self, client, name, namespace):
-        v1_service = client.resources.get(api_version='v1', kind='Service')
+    def get_services_for_namespace(self, name, namespace):
+        self.helper.set_model('v1', 'service_list')
         try:
-            obj = v1_service.get(namespace=namespace)
-        except DynamicApiError as exc:
+            obj = self.helper.get_object(namespace=namespace)
+        except KubernetesException as exc:
             raise K8sInventoryException('Error fetching Service list: {0}'.format(exc.message))
 
-        namespace_group = 'namespace_{0}'.format(namespace)
-        namespace_services_group = '{0}_services'.format(namespace_group)
+        namespace_service_group = '{0}_services'.format(namespace)
 
         self.inventory.add_group(name)
-        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)
-
+        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)
         for service in obj.items:
             service_name = service.metadata.name
             service_labels = {} if not service.metadata.labels else service.metadata.labels
@@ -188,54 +202,51 @@ class K8sInventoryHelper(K8sAnsibleMixin):
 
             if service.metadata.labels:
                 # create a group for each label_value
-                for key, value in service.metadata.labels:
-                    group_name = 'label_{0}_{1}'.format(key, value)
+                for key, value in iteritems(service.metadata.labels):
+                    group_name = '{0}_{1}'.format(key, value)
                     self.inventory.add_group(group_name)
                     self.inventory.add_child(group_name, service_name)
 
-            try:
-                self.inventory.add_child(namespace_services_group, service_name)
-            except AnsibleError as e:
-                raise
+            self.inventory.add_child(namespace_service_group, service_name)
 
             ports = [{'name': port.name,
                       'port': port.port,
                       'protocol': port.protocol,
-                      'targetPort': port.targetPort,
-                      'nodePort': port.nodePort} for port in service.spec.ports or []]
+                      'targetPort': port.target_port,
+                      'nodePort': port.node_port} for port in service.spec.ports]
 
             # 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.clusterName)
+            self.inventory.set_variable(service_name, 'cluster_name', service.metadata.cluster_name)
             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.selfLink)
-            self.inventory.set_variable(service_name, 'resource_version', service.metadata.resourceVersion)
+            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, 'uid', service.metadata.uid)
 
-            if service.spec.externalTrafficPolicy:
+            if service.spec.external_traffic_policy:
                 self.inventory.set_variable(service_name, 'external_traffic_policy',
-                                            service.spec.externalTrafficPolicy)
-            if service.spec.externalIPs:
-                self.inventory.set_variable(service_name, 'external_ips', service.spec.externalIPs)
+                                            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)
 
-            if service.spec.externalName:
-                self.inventory.set_variable(service_name, 'external_name', service.spec.externalName)
+            if service.spec.external_name:
+                self.inventory.set_variable(service_name, 'external_name', service.spec.external_name)
 
-            if service.spec.healthCheckNodePort:
+            if service.spec.health_check_node_port:
                 self.inventory.set_variable(service_name, 'health_check_node_port',
-                                            service.spec.healthCheckNodePort)
-            if service.spec.loadBalancerIP:
+                                            service.spec.health_check_node_port)
+            if service.spec.load_balancer_ip:
                 self.inventory.set_variable(service_name, 'load_balancer_ip',
-                                            service.spec.loadBalancerIP)
+                                            service.spec.load_balancer_ip)
             if service.spec.selector:
                 self.inventory.set_variable(service_name, 'selector', service.spec.selector)
 
-            if hasattr(service.status.loadBalancer, 'ingress') and service.status.loadBalancer.ingress:
+            if hasattr(service.status.load_balancer, 'ingress') and service.status.load_balancer.ingress:
                 load_balancer = [{'hostname': ingress.hostname,
-                                  'ip': ingress.ip} for ingress in service.status.loadBalancer.ingress]
+                                  'ip': ingress.ip} for ingress in service.status.load_balancer.ingress]
                 self.inventory.set_variable(service_name, 'load_balancer', load_balancer)
 
 
@@ -245,39 +256,46 @@ class OpenShiftInventoryHelper(K8sInventoryHelper):
 
     def fetch_objects(self, connections):
         super(OpenShiftInventoryHelper, self).fetch_objects(connections)
-        client = self.get_api_client()
+        self.helper = self.get_helper('v1', 'namespace_list')
 
         if connections:
             for connection in connections:
-                client = self.get_api_client(**connection)
-                name = connection.get('name', self.get_default_host_name(client.configuration.host))
+                self.authenticate(connection)
+                name = connection.get('name', self.get_default_host_name(self.helper.api_client.host))
                 if connection.get('namespaces'):
                     namespaces = connection['namespaces']
                 else:
-                    namespaces = self.get_available_namespaces(client)
+                    namespaces = self.get_available_namespaces()
                 for namespace in namespaces:
-                    self.get_routes_for_namespace(client, name, namespace)
+                    self.get_routes_for_namespace(name, namespace)
         else:
-            name = self.get_default_host_name(client.configuration.host)
-            namespaces = self.get_available_namespaces(client)
+            name = self.get_default_host_name(self.helper.api_client.host)
+            namespaces = self.get_available_namespaces()
             for namespace in namespaces:
-                self.get_routes_for_namespace(client, name, namespace)
+                self.get_routes_for_namespace(name, namespace)
 
-    def get_routes_for_namespace(self, client, name, namespace):
-        v1_route = client.resources.get(api_version='v1', kind='Route')
+    def get_helper(self, api_version, kind):
         try:
-            obj = v1_route.get(namespace=namespace)
-        except DynamicApiError as exc:
+            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')
+        try:
+            obj = self.helper.get_object(namespace=namespace)
+        except KubernetesException as exc:
             raise K8sInventoryException('Error fetching Routes list: {0}'.format(exc.message))
 
-        namespace_group = 'namespace_{0}'.format(namespace)
-        namespace_routes_group = '{0}_routes'.format(namespace_group)
+        namespace_routes_group = '{0}_routes'.format(namespace)
 
         self.inventory.add_group(name)
-        self.inventory.add_group(namespace_group)
-        self.inventory.add_child(name, namespace_group)
+        self.inventory.add_group(namespace)
+        self.inventory.add_child(name, namespace)
         self.inventory.add_group(namespace_routes_group)
-        self.inventory.add_child(namespace_group, namespace_routes_group)
+        self.inventory.add_child(namespace, namespace_routes_group)
         for route in obj.items:
             route_name = route.metadata.name
             route_labels = {} if not route.metadata.labels else route.metadata.labels
@@ -287,8 +305,8 @@ class OpenShiftInventoryHelper(K8sInventoryHelper):
 
             if route.metadata.labels:
                 # create a group for each label_value
-                for key, value in route.metadata.labels:
-                    group_name = 'label_{0}_{1}'.format(key, value)
+                for key, value in iteritems(route.metadata.labels):
+                    group_name = '{0}_{1}'.format(key, value)
                     self.inventory.add_group(group_name)
                     self.inventory.add_child(group_name, route_name)
 
@@ -297,10 +315,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.clusterName)
+            self.inventory.set_variable(route_name, 'cluster_name', route.metadata.cluster_name)
             self.inventory.set_variable(route_name, 'object_type', 'route')
-            self.inventory.set_variable(route_name, 'self_link', route.metadata.selfLink)
-            self.inventory.set_variable(route_name, 'resource_version', route.metadata.resourceVersion)
+            self.inventory.set_variable(route_name, 'self_link', route.metadata.self_link)
+            self.inventory.set_variable(route_name, 'resource_version', route.metadata.resource_version)
             self.inventory.set_variable(route_name, 'uid', route.metadata.uid)
 
             if route.spec.host:
@@ -309,5 +327,5 @@ class OpenShiftInventoryHelper(K8sInventoryHelper):
             if route.spec.path:
                 self.inventory.set_variable(route_name, 'path', route.spec.path)
 
-            if hasattr(route.spec.port, 'targetPort') and route.spec.port.targetPort:
+            if hasattr(route.spec.port, 'target_port') and route.spec.port.target_port:
                 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
new file mode 100644
index 00000000000..a05f3ca8120
--- /dev/null
+++ b/lib/ansible/module_utils/k8s/lookup.py
@@ -0,0 +1,208 @@
+#
+#  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 <http://www.gnu.org/licenses/>.
+
+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 c3e1ccb88f2..7b7a68ca373 100644
--- a/lib/ansible/module_utils/k8s/raw.py
+++ b/lib/ansible/module_utils/k8s/raw.py
@@ -18,12 +18,13 @@
 
 from __future__ import absolute_import, division, print_function
 
+import copy
 
-from ansible.module_utils.k8s.common import KubernetesAnsibleModule
-
+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
 
 try:
-    from openshift.dynamic.exceptions import DynamicApiError, NotFoundError, ConflictError
+    from openshift.helper.exceptions import KubernetesException
 except ImportError:
     # Exception handled in common
     pass
@@ -32,8 +33,6 @@ except ImportError:
 class KubernetesRawModule(KubernetesAnsibleModule):
 
     def __init__(self, *args, **kwargs):
-        self.client = None
-
         mutually_exclusive = [
             ('resource_definition', 'src'),
         ]
@@ -43,140 +42,170 @@ class KubernetesRawModule(KubernetesAnsibleModule):
                                          supports_check_mode=True,
                                          **kwargs)
 
-        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)
+        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 not resource_definition and not src:
-            self.resource_definitions = [{
-                'kind': kind,
-                'apiVersion': api_version,
-                'metadata': {
-                    'name': name,
-                    'namespace': namespace
-                }
-            }]
+        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
 
     def execute_module(self):
-        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 self.resource_definition:
+            resource_params = self.resource_to_parameters(self.resource_definition)
+            self.params.update(resource_params)
 
-        if len(results) == 1:
-            self.exit_json(**results[0])
+        self.authenticate()
 
-        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 = definition.get('metadata', {}).get('name')
-        namespace = definition.get('metadata', {}).get('namespace')
+        name = self.params.get('name')
+        namespace = self.params.get('namespace')
         existing = None
 
         self.remove_aliases()
 
-        if definition['kind'].endswith('list'):
-            result['result'] = resource.get(namespace=namespace).to_dict()
-            result['changed'] = False
-            result['method'] = 'get'
-            return result
+        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)
 
         try:
-            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)
+            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'))
 
         if state == 'absent':
-            result['method'] = "delete"
             if not existing:
                 # The object already does not exist
-                return result
+                self.exit_json(**return_attributes)
             else:
                 # Delete the object
                 if not self.check_mode:
                     try:
-                        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
+                        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)
         else:
             if not existing:
-                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
+                k8s_obj = self._create(namespace)
+                return_attributes['result'] = k8s_obj.to_dict()
+                return_attributes['changed'] = True
+                self.exit_json(**return_attributes)
 
             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 = 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)
+                        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)
             if match:
-                result['result'] = existing.to_dict()
-                return result
+                return_attributes['result'] = existing.to_dict()
+                self.exit_json(**return_attributes)
             # Differences exist between the existing obj and requested params
             if not self.check_mode:
                 try:
-                    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
+                    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
diff --git a/lib/ansible/module_utils/k8s/scale.py b/lib/ansible/module_utils/k8s/scale.py
index d0d6cc223c8..3441bb89fc8 100644
--- a/lib/ansible/module_utils/k8s/scale.py
+++ b/lib/ansible/module_utils/k8s/scale.py
@@ -22,12 +22,13 @@ 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.common import AUTH_ARG_SPEC, COMMON_ARG_SPEC
+from ansible.module_utils.k8s.helper 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):
@@ -46,12 +47,14 @@ SCALE_ARG_SPEC = {
 class KubernetesAnsibleScaleModule(KubernetesRawModule):
 
     def execute_module(self):
-        definition = self.resource_definitions[0]
+        if self.resource_definition:
+            resource_params = self.resource_to_parameters(self.resource_definition)
+            self.params.update(resource_params)
 
-        self.client = self.get_api_client()
+        self.authenticate()
 
-        name = definition['metadata']['name']
-        namespace = definition['metadata'].get('namespace')
+        name = self.params.get('name')
+        namespace = self.params.get('namespace')
         current_replicas = self.params.get('current_replicas')
         replicas = self.params.get('replicas')
         resource_version = self.params.get('resource_version')
@@ -62,10 +65,8 @@ 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 = resource.get(name=name, namespace=namespace)
+            existing = self.helper.get_object(name, namespace)
             return_attributes['result'] = existing.to_dict()
         except KubernetesException as exc:
             self.fail_json(msg='Failed to retrieve requested object: {0}'.format(exc.message),
@@ -79,7 +80,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.resourceVersion:
+        if resource_version and resource_version != existing.metadata.resource_version:
             self.exit_json(**return_attributes)
 
         if current_replicas is not None and existing_count != current_replicas:
@@ -90,13 +91,25 @@ class KubernetesAnsibleScaleModule(KubernetesRawModule):
             if not self.check_mode:
                 if self.kind == 'job':
                     existing.spec.parallelism = replicas
-                    k8s_obj = resource.patch(existing.to_dict())
+                    k8s_obj = self.helper.patch_object(name, namespace, existing)
                 else:
-                    k8s_obj = self.scale(resource, existing, replicas, wait, wait_time)
+                    k8s_obj = self.scale(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)
@@ -106,67 +119,91 @@ class KubernetesAnsibleScaleModule(KubernetesRawModule):
         args.update(SCALE_ARG_SPEC)
         return args
 
-    def scale(self, resource, existing_object, replicas, wait, wait_time):
+    def scale(self, 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
 
-        if not hasattr(resource, 'scale'):
+        try:
+            method = self.helper.lookup_method(method_name=method_name)
+        except KubernetesException:
             self.fail_json(
-                msg="Cannot perform scale on resource of kind {0}".format(resource.kind)
+                msg="Failed to get method {0}. Is 'scale' a valid operation for {1}?".format(method_name, self.kind)
             )
 
-        scale_obj = {'metadata': {'name': name, 'namespace': namespace}, 'spec': {'replicas': replicas}}
+        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
 
         return_obj = None
         stream = None
 
         if wait:
-            w, stream = self._create_stream(resource, namespace, wait_time)
+            w, stream = self._create_stream(namespace, wait_time)
 
         try:
-            resource.scale.patch(body=scale_obj)
+            method(name, namespace, 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(resource, w, stream, name, replicas)
+            return_obj = self._read_stream(w, stream, name, replicas)
 
         if not return_obj:
             return_obj = self._wait_for_response(name, namespace)
 
         return return_obj
 
-    def _create_stream(self, resource, namespace, wait_time):
+    def _create_stream(self, 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.client.client
+            w._api_client = self.helper.api_client
             if namespace:
-                stream = w.stream(resource.get, serialize=False, namespace=namespace, timeout_seconds=wait_time)
+                stream = w.stream(list_method, namespace, timeout_seconds=wait_time)
             else:
-                stream = w.stream(resource.get, serialize=False, namespace=namespace, timeout_seconds=wait_time)
+                stream = w.stream(list_method, timeout_seconds=wait_time)
         except KubernetesException:
             pass
+        except Exception:
+            raise
         return w, stream
 
-    def _read_stream(self, resource, watcher, stream, name, replicas):
+    def _read_stream(self, 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 = ResourceInstance(resource, event['object'])
+                    obj = event['object']
                     if obj.metadata.name == name and hasattr(obj, 'status'):
-                        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:
+                        if hasattr(obj.status, 'ready_replicas') and obj.status.ready_replicas == replicas:
                             return_obj = obj
                             watcher.stop()
                             break
@@ -175,23 +212,27 @@ class KubernetesAnsibleScaleModule(KubernetesRawModule):
 
         if not return_obj:
             self.fail_json(msg="Error fetching the patched object. Try a higher wait_timeout value.")
-        if replicas and return_obj.status.readyReplicas is None:
+        if return_obj.status.ready_replicas is None:
             self.fail_json(msg="Failed to fetch the number of ready replicas. Try a higher wait_timeout value.")
-        if replicas and return_obj.status.readyReplicas != replicas:
+        if return_obj.status.ready_replicas != 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, resource, name, namespace):
+    def _wait_for_response(self, name, namespace):
         """ Wait for an API response """
         tries = 0
         half = math.ceil(20 / 2)
         obj = None
 
         while tries <= half:
-            obj = resource.get(name=name, namespace=namespace)
+            obj = self.helper.get_object(name, 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
deleted file mode 120000
index b0ee072f962..00000000000
--- a/lib/ansible/modules/clustering/k8s/_k8s_raw.py
+++ /dev/null
@@ -1 +0,0 @@
-k8s.py
\ No newline at end of file
diff --git a/lib/ansible/modules/clustering/k8s/k8s.py b/lib/ansible/modules/clustering/k8s/k8s_raw.py
similarity index 96%
rename from lib/ansible/modules/clustering/k8s/k8s.py
rename to lib/ansible/modules/clustering/k8s/k8s_raw.py
index 57d0408dceb..ff54c678066 100644
--- a/lib/ansible/modules/clustering/k8s/k8s.py
+++ b/lib/ansible/modules/clustering/k8s/k8s_raw.py
@@ -15,15 +15,13 @@ ANSIBLE_METADATA = {'metadata_version': '1.1',
 
 DOCUMENTATION = '''
 
-module: k8s
+module: k8s_raw
 
 short_description: Manage Kubernetes (K8s) objects
 
-version_added: "2.6"
+version_added: "2.5"
 
-author:
-    - "Chris Houseknecht (@chouseknecht)"
-    - "Fabian von Feilitzsch (@fabianvf)"
+author: "Chris Houseknecht (@chouseknecht)"
 
 description:
   - Use the OpenShift Python client to perform CRUD operations on K8s objects.
@@ -41,7 +39,7 @@ extends_documentation_fragment:
 
 requirements:
   - "python >= 2.7"
-  - "openshift >= 0.6"
+  - "openshift == 0.4.3"
   - "PyYAML >= 3.11"
 '''
 
diff --git a/lib/ansible/modules/clustering/k8s/k8s_scale.py b/lib/ansible/modules/clustering/k8s/k8s_scale.py
index f438aaa0e4c..5fb3dec414b 100644
--- a/lib/ansible/modules/clustering/k8s/k8s_scale.py
+++ b/lib/ansible/modules/clustering/k8s/k8s_scale.py
@@ -21,9 +21,7 @@ short_description: Set a new size for a Deployment, ReplicaSet, Replication Cont
 
 version_added: "2.5"
 
-author:
-    - "Chris Houseknecht (@chouseknecht)"
-    - "Fabian von Feilitzsch (@fabianvf)"
+author: "Chris Houseknecht (@chouseknecht)"
 
 description:
   - Similar to the kubectl scale command. Use to set the number of replicas for a Deployment, ReplicatSet,
@@ -37,7 +35,7 @@ extends_documentation_fragment:
 
 requirements:
     - "python >= 2.7"
-    - "openshift >= 0.6"
+    - "openshift == 0.4.3"
     - "PyYAML >= 3.11"
 '''
 
diff --git a/lib/ansible/modules/clustering/openshift/_openshift_raw.py b/lib/ansible/modules/clustering/openshift/_openshift_raw.py
deleted file mode 120000
index 53f9af49a89..00000000000
--- a/lib/ansible/modules/clustering/openshift/_openshift_raw.py
+++ /dev/null
@@ -1 +0,0 @@
-../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
deleted file mode 120000
index bfa5417ea0e..00000000000
--- a/lib/ansible/modules/clustering/openshift/_openshift_scale.py
+++ /dev/null
@@ -1 +0,0 @@
-../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
new file mode 100644
index 00000000000..bf39917d111
--- /dev/null
+++ b/lib/ansible/modules/clustering/openshift/openshift_raw.py
@@ -0,0 +1,204 @@
+#!/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
new file mode 100644
index 00000000000..e8e3dd26d95
--- /dev/null
+++ b/lib/ansible/modules/clustering/openshift/openshift_scale.py
@@ -0,0 +1,127 @@
+#!/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 9ce2c4c2a1e..b5089288885 100644
--- a/lib/ansible/plugins/inventory/k8s.py
+++ b/lib/ansible/plugins/inventory/k8s.py
@@ -9,7 +9,6 @@ DOCUMENTATION = '''
     plugin_type: inventory
     authors:
       - Chris Houseknecht <@chouseknecht>
-      - Fabian von Feilitzsch <@fabianvf>
 
     short_description: Kubernetes (K8s) inventory source
 
@@ -77,7 +76,7 @@ DOCUMENTATION = '''
 
     requirements:
     - "python >= 2.7"
-    - "openshift >= 0.6"
+    - "openshift == 0.4.1"
     - "PyYAML >= 3.11"
 '''
 
diff --git a/lib/ansible/plugins/inventory/openshift.py b/lib/ansible/plugins/inventory/openshift.py
index 6b1dabe5b86..17b8f347ede 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.6"
+    - "openshift == 0.4.1"
     - "PyYAML >= 3.11"
 '''
 
diff --git a/lib/ansible/plugins/lookup/_openshift.py b/lib/ansible/plugins/lookup/_openshift.py
deleted file mode 120000
index b0ee072f962..00000000000
--- a/lib/ansible/plugins/lookup/_openshift.py
+++ /dev/null
@@ -1 +0,0 @@
-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 8e7abe64b03..5b7c6069482 100644
--- a/lib/ansible/plugins/lookup/k8s.py
+++ b/lib/ansible/plugins/lookup/k8s.py
@@ -29,15 +29,11 @@ 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, as well as information about the cluster.
+        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:
-      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
@@ -119,7 +115,7 @@ DOCUMENTATION = """
 
     requirements:
       - "python >= 2.7"
-      - "openshift >= 0.6"
+      - "openshift == 0.4.1"
       - "PyYAML >= 3.11"
 
     notes:
@@ -193,95 +189,7 @@ RETURN = """
 """
 
 from ansible.plugins.lookup import LookupBase
-
-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')
+from ansible.module_utils.k8s.lookup import KubernetesLookup
 
 
 class LookupModule(LookupBase):
diff --git a/lib/ansible/plugins/lookup/openshift.py b/lib/ansible/plugins/lookup/openshift.py
new file mode 100644
index 00000000000..fffeca777bc
--- /dev/null
+++ b/lib/ansible/plugins/lookup/openshift.py
@@ -0,0 +1,197 @@
+#
+#  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 <http://www.gnu.org/licenses/>.
+
+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)