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
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
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 ]
reconcilers: dict[str, type[authentik.outposts.controllers.k8s.base.KubernetesObjectReconciler]]
client: KubernetesClient
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
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
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