authentik.outposts.controllers.kubernetes

Kubernetes deployment controller

  1"""Kubernetes deployment controller"""
  2
  3from io import StringIO
  4
  5from kubernetes.client import VersionApi, VersionInfo
  6from kubernetes.client.api_client import ApiClient
  7from kubernetes.client.configuration import Configuration
  8from kubernetes.client.exceptions import OpenApiException
  9from kubernetes.config.config_exception import ConfigException
 10from kubernetes.config.incluster_config import load_incluster_config
 11from kubernetes.config.kube_config import load_kube_config_from_dict
 12from urllib3.exceptions import HTTPError
 13from yaml import dump_all
 14
 15from authentik.events.logs import LogEvent, capture_logs
 16from authentik.lib.utils.reflection import class_to_path
 17from authentik.outposts.controllers.base import BaseClient, BaseController, ControllerException
 18from authentik.outposts.controllers.k8s.base import KubernetesObjectReconciler
 19from authentik.outposts.controllers.k8s.deployment import DeploymentReconciler
 20from authentik.outposts.controllers.k8s.secret import SecretReconciler
 21from authentik.outposts.controllers.k8s.service import MetricsServiceReconciler, ServiceReconciler
 22from authentik.outposts.controllers.k8s.service_monitor import PrometheusServiceMonitorReconciler
 23from authentik.outposts.models import (
 24    KubernetesServiceConnection,
 25    Outpost,
 26    OutpostServiceConnectionState,
 27    ServiceConnectionInvalid,
 28)
 29
 30
 31class KubernetesClient(ApiClient, BaseClient):
 32    """Custom kubernetes client based on service connection"""
 33
 34    def __init__(self, connection: KubernetesServiceConnection):
 35        config = Configuration()
 36        try:
 37            if connection.local:
 38                load_incluster_config(client_configuration=config)
 39            else:
 40                load_kube_config_from_dict(connection.kubeconfig, client_configuration=config)
 41            config.verify_ssl = connection.verify_ssl
 42            super().__init__(config)
 43        except ConfigException as exc:
 44            raise ServiceConnectionInvalid(exc) from exc
 45
 46    def fetch_state(self) -> OutpostServiceConnectionState:
 47        """Get version info"""
 48        try:
 49            api_instance = VersionApi(self)
 50            version: VersionInfo = api_instance.get_code()
 51            return OutpostServiceConnectionState(version=version.git_version, healthy=True)
 52        except OpenApiException, HTTPError, ServiceConnectionInvalid:
 53            return OutpostServiceConnectionState(version="", healthy=False)
 54
 55
 56class KubernetesController(BaseController):
 57    """Manage deployment of outpost in kubernetes"""
 58
 59    reconcilers: dict[str, type[KubernetesObjectReconciler]]
 60    reconcile_order: list[str]
 61
 62    client: KubernetesClient
 63    connection: KubernetesServiceConnection
 64
 65    def __init__(
 66        self,
 67        outpost: Outpost,
 68        connection: KubernetesServiceConnection,
 69        client: KubernetesClient | None = None,
 70    ) -> None:
 71        super().__init__(outpost, connection)
 72        self.client = client if client else KubernetesClient(connection)
 73        self.reconcilers = {
 74            SecretReconciler.reconciler_name(): SecretReconciler,
 75            DeploymentReconciler.reconciler_name(): DeploymentReconciler,
 76            ServiceReconciler.reconciler_name(): ServiceReconciler,
 77            MetricsServiceReconciler.reconciler_name(): MetricsServiceReconciler,
 78            PrometheusServiceMonitorReconciler.reconciler_name(): (
 79                PrometheusServiceMonitorReconciler
 80            ),
 81        }
 82        self.reconcile_order = [
 83            SecretReconciler.reconciler_name(),
 84            DeploymentReconciler.reconciler_name(),
 85            ServiceReconciler.reconciler_name(),
 86            MetricsServiceReconciler.reconciler_name(),
 87            PrometheusServiceMonitorReconciler.reconciler_name(),
 88        ]
 89
 90    def up(self):
 91        try:
 92            for reconcile_key in self.reconcile_order:
 93                reconciler_cls = self.reconcilers.get(reconcile_key)
 94                if not reconciler_cls:
 95                    continue
 96                reconciler = reconciler_cls(self)
 97                reconciler.up()
 98
 99        except (OpenApiException, HTTPError, ServiceConnectionInvalid) as exc:
100            raise ControllerException(str(exc)) from exc
101
102    def up_with_logs(self) -> list[LogEvent]:
103        try:
104            all_logs = []
105            for reconcile_key in self.reconcile_order:
106                if reconcile_key in self.outpost.config.kubernetes_disabled_components:
107                    all_logs.append(
108                        LogEvent(
109                            log_level="info",
110                            event=f"{reconcile_key.title()}: Disabled",
111                            logger=class_to_path(self.__class__),
112                        )
113                    )
114                    continue
115                with capture_logs() as logs:
116                    reconciler_cls = self.reconcilers.get(reconcile_key)
117                    if not reconciler_cls:
118                        continue
119                    reconciler = reconciler_cls(self)
120                    reconciler.up()
121                for log in logs:
122                    log.logger = reconcile_key.title()
123                all_logs.extend(logs)
124            return all_logs
125        except (OpenApiException, HTTPError, ServiceConnectionInvalid) as exc:
126            raise ControllerException(str(exc)) from exc
127
128    def down(self):
129        try:
130            for reconcile_key in self.reconcile_order:
131                reconciler_cls = self.reconcilers.get(reconcile_key)
132                if not reconciler_cls:
133                    continue
134                reconciler = reconciler_cls(self)
135                self.logger.debug("Tearing down object", name=reconcile_key)
136                reconciler.down()
137
138        except (OpenApiException, HTTPError, ServiceConnectionInvalid) as exc:
139            raise ControllerException(str(exc)) from exc
140
141    def down_with_logs(self) -> list[LogEvent]:
142        try:
143            all_logs = []
144            for reconcile_key in self.reconcile_order:
145                if reconcile_key in self.outpost.config.kubernetes_disabled_components:
146                    all_logs.append(
147                        LogEvent(
148                            log_level="info",
149                            event=f"{reconcile_key.title()}: Disabled",
150                            logger=class_to_path(self.__class__),
151                        )
152                    )
153                    continue
154                with capture_logs() as logs:
155                    reconciler_cls = self.reconcilers.get(reconcile_key)
156                    if not reconciler_cls:
157                        continue
158                    reconciler = reconciler_cls(self)
159                    reconciler.down()
160                for log in logs:
161                    log.logger = reconcile_key.title()
162                all_logs.extend(logs)
163            return all_logs
164        except (OpenApiException, HTTPError, ServiceConnectionInvalid) as exc:
165            raise ControllerException(str(exc)) from exc
166
167    def get_static_deployment(self) -> str:
168        documents = []
169        for reconcile_key in self.reconcile_order:
170            reconciler_cls = self.reconcilers.get(reconcile_key)
171            if not reconciler_cls:
172                continue
173            reconciler = reconciler_cls(self)
174            if reconciler.noop:
175                continue
176            documents.append(reconciler.get_reference_object().to_dict())
177
178        with StringIO() as _str:
179            dump_all(
180                documents,
181                stream=_str,
182                default_flow_style=False,
183            )
184            return _str.getvalue()
class KubernetesClient(kubernetes.client.api_client.ApiClient, authentik.outposts.controllers.base.BaseClient):
32class KubernetesClient(ApiClient, BaseClient):
33    """Custom kubernetes client based on service connection"""
34
35    def __init__(self, connection: KubernetesServiceConnection):
36        config = Configuration()
37        try:
38            if connection.local:
39                load_incluster_config(client_configuration=config)
40            else:
41                load_kube_config_from_dict(connection.kubeconfig, client_configuration=config)
42            config.verify_ssl = connection.verify_ssl
43            super().__init__(config)
44        except ConfigException as exc:
45            raise ServiceConnectionInvalid(exc) from exc
46
47    def fetch_state(self) -> OutpostServiceConnectionState:
48        """Get version info"""
49        try:
50            api_instance = VersionApi(self)
51            version: VersionInfo = api_instance.get_code()
52            return OutpostServiceConnectionState(version=version.git_version, healthy=True)
53        except OpenApiException, HTTPError, ServiceConnectionInvalid:
54            return OutpostServiceConnectionState(version="", healthy=False)

Custom kubernetes client based on service connection

KubernetesClient(connection: authentik.outposts.models.KubernetesServiceConnection)
35    def __init__(self, connection: KubernetesServiceConnection):
36        config = Configuration()
37        try:
38            if connection.local:
39                load_incluster_config(client_configuration=config)
40            else:
41                load_kube_config_from_dict(connection.kubeconfig, client_configuration=config)
42            config.verify_ssl = connection.verify_ssl
43            super().__init__(config)
44        except ConfigException as exc:
45            raise ServiceConnectionInvalid(exc) from exc
def fetch_state(self) -> authentik.outposts.models.OutpostServiceConnectionState:
47    def fetch_state(self) -> OutpostServiceConnectionState:
48        """Get version info"""
49        try:
50            api_instance = VersionApi(self)
51            version: VersionInfo = api_instance.get_code()
52            return OutpostServiceConnectionState(version=version.git_version, healthy=True)
53        except OpenApiException, HTTPError, ServiceConnectionInvalid:
54            return OutpostServiceConnectionState(version="", healthy=False)

Get version info

class KubernetesController(authentik.outposts.controllers.base.BaseController):
 57class KubernetesController(BaseController):
 58    """Manage deployment of outpost in kubernetes"""
 59
 60    reconcilers: dict[str, type[KubernetesObjectReconciler]]
 61    reconcile_order: list[str]
 62
 63    client: KubernetesClient
 64    connection: KubernetesServiceConnection
 65
 66    def __init__(
 67        self,
 68        outpost: Outpost,
 69        connection: KubernetesServiceConnection,
 70        client: KubernetesClient | None = None,
 71    ) -> None:
 72        super().__init__(outpost, connection)
 73        self.client = client if client else KubernetesClient(connection)
 74        self.reconcilers = {
 75            SecretReconciler.reconciler_name(): SecretReconciler,
 76            DeploymentReconciler.reconciler_name(): DeploymentReconciler,
 77            ServiceReconciler.reconciler_name(): ServiceReconciler,
 78            MetricsServiceReconciler.reconciler_name(): MetricsServiceReconciler,
 79            PrometheusServiceMonitorReconciler.reconciler_name(): (
 80                PrometheusServiceMonitorReconciler
 81            ),
 82        }
 83        self.reconcile_order = [
 84            SecretReconciler.reconciler_name(),
 85            DeploymentReconciler.reconciler_name(),
 86            ServiceReconciler.reconciler_name(),
 87            MetricsServiceReconciler.reconciler_name(),
 88            PrometheusServiceMonitorReconciler.reconciler_name(),
 89        ]
 90
 91    def up(self):
 92        try:
 93            for reconcile_key in self.reconcile_order:
 94                reconciler_cls = self.reconcilers.get(reconcile_key)
 95                if not reconciler_cls:
 96                    continue
 97                reconciler = reconciler_cls(self)
 98                reconciler.up()
 99
100        except (OpenApiException, HTTPError, ServiceConnectionInvalid) as exc:
101            raise ControllerException(str(exc)) from exc
102
103    def up_with_logs(self) -> list[LogEvent]:
104        try:
105            all_logs = []
106            for reconcile_key in self.reconcile_order:
107                if reconcile_key in self.outpost.config.kubernetes_disabled_components:
108                    all_logs.append(
109                        LogEvent(
110                            log_level="info",
111                            event=f"{reconcile_key.title()}: Disabled",
112                            logger=class_to_path(self.__class__),
113                        )
114                    )
115                    continue
116                with capture_logs() as logs:
117                    reconciler_cls = self.reconcilers.get(reconcile_key)
118                    if not reconciler_cls:
119                        continue
120                    reconciler = reconciler_cls(self)
121                    reconciler.up()
122                for log in logs:
123                    log.logger = reconcile_key.title()
124                all_logs.extend(logs)
125            return all_logs
126        except (OpenApiException, HTTPError, ServiceConnectionInvalid) as exc:
127            raise ControllerException(str(exc)) from exc
128
129    def down(self):
130        try:
131            for reconcile_key in self.reconcile_order:
132                reconciler_cls = self.reconcilers.get(reconcile_key)
133                if not reconciler_cls:
134                    continue
135                reconciler = reconciler_cls(self)
136                self.logger.debug("Tearing down object", name=reconcile_key)
137                reconciler.down()
138
139        except (OpenApiException, HTTPError, ServiceConnectionInvalid) as exc:
140            raise ControllerException(str(exc)) from exc
141
142    def down_with_logs(self) -> list[LogEvent]:
143        try:
144            all_logs = []
145            for reconcile_key in self.reconcile_order:
146                if reconcile_key in self.outpost.config.kubernetes_disabled_components:
147                    all_logs.append(
148                        LogEvent(
149                            log_level="info",
150                            event=f"{reconcile_key.title()}: Disabled",
151                            logger=class_to_path(self.__class__),
152                        )
153                    )
154                    continue
155                with capture_logs() as logs:
156                    reconciler_cls = self.reconcilers.get(reconcile_key)
157                    if not reconciler_cls:
158                        continue
159                    reconciler = reconciler_cls(self)
160                    reconciler.down()
161                for log in logs:
162                    log.logger = reconcile_key.title()
163                all_logs.extend(logs)
164            return all_logs
165        except (OpenApiException, HTTPError, ServiceConnectionInvalid) as exc:
166            raise ControllerException(str(exc)) from exc
167
168    def get_static_deployment(self) -> str:
169        documents = []
170        for reconcile_key in self.reconcile_order:
171            reconciler_cls = self.reconcilers.get(reconcile_key)
172            if not reconciler_cls:
173                continue
174            reconciler = reconciler_cls(self)
175            if reconciler.noop:
176                continue
177            documents.append(reconciler.get_reference_object().to_dict())
178
179        with StringIO() as _str:
180            dump_all(
181                documents,
182                stream=_str,
183                default_flow_style=False,
184            )
185            return _str.getvalue()

Manage deployment of outpost in kubernetes

KubernetesController( outpost: authentik.outposts.models.Outpost, connection: authentik.outposts.models.KubernetesServiceConnection, client: KubernetesClient | None = None)
66    def __init__(
67        self,
68        outpost: Outpost,
69        connection: KubernetesServiceConnection,
70        client: KubernetesClient | None = None,
71    ) -> None:
72        super().__init__(outpost, connection)
73        self.client = client if client else KubernetesClient(connection)
74        self.reconcilers = {
75            SecretReconciler.reconciler_name(): SecretReconciler,
76            DeploymentReconciler.reconciler_name(): DeploymentReconciler,
77            ServiceReconciler.reconciler_name(): ServiceReconciler,
78            MetricsServiceReconciler.reconciler_name(): MetricsServiceReconciler,
79            PrometheusServiceMonitorReconciler.reconciler_name(): (
80                PrometheusServiceMonitorReconciler
81            ),
82        }
83        self.reconcile_order = [
84            SecretReconciler.reconciler_name(),
85            DeploymentReconciler.reconciler_name(),
86            ServiceReconciler.reconciler_name(),
87            MetricsServiceReconciler.reconciler_name(),
88            PrometheusServiceMonitorReconciler.reconciler_name(),
89        ]
reconcile_order: list[str]
def up(self):
 91    def up(self):
 92        try:
 93            for reconcile_key in self.reconcile_order:
 94                reconciler_cls = self.reconcilers.get(reconcile_key)
 95                if not reconciler_cls:
 96                    continue
 97                reconciler = reconciler_cls(self)
 98                reconciler.up()
 99
100        except (OpenApiException, HTTPError, ServiceConnectionInvalid) as exc:
101            raise ControllerException(str(exc)) from exc

Called by scheduled task to reconcile deployment/service/etc

def up_with_logs(self) -> list[authentik.events.logs.LogEvent]:
103    def up_with_logs(self) -> list[LogEvent]:
104        try:
105            all_logs = []
106            for reconcile_key in self.reconcile_order:
107                if reconcile_key in self.outpost.config.kubernetes_disabled_components:
108                    all_logs.append(
109                        LogEvent(
110                            log_level="info",
111                            event=f"{reconcile_key.title()}: Disabled",
112                            logger=class_to_path(self.__class__),
113                        )
114                    )
115                    continue
116                with capture_logs() as logs:
117                    reconciler_cls = self.reconcilers.get(reconcile_key)
118                    if not reconciler_cls:
119                        continue
120                    reconciler = reconciler_cls(self)
121                    reconciler.up()
122                for log in logs:
123                    log.logger = reconcile_key.title()
124                all_logs.extend(logs)
125            return all_logs
126        except (OpenApiException, HTTPError, ServiceConnectionInvalid) as exc:
127            raise ControllerException(str(exc)) from exc

Call .up() but capture all log output and return it.

def down(self):
129    def down(self):
130        try:
131            for reconcile_key in self.reconcile_order:
132                reconciler_cls = self.reconcilers.get(reconcile_key)
133                if not reconciler_cls:
134                    continue
135                reconciler = reconciler_cls(self)
136                self.logger.debug("Tearing down object", name=reconcile_key)
137                reconciler.down()
138
139        except (OpenApiException, HTTPError, ServiceConnectionInvalid) as exc:
140            raise ControllerException(str(exc)) from exc

Handler to delete everything we've created

def down_with_logs(self) -> list[authentik.events.logs.LogEvent]:
142    def down_with_logs(self) -> list[LogEvent]:
143        try:
144            all_logs = []
145            for reconcile_key in self.reconcile_order:
146                if reconcile_key in self.outpost.config.kubernetes_disabled_components:
147                    all_logs.append(
148                        LogEvent(
149                            log_level="info",
150                            event=f"{reconcile_key.title()}: Disabled",
151                            logger=class_to_path(self.__class__),
152                        )
153                    )
154                    continue
155                with capture_logs() as logs:
156                    reconciler_cls = self.reconcilers.get(reconcile_key)
157                    if not reconciler_cls:
158                        continue
159                    reconciler = reconciler_cls(self)
160                    reconciler.down()
161                for log in logs:
162                    log.logger = reconcile_key.title()
163                all_logs.extend(logs)
164            return all_logs
165        except (OpenApiException, HTTPError, ServiceConnectionInvalid) as exc:
166            raise ControllerException(str(exc)) from exc

Call .down() but capture all log output and return it.

def get_static_deployment(self) -> str:
168    def get_static_deployment(self) -> str:
169        documents = []
170        for reconcile_key in self.reconcile_order:
171            reconciler_cls = self.reconcilers.get(reconcile_key)
172            if not reconciler_cls:
173                continue
174            reconciler = reconciler_cls(self)
175            if reconciler.noop:
176                continue
177            documents.append(reconciler.get_reference_object().to_dict())
178
179        with StringIO() as _str:
180            dump_all(
181                documents,
182                stream=_str,
183                default_flow_style=False,
184            )
185            return _str.getvalue()

Return a static deployment configuration