From 978f218d42c2d3efe53d0179d7a18ba832ac06f9 Mon Sep 17 00:00:00 2001 From: David Roberts Date: Tue, 30 Apr 2019 00:11:35 +0100 Subject: [PATCH] Initial implementation of kubectl --- kubernetes/kubectl/Kubectl.py | 190 ++++++++++++++++++ kubernetes/kubectl/__init__.py | 0 kubernetes/kubectl/registry/ApiDefinition.py | 26 +++ .../kubectl/registry/GroupVersionKind.py | 80 ++++++++ .../kubectl/registry/ModelDefinition.py | 13 ++ kubernetes/kubectl/registry/ModelRegistry.py | 183 +++++++++++++++++ kubernetes/kubectl/registry/__init__.py | 0 kubernetes/kubectl/utils/Parser.py | 119 +++++++++++ kubernetes/kubectl/utils/__init__.py | 1 + 9 files changed, 612 insertions(+) create mode 100644 kubernetes/kubectl/Kubectl.py create mode 100644 kubernetes/kubectl/__init__.py create mode 100644 kubernetes/kubectl/registry/ApiDefinition.py create mode 100644 kubernetes/kubectl/registry/GroupVersionKind.py create mode 100644 kubernetes/kubectl/registry/ModelDefinition.py create mode 100644 kubernetes/kubectl/registry/ModelRegistry.py create mode 100644 kubernetes/kubectl/registry/__init__.py create mode 100644 kubernetes/kubectl/utils/Parser.py create mode 100644 kubernetes/kubectl/utils/__init__.py diff --git a/kubernetes/kubectl/Kubectl.py b/kubernetes/kubectl/Kubectl.py new file mode 100644 index 0000000000..8436d5c4bc --- /dev/null +++ b/kubernetes/kubectl/Kubectl.py @@ -0,0 +1,190 @@ +from kubernetes.client.rest import ApiException +from kubectl.registry.GroupVersionKind import create_group_version_kind +from kubectl.registry.ModelRegistry import ModelRegistry +from kubectl.utils.Parser import Parser +import yaml +from kubernetes.client.models.v1_delete_options import V1DeleteOptions + + +class Kubectl(object): + def __init__(self, api_client): + + self.model_registry = ModelRegistry() + self.model_registry.build_core_register(api_client) + self.parser = Parser(self.model_registry) + + def register_custom_resource(self, group_version_kind, model_clazz, api_clazz=None): + self.model_registry.register_custom_resource(group_version_kind, model_clazz, api_clazz) + + def parse_model(self, body): + if body is None: + raise Exception('body parameter not supplied') + + if isinstance(body, str): + body = yaml.load(body) + + if isinstance(body, dict): + model = self.parser.parse(body) + else: + model = body + gvk = create_group_version_kind(model=model) + return gvk, model + + def create_resource(self, body, namespace='default', **kwargs): + gvk, model = self.parse_model(body) + return self.__create_resource(gvk, model, namespace, **kwargs) + + def __create_resource(self, gvk, model, **kwargs): + try: + if self.model_registry.requires_namespace(gvk): + ns = model.metadata.namespace + if ns is not None: + namespace = ns + return self.model_registry.invoke_create_api(gvk, namespace=namespace, body=model, **kwargs) + else: + return self.model_registry.invoke_create_api(gvk, body=model, **kwargs) + except ApiException, ex: + model = self.parser.parse(ex.body) + return model + + def update_resource(self, body, namespace='default', **kwargs): + gvk, model = self.parse_model(body) + return self.__update_resource(gvk, model, namespace, **kwargs) + + def __update_resource(self, gvk, model, namespace='default', **kwargs): + try: + if self.model_registry.requires_namespace(gvk): + ns = model.metadata.namespace + if ns is not None: + namespace = ns + return self.model_registry.invoke_replace_api(gvk, + name=model.metadata.name, + namespace=namespace, + body=model, + **kwargs) + else: + return self.model_registry.invoke_replace_api(gvk, + name=model.metadata.name, + body=model, + **kwargs) + except ApiException, ex: + model = self.parser.parse(ex.body) + return model + + def read_resource(self, kind=None, name=None, namespace='default', body=None, **kwargs): + if body is not None: + gvk, model = self.parse_model(body) + ns = model.metadata.namespace + if ns is not None: + namespace = ns + name = model.metadata.name + elif kind is not None and name is not None: + gvk = self.model_registry.kind_map.get(kind, None) + else: + raise Exception('name and/or kind not specified') + return self.__read_resource(gvk, name, namespace, **kwargs) + + def __read_resource(self, gvk, name, namespace, **kwargs): + try: + if self.model_registry.requires_namespace(gvk): + return self.model_registry.invoke_get_api(gvk, + name=name, + namespace=namespace, + **kwargs) + else: + return self.model_registry.invoke_get_api(gvk, + name=name, + **kwargs) + except ApiException, ex: + model = self.parser.parse(ex.body) + return model + + def delete_resource(self, kind=None, name=None, namespace='default', body=None, **kwargs): + if body is not None: + gvk, model = self.parse_model(body) + ns = model.metadata.namespace + if ns is not None: + namespace = ns + + name = model.metadata.name + elif kind is not None and name is not None: + gvk = self.model_registry.kind_map.get(kind, None) + else: + raise Exception('name and/or kind not specified') + return self.__delete_resource(gvk, name=name, namespace=namespace, **kwargs) + + def __delete_resource(self, gvk, name, namespace='default', **kwargs): + try: + delete_options = V1DeleteOptions() + if self.model_registry.requires_namespace(gvk): + if namespace is None: + namespace = 'default' + return self.model_registry.invoke_delete_api(gvk, + name=name, + namespace=namespace, + body=delete_options, + **kwargs) + else: + return self.model_registry.invoke_delete_api(gvk, + name=name, + body=delete_options, + **kwargs) + except ApiException, ex: + model = self.parser.parse(ex.body) + return model + + def list_resource(self, kind=None, namespace='default', **kwargs): + if kind is not None: + gvk = self.model_registry.kind_map.get(kind, None) + else: + raise Exception('kind not specified') + return self.__list_resource(gvk, namespace=namespace, **kwargs) + + def __list_resource(self, gvk, namespace, **kwargs): + try: + if self.model_registry.requires_namespace(gvk): + if namespace is None: + namespace = 'default' + return self.model_registry.invoke_list_api(gvk, + namespace=namespace, + **kwargs) + else: + return self.model_registry.invoke_list_api(gvk, + **kwargs) + except ApiException, ex: + model = self.parser.parse(ex.body) + return model + + def list_resource_all_namespaces(self, kind=None, **kwargs): + if kind is not None: + gvk = self.model_registry.kind_map.get(kind, None) + else: + raise Exception('kind not specified') + return self.__list_resource_all_namespaces(gvk, **kwargs) + + def __list_resource_all_namespaces(self, gvk, **kwargs): + try: + return self.model_registry.invoke_list_for_all_namespaces_api(gvk, + **kwargs) + except ApiException, ex: + model = self.parser.parse(ex.body) + return model + + def apply(self, body): + gvk, model = self.parse_model(body) + result = self.__read_resource(gvk, name=model.metadata.name, namespace=model.metadata.namespace) + + if result.kind == 'Status' and result.reason == 'NotFound': + action = 'created' + result = self.__create_resource(gvk, model) + else: + model.metadata = result.metadata + action = 'updated' + result = self.__update_resource(gvk, model) + + if result.kind != 'Status': + print "{0}.{1} \"{2}\" {3}".format(result.kind.lower(), result.api_version, result.metadata.name, action) + else: + print result.message + + return result diff --git a/kubernetes/kubectl/__init__.py b/kubernetes/kubectl/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/kubernetes/kubectl/registry/ApiDefinition.py b/kubernetes/kubectl/registry/ApiDefinition.py new file mode 100644 index 0000000000..cb8f64d166 --- /dev/null +++ b/kubernetes/kubectl/registry/ApiDefinition.py @@ -0,0 +1,26 @@ +from kubectl.registry.GroupVersionKind import GroupVersionKind + + +class ApiDefinition(object): + def __init__(self, group_version_kind, api_clazz, requires_namespace=False): + """ + + :type requires_namespace: bool + :type api_clazz: type + :type group_version_kind: GroupVersionKind + + """ + self.group_version_kind = group_version_kind + self.api_clazz = api_clazz + self.requires_namespace = requires_namespace + self.actions = {} + + def add_action(self, action_type, action_method): + """ + + :param action_method: str + :type action_type: str + """ + self.actions[action_type] = action_method + + diff --git a/kubernetes/kubectl/registry/GroupVersionKind.py b/kubernetes/kubectl/registry/GroupVersionKind.py new file mode 100644 index 0000000000..b9ea5007b5 --- /dev/null +++ b/kubernetes/kubectl/registry/GroupVersionKind.py @@ -0,0 +1,80 @@ +import yaml + +def create_group_version_kind(**kwargs): + if 'kind' in kwargs and 'api_version' in kwargs or 'registry' in kwargs: + if 'kind' in kwargs and 'api_version': + kind = kwargs['kind'] + api_version = kwargs['api_version'] + else: + model = kwargs['registry'] + if isinstance(model, dict): + kind = model.get('kind') + api_version = model.get('apiVersion') + elif isinstance(model, str): + model = yaml.load(model) + kind = model.get('kind') + api_version = model.get('apiVersion') + else: + kind = model.kind + api_version = model.api_version + group, _, version = api_version.partition('/') + version = version.capitalize() + if version == "": + version = group.capitalize() + group = 'Core' + group = "".join(group.rsplit(".k8s.io", 1)) + group = "".join(word.capitalize() for word in group.split('.')) + elif 'kind' in kwargs and 'group' in kwargs and 'version' in kwargs: + kind = kwargs['kind'] + group = kwargs['group'] + version = kwargs['version'].capitalize() + return GroupVersionKind(group, version, kind) + + +class GroupVersion(object): + def __init__(self, group, version): + self.version = version + self.group = group + + def with_kind(self, kind): + return GroupVersionKind(self.group, version=self.version, kind=kind) + + def __str__(self): + return "{0}.{1}".format(self.group, self.version) + + def __eq__(self, other): + if other is None: + return False + + return self.group == other.group and self.version == other.version + + def __hash__(self): + return self.__str__().__hash__() + + +class GroupVersionKind(GroupVersion): + def __init__(self, group, version, kind): + super(GroupVersionKind, self).__init__(group, version) + self.kind = kind + + def get_group_version(self): + return GroupVersion(self.group, self.version) + + def __str__(self): + return "{0}/{1}.{2}".format(self.kind, self.group, self.version) + + def __eq__(self, other): + if other is None: + return False + + return self.kind == other.kind and self.group == other.group and self.version == other.version + + def __hash__(self): + return self.__str__().__hash__() + + def __repr__(self): + return self.__str__() + + + + diff --git a/kubernetes/kubectl/registry/ModelDefinition.py b/kubernetes/kubectl/registry/ModelDefinition.py new file mode 100644 index 0000000000..0ef46e90d1 --- /dev/null +++ b/kubernetes/kubectl/registry/ModelDefinition.py @@ -0,0 +1,13 @@ +def create_model_definition(clazz): + return ModelDefinition(clazz.swagger_types, clazz.attribute_map, clazz) + + +class ModelDefinition(object): + def __init__(self, swagger_types, attribute_map, model_clazz): + self.swagger_types = swagger_types + self.attribute_map = attribute_map + self.model_clazz = model_clazz + + def create_model(self, **kwargs): + return self.model_clazz(**kwargs) + diff --git a/kubernetes/kubectl/registry/ModelRegistry.py b/kubernetes/kubectl/registry/ModelRegistry.py new file mode 100644 index 0000000000..5a06d71484 --- /dev/null +++ b/kubernetes/kubectl/registry/ModelRegistry.py @@ -0,0 +1,183 @@ +from kubectl.registry.ApiDefinition import ApiDefinition +from kubernetes.client import models +from kubernetes.client import apis +from kubectl.registry.GroupVersionKind import GroupVersionKind, GroupVersion +import re +from kubectl.registry.ModelDefinition import create_model_definition + + +method_reg_ex = re.compile('(?Pdelete|read|create|replace|list)_(?Pnamespaced_)?(?P.*)') + + +def get_apis(group_version, action_map, clazz, api_client): + all_methods = dir(clazz) + + for method in all_methods: + if not method.startswith("__") and not method.endswith('_with_http_info'): + match = method_reg_ex.search(method) + if match is not None: + action = match.group('action') + kind = match.group('kind') + if not kind.startswith('collection'): + if kind.endswith('_for_all_namespaces'): + action = "{0}_for_all_namespaces".format(action) + kind = kind.replace('_for_all_namespaces', '') + kind = ''.join([part.capitalize() for part in kind.split('_')]) + current_versions = action_map.get(kind, {}) + action_map[kind] = current_versions + actions = current_versions.get(group_version.version, ApiDefinition(group_version.with_kind(kind), clazz(api_client))) + current_versions[group_version.version] = actions + if action == 'list_for_all_namespaces': + actions.requires_namespace = True + actions.add_action(action, method) + +def get_supported_kinds(api_client): + api_objects = apis.__dict__ + supported_kinds = {} + for name, clazz in api_objects.iteritems(): + if isinstance(clazz, type): + parts = re.findall('[A-Z][^A-Z]*', name) + parts = parts[0:-1] + if len(parts) > 1: + if parts[0].startswith('V'): + gv = GroupVersion(version=parts[0]) + else: + gv = GroupVersion(group="".join(parts[0:-1]), version=parts[-1]) + + get_apis(gv, supported_kinds, clazz, api_client) + + keys = supported_kinds.keys() + for kind in keys: + remove_apis_which_dont_implement_all_methods(kind, supported_kinds) + return supported_kinds + + +def remove_apis_which_dont_implement_all_methods(kind, supported_kinds): + current_action_map = supported_kinds[kind] + version_map = dict(current_action_map) + for version, actions in version_map.iteritems(): + if len(actions.actions) < 5: + del current_action_map[version] + if len(supported_kinds[kind]) == 0: + del supported_kinds[kind] + + +def resolve_group_version_kind(instance_type): + temp_type = re.sub('([a-z0-9])([A-Z])', r'\1_\2', instance_type) + parts = temp_type.split('_') + if parts[0].startswith('V'): + group = 'Core' + version = parts[0] + kind = "".join(parts[1:]) + else: + group = parts[0] + version = parts[1] + kind = "".join(parts[2:]) + return GroupVersionKind(group=group, version=version, kind=kind) + + +def sort_group_version_kind_by_version(gvk): + return gvk.version + + +class ModelRegistry(object): + def __init__(self): + self.models = {} + self.apis = {} + self.kind_map = {} + + def build_core_register(self, api_client): + supported_kinds = get_supported_kinds(api_client) + core_models = models.__dict__ + for name, clazz in core_models.iteritems(): + if isinstance(clazz, type): + gvk = resolve_group_version_kind(name) + api_versions = supported_kinds.get(gvk.kind, {}) + api_definition = api_versions.get(gvk.version, None) + if api_definition is not None: + gvk = api_definition.group_version_kind + self.apis[gvk] = api_definition + + self.models[gvk] = create_model_definition(clazz) + kind_map = {} + for gvk in self.apis.keys(): + gv_by_kind = kind_map.get(gvk.kind, []) + gv_by_kind.append(gvk) + kind_map[gvk.kind] = gv_by_kind + + for kind, gvks in kind_map.iteritems(): + gvks.sort(key=sort_group_version_kind_by_version) + self.kind_map[kind] = gvks[-1] + + def register_custom_resource(self, group_version_kind, model_clazz, api_clazz=None): + self.models[group_version_kind] = create_model_definition(model_clazz) + if api_clazz is not None: + action_map = {} + get_apis(group_version_kind.get_group_version(), action_map, api_clazz, self.api_client) + remove_apis_which_dont_implement_all_methods(group_version_kind.kind, action_map) + api_definition=action_map.get(group_version_kind.kind, {}).get(group_version_kind.version, None) + if api_definition is not None: + self.apis[group_version_kind] = api_definition + self.kind_map[group_version_kind.kind] = group_version_kind + + def get_model_definition(self, gvk): + return self.models.get(gvk, None) + + def resolve_model_for_type(self, instance_type): + gvk = resolve_group_version_kind(instance_type) + return self.models.get(gvk, None) + + def __resolve_method(self, gvk, action): + api_definition = self.apis.get(gvk, None) + if api_definition is None: + raise Exception('This registry is not supported') + action_method = api_definition.actions.get(action, None) + if action_method is None: + raise Exception('{0} method is not supported'.format(action.capitalize())) + return action_method, api_definition.api_clazz + + def invoke_create_api(self, gvk, namespace=None, **kwargs): + action_method, api_clazz = self.__resolve_method(gvk, 'create') + if self.requires_namespace(gvk): + return getattr(api_clazz, action_method)(namespace=namespace, **kwargs) + else: + return getattr(api_clazz, action_method)(**kwargs) + + def invoke_replace_api(self, gvk, name, namespace=None, **kwargs): + action_method, api_clazz = self.__resolve_method(gvk, 'replace') + if self.requires_namespace(gvk): + return getattr(api_clazz, action_method)(name=name, namespace=namespace, **kwargs) + else: + return getattr(api_clazz, action_method)(name=name, **kwargs) + + def invoke_delete_api(self, gvk, name, namespace=None, **kwargs): + action_method, api_clazz = self.__resolve_method(gvk, 'delete') + if self.requires_namespace(gvk): + return getattr(api_clazz, action_method)(name, namespace, **kwargs) + else: + return getattr(api_clazz, action_method)(name, **kwargs) + + def invoke_get_api(self, gvk, name, namespace=None, **kwargs): + action_method, api_clazz = self.__resolve_method(gvk, 'read') + if self.requires_namespace(gvk): + return getattr(api_clazz, action_method)(name=name, namespace=namespace, **kwargs) + else: + return getattr(api_clazz, action_method)(name=name, **kwargs) + + def invoke_list_api(self, gvk, namespace=None, **kwargs): + action_method, api_clazz = self.__resolve_method(gvk, 'list') + if self.requires_namespace(gvk): + return getattr(api_clazz, action_method)(namespace, **kwargs) + else: + return getattr(api_clazz, action_method)(**kwargs) + + def invoke_list_for_all_namespaces_api(self, gvk, **kwargs): + action_method, api_clazz = self.__resolve_method(gvk, 'list_for_all_namespaces') + return getattr(api_clazz, action_method)(**kwargs) + + def requires_namespace(self, gvk): + api_definition = self.apis.get(gvk, None) + if api_definition is None: + raise Exception('This registry is not supported') + return api_definition.requires_namespace + diff --git a/kubernetes/kubectl/registry/__init__.py b/kubernetes/kubectl/registry/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/kubernetes/kubectl/utils/Parser.py b/kubernetes/kubectl/utils/Parser.py new file mode 100644 index 0000000000..dddb0df37c --- /dev/null +++ b/kubernetes/kubectl/utils/Parser.py @@ -0,0 +1,119 @@ +from kubernetes.client.rest import ApiException +from kubectl.registry.GroupVersionKind import create_group_version_kind +import yaml + + +class Parser(object): + primitives = {'str': '_Parser__deserialize_object', + 'int': '_Parser__deserialize_primitive', + 'datetime': '_Parser__deserialize_datetime', + 'date': '_Parser__deserialize_date', + 'bool': '_Parser__deserialize_primitive', + 'object': '_Parser__deserialize_object', + 'dict(str, str)': '_Parser__deserialize_object'} + + def __init__(self, model_registry): + self.type_definitions = {} + self.model_registry = model_registry + + def parse(self, source): + """ + + :param model_definition: ModelDefinition + :type source: dict, str + """ + if isinstance(source, str): + source = yaml.safe_load(source) + gvk = create_group_version_kind(model=source) + model_definition = self.model_registry.get_model_definition(gvk) + return self.__deserialize_model(model_definition, source) + + def __deserialize_list(self, field_type, values): + instance_type = field_type[5:-1] + return [self.__deserialize_item(instance_type, value) for value in values] + + def __deserialize_model(self, model_definition, value): + if model_definition is not None and value is not None: + kwargs = {} + for attribute, attribute_type in model_definition.swagger_types.iteritems(): + field_name = model_definition.attribute_map.get(attribute) + result = value.get(field_name, None) + if result is not None: + kwargs[attribute] = self.__deserialize_item(attribute_type, result) + + return model_definition.create_model(**kwargs) + return None + + def __deserialize_class(self, instance_type, value): + model_definition = self.model_registry.resolve_model_for_type(instance_type) + return self.__deserialize_model(model_definition, value) + + def __deserialize_item(self, field_type, value): + if field_type.startswith('list'): + return self.__deserialize_list(field_type, value) + else: + deserializer = self.primitives.get(field_type, '_Parser__deserialize_class') + return getattr(self, deserializer)(field_type, value) + + def __deserialize_primitive(self, field_type, value): + # type: (str, object) -> object + """ + Deserializes string to primitive type. + :param field_type: str. + :param value: object + :return: int, long, float, bool. + """ + try: + klass=eval(field_type) + value = klass(value) + except UnicodeEncodeError: + value = unicode(value) + except TypeError: + value = value + return value + + def __deserialize_date(self, field_type, value): + """ + Deserializes string to date. + :param field_type: str. + :param value: str. + :return: date. + """ + if not value: + return None + try: + from dateutil.parser import parse + return parse(value).date() + except ImportError: + return value + except ValueError: + raise ApiException( + status=0, + reason="Failed to parse `{0}` into a date object" + .format(value) + ) + + def __deserialize_datetime(self, field_type, value): + """ + Deserializes string to datetime. + The string should be in iso8601 datetime format. + :param field_type: str. + :param value: str. + :return: datetime. + """ + if not value: + return None + try: + from dateutil.parser import parse + return parse(value) + except ImportError: + return value + except ValueError: + raise ApiException( + status=0, + reason="Failed to parse `{0}` into a datetime object". + format(value) + ) + + def __deserialize_object(self, field_type, value): + return value diff --git a/kubernetes/kubectl/utils/__init__.py b/kubernetes/kubectl/utils/__init__.py new file mode 100644 index 0000000000..f7d7daaeba --- /dev/null +++ b/kubernetes/kubectl/utils/__init__.py @@ -0,0 +1 @@ +__name__ = 'utils'