K8s dynamic collected changes (#40745)

* Move k8s modules to dynamic backend

* update required openshift version

* update -> patch

* use new dynamic client exceptions

* style

* guard urllib3 import

* guard ansibleerror import

* give more information about error cause

* format in variable

* style

* rename tests

* Search for provided kind in a few more places to match old behavior, properly handle failure

* make common code use fail instead of fail_json, to work for lookup plugins as well

* update docs

* move openshift_raw tests into k8s tests

* fix typo

* Use diff of response and resource to determine change, don't do any checking client-side before making requests

* remove duplicate yaml blocks

* Update porting guide for k8s module

* remove invalid doc refs

* If fuzzy searching finds a resource, update resource_definition to match proper kind and version

* remote unsupported openshift_raw variables

* properly check environment variables when determining auth method:
pull/40981/head
Fabian von Feilitzsch 7 years ago committed by Matt Clay
parent cad351328d
commit 65d96bf354

@ -42,6 +42,9 @@ Deprecation notices
The following modules will be removed in Ansible 2.10. Please update your playbooks accordingly.
* ``k8s_raw`` use :ref:`k8s <k8s_module>` instead.
* ``openshift_raw`` use :ref:`k8s <k8s_module>` instead.
* ``openshift_scale`` use :ref:`k8s_scale <k8s_scale_module>` instead.
Noteworthy module changes
-------------------------
@ -62,12 +65,34 @@ Noteworthy module changes
destination path like this::
$ ansible localhost -m file -a 'path=/tmp/lib state=directory'
* The ``k8s_raw`` and ``openshift_raw`` modules have been aliased to the new ``k8s`` module.
* The ``k8s`` module supports all Kubernetes resources including those from Custom Resource Definitions and aggregated API servers. This includes all OpenShift resources.
* The ``k8s`` module will not accept resources where subkeys have been snake_cased. This was a workaround that was suggested with the ``k8s_raw`` and ``openshift_raw`` modules.
* The ``k8s`` module may not accept resources where the ``api_version`` has been changed to match the shortened version in the Kubernetes Python client. You should now specify the proper full Kubernetes ``api_version`` for a resource.
* The ``k8s`` module can now process multi-document YAML files if they are passed with the ``src`` parameter. It will process each document as a separate resource. Resources provided inline with the ``resource_definition`` parameter must still be a single document.
* The ``k8s`` module will not automatically change ``Project`` creation requests into ``ProjectRequest`` creation requests as the ``openshift_raw`` module did. You must now specify the ``ProjectRequest`` kind explicitly.
* The ``k8s`` module will not automatically remove secrets from the Ansible return values (and by extension the log). In order to prevent secret values in a task from being logged, specify the ``no_log`` parameter on the task block.
* The ``k8s_scale`` module now supports scalable OpenShift objects, such as ``DeploymentConfig``.
Plugins
=======
No notable changes.
Deprecation notices
-------------------
The following modules will be removed in Ansible 2.10. Please update your playbooks accordingly.
* ``openshift`` use ``k8s`` instead.
Noteworthy plugin changes
-------------------------
* The ``k8s`` lookup plugin now supports all Kubernetes resources including those from Custom Resource Definitions and aggregated API servers. This includes all OpenShift resources.
* The ``k8s`` lookup plugin may not accept resources where the ``api_version`` has been changed to match the shortened version in the Kubernetes Python client. You should now specify the proper full Kubernetes ``api_version`` for a resource.
* The ``k8s`` lookup plugin will no longer remove secrets from the Ansible return values (and by extension the log). In order to prevent secret values in a task from being logged, specify the ``no_log`` parameter on the task block.
Porting custom scripts
======================

@ -19,31 +19,18 @@
from __future__ import absolute_import, division, print_function
import os
import re
import copy
import json
from datetime import datetime
from ansible.module_utils.six import iteritems
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.k8s.helper import\
AnsibleMixin,\
HAS_STRING_UTILS
try:
from openshift.helper.kubernetes import KubernetesObjectHelper
from openshift.helper.openshift import OpenShiftObjectHelper
from openshift.helper.exceptions import KubernetesException
import kubernetes
from openshift.dynamic import DynamicClient
from openshift.dynamic.exceptions import ResourceNotFoundError, ResourceNotUniqueError
HAS_K8S_MODULE_HELPER = True
except ImportError as exc:
class KubernetesObjectHelper(object):
pass
class OpenShiftObjectHelper(object):
pass
except ImportError:
HAS_K8S_MODULE_HELPER = False
try:
@ -52,104 +39,155 @@ try:
except ImportError:
HAS_YAML = False
try:
import dictdiffer
HAS_DICTDIFFER = True
except ImportError:
HAS_DICTDIFFER = False
def remove_secret_data(obj_dict):
""" Remove any sensitive data from a K8s dict"""
if obj_dict.get('data'):
# Secret data
obj_dict.pop('data')
if obj_dict.get('string_data'):
# The API should not return sting_data in Secrets, but just in case
obj_dict.pop('string_data')
if obj_dict['metadata'].get('annotations'):
# Remove things like 'openshift.io/token-secret' from metadata
for key in [k for k in obj_dict['metadata']['annotations'] if 'secret' in k]:
obj_dict['metadata']['annotations'].pop(key)
def to_snake(name):
""" Convert a string from camel to snake """
if not name:
return name
def _replace(m):
m = m.group(0)
return m[0] + '_' + m[1:]
p = r'[a-z][A-Z]|' \
r'[A-Z]{2}[a-z]'
return re.sub(p, _replace, name).lower()
class DateTimeEncoder(json.JSONEncoder):
# When using json.dumps() with K8s object, pass cls=DateTimeEncoder to handle any datetime objects
def default(self, o):
if isinstance(o, datetime):
return o.isoformat()
return json.JSONEncoder.default(self, o)
class KubernetesAnsibleModuleHelper(AnsibleMixin, KubernetesObjectHelper):
try:
import urllib3
urllib3.disable_warnings()
except ImportError:
pass
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
class KubernetesAnsibleModule(AnsibleModule):
resource_definition = None
api_version = None
kind = None
helper = 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 __init__(self, *args, **kwargs):
def get_api_client(self, **auth_params):
auth_args = AUTH_ARG_SPEC.keys()
kwargs['argument_spec'] = self.argspec
AnsibleModule.__init__(self, *args, **kwargs)
auth_params = auth_params or getattr(self, 'params', {})
auth = copy.deepcopy(auth_params)
if not HAS_K8S_MODULE_HELPER:
self.fail_json(msg="This module requires the OpenShift Python client. Try `pip install openshift`")
configuration = kubernetes.client.Configuration()
for key, value in iteritems(auth_params):
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)
auth[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'
if not HAS_YAML:
self.fail_json(msg="This module requires PyYAML. Try `pip install PyYAML`")
# 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 not HAS_STRING_UTILS:
self.fail_json(msg="This module requires Python string utils. Try `pip install python-string-utils`")
if auth_method == 'file':
return DynamicClient(self.client_from_kubeconfig(auth.get('kubeconfig'), auth.get('context')))
@property
def argspec(self):
raise NotImplementedError()
if auth_method == 'params':
return DynamicClient(kubernetes.client.ApiClient(configuration))
def get_helper(self, api_version, kind):
def client_from_kubeconfig(self, config_file, context):
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):
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 find_resource(self, kind, api_version, fail=False):
for attribute in ['kind', 'name', 'singular_name']:
try:
auth_options = {}
auth_args = ('host', 'api_key', 'kubeconfig', 'context', 'username', 'password',
'cert_file', 'key_file', 'ssl_ca_cert', 'verify_ssl')
for key, value in iteritems(self.params):
if key in auth_args and value is not None:
auth_options[key] = value
self.helper.set_client_config(**auth_options)
except KubernetesException as e:
self.fail_json(msg='Error loading config', error=str(e))
return self.client.resources.get(**{'api_version': api_version, attribute: kind})
except (ResourceNotFoundError, ResourceNotUniqueError):
pass
try:
return self.client.resources.get(api_version=api_version, short_names=[kind])
except (ResourceNotFoundError, ResourceNotUniqueError):
if fail:
self.fail(msg='Failed to find exact match for {0}.{1} by [kind, name, singularName, shortNames]'.format(api_version, kind))
def remove_aliases(self):
"""
@ -161,63 +199,47 @@ class KubernetesAnsibleModule(AnsibleModule):
if alias in self.params:
self.params.pop(alias)
def load_resource_definition(self, src):
def load_resource_definitions(self, src):
""" Load the requested src path """
result = None
path = os.path.normpath(src)
if not os.path.exists(path):
self.fail_json(msg="Error accessing {0}. Does the file exist?".format(path))
self.fail(msg="Error accessing {0}. Does the file exist?".format(path))
try:
result = yaml.safe_load(open(path, 'r'))
with open(path, 'r') as f:
result = list(yaml.safe_load_all(f))
except (IOError, yaml.YAMLError) as exc:
self.fail_json(msg="Error loading resource_definition: {0}".format(exc))
self.fail(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))
)
@staticmethod
def diff_objects(existing, new):
if not HAS_DICTDIFFER:
return False, []
diffs = list(dictdiffer.diff(new, existing))
match = len(diffs) == 0
return match, diffs
class OpenShiftAnsibleModuleHelper(AnsibleMixin, OpenShiftObjectHelper):
pass
class KubernetesAnsibleModule(AnsibleModule, K8sAnsibleMixin):
resource_definition = None
api_version = None
kind = None
class OpenShiftAnsibleModuleMixin(object):
def __init__(self, *args, **kwargs):
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))
kwargs['argument_spec'] = self.argspec
AnsibleModule.__init__(self, *args, **kwargs)
if not HAS_K8S_MODULE_HELPER:
self.fail_json(msg="This module requires the OpenShift Python client. Try `pip install openshift`")
if not HAS_YAML:
self.fail_json(msg="This module requires PyYAML. Try `pip install PyYAML`")
def execute_module(self):
raise NotImplementedError()
def fail(self, msg=None):
self.fail_json(msg=msg)

@ -1,633 +0,0 @@
#
# Copyright 2018 Red Hat | Ansible
#
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <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

@ -18,22 +18,24 @@
from __future__ import absolute_import, division, print_function
from ansible.module_utils.six import iteritems
from ansible.module_utils.k8s.common import K8sAnsibleMixin, HAS_K8S_MODULE_HELPER
try:
from openshift.helper.kubernetes import KubernetesObjectHelper
from openshift.helper.openshift import OpenShiftObjectHelper
from openshift.helper.exceptions import KubernetesException
HAS_K8S_MODULE_HELPER = True
except ImportError as exc:
HAS_K8S_MODULE_HELPER = False
from ansible.errors import AnsibleError
except ImportError:
AnsibleError = Exception
try:
from openshift.dynamic.exceptions import DynamicApiError
except ImportError:
pass
class K8sInventoryException(Exception):
pass
class K8sInventoryHelper(object):
class K8sInventoryHelper(K8sAnsibleMixin):
helper = None
transport = 'kubectl'
@ -56,7 +58,7 @@ class K8sInventoryHelper(object):
self.fetch_objects(connections)
def fetch_objects(self, connections):
self.helper = self.get_helper('v1', 'namespace_list')
client = self.get_api_client()
if connections:
if not isinstance(connections, list):
@ -65,68 +67,50 @@ class K8sInventoryHelper(object):
for connection in connections:
if not isinstance(connection, dict):
raise K8sInventoryException("Expecting connection to be a dictionary.")
self.authenticate(connection)
name = connection.get('name', self.get_default_host_name(self.helper.api_client.host))
client = self.get_api_client(**connection)
name = connection.get('name', self.get_default_host_name(client.configuration.host))
if connection.get('namespaces'):
namespaces = connections['namespaces']
else:
namespaces = self.get_available_namespaces()
namespaces = self.get_available_namespaces(client)
for namespace in namespaces:
self.get_pods_for_namespace(name, namespace)
self.get_services_for_namespace(name, namespace)
self.get_pods_for_namespace(client, name, namespace)
self.get_services_for_namespace(client, name, namespace)
else:
name = self.get_default_host_name(self.helper.api_client.host)
namespaces = self.get_available_namespaces()
name = self.get_default_host_name(client.configuration.host)
namespaces = self.get_available_namespaces(client)
for namespace in namespaces:
self.get_pods_for_namespace(name, namespace)
self.get_services_for_namespace(name, namespace)
def authenticate(self, connection=None):
auth_options = {}
if connection:
auth_args = ('host', 'api_key', 'kubeconfig', 'context', 'username', 'password',
'cert_file', 'key_file', 'ssl_ca_cert', 'verify_ssl')
for key, value in iteritems(connection):
if key in auth_args and value is not None:
auth_options[key] = value
try:
self.helper.set_client_config(**auth_options)
except KubernetesException as exc:
raise K8sInventoryException('Error connecting to the API: {0}'.format(exc.message))
self.get_pods_for_namespace(client, name, namespace)
self.get_services_for_namespace(client, name, namespace)
@staticmethod
def get_default_host_name(host):
return host.replace('https://', '').replace('http://', '').replace('.', '-').replace(':', '_')
def get_helper(self, api_version, kind):
try:
helper = KubernetesObjectHelper(api_version=api_version, kind=kind, debug=False)
helper.get_model(api_version, kind)
return helper
except KubernetesException as exc:
raise K8sInventoryException('Error initializing object helper: {0}'.format(exc.message))
def get_available_namespaces(self):
def get_available_namespaces(self, client):
v1_namespace = client.resources.get(api_version='v1', kind='Namespace')
try:
obj = self.helper.get_object()
except KubernetesObjectHelper as exc:
obj = v1_namespace.get()
except DynamicApiError as exc:
raise K8sInventoryException('Error fetching Namespace list: {0}'.format(exc.message))
return [namespace.metadata.name for namespace in obj.items]
def get_pods_for_namespace(self, name, namespace):
self.helper.set_model('v1', 'pod_list')
def get_pods_for_namespace(self, client, name, namespace):
v1_pod = client.resources.get(api_version='v1', kind='Pod')
try:
obj = self.helper.get_object(namespace=namespace)
except KubernetesException as exc:
obj = v1_pod.get(namespace=namespace)
except DynamicApiError as exc:
raise K8sInventoryException('Error fetching Pod list: {0}'.format(exc.message))
namespace_pod_group = '{0}_pods'.format(namespace)
namespace_group = 'namespace_{0}'.format(namespace)
namespace_pods_group = '{0}_pods'.format(namespace_group)
self.inventory.add_group(name)
self.inventory.add_group(namespace)
self.inventory.add_child(name, namespace)
self.inventory.add_group(namespace_pod_group)
self.inventory.add_child(namespace, namespace_pod_group)
self.inventory.add_group(namespace_group)
self.inventory.add_child(name, namespace_group)
self.inventory.add_group(namespace_pods_group)
self.inventory.add_child(namespace_group, namespace_pods_group)
for pod in obj.items:
pod_name = pod.metadata.name
pod_groups = []
@ -136,17 +120,17 @@ class K8sInventoryHelper(object):
if pod.metadata.labels:
pod_labels = pod.metadata.labels
# create a group for each label_value
for key, value in iteritems(pod.metadata.labels):
group_name = '{0}_{1}'.format(key, value)
for key, value in pod.metadata.labels:
group_name = 'label_{0}_{1}'.format(key, value)
if group_name not in pod_groups:
pod_groups.append(group_name)
self.inventory.add_group(group_name)
for container in pod.status.container_statuses:
for container in pod.status.containerStatuses:
# add each pod_container to the namespace group, and to each label_value group
container_name = '{0}_{1}'.format(pod.metadata.name, container.name)
self.inventory.add_host(container_name)
self.inventory.add_child(namespace_pod_group, container_name)
self.inventory.add_child(namespace_pods_group, container_name)
if pod_groups:
for group in pod_groups:
self.inventory.add_child(group, container_name)
@ -155,14 +139,14 @@ class K8sInventoryHelper(object):
self.inventory.set_variable(container_name, 'object_type', 'pod')
self.inventory.set_variable(container_name, 'labels', pod_labels)
self.inventory.set_variable(container_name, 'annotations', pod_annotations)
self.inventory.set_variable(container_name, 'cluster_name', pod.metadata.cluster_name)
self.inventory.set_variable(container_name, 'pod_node_name', pod.spec.node_name)
self.inventory.set_variable(container_name, 'pod_name', pod.spec.node_name)
self.inventory.set_variable(container_name, 'pod_host_ip', pod.status.host_ip)
self.inventory.set_variable(container_name, 'cluster_name', pod.metadata.clusterName)
self.inventory.set_variable(container_name, 'pod_node_name', pod.spec.nodeName)
self.inventory.set_variable(container_name, 'pod_name', pod.spec.name)
self.inventory.set_variable(container_name, 'pod_host_ip', pod.status.hostIP)
self.inventory.set_variable(container_name, 'pod_phase', pod.status.phase)
self.inventory.set_variable(container_name, 'pod_ip', pod.status.pod_ip)
self.inventory.set_variable(container_name, 'pod_self_link', pod.metadata.self_link)
self.inventory.set_variable(container_name, 'pod_resource_version', pod.metadata.resource_version)
self.inventory.set_variable(container_name, 'pod_ip', pod.status.podIP)
self.inventory.set_variable(container_name, 'pod_self_link', pod.metadata.selfLink)
self.inventory.set_variable(container_name, 'pod_resource_version', pod.metadata.resourceVersion)
self.inventory.set_variable(container_name, 'pod_uid', pod.metadata.uid)
self.inventory.set_variable(container_name, 'container_name', container.image)
self.inventory.set_variable(container_name, 'container_image', container.image)
@ -179,20 +163,22 @@ class K8sInventoryHelper(object):
self.inventory.set_variable(container_name, 'ansible_{0}_container'.format(self.transport),
container.name)
def get_services_for_namespace(self, name, namespace):
self.helper.set_model('v1', 'service_list')
def get_services_for_namespace(self, client, name, namespace):
v1_service = client.resources.get(api_version='v1', kind='Service')
try:
obj = self.helper.get_object(namespace=namespace)
except KubernetesException as exc:
obj = v1_service.get(namespace=namespace)
except DynamicApiError as exc:
raise K8sInventoryException('Error fetching Service list: {0}'.format(exc.message))
namespace_service_group = '{0}_services'.format(namespace)
namespace_group = 'namespace_{0}'.format(namespace)
namespace_services_group = '{0}_services'.format(namespace_group)
self.inventory.add_group(name)
self.inventory.add_group(namespace)
self.inventory.add_child(name, namespace)
self.inventory.add_group(namespace_service_group)
self.inventory.add_child(namespace, namespace_service_group)
self.inventory.add_group(namespace_group)
self.inventory.add_child(name, namespace_group)
self.inventory.add_group(namespace_services_group)
self.inventory.add_child(namespace_group, namespace_services_group)
for service in obj.items:
service_name = service.metadata.name
service_labels = {} if not service.metadata.labels else service.metadata.labels
@ -202,51 +188,54 @@ class K8sInventoryHelper(object):
if service.metadata.labels:
# create a group for each label_value
for key, value in iteritems(service.metadata.labels):
group_name = '{0}_{1}'.format(key, value)
for key, value in service.metadata.labels:
group_name = 'label_{0}_{1}'.format(key, value)
self.inventory.add_group(group_name)
self.inventory.add_child(group_name, service_name)
self.inventory.add_child(namespace_service_group, service_name)
try:
self.inventory.add_child(namespace_services_group, service_name)
except AnsibleError as e:
raise
ports = [{'name': port.name,
'port': port.port,
'protocol': port.protocol,
'targetPort': port.target_port,
'nodePort': port.node_port} for port in service.spec.ports]
'targetPort': port.targetPort,
'nodePort': port.nodePort} for port in service.spec.ports or []]
# add hostvars
self.inventory.set_variable(service_name, 'object_type', 'service')
self.inventory.set_variable(service_name, 'labels', service_labels)
self.inventory.set_variable(service_name, 'annotations', service_annotations)
self.inventory.set_variable(service_name, 'cluster_name', service.metadata.cluster_name)
self.inventory.set_variable(service_name, 'cluster_name', service.metadata.clusterName)
self.inventory.set_variable(service_name, 'ports', ports)
self.inventory.set_variable(service_name, 'type', service.spec.type)
self.inventory.set_variable(service_name, 'self_link', service.metadata.self_link)
self.inventory.set_variable(service_name, 'resource_version', service.metadata.resource_version)
self.inventory.set_variable(service_name, 'self_link', service.metadata.selfLink)
self.inventory.set_variable(service_name, 'resource_version', service.metadata.resourceVersion)
self.inventory.set_variable(service_name, 'uid', service.metadata.uid)
if service.spec.external_traffic_policy:
if service.spec.externalTrafficPolicy:
self.inventory.set_variable(service_name, 'external_traffic_policy',
service.spec.external_traffic_policy)
if hasattr(service.spec, 'external_ips') and service.spec.external_ips:
self.inventory.set_variable(service_name, 'external_ips', service.spec.external_ips)
service.spec.externalTrafficPolicy)
if service.spec.externalIPs:
self.inventory.set_variable(service_name, 'external_ips', service.spec.externalIPs)
if service.spec.external_name:
self.inventory.set_variable(service_name, 'external_name', service.spec.external_name)
if service.spec.externalName:
self.inventory.set_variable(service_name, 'external_name', service.spec.externalName)
if service.spec.health_check_node_port:
if service.spec.healthCheckNodePort:
self.inventory.set_variable(service_name, 'health_check_node_port',
service.spec.health_check_node_port)
if service.spec.load_balancer_ip:
service.spec.healthCheckNodePort)
if service.spec.loadBalancerIP:
self.inventory.set_variable(service_name, 'load_balancer_ip',
service.spec.load_balancer_ip)
service.spec.loadBalancerIP)
if service.spec.selector:
self.inventory.set_variable(service_name, 'selector', service.spec.selector)
if hasattr(service.status.load_balancer, 'ingress') and service.status.load_balancer.ingress:
if hasattr(service.status.loadBalancer, 'ingress') and service.status.loadBalancer.ingress:
load_balancer = [{'hostname': ingress.hostname,
'ip': ingress.ip} for ingress in service.status.load_balancer.ingress]
'ip': ingress.ip} for ingress in service.status.loadBalancer.ingress]
self.inventory.set_variable(service_name, 'load_balancer', load_balancer)
@ -256,46 +245,39 @@ class OpenShiftInventoryHelper(K8sInventoryHelper):
def fetch_objects(self, connections):
super(OpenShiftInventoryHelper, self).fetch_objects(connections)
self.helper = self.get_helper('v1', 'namespace_list')
client = self.get_api_client()
if connections:
for connection in connections:
self.authenticate(connection)
name = connection.get('name', self.get_default_host_name(self.helper.api_client.host))
client = self.get_api_client(**connection)
name = connection.get('name', self.get_default_host_name(client.configuration.host))
if connection.get('namespaces'):
namespaces = connection['namespaces']
else:
namespaces = self.get_available_namespaces()
namespaces = self.get_available_namespaces(client)
for namespace in namespaces:
self.get_routes_for_namespace(name, namespace)
self.get_routes_for_namespace(client, name, namespace)
else:
name = self.get_default_host_name(self.helper.api_client.host)
namespaces = self.get_available_namespaces()
name = self.get_default_host_name(client.configuration.host)
namespaces = self.get_available_namespaces(client)
for namespace in namespaces:
self.get_routes_for_namespace(name, namespace)
self.get_routes_for_namespace(client, name, namespace)
def get_helper(self, api_version, kind):
try:
helper = OpenShiftObjectHelper(api_version=api_version, kind=kind, debug=False)
helper.get_model(api_version, kind)
return helper
except KubernetesException as exc:
raise K8sInventoryException('Error initializing object helper: {0}'.format(exc.message))
def get_routes_for_namespace(self, name, namespace):
self.helper.set_model('v1', 'route_list')
def get_routes_for_namespace(self, client, name, namespace):
v1_route = client.resources.get(api_version='v1', kind='Route')
try:
obj = self.helper.get_object(namespace=namespace)
except KubernetesException as exc:
obj = v1_route.get(namespace=namespace)
except DynamicApiError as exc:
raise K8sInventoryException('Error fetching Routes list: {0}'.format(exc.message))
namespace_routes_group = '{0}_routes'.format(namespace)
namespace_group = 'namespace_{0}'.format(namespace)
namespace_routes_group = '{0}_routes'.format(namespace_group)
self.inventory.add_group(name)
self.inventory.add_group(namespace)
self.inventory.add_child(name, namespace)
self.inventory.add_group(namespace_group)
self.inventory.add_child(name, namespace_group)
self.inventory.add_group(namespace_routes_group)
self.inventory.add_child(namespace, namespace_routes_group)
self.inventory.add_child(namespace_group, namespace_routes_group)
for route in obj.items:
route_name = route.metadata.name
route_labels = {} if not route.metadata.labels else route.metadata.labels
@ -305,8 +287,8 @@ class OpenShiftInventoryHelper(K8sInventoryHelper):
if route.metadata.labels:
# create a group for each label_value
for key, value in iteritems(route.metadata.labels):
group_name = '{0}_{1}'.format(key, value)
for key, value in route.metadata.labels:
group_name = 'label_{0}_{1}'.format(key, value)
self.inventory.add_group(group_name)
self.inventory.add_child(group_name, route_name)
@ -315,10 +297,10 @@ class OpenShiftInventoryHelper(K8sInventoryHelper):
# add hostvars
self.inventory.set_variable(route_name, 'labels', route_labels)
self.inventory.set_variable(route_name, 'annotations', route_annotations)
self.inventory.set_variable(route_name, 'cluster_name', route.metadata.cluster_name)
self.inventory.set_variable(route_name, 'cluster_name', route.metadata.clusterName)
self.inventory.set_variable(route_name, 'object_type', 'route')
self.inventory.set_variable(route_name, 'self_link', route.metadata.self_link)
self.inventory.set_variable(route_name, 'resource_version', route.metadata.resource_version)
self.inventory.set_variable(route_name, 'self_link', route.metadata.selfLink)
self.inventory.set_variable(route_name, 'resource_version', route.metadata.resourceVersion)
self.inventory.set_variable(route_name, 'uid', route.metadata.uid)
if route.spec.host:
@ -327,5 +309,5 @@ class OpenShiftInventoryHelper(K8sInventoryHelper):
if route.spec.path:
self.inventory.set_variable(route_name, 'path', route.spec.path)
if hasattr(route.spec.port, 'target_port') and route.spec.port.target_port:
if hasattr(route.spec.port, 'targetPort') and route.spec.port.targetPort:
self.inventory.set_variable(route_name, 'port', route.spec.port)

@ -1,208 +0,0 @@
#
# Copyright 2018 Red Hat | Ansible
#
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <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

@ -18,13 +18,12 @@
from __future__ import absolute_import, division, print_function
import copy
from ansible.module_utils.k8s.helper import COMMON_ARG_SPEC, AUTH_ARG_SPEC, OPENSHIFT_ARG_SPEC
from ansible.module_utils.k8s.common import KubernetesAnsibleModule, OpenShiftAnsibleModuleMixin, to_snake
from ansible.module_utils.k8s.common import KubernetesAnsibleModule
try:
from openshift.helper.exceptions import KubernetesException
from openshift.dynamic.exceptions import DynamicApiError, NotFoundError, ConflictError
except ImportError:
# Exception handled in common
pass
@ -33,6 +32,8 @@ except ImportError:
class KubernetesRawModule(KubernetesAnsibleModule):
def __init__(self, *args, **kwargs):
self.client = None
mutually_exclusive = [
('resource_definition', 'src'),
]
@ -42,170 +43,138 @@ class KubernetesRawModule(KubernetesAnsibleModule):
supports_check_mode=True,
**kwargs)
self.kind = self.params.pop('kind')
self.api_version = self.params.pop('api_version')
self.resource_definition = self.params.pop('resource_definition')
self.src = self.params.pop('src')
if self.src:
self.resource_definition = self.load_resource_definition(self.src)
if self.resource_definition:
self.api_version = self.resource_definition.get('apiVersion')
self.kind = self.resource_definition.get('kind')
self.api_version = self.api_version.lower()
self.kind = to_snake(self.kind)
if not self.api_version:
self.fail_json(
msg=("Error: no api_version specified. Use the api_version parameter, or provide it as part of a ",
"resource_definition.")
)
if not self.kind:
self.fail_json(
msg="Error: no kind specified. Use the kind parameter, or provide it as part of a resource_definition"
)
self.helper = self.get_helper(self.api_version, self.kind)
@property
def argspec(self):
argspec = copy.deepcopy(COMMON_ARG_SPEC)
argspec.update(copy.deepcopy(AUTH_ARG_SPEC))
return argspec
kind = self.params.pop('kind')
api_version = self.params.pop('api_version')
name = self.params.pop('name')
namespace = self.params.pop('namespace')
resource_definition = self.params.pop('resource_definition')
if resource_definition:
self.resource_definitions = [resource_definition]
src = self.params.pop('src')
if src:
self.resource_definitions = self.load_resource_definitions(src)
if not resource_definition and not src:
self.resource_definitions = [{
'kind': kind,
'apiVersion': api_version,
'metadata': {
'name': name,
'namespace': namespace
}
}]
def execute_module(self):
if self.resource_definition:
resource_params = self.resource_to_parameters(self.resource_definition)
self.params.update(resource_params)
self.authenticate()
changed = False
results = []
self.client = self.get_api_client()
for definition in self.resource_definitions:
kind = definition.get('kind')
search_kind = kind
if kind.lower().endswith('list'):
search_kind = kind[:-4]
api_version = definition.get('apiVersion')
resource = self.find_resource(search_kind, api_version, fail=True)
definition['kind'] = resource.kind
definition['apiVersion'] = resource.group_version
result = self.perform_action(resource, definition)
changed = changed or result['changed']
results.append(result)
if len(results) == 1:
self.exit_json(**results[0])
self.exit_json(**{
'changed': changed,
'result': {
'results': results
}
})
def perform_action(self, resource, definition):
result = {'changed': False, 'result': {}}
state = self.params.pop('state', None)
force = self.params.pop('force', False)
name = self.params.get('name')
namespace = self.params.get('namespace')
name = definition.get('metadata', {}).get('name')
namespace = definition.get('metadata', {}).get('namespace')
existing = None
self.remove_aliases()
return_attributes = dict(changed=False, result=dict())
if self.helper.base_model_name_snake.endswith('list'):
k8s_obj = self._read(name, namespace)
return_attributes['result'] = k8s_obj.to_dict()
self.exit_json(**return_attributes)
if definition['kind'].endswith('list'):
result['result'] = resource.get(namespace=namespace).to_dict()
result['changed'] = False
result['method'] = 'get'
return result
try:
existing = self.helper.get_object(name, namespace)
except KubernetesException as exc:
self.fail_json(msg='Failed to retrieve requested object: {0}'.format(exc.message),
error=exc.value.get('status'))
existing = resource.get(name=name, namespace=namespace)
except NotFoundError:
pass
except DynamicApiError as exc:
self.fail_json(msg='Failed to retrieve requested object: {0}'.format(exc.body),
error=exc.status, status=exc.status, reason=exc.reason)
if state == 'absent':
result['method'] = "delete"
if not existing:
# The object already does not exist
self.exit_json(**return_attributes)
return result
else:
# Delete the object
if not self.check_mode:
try:
self.helper.delete_object(name, namespace)
except KubernetesException as exc:
self.fail_json(msg="Failed to delete object: {0}".format(exc.message),
error=exc.value.get('status'))
return_attributes['changed'] = True
self.exit_json(**return_attributes)
k8s_obj = resource.delete(name, namespace=namespace)
result['result'] = k8s_obj.to_dict()
except DynamicApiError as exc:
self.fail_json(msg="Failed to delete object: {0}".format(exc.body),
error=exc.status, status=exc.status, reason=exc.reason)
result['changed'] = True
return result
else:
if not existing:
k8s_obj = self._create(namespace)
return_attributes['result'] = k8s_obj.to_dict()
return_attributes['changed'] = True
self.exit_json(**return_attributes)
if not self.check_mode:
try:
k8s_obj = resource.create(definition, namespace=namespace)
except ConflictError:
# Some resources, like ProjectRequests, can't be created multiple times,
# because the resources that they create don't match their kind
# In this case we'll mark it as unchanged and warn the user
self.warn("{0} was not found, but creating it returned a 409 Conflict error. This can happen \
if the resource you are creating does not directly create a resource of the same kind.".format(name))
return result
result['result'] = k8s_obj.to_dict()
result['changed'] = True
result['method'] = 'create'
return result
match = False
diffs = []
if existing and force:
k8s_obj = None
request_body = self.helper.request_body_from_params(self.params)
if not self.check_mode:
try:
k8s_obj = self.helper.replace_object(name, namespace, body=request_body)
except KubernetesException as exc:
self.fail_json(msg="Failed to replace object: {0}".format(exc.message),
error=exc.value.get('status'))
return_attributes['result'] = k8s_obj.to_dict()
return_attributes['changed'] = True
self.exit_json(**return_attributes)
# Check if existing object should be patched
k8s_obj = copy.deepcopy(existing)
try:
self.helper.object_from_params(self.params, obj=k8s_obj)
except KubernetesException as exc:
self.fail_json(msg="Failed to patch object: {0}".format(exc.message))
match, diff = self.helper.objects_match(self.helper.fix_serialization(existing), k8s_obj)
if match:
return_attributes['result'] = existing.to_dict()
self.exit_json(**return_attributes)
k8s_obj = resource.replace(definition, name=name, namespace=namespace).to_dict()
match, diffs = self.diff_objects(existing.to_dict(), k8s_obj)
result['result'] = k8s_obj
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'] = not match
result['method'] = 'replace'
result['diff'] = diffs
return result
# Differences exist between the existing obj and requested params
if not self.check_mode:
try:
k8s_obj = self.helper.patch_object(name, namespace, k8s_obj)
except KubernetesException as exc:
self.fail_json(msg="Failed to patch object: {0}".format(exc.message))
return_attributes['result'] = k8s_obj.to_dict()
return_attributes['changed'] = True
self.exit_json(**return_attributes)
def _create(self, namespace):
request_body = None
k8s_obj = None
try:
request_body = self.helper.request_body_from_params(self.params)
except KubernetesException as exc:
self.fail_json(msg="Failed to create object: {0}".format(exc.message))
if not self.check_mode:
try:
k8s_obj = self.helper.create_object(namespace, body=request_body)
except KubernetesException as exc:
self.fail_json(msg="Failed to create object: {0}".format(exc.message),
error=exc.value.get('status'))
return k8s_obj
def _read(self, name, namespace):
k8s_obj = None
try:
k8s_obj = self.helper.get_object(name, namespace)
except KubernetesException as exc:
self.fail_json(msg='Failed to retrieve requested object',
error=exc.value.get('status'))
return k8s_obj
class OpenShiftRawModule(OpenShiftAnsibleModuleMixin, KubernetesRawModule):
@property
def argspec(self):
args = super(OpenShiftRawModule, self).argspec
args.update(copy.deepcopy(OPENSHIFT_ARG_SPEC))
return args
def _create(self, namespace):
if self.kind.lower() == 'project':
return self._create_project()
return KubernetesRawModule._create(self, namespace)
def _create_project(self):
new_obj = None
k8s_obj = None
try:
new_obj = self.helper.object_from_params(self.params)
except KubernetesException as exc:
self.fail_json(msg="Failed to create object: {0}".format(exc.message))
try:
k8s_obj = self.helper.create_project(metadata=new_obj.metadata,
display_name=self.params.get('display_name'),
description=self.params.get('description'))
except KubernetesException as exc:
self.fail_json(msg='Failed to retrieve requested object',
error=exc.value.get('status'))
return k8s_obj
k8s_obj = resource.patch(definition, name=name, namespace=namespace).to_dict()
match, diffs = self.diff_objects(existing.to_dict(), k8s_obj)
result['result'] = k8s_obj
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'] = not match
result['method'] = 'patch'
result['diff'] = diffs
return result

@ -22,13 +22,12 @@ import copy
import math
import time
from ansible.module_utils.six import iteritems
from ansible.module_utils.k8s.common import OpenShiftAnsibleModuleMixin
from ansible.module_utils.k8s.raw import KubernetesRawModule
from ansible.module_utils.k8s.helper import AUTH_ARG_SPEC, COMMON_ARG_SPEC
from ansible.module_utils.k8s.common import AUTH_ARG_SPEC, COMMON_ARG_SPEC
try:
from openshift import watch
from openshift.dynamic.client import ResourceInstance
from openshift.helper.exceptions import KubernetesException
except ImportError as exc:
class KubernetesException(Exception):
@ -47,14 +46,14 @@ SCALE_ARG_SPEC = {
class KubernetesAnsibleScaleModule(KubernetesRawModule):
def execute_module(self):
if self.resource_definition:
resource_params = self.resource_to_parameters(self.resource_definition)
self.params.update(resource_params)
definition = self.resource_definitions[0]
self.authenticate()
self.client = self.get_api_client()
name = self.params.get('name')
namespace = self.params.get('namespace')
name = definition['metadata']['name']
namespace = definition['metadata'].get('namespace')
api_version = definition['apiVersion']
kind = definition['kind']
current_replicas = self.params.get('current_replicas')
replicas = self.params.get('replicas')
resource_version = self.params.get('resource_version')
@ -65,8 +64,10 @@ class KubernetesAnsibleScaleModule(KubernetesRawModule):
existing_count = None
return_attributes = dict(changed=False, result=dict())
resource = self.find_resource(kind, api_version, fail=True)
try:
existing = self.helper.get_object(name, namespace)
existing = resource.get(name=name, namespace=namespace)
return_attributes['result'] = existing.to_dict()
except KubernetesException as exc:
self.fail_json(msg='Failed to retrieve requested object: {0}'.format(exc.message),
@ -80,7 +81,7 @@ class KubernetesAnsibleScaleModule(KubernetesRawModule):
if existing_count is None:
self.fail_json(msg='Failed to retrieve the available count for the requested object.')
if resource_version and resource_version != existing.metadata.resource_version:
if resource_version and resource_version != existing.metadata.resourceVersion:
self.exit_json(**return_attributes)
if current_replicas is not None and existing_count != current_replicas:
@ -91,25 +92,13 @@ class KubernetesAnsibleScaleModule(KubernetesRawModule):
if not self.check_mode:
if self.kind == 'job':
existing.spec.parallelism = replicas
k8s_obj = self.helper.patch_object(name, namespace, existing)
k8s_obj = resource.patch(existing.to_dict())
else:
k8s_obj = self.scale(existing, replicas, wait, wait_time)
k8s_obj = self.scale(resource, existing, replicas, wait, wait_time)
return_attributes['result'] = k8s_obj.to_dict()
self.exit_json(**return_attributes)
def resource_to_parameters(self, resource):
""" Converts a resource definition to module parameters """
parameters = {}
for key, value in iteritems(resource):
if key in ('apiVersion', 'kind', 'status'):
continue
elif key == 'metadata' and isinstance(value, dict):
for meta_key, meta_value in iteritems(value):
if meta_key in ('name', 'namespace', 'resourceVersion'):
parameters[meta_key] = meta_value
return parameters
@property
def argspec(self):
args = copy.deepcopy(COMMON_ARG_SPEC)
@ -119,91 +108,67 @@ class KubernetesAnsibleScaleModule(KubernetesRawModule):
args.update(SCALE_ARG_SPEC)
return args
def scale(self, existing_object, replicas, wait, wait_time):
def scale(self, resource, existing_object, replicas, wait, wait_time):
name = existing_object.metadata.name
namespace = existing_object.metadata.namespace
method_name = 'patch_namespaced_{0}_scale'.format(self.kind)
method = None
model = None
try:
method = self.helper.lookup_method(method_name=method_name)
except KubernetesException:
if not hasattr(resource, 'scale'):
self.fail_json(
msg="Failed to get method {0}. Is 'scale' a valid operation for {1}?".format(method_name, self.kind)
msg="Cannot perform scale on resource of kind {0}".format(resource.kind)
)
try:
model = self.helper.get_model(self.api_version, 'scale')
except KubernetesException:
self.fail_json(
msg="Failed to fetch the 'Scale' model for API version {0}. Are you using the correct "
"API?".format(self.api_version)
)
scale_obj = model()
scale_obj.kind = 'scale'
scale_obj.api_version = self.api_version.lower()
scale_obj.metadata = self.helper.get_model(
self.api_version,
self.helper.get_base_model_name(scale_obj.swagger_types['metadata'])
)()
scale_obj.metadata.name = name
scale_obj.metadata.namespace = namespace
scale_obj.spec = self.helper.get_model(
self.api_version,
self.helper.get_base_model_name(scale_obj.swagger_types['spec'])
)()
scale_obj.spec.replicas = replicas
scale_obj = {'metadata': {'name': name, 'namespace': namespace}, 'spec': {'replicas': replicas}}
return_obj = None
stream = None
if wait:
w, stream = self._create_stream(namespace, wait_time)
w, stream = self._create_stream(resource, namespace, wait_time)
try:
method(name, namespace, scale_obj)
resource.scale.patch(body=scale_obj)
except Exception as exc:
self.fail_json(
msg="Scale request failed: {0}".format(exc.message)
)
if wait and stream is not None:
return_obj = self._read_stream(w, stream, name, replicas)
return_obj = self._read_stream(resource, w, stream, name, replicas)
if not return_obj:
return_obj = self._wait_for_response(name, namespace)
return return_obj
def _create_stream(self, namespace, wait_time):
def _create_stream(self, resource, namespace, wait_time):
""" Create a stream of events for the object """
w = None
stream = None
try:
list_method = self.helper.lookup_method('list', namespace)
w = watch.Watch()
w._api_client = self.helper.api_client
w._api_client = self.client.client
if namespace:
stream = w.stream(list_method, namespace, timeout_seconds=wait_time)
stream = w.stream(resource.get, serialize=False, namespace=namespace, timeout_seconds=wait_time)
else:
stream = w.stream(list_method, timeout_seconds=wait_time)
stream = w.stream(resource.get, serialize=False, namespace=namespace, timeout_seconds=wait_time)
except KubernetesException:
pass
except Exception:
raise
return w, stream
def _read_stream(self, watcher, stream, name, replicas):
def _read_stream(self, resource, watcher, stream, name, replicas):
""" Wait for ready_replicas to equal the requested number of replicas. """
return_obj = None
try:
for event in stream:
if event.get('object'):
obj = event['object']
obj = ResourceInstance(resource, event['object'])
if obj.metadata.name == name and hasattr(obj, 'status'):
if hasattr(obj.status, 'ready_replicas') and obj.status.ready_replicas == replicas:
if replicas == 0:
if not hasattr(obj.status, 'readyReplicas') or not obj.status.readyReplicas:
return_obj = obj
watcher.stop()
break
if hasattr(obj.status, 'readyReplicas') and obj.status.readyReplicas == replicas:
return_obj = obj
watcher.stop()
break
@ -212,27 +177,23 @@ class KubernetesAnsibleScaleModule(KubernetesRawModule):
if not return_obj:
self.fail_json(msg="Error fetching the patched object. Try a higher wait_timeout value.")
if return_obj.status.ready_replicas is None:
if replicas and return_obj.status.readyReplicas is None:
self.fail_json(msg="Failed to fetch the number of ready replicas. Try a higher wait_timeout value.")
if return_obj.status.ready_replicas != replicas:
if replicas and return_obj.status.readyReplicas != replicas:
self.fail_json(msg="Number of ready replicas is {0}. Failed to reach {1} ready replicas within "
"the wait_timeout period.".format(return_obj.status.ready_replicas, replicas))
return return_obj
def _wait_for_response(self, name, namespace):
def _wait_for_response(self, resource, name, namespace):
""" Wait for an API response """
tries = 0
half = math.ceil(20 / 2)
obj = None
while tries <= half:
obj = self.helper.get_object(name, namespace)
obj = resource.get(name=name, namespace=namespace)
if obj:
break
tries += 2
time.sleep(2)
return obj
class OpenShiftAnsibleScaleModule(OpenShiftAnsibleModuleMixin, KubernetesAnsibleScaleModule):
pass

@ -15,13 +15,15 @@ ANSIBLE_METADATA = {'metadata_version': '1.1',
DOCUMENTATION = '''
module: k8s_raw
module: k8s
short_description: Manage Kubernetes (K8s) objects
version_added: "2.5"
version_added: "2.6"
author: "Chris Houseknecht (@chouseknecht)"
author:
- "Chris Houseknecht (@chouseknecht)"
- "Fabian von Feilitzsch (@fabianvf)"
description:
- Use the OpenShift Python client to perform CRUD operations on K8s objects.
@ -39,20 +41,20 @@ extends_documentation_fragment:
requirements:
- "python >= 2.7"
- "openshift == 0.4.3"
- "openshift >= 0.6"
- "PyYAML >= 3.11"
'''
EXAMPLES = '''
- name: Create a k8s namespace
k8s_raw:
k8s:
name: testing
api_version: v1
kind: Namespace
state: present
- name: Create a Service object from an inline definition
k8s_raw:
k8s:
state: present
definition:
apiVersion: v1
@ -74,12 +76,12 @@ EXAMPLES = '''
port: 8000
- name: Create a Service object by reading the definition from a file
k8s_raw:
k8s:
state: present
src: /testing/service.yml
- name: Get an existing Service object
k8s_raw:
k8s:
api_version: v1
kind: Service
name: web
@ -87,14 +89,14 @@ EXAMPLES = '''
register: web_service
- name: Get a list of all service objects
k8s_raw:
k8s:
api_version: v1
kind: ServiceList
namespace: testing
register: service_list
- name: Remove an existing Service object
k8s_raw:
k8s:
state: absent
api_version: v1
kind: Service
@ -104,17 +106,17 @@ EXAMPLES = '''
# Passing the object definition from a file
- name: Create a Deployment by reading the definition from a local file
k8s_raw:
k8s:
state: present
src: /testing/deployment.yml
- name: Read definition file from the Ansible controller file system
k8s_raw:
k8s:
state: present
definition: "{{ lookup('file', '/testing/deployment.yml') | from_yaml }}"
- name: Read definition file from the Ansible controller file system after Jinja templating
k8s_raw:
k8s:
state: present
definition: "{{ lookup('template', '/testing/deployment.yml') | from_yaml }}"
'''

@ -21,7 +21,9 @@ short_description: Set a new size for a Deployment, ReplicaSet, Replication Cont
version_added: "2.5"
author: "Chris Houseknecht (@chouseknecht)"
author:
- "Chris Houseknecht (@chouseknecht)"
- "Fabian von Feilitzsch (@fabianvf)"
description:
- Similar to the kubectl scale command. Use to set the number of replicas for a Deployment, ReplicatSet,
@ -35,7 +37,7 @@ extends_documentation_fragment:
requirements:
- "python >= 2.7"
- "openshift == 0.4.3"
- "openshift >= 0.6"
- "PyYAML >= 3.11"
'''

@ -1,204 +0,0 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# (c) 2018, Chris Houseknecht <@chouseknecht>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'}
DOCUMENTATION = '''
module: openshift_raw
short_description: Manage OpenShift objects
version_added: "2.5"
author: "Chris Houseknecht (@chouseknecht)"
description:
- Use the OpenShift Python client to perform CRUD operations on OpenShift objects.
- Pass the object definition from a source file or inline. See examples for reading
files and using Jinja templates.
- Access to the full range of K8s and OpenShift APIs.
- Authenticate using either a config file, certificates, password or token.
- Supports check mode.
extends_documentation_fragment:
- k8s_state_options
- k8s_name_options
- k8s_resource_options
- k8s_auth_options
options:
description:
description:
- Use only when creating a project, otherwise ignored. Adds a description to the project
metadata.
display_name:
description:
- Use only when creating a project, otherwise ignored. Adds a display name to the project
metadata.
requirements:
- "python >= 2.7"
- "openshift == 0.4.3"
- "PyYAML >= 3.11"
'''
EXAMPLES = '''
- name: Create a project
openshift_raw:
api_version: v1
kind: Project
name: testing
description: Testing
display_name: "This is a test project."
state: present
- name: Create a Persistent Volume Claim from an inline definition
openshift_raw:
state: present
definition:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: elastic-volume
namespace: testing
spec:
resources:
requests:
storage: 5Gi
accessModes:
- ReadWriteOnce
- name: Create a Deployment from an inline definition
openshift_raw:
state: present
definition:
apiVersion: v1
kind: DeploymentConfig
metadata:
name: elastic
labels:
app: galaxy
service: elastic
namespace: testing
spec:
template:
metadata:
labels:
app: galaxy
service: elastic
spec:
containers:
- name: elastic
volumeMounts:
- mountPath: /usr/share/elasticsearch/data
name: elastic-volume
command: ["elasticsearch"]
image: "ansible/galaxy-elasticsearch:2.4.6"
volumes:
- name: elastic-volume
persistentVolumeClaim:
claimName: elastic-volume
replicas: 1
strategy:
type: Rolling
- name: Remove an existing Deployment
openshift_raw:
api_version: v1
kind: DeploymentConfig
name: elastic
namespace: testing
state: absent
- name: Create a Secret
openshift_raw:
definition:
apiVersion: v1
kind: Secret
metadata:
name: mysecret
namespace: testing
type: Opaque
data:
username: "{{ 'admin' | b64encode }}"
password: "{{ 'foobard' | b64encode }}"
- name: Retrieve a Secret
openshift_raw:
api: v1
kind: Secret
name: mysecret
namespace: testing
register: mysecret
# Passing the object definition from a file
- name: Create a Deployment by reading the definition from a local file
openshift_raw:
state: present
src: /testing/deployment.yml
- name: Read definition file from the Ansible controller file system
openshift_raw:
state: present
definition: "{{ lookup('file', '/testing/deployment.yml') | from_yaml }}"
- name: Read definition file from the Ansible controller file system after Jinja templating
openshift_raw:
state: present
definition: "{{ lookup('template', '/testing/deployment.yml') | from_yaml }}"
'''
RETURN = '''
result:
description:
- The created, patched, or otherwise present object. Will be empty in the case of a deletion.
returned: success
type: complex
contains:
api_version:
description: The versioned schema of this representation of an object.
returned: success
type: str
kind:
description: Represents the REST resource this object represents.
returned: success
type: str
metadata:
description: Standard object metadata. Includes name, namespace, annotations, labels, etc.
returned: success
type: complex
spec:
description: Specific attributes of the object. Will vary based on the I(api_version) and I(kind).
returned: success
type: complex
status:
description: Current status details for the object.
returned: success
type: complex
items:
description: Returned only when the I(kind) is a List type resource. Contains a set of objects.
returned: when resource is a List
type: list
'''
from ansible.module_utils.k8s.raw import OpenShiftRawModule
def main():
OpenShiftRawModule().execute_module()
if __name__ == '__main__':
main()

@ -1,127 +0,0 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# (c) 2018, Chris Houseknecht <@chouseknecht>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'}
DOCUMENTATION = '''
module: openshift_scale
short_description: Set a new size for a Deployment Config, Deployment, Replica Set, Replication Controller, or Job.
version_added: "2.5"
author: "Chris Houseknecht (@chouseknecht)"
description:
- Similar to the oc scale command. Use to set the number of replicas for a Deployment Config, Deployment,
ReplicatSet, or Replication Controller, or the parallelism attribute of a Job. Supports check mode.
extends_documentation_fragment:
- k8s_name_options
- k8s_auth_options
- k8s_resource_options
- k8s_scale_options
requirements:
- "python >= 2.7"
- "openshift == 0.4.3"
- "PyYAML >= 3.11"
'''
EXAMPLES = '''
- name: Scale deployment config up, and extend timeout
openshift_scale:
api_version: v1
kind: DeploymentConfig
name: elastic
namespace: myproject
replicas: 3
wait_timeout: 60
- name: Scale deployment config down when current replicas match
openshift_scale:
api_version: v1
kind: DeploymentConfig
name: elastic
namespace: myproject
current_replicas: 3
replicas: 2
- name: Increase job parallelism
openshift_scale:
api_version: batch/v1
kind: job
name: pi-with-timeout
namespace: testing
replicas: 2
# Match object using local file or inline definition
- name: Scale deployment based on a file from the local filesystem
openshift_scale:
src: /myproject/elastic_deployment.yml
replicas: 3
wait: no
- name: Scale deployment based on a template output
openshift_scale:
resource_definition: "{{ lookup('template', '/myproject/elastic_deployment.yml') | from_yaml }}"
replicas: 3
wait: no
- name: Scale deployment based on a file from the Ansible controller filesystem
openshift_scale:
resource_definition: "{{ lookup('file', '/myproject/elastic_deployment.yml') | from_yaml }}"
replicas: 3
wait: no
'''
RETURN = '''
result:
description:
- If a change was made, will return the patched object, otherwise returns the existing object.
returned: success
type: complex
contains:
api_version:
description: The versioned schema of this representation of an object.
returned: success
type: str
kind:
description: Represents the REST resource this object represents.
returned: success
type: str
metadata:
description: Standard object metadata. Includes name, namespace, annotations, labels, etc.
returned: success
type: complex
spec:
description: Specific attributes of the object. Will vary based on the I(api_version) and I(kind).
returned: success
type: complex
status:
description: Current status details for the object.
returned: success
type: complex
'''
from ansible.module_utils.k8s.scale import OpenShiftAnsibleScaleModule
def main():
OpenShiftAnsibleScaleModule().execute_module()
if __name__ == '__main__':
main()

@ -9,6 +9,7 @@ DOCUMENTATION = '''
plugin_type: inventory
authors:
- Chris Houseknecht <@chouseknecht>
- Fabian von Feilitzsch <@fabianvf>
short_description: Kubernetes (K8s) inventory source
@ -76,7 +77,7 @@ DOCUMENTATION = '''
requirements:
- "python >= 2.7"
- "openshift == 0.4.1"
- "openshift >= 0.6"
- "PyYAML >= 3.11"
'''

@ -77,7 +77,7 @@ DOCUMENTATION = '''
requirements:
- "python >= 2.7"
- "openshift == 0.4.1"
- "openshift >= 0.6"
- "PyYAML >= 3.11"
'''

@ -29,11 +29,15 @@ DOCUMENTATION = """
description:
- Uses the OpenShift Python client to fetch a specific object by name, all matching objects within a
namespace, or all matching objects for all namespaces.
namespace, or all matching objects for all namespaces, as well as information about the cluster.
- Provides access the full range of K8s APIs.
- Enables authentication via config file, certificates, password or token.
options:
cluster_info:
description:
- Use to specify the type of cluster information you are attempting to retrieve. Will take priority
over all the other options.
api_version:
description:
- Use to specify the API version. If I(resource definition) is provided, the I(apiVersion) from the
@ -115,7 +119,7 @@ DOCUMENTATION = """
requirements:
- "python >= 2.7"
- "openshift == 0.4.1"
- "openshift >= 0.6"
- "PyYAML >= 3.11"
notes:
@ -189,7 +193,98 @@ RETURN = """
"""
from ansible.plugins.lookup import LookupBase
from ansible.module_utils.k8s.lookup import KubernetesLookup
from ansible.module_utils.k8s.common import K8sAnsibleMixin
from ansible.errors import AnsibleError
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 fail(self, msg=None):
raise AnsibleError(msg)
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 AnsibleError(
"Error: no Kind specified. Use the 'kind' parameter, or provide an object YAML configuration "
"using the 'resource_definition' parameter."
)
resource = self.find_resource(self.kind, self.api_version, fail=True)
try:
k8s_obj = resource.get(name=self.name, namespace=self.namespace, label_selector=self.label_selector, field_selector=self.field_selector)
except NotFoundError:
return []
if self.name:
return [k8s_obj.to_dict()]
return k8s_obj.to_dict().get('items')
class LookupModule(LookupBase):

@ -1,197 +0,0 @@
#
# Copyright 2018 Red Hat | Ansible
#
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <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)

@ -2,13 +2,11 @@
pip:
name: openshift
- name: Create a project
openshift_raw:
# Kubernetes resources
- name: Create a namespace
k8s:
name: testing
kind: project
description: My testing project
display_name: Testing Project
api_version: v1
kind: namespace
register: output
- name: show output
@ -16,7 +14,7 @@
var: output
- name: Create a service
openshift_raw:
k8s:
state: present
resource_definition: &svc
apiVersion: v1
@ -43,7 +41,7 @@
var: output
- name: Create the service again
openshift_raw:
k8s:
state: present
resource_definition: *svc
register: output
@ -53,7 +51,7 @@
that: not output.changed
- name: Create PVC
openshift_raw:
k8s:
state: present
inline: &pvc
apiVersion: v1
@ -73,7 +71,7 @@
var: output
- name: Create the PVC again
openshift_raw:
k8s:
state: present
inline: *pvc
@ -81,8 +79,69 @@
assert:
that: not output.changed
- name: Create deployment
k8s:
state: present
inline: &deployment
apiVersion: apps/v1beta1
kind: Deployment
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: RollingUpdate
register: output
- name: Show output
debug:
var: output
- name: Create deployment again
k8s:
state: present
inline: *deployment
register: output
- name: Deployment creation should be idempotent
assert:
that: not output.changed
# OpenShift Resources
- name: Create a project
k8s:
name: testing
kind: project
api_version: v1
register: output
- name: show output
debug:
var: output
- name: Create deployment config
openshift_raw:
k8s:
state: present
inline: &dc
apiVersion: v1
@ -121,7 +180,7 @@
var: output
- name: Create deployment config again
openshift_raw:
k8s:
state: present
inline: *dc
register: output

@ -1,128 +0,0 @@
- name: Install requirements
pip:
name: openshift
- name: Create a namespace
k8s_raw:
name: testing
kind: namespace
register: output
- name: show output
debug:
var: output
- name: Create a service
k8s_raw:
state: present
resource_definition: &svc
apiVersion: v1
kind: Service
metadata:
name: web
namespace: testing
labels:
app: galaxy
service: web
spec:
selector:
app: galaxy
service: web
ports:
- protocol: TCP
targetPort: 8000
name: port-8000-tcp
port: 8000
register: output
- name: show output
debug:
var: output
- name: Create the service again
k8s_raw:
state: present
resource_definition: *svc
register: output
- name: Service creation should be idempotent
assert:
that: not output.changed
- name: Create PVC
k8s_raw:
state: present
inline: &pvc
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: elastic-volume
namespace: testing
spec:
resources:
requests:
storage: 5Gi
accessModes:
- ReadWriteOnce
- name: Show output
debug:
var: output
- name: Create the PVC again
k8s_raw:
state: present
inline: *pvc
- name: PVC creation should be idempotent
assert:
that: not output.changed
- name: Create deployment
k8s_raw:
state: present
inline: &dc
apiVersion: Apps/V1Beta1
kind: Deployment
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: RollingUpdate
register: output
- name: Show output
debug:
var: output
- name: Create deployment again
k8s_raw:
state: present
inline: *dc
register: output
- name: Deployment creation should be idempotent
assert:
that: not output.changed

@ -1,2 +0,0 @@
cloud/openshift
posix/ci/cloud/group4/openshift
Loading…
Cancel
Save