From ed3855d2dbb88672913e16dbf84cf1bcc42432b7 Mon Sep 17 00:00:00 2001 From: Michael Schilonka Date: Mon, 4 Apr 2022 20:26:08 +0200 Subject: [PATCH] feat: support k8s apis which are not supported by the python binding anymore --- deck.example.yaml | 55 +++----------- getdeck/api/get.py | 4 - getdeck/api/remove.py | 3 + getdeck/deckfile/file.py | 3 +- getdeck/k8s.py | 122 +++++++++++++++++++++---------- getdeck/provider/k3d/__init__.py | 5 +- getdeck/sources/tooler.py | 4 +- tooler/Dockerfile | 2 - 8 files changed, 107 insertions(+), 91 deletions(-) diff --git a/deck.example.yaml b/deck.example.yaml index 006da90..6c4c017 100644 --- a/deck.example.yaml +++ b/deck.example.yaml @@ -4,7 +4,7 @@ version: "1" cluster: provider: k3d minVersion: 4.0.0 - name: another-cluster + name: mumpiz nativeConfig: # content of the native config file (e.g. https://k3d.io/v5.0.0/usage/configfile/) apiVersion: k3d.io/v1alpha2 kind: Simple @@ -20,48 +20,15 @@ cluster: - agent[0] decks: - - name: buzzword-counter - namespace: buzzword + - name: mumpiz-platform sources: - type: helm - ref: https://kubernetes.github.io/dashboard/ - chart: kubernetes-dashboard - releaseName: dashboard - parameters: - - name: ingress.enabled - value: true - - name: ingress.hosts - value: '{dashboard.127.0.0.1.nip.io}' - - name: protocolHttp - value: true - - name: service.externalPort - value: 8080 - - name: serviceAccount.create - value: true - - name: rbac.clusterReadOnlyRole - value: true - - - type: helm - ref: https://charts.bitnami.com/bitnami - chart: solr - releaseName: solr - parameters: - - name: ingress.enabled - value: true - - name: ingress.hostname - value: solr.127.0.0.1.nip.io - - - type: helm - ref: git@github.com:Blueshoe/buzzword-charts.git - targetRevision: HEAD # only relevant for git - releaseName: buzzword-counter # Release name override (defaults to application name) - path: buzzword-counter -# valueFiles: # Helm values (files) relative to 'path' -# - helm_vars/development/values.development.yaml - -# - type: kustomize -# - type: directory -# ref: git@github.com:Blueshoe/buzzword-charts.git -# targetRevision: HEAD -# path: . -# recursive: true + ref: git@github.com:Blueshoe/mumpiz-helm.git + path: mumpiz + releaseName: mumpiz + helmPlugins: + - secret + valueFiles: # Helm values (files) relative to 'path' + - helm_vars/development/values.development.yaml + - helm_vars/development/secrets.development.yaml + - helm_vars/development/values.shared.yaml \ No newline at end of file diff --git a/getdeck/api/get.py b/getdeck/api/get.py index 89fd249..fb9df75 100644 --- a/getdeck/api/get.py +++ b/getdeck/api/get.py @@ -37,10 +37,6 @@ def run_deck( # 1.b check or set up local cluster if not k8s_provider.exists(): k8s_provider.create() - logger.info( - f"Create a new cluster with provider {k8s_provider.k8s_provider_type} " - f"named {k8s_provider.display_name}" - ) logger.info(f"Published ports are: {k8s_provider.get_ports()}") cluster_created = True else: diff --git a/getdeck/api/remove.py b/getdeck/api/remove.py index 9b5b9a3..a16c91e 100644 --- a/getdeck/api/remove.py +++ b/getdeck/api/remove.py @@ -62,6 +62,9 @@ def remove_deck( k8s_delete_object(config, file.content, generated_deck.namespace) if progress_callback: progress_callback(max(50, int(i / total * 50) - 1)) + except RuntimeError as e: + logger.error(f"There was an error removing a workload: {e}") + continue except Exception as e: logger.error(f"There was an error removing the workload: {e}") raise e diff --git a/getdeck/deckfile/file.py b/getdeck/deckfile/file.py index 62b9d7b..8aed24f 100644 --- a/getdeck/deckfile/file.py +++ b/getdeck/deckfile/file.py @@ -43,6 +43,7 @@ class DeckfileHelmSource(BaseModel): releaseName: str valueFiles: List[str] = ["values.yaml"] helmArgs: List[str] = None + helmPlugins: List[str] = None class DeckfileDirectorySource(BaseModel): @@ -54,7 +55,7 @@ class DeckfileDirectorySource(BaseModel): class DeckfileDeck(BaseModel): name: str - namespace: str + namespace: str = "default" sources: List[Union[DeckfileHelmSource, DeckfileDirectorySource]] diff --git a/getdeck/k8s.py b/getdeck/k8s.py index 70fe906..ceff91f 100644 --- a/getdeck/k8s.py +++ b/getdeck/k8s.py @@ -1,6 +1,6 @@ import logging import re -from typing import Callable, Any, Tuple, List +from typing import Callable, Any, Tuple, List, Optional from getdeck.configuration import ClientConfiguration @@ -14,21 +14,25 @@ def k8s_create_or_patch( api = k8s_select_api(config, obj) try: - res = k8s_call_api(api, "create", obj, namespace, **kwargs) - logger.debug( - f"Kubernetes: {k8s_describe_object(obj)} created with uid={res.metadata.uid}" - ) + res = k8s_call_api(config, api, "create", obj, namespace, **kwargs) + if api: + logger.debug( + f"Kubernetes: {k8s_describe_object(obj)} created with uid={res.metadata.uid}" + ) except ApiException as e: if e.reason != "Conflict": raise try: - res = k8s_call_api(api, "patch", obj, namespace, **kwargs) - logger.debug( - f"Kubernetes: {k8s_describe_object(obj)} patched with uid={res.metadata.uid}" - ) + res = k8s_call_api(config, api, "patch", obj, namespace, **kwargs) + if api: + logger.debug( + f"Kubernetes: {k8s_describe_object(obj)} patched with uid={res.metadata.uid}" + ) except ApiException as e: if e.reason != "Unprocessable Entity": - logger.error(f"Error installing object: {e.reason}") + logger.error( + f"Error installing object {obj['metadata']['name']}: {e.reason}" + ) raise try: # try again @@ -36,24 +40,29 @@ def k8s_create_or_patch( f"Kubernetes: replacing {k8s_describe_object(obj)} " f"failed. Attempting deletion and recreation." ) - res = k8s_call_api(api, "delete", obj, namespace, **kwargs) - logger.debug(f"Kubernetes: {k8s_describe_object(obj)} deleted") - res = k8s_call_api(api, "create", obj, namespace, **kwargs) - logger.debug( - f"Kubernetes: {k8s_describe_object(obj)} created with uid={res.metadata.uid}" - ) + res = k8s_call_api(config, api, "delete", obj, namespace, **kwargs) + if api: + logger.debug(f"Kubernetes: {k8s_describe_object(obj)} deleted") + res = k8s_call_api(config, api, "create", obj, namespace, **kwargs) + if api: + logger.debug( + f"Kubernetes: {k8s_describe_object(obj)} created with uid={res.metadata.uid}" + ) except Exception as ex: - logger.error( - f"Kubernetes: failure updating {k8s_describe_object(obj)}: {ex}" - ) + if api: + logger.error( + f"Kubernetes: failure updating {k8s_describe_object(obj)}: {ex}" + ) raise RuntimeError(ex) except ValueError as e: logger.warning( - f"Error installing object: {e}. This is probably caused by a skew in the Kubernetes version." + f"Error installing object {obj['metadata']['name']}: {e}. " + f"This is probably caused by a skew in the Kubernetes version." ) except ValueError as e: logger.warning( - f"Error installing object: {e}. This is probably caused by a skew in the Kubernetes version." + f"Error installing object {obj['metadata']['name']}: {e}. " + f"This is probably caused by a skew in the Kubernetes version." ) @@ -62,7 +71,7 @@ def k8s_delete_object(config: ClientConfiguration, obj, namespace, **kwargs) -> api = k8s_select_api(config, obj) try: - k8s_call_api(api, "delete", obj, namespace, **kwargs) + k8s_call_api(config, api, "delete", obj, namespace, **kwargs) logger.debug(f"Kubernetes: {k8s_describe_object(obj)} deleted") return True except ApiException as e: @@ -74,7 +83,7 @@ def k8s_delete_object(config: ClientConfiguration, obj, namespace, **kwargs) -> raise RuntimeError(e) -def k8s_select_api(config: ClientConfiguration, obj) -> Callable: +def k8s_select_api(config: ClientConfiguration, obj) -> Optional[Callable]: group, _, version = obj["apiVersion"].partition("/") if version == "": version = group @@ -88,11 +97,14 @@ def k8s_select_api(config: ClientConfiguration, obj) -> Callable: logger.debug(f"Selecting api {api}") k8s_api = config.get_k8s_api(api) if k8s_api is None: - raise ValueError(f"Kubernetes API {api} not available") + logger.debug(f"Kubernetes API {api} not available. Error causing object: {obj}") + return None return k8s_api -def k8s_call_api(api, action, obj, namespace: str, **args) -> Any: +def k8s_call_api( + config: ClientConfiguration, api, action, obj, namespace: str, **args +) -> Any: kind = convert_camel_2_snake(obj["kind"]) try: namespace = obj["metadata"]["namespace"] @@ -100,19 +112,55 @@ def k8s_call_api(api, action, obj, namespace: str, **args) -> Any: # take the namespace passed as installation target pass _func = f"{action}_{kind}" - if hasattr(api, _func): - function = getattr(api, _func) + if api is None: + # api could not be found, try a manual REST call + group, _, version = obj["apiVersion"].partition("/") + kind_plural = {"ingress": "ingresses"}.get( + obj["kind"].lower(), f"{obj['kind']}s" + ) + logger.debug( + f"Running a REST {action} operation for {obj['kind']} " + f"/apis/{obj['apiVersion']}/namespaces/{namespace}/{kind_plural}/" + ) + if action == "create": + return config.K8S_CORE_API.api_client.call_api( + f"/apis/{obj['apiVersion']}/namespaces/{namespace}/{kind_plural}/", + "POST", + body=obj, + ) + elif action == "patch": + return config.K8S_CORE_API.api_client.call_api( + f"/apis/{obj['apiVersion']}/namespaces/{namespace}/{kind_plural}/{obj['metadata']['name']}", + "PUT", + body=obj, + ) + elif action == "delete": + from kubernetes.client.models.v1_delete_options import V1DeleteOptions + + delobj = V1DeleteOptions() + return config.K8S_CORE_API.api_client.call_api( + f"/apis/{obj['apiVersion']}/namespaces/{namespace}/{kind_plural}/{obj['metadata']['name']}", + "DELETE", + body=delobj, + ) + else: + raise ValueError(f"The action {action} is not supported at the moment") + else: - _func = f"{action}_namespaced_{kind}" - function = getattr(api, _func) - args["namespace"] = namespace - if "create" not in _func: - args["name"] = obj["metadata"]["name"] - if "delete" in _func: - from kubernetes.client.models.v1_delete_options import V1DeleteOptions - - obj = V1DeleteOptions() - return function(body=obj, **args) + if hasattr(api, _func): + function = getattr(api, _func) + else: + _func = f"{action}_namespaced_{kind}" + function = getattr(api, _func) + args["namespace"] = namespace + if "create" not in _func: + args["name"] = obj["metadata"]["name"] + if "delete" in _func: + from kubernetes.client.models.v1_delete_options import V1DeleteOptions + + obj = V1DeleteOptions() + + return function(body=obj, **args) def k8s_describe_object(obj: dict) -> str: diff --git a/getdeck/provider/k3d/__init__.py b/getdeck/provider/k3d/__init__.py index a5a3950..85eb696 100644 --- a/getdeck/provider/k3d/__init__.py +++ b/getdeck/provider/k3d/__init__.py @@ -138,7 +138,10 @@ def create(self): temp.flush() arguments.extend(["--config", temp.name]) logger.debug(arguments) - self._execute(arguments, print_output=True) + self._execute( + arguments, + print_output=True if logger.level == logging.DEBUG else False, + ) except Exception as e: logger.debug(traceback.print_exc()) raise e diff --git a/getdeck/sources/tooler.py b/getdeck/sources/tooler.py index db58c68..8a69d30 100644 --- a/getdeck/sources/tooler.py +++ b/getdeck/sources/tooler.py @@ -44,9 +44,9 @@ def build_user_container(config: ClientConfiguration): RUN addgroup -g ${GROUP_ID} -S tooler && adduser -u ${USER_ID} -S tooler -G tooler RUN chown ${USER_ID}:${GROUP_ID} /sources RUN chown ${USER_ID}:${GROUP_ID} /output + WORKDIR /sources - USER tooler - """ + USER tooler""" ).encode("utf-8") ) build_args = {"USER_ID": str(uid), "GROUP_ID": str(gid)} diff --git a/tooler/Dockerfile b/tooler/Dockerfile index b3e29f2..7dd2985 100644 --- a/tooler/Dockerfile +++ b/tooler/Dockerfile @@ -1,8 +1,6 @@ FROM alpine LABEL maintainer="Schille" -# Ignore to update versions here -# docker build --no-cache --build-arg KUBECTL_VERSION=${tag} --build-arg HELM_VERSION=${helm} --build-arg KUSTOMIZE_VERSION=${kustomize_version} -t ${image}:${tag} . ARG HELM_VERSION=3.8.1 ARG KUBECTL_VERSION=1.23.5 ARG KUSTOMIZE_VERSION=v3.8.1