Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ python:

env:
- OPENSHIFT_VERSION=latest
- OPENSHIFT_VERSION=3.6
- OPENSHIFT_VERSION=1.5
- OPENSHIFT_VERSION=1.4
- OPENSHIFT_VERSION=1.3
Expand Down
12 changes: 6 additions & 6 deletions openshift/ansiblegen/examples/v1_route.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
name: myroute
namespace: k8s-project
host: www.example.com
target_reference_kind: Service
target_reference_name: service-name
spec_to_kind: Service
spec_to_name: service-name
tls_termination: edge
tls_key: |-
-----BEGIN PRIVATE KEY-----
Expand All @@ -28,8 +28,8 @@
namespace: k8s-project
host: www.example.com
tls_termination: reencrypt
target_reference_kind: Service
target_reference_name: other-service-name
spec_to_kind: Service
spec_to_name: other-service-name
tls_destination_ca_certificate: |-
-----BEGIN CERTIFICATE-----
destination cetricate_contents
Expand All @@ -41,8 +41,8 @@
namespace: k8s-project
host: www.example.com
path: /foo/bar/baz.html
target_reference_kind: Service
target_reference_name: whimsy-name
spec_to_kind: Service
spec_to_name: whimsy-name
tls_termination: edge
tls_key: |-
-----BEGIN PRIVATE KEY-----
Expand Down
7 changes: 6 additions & 1 deletion openshift/client/api_client.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
from __future__ import absolute_import

from kubernetes.client.api_client import ApiClient as K8sApiClient

from . import models

class ApiClient(K8sApiClient):
def _ApiClient__deserialize(self, data, klass):
try:
return super(ApiClient, self).__deserialize(data, klass)
except AttributeError:
klass = eval('models.' + klass)
klass = getattr(models, klass)
return super(ApiClient, self).__deserialize_model(data, klass)

4 changes: 4 additions & 0 deletions openshift/config/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from kubernetes.config.config_exception import ConfigException
from kubernetes.config.incluster_config import load_incluster_config
from kubernetes.config.kube_config import list_kube_config_contexts, load_kube_config
from .kube_config import new_client_from_config
12 changes: 12 additions & 0 deletions openshift/config/kube_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from kubernetes.config import load_kube_config
from openshift.client import ApiClient, ConfigurationObject


def new_client_from_config(config_file=None, context=None):
"""Loads configuration the same as load_kube_config but returns an ApiClient
to be used with any API object. This will allow the caller to concurrently
talk with multiple clusters."""
client_config = ConfigurationObject()
load_kube_config(config_file=config_file, context=context,
client_configuration=client_config)
return ApiClient(config=client_config)
164 changes: 97 additions & 67 deletions openshift/helper/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,11 @@

from dictdiffer import diff

from kubernetes import config, watch
from kubernetes import watch
from kubernetes.client.rest import ApiException
from kubernetes.config.config_exception import ConfigException

from openshift import client
from openshift.client import configuration, ApiClient
from openshift import client, config
from openshift.client.models import V1DeleteOptions

from .exceptions import OpenShiftException
Expand Down Expand Up @@ -63,7 +62,7 @@

class KubernetesObjectHelper(object):

def __init__(self, api_version, kind, debug=False, reset_logfile=True, timeout=20):
def __init__(self, api_version, kind, debug=False, reset_logfile=True, timeout=20, **auth):
self.api_version = api_version
self.kind = kind
self.model = self.get_model(api_version, kind)
Expand All @@ -75,36 +74,39 @@ def __init__(self, api_version, kind, debug=False, reset_logfile=True, timeout=2
if debug:
self.enable_debug(reset_logfile)

try:
config.load_kube_config()
except Exception as exc:
logger.debug("Unable to load default config: {}".format(exc))

self.api_client = self.api_client = ApiClient()
self.set_client_config(**auth)

def set_client_config(self, **kwargs):
def set_client_config(self, **auth):
""" Convenience method for updating the configuration object, and instantiating a new client """
if kwargs.get('kubeconfig') or kwargs.get('context'):
# Attempt to load config from file
try:
config.load_kube_config(config_file=kwargs.get('kubeconfig'),
context=kwargs.get('context'))
except IOError as e:
config_file = auth.get('kubeconfig')
context = auth.get('context')

try:
self.api_client = config.new_client_from_config(config_file, context)
except ConfigException as e:
raise OpenShiftException(
"Error accessing context {}.".format(auth.get('context')), error=str(e))
except IOError as e:
if config_file is not None:
# Missing specified config file, cannot continue
raise OpenShiftException(
"Failed to access {}. Does the file exist?".format(kwargs.get('kubeconfig')), error=str(e)
"Failed to access {}. Does the file exist?".format(config_file), error=str(e)
)
except ConfigException as e:
raise OpenShiftException(
"Error accessing context {}.".format(kwargs.get('context')), error=str(e))
else:
# Default config is missing, but other auth params may be provided, continue
logger.debug("Unable to load default config: {}".format(e))

if auth.get('host') is not None:
self.api_client.host = auth['host']

auth_keys = ['api_key', 'ssl_ca_cert', 'cert_file', 'key_file', 'verify_ssl']

auth_keys = ['api_key', 'host', 'ssl_ca_cert', 'cert_file', 'key_file', 'verify_ssl']
for key in auth_keys:
if kwargs.get(key, None) is not None:
if auth.get(key, None) is not None:
if key == 'api_key':
configuration.api_key = {'authorization': kwargs[key]}
self.api_client.config.api_key = {'authorization': auth[key]}
else:
setattr(configuration, key, kwargs[key])
self.api_client = self.api_client = ApiClient()
setattr(self.api_client.config, key, auth[key])

@staticmethod
def enable_debug(reset_logfile=True):
Expand Down Expand Up @@ -135,7 +137,7 @@ def get_object(self, name, namespace=None):
return k8s_obj

def patch_object(self, name, namespace, k8s_obj):
logger.debug('Starting create object')
logger.debug('Starting patch object')
empty_status = self.properties['status']['class']()
k8s_obj.status = empty_status
k8s_obj.metadata.resource_version = None
Expand All @@ -151,7 +153,7 @@ def patch_object(self, name, namespace, k8s_obj):
except ApiException as exc:
msg = json.loads(exc.body).get('message', exc.reason) if exc.body.startswith('{') else exc.body
raise OpenShiftException(msg, status=exc.status)
return_obj = self.__read_stream(w, stream)
return_obj = self.__read_stream(w, stream, name)
if not return_obj:
self.__wait_for_response(name, namespace, 'patch')
return return_obj
Expand All @@ -165,14 +167,17 @@ def create_project(self, metadata, display_name=None, description=None):
"""
# TODO: handle admin-level project creation

w, stream = self.__create_stream(None)
try:
proj_req = client.V1ProjectRequest(metadata=metadata, display_name=display_name, description=description)
client.OapiApi().create_project_request(proj_req)
client.OapiApi(self.api_client).create_project_request(proj_req)
except ApiException as exc:
msg = json.loads(exc.body).get('message', exc.reason) if exc.body.startswith('{') else exc.body
raise OpenShiftException(msg, status=exc.status)

return_obj = self.__wait_for_response(metadata.name, None, 'create')
return_obj = self.__read_stream(w, stream, metadata.name)
if not return_obj:
return_obj = self.__wait_for_response(metadata.name, None, 'create')

return return_obj

Expand All @@ -186,6 +191,7 @@ def create_object(self, namespace, k8s_obj=None, body=None):
"""
logger.debug('Starting create object')
w, stream = self.__create_stream(namespace)
name = None
if k8s_obj:
name = k8s_obj.metadata.name
elif body:
Expand All @@ -206,7 +212,7 @@ def create_object(self, namespace, k8s_obj=None, body=None):
msg = json.loads(exc.body).get('message', exc.reason) if exc.body.startswith('{') else exc.body
raise OpenShiftException(msg, status=exc.status)

return_obj = self.__read_stream(w, stream)
return_obj = self.__read_stream(w, stream, name)

# Allow OpenShift annotations to be added to Namespace
#if isinstance(k8s_obj, client.models.V1Namespace):
Expand All @@ -220,27 +226,39 @@ def create_object(self, namespace, k8s_obj=None, body=None):
def delete_object(self, name, namespace):
logger.debug('Starting delete object {0} {1} {2}'.format(self.kind, name, namespace))
delete_method = self.lookup_method('delete', namespace)
w, stream = self.__create_stream(namespace)

if self.kind in ('project', 'namespace'):
w, stream = self.__create_stream(namespace)

status_obj = None
if not namespace:
try:
if 'body' in inspect.getargspec(delete_method).args:
delete_method(name, body=V1DeleteOptions())
status_obj = delete_method(name, body=V1DeleteOptions())
else:
delete_method(name)
status_obj = delete_method(name)
except ApiException as exc:
msg = json.loads(exc.body).get('message', exc.reason)
raise OpenShiftException(msg, status=exc.status)
else:
try:
if 'body' in inspect.getargspec(delete_method).args:
delete_method(name, namespace, body=V1DeleteOptions())
status_obj = delete_method(name, namespace, body=V1DeleteOptions())
else:
delete_method(name, namespace)
status_obj = delete_method(name, namespace)
except ApiException as exc:
msg = json.loads(exc.body).get('message', exc.reason) if exc.body.startswith('{') else exc.body
raise OpenShiftException(msg, status=exc.status)
self.__read_stream(w, stream)
#self.__wait_for_response(name, namespace, 'delete')

if status_obj is None or status_obj.status == 'Failure':
msg = 'Failed to delete {}'.format(name)
if namespace is not None:
msg += ' in namespace {}'.format(namespace)
msg += ' status: {}'.format(status_obj)
raise OpenShiftException(msg)

if self.kind in ('project', 'namespace'):
self.__read_stream(w, stream, name)

def replace_object(self, name, namespace, k8s_obj=None, body=None):
""" Replace an existing object. Pass in a model object or request dict().
Expand Down Expand Up @@ -276,13 +294,14 @@ def replace_object(self, name, namespace, k8s_obj=None, body=None):
msg = json.loads(exc.body).get('message', exc.reason) if exc.body.startswith('{') else exc.body
raise OpenShiftException(msg, status=exc.status)

return_obj = self.__read_stream(w, stream)
return_obj = self.__read_stream(w, stream, name)
if not return_obj:
return_obj = self.__wait_for_response(name, namespace, 'replace')

return return_obj

def objects_match(self, obj_a, obj_b):
@staticmethod
def objects_match(obj_a, obj_b):
""" Test the equality of two objects. Returns bool, diff object. Use list(diff object) to
log or iterate over differences """
if obj_a is None and obj_b is None:
Expand Down Expand Up @@ -352,7 +371,7 @@ def lookup_method(self, operation, namespace=None):
method = None
for api in apis:
api_class = getattr(client.apis, api)
method = getattr(api_class(), method_name, None)
method = getattr(api_class(self.api_client), method_name, None)
if method is not None:
break
if method is None:
Expand Down Expand Up @@ -438,15 +457,15 @@ def __wait_for_response(self, name, namespace, action):
def __create_stream(self, namespace):
""" Create a stream that gets events for the our model """
w = watch.Watch()
w._api_client = self.api_client # monkey patch for access to OpenShift models
w._api_client = self.api_client # monkey patch for access to OpenShift models
list_method = self.lookup_method('list', namespace)
if namespace:
stream = w.stream(list_method, namespace, _request_timeout=self.timeout)
else:
stream = w.stream(list_method, _request_timeout=self.timeout)
return w, stream

def __read_stream(self, watcher, stream):
def __read_stream(self, watcher, stream, name):
#TODO https://cobe.io/blog/posts/kubernetes-watch-python/ <--- might help?

return_obj = None
Expand All @@ -463,33 +482,44 @@ def __read_stream(self, watcher, stream):
else:
logger.debug(repr(event))

if event['type'] == 'DELETED':
# Object was deleted
return_obj = obj
watcher.stop()
break
elif obj is not None:
# Object is either added or modified. Check the status and determine if we
# should continue waiting
if hasattr(obj, 'status'):
status = getattr(obj, 'status')
if hasattr(status, 'phase'):
if status.phase == 'Active':
# TODO other phase values ??
return_obj = obj
watcher.stop()
break
elif hasattr(status, 'conditions'):
conditions = getattr(status, 'conditions')
if conditions and len(conditions) > 0:
# We know there is a status, but it's up to the user to determine meaning.
if event['object'].metadata.name == name:
if event['type'] == 'DELETED':
# Object was deleted
return_obj = obj
watcher.stop()
break
elif obj is not None:
# Object is either added or modified. Check the status and determine if we
# should continue waiting
if hasattr(obj, 'status'):
status = getattr(obj, 'status')
if hasattr(status, 'phase'):
if status.phase == 'Active':
# TODO other phase values ??
# TODO test namespaces for OpenShift annotations if needed
return_obj = obj
watcher.stop()
break
elif hasattr(status, 'conditions'):
conditions = getattr(status, 'conditions')
if conditions and len(conditions) > 0:
# We know there is a status, but it's up to the user to determine meaning.
return_obj = obj
watcher.stop()
break
elif obj.kind == 'Service' and status is not None:
return_obj = obj
watcher.stop()
break
elif obj.kind == 'Service' and status is not None:
return_obj = obj
watcher.stop()
break
elif obj.kind == 'Route':
route_statuses = set()
for route_ingress in status.ingress:
for condition in route_ingress.conditions:
route_statuses.add(condition.type)
if route_statuses <= set(['Ready', 'Admitted']):
return_obj = obj
watcher.stop()
break

except Exception as exc:
# A timeout occurred
Expand Down
3 changes: 1 addition & 2 deletions openshift/helper/ansible.py
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,7 @@ def __set_obj_attribute(self, obj, property_path, 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(']','')
obj_type = prop_kind.replace('list[', '').replace(']', '')
if obj_type not in ('str', 'int', 'bool', 'list', 'dict'):
self.__compare_obj_list(getattr(obj, prop_name), param_value, obj_type, param_name)
else:
Expand Down Expand Up @@ -678,4 +678,3 @@ def add_meta(prop_name, prop_prefix, prop_alt_prefix):
if prop == 'type':
args[arg_prefix + prop]['choices'] = self.__convert_params_to_choices(properties)
return args

Loading