"""Kubernetes deployment controller""" from io import StringIO from kubernetes.client import VersionApi, VersionInfo from kubernetes.client.api_client import ApiClient from kubernetes.client.configuration import Configuration from kubernetes.client.exceptions import OpenApiException from kubernetes.config.config_exception import ConfigException from kubernetes.config.incluster_config import load_incluster_config from kubernetes.config.kube_config import load_kube_config_from_dict from urllib3.exceptions import HTTPError from yaml import dump_all from authentik.events.logs import LogEvent, capture_logs from authentik.outposts.controllers.base import BaseClient, BaseController, ControllerException from authentik.outposts.controllers.k8s.base import KubernetesObjectReconciler from authentik.outposts.controllers.k8s.deployment import DeploymentReconciler from authentik.outposts.controllers.k8s.secret import SecretReconciler from authentik.outposts.controllers.k8s.service import ServiceReconciler from authentik.outposts.controllers.k8s.service_monitor import PrometheusServiceMonitorReconciler from authentik.outposts.models import ( KubernetesServiceConnection, Outpost, OutpostServiceConnectionState, ServiceConnectionInvalid, ) class KubernetesClient(ApiClient, BaseClient): """Custom kubernetes client based on service connection""" def __init__(self, connection: KubernetesServiceConnection): config = Configuration() try: if connection.local: load_incluster_config(client_configuration=config) else: load_kube_config_from_dict(connection.kubeconfig, client_configuration=config) config.verify_ssl = connection.verify_ssl super().__init__(config) except ConfigException as exc: raise ServiceConnectionInvalid(exc) from exc def fetch_state(self) -> OutpostServiceConnectionState: """Get version info""" try: api_instance = VersionApi(self) version: VersionInfo = api_instance.get_code() return OutpostServiceConnectionState(version=version.git_version, healthy=True) except (OpenApiException, HTTPError, ServiceConnectionInvalid): return OutpostServiceConnectionState(version="", healthy=False) class KubernetesController(BaseController): """Manage deployment of outpost in kubernetes""" reconcilers: dict[str, type[KubernetesObjectReconciler]] reconcile_order: list[str] client: KubernetesClient connection: KubernetesServiceConnection def __init__( self, outpost: Outpost, connection: KubernetesServiceConnection, client: KubernetesClient | None = None, ) -> None: super().__init__(outpost, connection) self.client = client if client else KubernetesClient(connection) self.reconcilers = { SecretReconciler.reconciler_name(): SecretReconciler, DeploymentReconciler.reconciler_name(): DeploymentReconciler, ServiceReconciler.reconciler_name(): ServiceReconciler, PrometheusServiceMonitorReconciler.reconciler_name(): ( PrometheusServiceMonitorReconciler ), } self.reconcile_order = [ SecretReconciler.reconciler_name(), DeploymentReconciler.reconciler_name(), ServiceReconciler.reconciler_name(), PrometheusServiceMonitorReconciler.reconciler_name(), ] def up(self): try: for reconcile_key in self.reconcile_order: reconciler_cls = self.reconcilers.get(reconcile_key) if not reconciler_cls: continue reconciler = reconciler_cls(self) reconciler.up() except (OpenApiException, HTTPError, ServiceConnectionInvalid) as exc: raise ControllerException(str(exc)) from exc def up_with_logs(self) -> list[LogEvent]: try: all_logs = [] for reconcile_key in self.reconcile_order: if reconcile_key in self.outpost.config.kubernetes_disabled_components: all_logs.append( LogEvent( log_level="info", event=f"{reconcile_key.title()}: Disabled", logger=str(type(self)), ) ) continue with capture_logs() as logs: reconciler_cls = self.reconcilers.get(reconcile_key) if not reconciler_cls: continue reconciler = reconciler_cls(self) reconciler.up() for log in logs: log.logger = reconcile_key.title() all_logs.extend(logs) return all_logs except (OpenApiException, HTTPError, ServiceConnectionInvalid) as exc: raise ControllerException(str(exc)) from exc def down(self): try: for reconcile_key in self.reconcile_order: reconciler_cls = self.reconcilers.get(reconcile_key) if not reconciler_cls: continue reconciler = reconciler_cls(self) self.logger.debug("Tearing down object", name=reconcile_key) reconciler.down() except (OpenApiException, HTTPError, ServiceConnectionInvalid) as exc: raise ControllerException(str(exc)) from exc def down_with_logs(self) -> list[LogEvent]: try: all_logs = [] for reconcile_key in self.reconcile_order: if reconcile_key in self.outpost.config.kubernetes_disabled_components: all_logs.append( LogEvent( log_level="info", event=f"{reconcile_key.title()}: Disabled", logger=str(type(self)), ) ) continue with capture_logs() as logs: reconciler_cls = self.reconcilers.get(reconcile_key) if not reconciler_cls: continue reconciler = reconciler_cls(self) reconciler.down() for log in logs: log.logger = reconcile_key.title() all_logs.extend(logs) return all_logs except (OpenApiException, HTTPError, ServiceConnectionInvalid) as exc: raise ControllerException(str(exc)) from exc def get_static_deployment(self) -> str: documents = [] for reconcile_key in self.reconcile_order: reconciler_cls = self.reconcilers.get(reconcile_key) if not reconciler_cls: continue reconciler = reconciler_cls(self) if reconciler.noop: continue documents.append(reconciler.get_reference_object().to_dict()) with StringIO() as _str: dump_all( documents, stream=_str, default_flow_style=False, ) return _str.getvalue()