authentik.outposts.controllers.k8s.base

Base Kubernetes Reconciler

  1"""Base Kubernetes Reconciler"""
  2
  3import re
  4import ssl
  5from dataclasses import asdict
  6from json import dumps
  7from typing import TYPE_CHECKING, TypeVar
  8
  9import urllib3
 10from dacite.core import from_dict
 11from django.http import HttpResponseNotFound
 12from django.utils.text import slugify
 13from jsonpatch import JsonPatchConflict, JsonPatchException, JsonPatchTestFailed, apply_patch
 14from kubernetes.client import ApiClient, V1ObjectMeta
 15from kubernetes.client.exceptions import ApiException, OpenApiException
 16from kubernetes.client.models.v1_deployment import V1Deployment
 17from kubernetes.client.models.v1_pod import V1Pod
 18from requests import Response
 19from structlog.stdlib import get_logger
 20from urllib3.exceptions import HTTPError
 21
 22from authentik import authentik_version
 23from authentik.outposts.apps import MANAGED_OUTPOST
 24from authentik.outposts.controllers.base import ControllerException
 25from authentik.outposts.controllers.k8s.triggers import NeedsRecreate, NeedsUpdate
 26
 27if TYPE_CHECKING:
 28    from authentik.outposts.controllers.kubernetes import KubernetesController
 29
 30T = TypeVar("T", V1Pod, V1Deployment)
 31
 32
 33def get_version() -> str:
 34    """Wrapper for authentik_version() to make testing easier"""
 35    return authentik_version()
 36
 37
 38class KubernetesObjectReconciler[T]:
 39    """Base Kubernetes Reconciler, handles the basic logic."""
 40
 41    controller: KubernetesController
 42
 43    def __init__(self, controller: KubernetesController):
 44        self.controller = controller
 45        self.namespace = controller.outpost.config.kubernetes_namespace
 46        self.kubernetes_disable_x509_strict = (
 47            controller.outpost.config.kubernetes_disable_x509_strict
 48        )
 49        self.logger = get_logger().bind(type=self.__class__.__name__)
 50        self.api_client = self.k8s_client()
 51
 52    def get_patch(self):
 53        """Get any patches that apply to this CRD"""
 54        patches = self.controller.outpost.config.kubernetes_json_patches
 55        if not patches:
 56            return None
 57        return patches.get(self.reconciler_name(), None)
 58
 59    @property
 60    def is_embedded(self) -> bool:
 61        """Return true if the current outpost is embedded"""
 62        return self.controller.outpost.managed == MANAGED_OUTPOST
 63
 64    @staticmethod
 65    def reconciler_name() -> str:
 66        """A name this reconciler is identified by in the configuration"""
 67        raise NotImplementedError
 68
 69    @property
 70    def noop(self) -> bool:
 71        """Return true if this object should not be created/updated/deleted in this cluster"""
 72        return False
 73
 74    @property
 75    def name(self) -> str:
 76        """Get the name of the object this reconciler manages"""
 77
 78        base_name = (
 79            self.controller.outpost.config.object_naming_template
 80            % {
 81                "name": slugify(self.controller.outpost.name),
 82                "uuid": self.controller.outpost.uuid.hex,
 83            }
 84        ).lower()
 85
 86        formatted = slugify(base_name)
 87        formatted = re.sub(r"[^a-z0-9-]", "-", formatted)
 88        formatted = re.sub(r"-+", "-", formatted)
 89        formatted = formatted[:63]
 90
 91        if not formatted:
 92            formatted = f"outpost-{self.controller.outpost.uuid.hex}"[:63]
 93
 94        return formatted
 95
 96    def get_patched_reference_object(self) -> T:
 97        """Get patched reference object"""
 98        reference = self.get_reference_object()
 99        patch = self.get_patch()
100        try:
101            json = self.api_client.sanitize_for_serialization(reference)
102        # Custom objects will not be known to the clients openapi types
103        except AttributeError:
104            json = asdict(reference)
105        try:
106            ref = json
107            if patch is not None:
108                ref = apply_patch(json, patch)
109        except (JsonPatchException, JsonPatchConflict, JsonPatchTestFailed) as exc:
110            raise ControllerException(f"JSON Patch failed: {exc}") from exc
111        mock_response = Response()
112        mock_response.data = dumps(ref)
113
114        try:
115            result = self.api_client.deserialize(mock_response, reference.__class__.__name__)
116        # Custom objects will not be known to the clients openapi types
117        except AttributeError:
118            result = from_dict(reference.__class__, data=ref)
119
120        return result
121
122    def up(self):
123        """Create object if it doesn't exist, update if needed or recreate if needed."""
124        current = None
125        if self.noop:
126            self.logger.debug("Object is noop")
127            return
128        reference = self.get_patched_reference_object()
129        try:
130            try:
131                current = self.retrieve()
132            except (OpenApiException, HTTPError) as exc:
133                if isinstance(exc, ApiException) and exc.status == HttpResponseNotFound.status_code:
134                    self.logger.debug("Failed to get current, triggering recreate")
135                    raise NeedsRecreate from exc
136                self.logger.debug("Other unhandled error", exc=exc)
137                raise exc
138            self.reconcile(current, reference)
139        except NeedsUpdate:
140            try:
141                self.update(current, reference)
142                self.logger.debug("Updating")
143            except (OpenApiException, HTTPError) as exc:
144                if isinstance(exc, ApiException) and exc.status == 422:  # noqa: PLR2004
145                    self.logger.debug("Failed to update current, triggering re-create")
146                    self._recreate(current=current, reference=reference)
147                    return
148                self.logger.debug("Other unhandled error", exc=exc)
149                raise exc
150        except NeedsRecreate:
151            self._recreate(current=current, reference=reference)
152        else:
153            self.logger.debug("Object is up-to-date.")
154
155    def _recreate(self, reference: T, current: T | None = None):
156        """Recreate object"""
157        self.logger.debug("Recreate requested")
158        if current:
159            self.logger.debug("Deleted old")
160            self.delete(current)
161        else:
162            self.logger.debug("No old found, creating")
163        self.logger.debug("Creating")
164        self.create(reference)
165
166    def down(self):
167        """Delete object if found"""
168        if self.noop:
169            self.logger.debug("Object is noop")
170            return
171        try:
172            current = self.retrieve()
173            self.delete(current)
174            self.logger.debug("Removing")
175        except (OpenApiException, HTTPError) as exc:
176            if isinstance(exc, ApiException) and exc.status == HttpResponseNotFound.status_code:
177                self.logger.debug("Failed to get current, assuming non-existent")
178                return
179            self.logger.debug("Other unhandled error", exc=exc)
180            raise exc
181
182    def get_reference_object(self) -> T:
183        """Return object as it should be"""
184        raise NotImplementedError
185
186    def reconcile(self, current: T, reference: T):
187        """Check what operations should be done, should be raised as
188        ReconcileTrigger"""
189        if current.metadata.labels != reference.metadata.labels:
190            raise NeedsUpdate()
191
192        patch = self.get_patch()
193        if patch is not None:
194            try:
195                current_json = self.api_client.sanitize_for_serialization(current)
196            except AttributeError:
197                current_json = asdict(current)
198            try:
199                if apply_patch(current_json, patch) != current_json:
200                    raise NeedsUpdate()
201            except (JsonPatchException, JsonPatchConflict, JsonPatchTestFailed) as exc:
202                raise ControllerException(f"JSON Patch failed: {exc}") from exc
203
204    def create(self, reference: T):
205        """API Wrapper to create object"""
206        raise NotImplementedError
207
208    def retrieve(self) -> T:
209        """API Wrapper to retrieve object"""
210        raise NotImplementedError
211
212    def delete(self, reference: T):
213        """API Wrapper to delete object"""
214        raise NotImplementedError
215
216    def update(self, current: T, reference: T):
217        """API Wrapper to update object"""
218        raise NotImplementedError
219
220    def get_object_meta(self, **kwargs) -> V1ObjectMeta:
221        """Get common object metadata"""
222        return V1ObjectMeta(
223            namespace=self.namespace,
224            labels={
225                "app.kubernetes.io/instance": slugify(self.controller.outpost.name),
226                "app.kubernetes.io/managed-by": "goauthentik.io",
227                "app.kubernetes.io/name": f"authentik-{self.controller.outpost.type.lower()}",
228                "app.kubernetes.io/version": get_version().replace("+", "-"),
229                "goauthentik.io/outpost-name": slugify(self.controller.outpost.name),
230                "goauthentik.io/outpost-type": str(self.controller.outpost.type),
231                "goauthentik.io/outpost-uuid": self.controller.outpost.uuid.hex,
232            },
233            **kwargs,
234        )
235
236    def k8s_client(self) -> ApiClient:
237        """Get Kubernetes API client"""
238        api_client = ApiClient()
239        if self.kubernetes_disable_x509_strict:
240            self.logger.warning("Disabling strict X.509 certificate verification")
241            # Relax OpenSSL TLS validation to support legacy root CA certificates
242            # (e.g. from Kubernetes <= 1.16) which may not satisfy the stricter
243            # VERIFY_X509_STRICT flags enforced by default in Python 3.13+.
244            # See https://github.com/kubernetes-client/python/issues/2394
245            ctx = ssl.create_default_context()
246            ctx.verify_flags = ctx.verify_flags & ~ssl.VERIFY_X509_STRICT
247
248            # We need to recreate the pool manager with the new SSL context
249            # We try to preserve existing pool manager arguments
250            pool_args = api_client.rest_client.pool_manager.connection_pool_kw
251
252            api_client.rest_client.pool_manager = urllib3.PoolManager(
253                num_pools=4,
254                ssl_context=ctx,
255                **pool_args,
256            )
257        return api_client
def get_version() -> str:
34def get_version() -> str:
35    """Wrapper for authentik_version() to make testing easier"""
36    return authentik_version()

Wrapper for authentik_version() to make testing easier

class KubernetesObjectReconciler(typing.Generic[T]):
 39class KubernetesObjectReconciler[T]:
 40    """Base Kubernetes Reconciler, handles the basic logic."""
 41
 42    controller: KubernetesController
 43
 44    def __init__(self, controller: KubernetesController):
 45        self.controller = controller
 46        self.namespace = controller.outpost.config.kubernetes_namespace
 47        self.kubernetes_disable_x509_strict = (
 48            controller.outpost.config.kubernetes_disable_x509_strict
 49        )
 50        self.logger = get_logger().bind(type=self.__class__.__name__)
 51        self.api_client = self.k8s_client()
 52
 53    def get_patch(self):
 54        """Get any patches that apply to this CRD"""
 55        patches = self.controller.outpost.config.kubernetes_json_patches
 56        if not patches:
 57            return None
 58        return patches.get(self.reconciler_name(), None)
 59
 60    @property
 61    def is_embedded(self) -> bool:
 62        """Return true if the current outpost is embedded"""
 63        return self.controller.outpost.managed == MANAGED_OUTPOST
 64
 65    @staticmethod
 66    def reconciler_name() -> str:
 67        """A name this reconciler is identified by in the configuration"""
 68        raise NotImplementedError
 69
 70    @property
 71    def noop(self) -> bool:
 72        """Return true if this object should not be created/updated/deleted in this cluster"""
 73        return False
 74
 75    @property
 76    def name(self) -> str:
 77        """Get the name of the object this reconciler manages"""
 78
 79        base_name = (
 80            self.controller.outpost.config.object_naming_template
 81            % {
 82                "name": slugify(self.controller.outpost.name),
 83                "uuid": self.controller.outpost.uuid.hex,
 84            }
 85        ).lower()
 86
 87        formatted = slugify(base_name)
 88        formatted = re.sub(r"[^a-z0-9-]", "-", formatted)
 89        formatted = re.sub(r"-+", "-", formatted)
 90        formatted = formatted[:63]
 91
 92        if not formatted:
 93            formatted = f"outpost-{self.controller.outpost.uuid.hex}"[:63]
 94
 95        return formatted
 96
 97    def get_patched_reference_object(self) -> T:
 98        """Get patched reference object"""
 99        reference = self.get_reference_object()
100        patch = self.get_patch()
101        try:
102            json = self.api_client.sanitize_for_serialization(reference)
103        # Custom objects will not be known to the clients openapi types
104        except AttributeError:
105            json = asdict(reference)
106        try:
107            ref = json
108            if patch is not None:
109                ref = apply_patch(json, patch)
110        except (JsonPatchException, JsonPatchConflict, JsonPatchTestFailed) as exc:
111            raise ControllerException(f"JSON Patch failed: {exc}") from exc
112        mock_response = Response()
113        mock_response.data = dumps(ref)
114
115        try:
116            result = self.api_client.deserialize(mock_response, reference.__class__.__name__)
117        # Custom objects will not be known to the clients openapi types
118        except AttributeError:
119            result = from_dict(reference.__class__, data=ref)
120
121        return result
122
123    def up(self):
124        """Create object if it doesn't exist, update if needed or recreate if needed."""
125        current = None
126        if self.noop:
127            self.logger.debug("Object is noop")
128            return
129        reference = self.get_patched_reference_object()
130        try:
131            try:
132                current = self.retrieve()
133            except (OpenApiException, HTTPError) as exc:
134                if isinstance(exc, ApiException) and exc.status == HttpResponseNotFound.status_code:
135                    self.logger.debug("Failed to get current, triggering recreate")
136                    raise NeedsRecreate from exc
137                self.logger.debug("Other unhandled error", exc=exc)
138                raise exc
139            self.reconcile(current, reference)
140        except NeedsUpdate:
141            try:
142                self.update(current, reference)
143                self.logger.debug("Updating")
144            except (OpenApiException, HTTPError) as exc:
145                if isinstance(exc, ApiException) and exc.status == 422:  # noqa: PLR2004
146                    self.logger.debug("Failed to update current, triggering re-create")
147                    self._recreate(current=current, reference=reference)
148                    return
149                self.logger.debug("Other unhandled error", exc=exc)
150                raise exc
151        except NeedsRecreate:
152            self._recreate(current=current, reference=reference)
153        else:
154            self.logger.debug("Object is up-to-date.")
155
156    def _recreate(self, reference: T, current: T | None = None):
157        """Recreate object"""
158        self.logger.debug("Recreate requested")
159        if current:
160            self.logger.debug("Deleted old")
161            self.delete(current)
162        else:
163            self.logger.debug("No old found, creating")
164        self.logger.debug("Creating")
165        self.create(reference)
166
167    def down(self):
168        """Delete object if found"""
169        if self.noop:
170            self.logger.debug("Object is noop")
171            return
172        try:
173            current = self.retrieve()
174            self.delete(current)
175            self.logger.debug("Removing")
176        except (OpenApiException, HTTPError) as exc:
177            if isinstance(exc, ApiException) and exc.status == HttpResponseNotFound.status_code:
178                self.logger.debug("Failed to get current, assuming non-existent")
179                return
180            self.logger.debug("Other unhandled error", exc=exc)
181            raise exc
182
183    def get_reference_object(self) -> T:
184        """Return object as it should be"""
185        raise NotImplementedError
186
187    def reconcile(self, current: T, reference: T):
188        """Check what operations should be done, should be raised as
189        ReconcileTrigger"""
190        if current.metadata.labels != reference.metadata.labels:
191            raise NeedsUpdate()
192
193        patch = self.get_patch()
194        if patch is not None:
195            try:
196                current_json = self.api_client.sanitize_for_serialization(current)
197            except AttributeError:
198                current_json = asdict(current)
199            try:
200                if apply_patch(current_json, patch) != current_json:
201                    raise NeedsUpdate()
202            except (JsonPatchException, JsonPatchConflict, JsonPatchTestFailed) as exc:
203                raise ControllerException(f"JSON Patch failed: {exc}") from exc
204
205    def create(self, reference: T):
206        """API Wrapper to create object"""
207        raise NotImplementedError
208
209    def retrieve(self) -> T:
210        """API Wrapper to retrieve object"""
211        raise NotImplementedError
212
213    def delete(self, reference: T):
214        """API Wrapper to delete object"""
215        raise NotImplementedError
216
217    def update(self, current: T, reference: T):
218        """API Wrapper to update object"""
219        raise NotImplementedError
220
221    def get_object_meta(self, **kwargs) -> V1ObjectMeta:
222        """Get common object metadata"""
223        return V1ObjectMeta(
224            namespace=self.namespace,
225            labels={
226                "app.kubernetes.io/instance": slugify(self.controller.outpost.name),
227                "app.kubernetes.io/managed-by": "goauthentik.io",
228                "app.kubernetes.io/name": f"authentik-{self.controller.outpost.type.lower()}",
229                "app.kubernetes.io/version": get_version().replace("+", "-"),
230                "goauthentik.io/outpost-name": slugify(self.controller.outpost.name),
231                "goauthentik.io/outpost-type": str(self.controller.outpost.type),
232                "goauthentik.io/outpost-uuid": self.controller.outpost.uuid.hex,
233            },
234            **kwargs,
235        )
236
237    def k8s_client(self) -> ApiClient:
238        """Get Kubernetes API client"""
239        api_client = ApiClient()
240        if self.kubernetes_disable_x509_strict:
241            self.logger.warning("Disabling strict X.509 certificate verification")
242            # Relax OpenSSL TLS validation to support legacy root CA certificates
243            # (e.g. from Kubernetes <= 1.16) which may not satisfy the stricter
244            # VERIFY_X509_STRICT flags enforced by default in Python 3.13+.
245            # See https://github.com/kubernetes-client/python/issues/2394
246            ctx = ssl.create_default_context()
247            ctx.verify_flags = ctx.verify_flags & ~ssl.VERIFY_X509_STRICT
248
249            # We need to recreate the pool manager with the new SSL context
250            # We try to preserve existing pool manager arguments
251            pool_args = api_client.rest_client.pool_manager.connection_pool_kw
252
253            api_client.rest_client.pool_manager = urllib3.PoolManager(
254                num_pools=4,
255                ssl_context=ctx,
256                **pool_args,
257            )
258        return api_client

Base Kubernetes Reconciler, handles the basic logic.

KubernetesObjectReconciler( controller: authentik.outposts.controllers.kubernetes.KubernetesController)
44    def __init__(self, controller: KubernetesController):
45        self.controller = controller
46        self.namespace = controller.outpost.config.kubernetes_namespace
47        self.kubernetes_disable_x509_strict = (
48            controller.outpost.config.kubernetes_disable_x509_strict
49        )
50        self.logger = get_logger().bind(type=self.__class__.__name__)
51        self.api_client = self.k8s_client()
namespace
kubernetes_disable_x509_strict
logger
api_client
def get_patch(self):
53    def get_patch(self):
54        """Get any patches that apply to this CRD"""
55        patches = self.controller.outpost.config.kubernetes_json_patches
56        if not patches:
57            return None
58        return patches.get(self.reconciler_name(), None)

Get any patches that apply to this CRD

is_embedded: bool
60    @property
61    def is_embedded(self) -> bool:
62        """Return true if the current outpost is embedded"""
63        return self.controller.outpost.managed == MANAGED_OUTPOST

Return true if the current outpost is embedded

@staticmethod
def reconciler_name() -> str:
65    @staticmethod
66    def reconciler_name() -> str:
67        """A name this reconciler is identified by in the configuration"""
68        raise NotImplementedError

A name this reconciler is identified by in the configuration

noop: bool
70    @property
71    def noop(self) -> bool:
72        """Return true if this object should not be created/updated/deleted in this cluster"""
73        return False

Return true if this object should not be created/updated/deleted in this cluster

name: str
75    @property
76    def name(self) -> str:
77        """Get the name of the object this reconciler manages"""
78
79        base_name = (
80            self.controller.outpost.config.object_naming_template
81            % {
82                "name": slugify(self.controller.outpost.name),
83                "uuid": self.controller.outpost.uuid.hex,
84            }
85        ).lower()
86
87        formatted = slugify(base_name)
88        formatted = re.sub(r"[^a-z0-9-]", "-", formatted)
89        formatted = re.sub(r"-+", "-", formatted)
90        formatted = formatted[:63]
91
92        if not formatted:
93            formatted = f"outpost-{self.controller.outpost.uuid.hex}"[:63]
94
95        return formatted

Get the name of the object this reconciler manages

def get_patched_reference_object(self) -> T:
 97    def get_patched_reference_object(self) -> T:
 98        """Get patched reference object"""
 99        reference = self.get_reference_object()
100        patch = self.get_patch()
101        try:
102            json = self.api_client.sanitize_for_serialization(reference)
103        # Custom objects will not be known to the clients openapi types
104        except AttributeError:
105            json = asdict(reference)
106        try:
107            ref = json
108            if patch is not None:
109                ref = apply_patch(json, patch)
110        except (JsonPatchException, JsonPatchConflict, JsonPatchTestFailed) as exc:
111            raise ControllerException(f"JSON Patch failed: {exc}") from exc
112        mock_response = Response()
113        mock_response.data = dumps(ref)
114
115        try:
116            result = self.api_client.deserialize(mock_response, reference.__class__.__name__)
117        # Custom objects will not be known to the clients openapi types
118        except AttributeError:
119            result = from_dict(reference.__class__, data=ref)
120
121        return result

Get patched reference object

def up(self):
123    def up(self):
124        """Create object if it doesn't exist, update if needed or recreate if needed."""
125        current = None
126        if self.noop:
127            self.logger.debug("Object is noop")
128            return
129        reference = self.get_patched_reference_object()
130        try:
131            try:
132                current = self.retrieve()
133            except (OpenApiException, HTTPError) as exc:
134                if isinstance(exc, ApiException) and exc.status == HttpResponseNotFound.status_code:
135                    self.logger.debug("Failed to get current, triggering recreate")
136                    raise NeedsRecreate from exc
137                self.logger.debug("Other unhandled error", exc=exc)
138                raise exc
139            self.reconcile(current, reference)
140        except NeedsUpdate:
141            try:
142                self.update(current, reference)
143                self.logger.debug("Updating")
144            except (OpenApiException, HTTPError) as exc:
145                if isinstance(exc, ApiException) and exc.status == 422:  # noqa: PLR2004
146                    self.logger.debug("Failed to update current, triggering re-create")
147                    self._recreate(current=current, reference=reference)
148                    return
149                self.logger.debug("Other unhandled error", exc=exc)
150                raise exc
151        except NeedsRecreate:
152            self._recreate(current=current, reference=reference)
153        else:
154            self.logger.debug("Object is up-to-date.")

Create object if it doesn't exist, update if needed or recreate if needed.

def down(self):
167    def down(self):
168        """Delete object if found"""
169        if self.noop:
170            self.logger.debug("Object is noop")
171            return
172        try:
173            current = self.retrieve()
174            self.delete(current)
175            self.logger.debug("Removing")
176        except (OpenApiException, HTTPError) as exc:
177            if isinstance(exc, ApiException) and exc.status == HttpResponseNotFound.status_code:
178                self.logger.debug("Failed to get current, assuming non-existent")
179                return
180            self.logger.debug("Other unhandled error", exc=exc)
181            raise exc

Delete object if found

def get_reference_object(self) -> T:
183    def get_reference_object(self) -> T:
184        """Return object as it should be"""
185        raise NotImplementedError

Return object as it should be

def reconcile(self, current: T, reference: T):
187    def reconcile(self, current: T, reference: T):
188        """Check what operations should be done, should be raised as
189        ReconcileTrigger"""
190        if current.metadata.labels != reference.metadata.labels:
191            raise NeedsUpdate()
192
193        patch = self.get_patch()
194        if patch is not None:
195            try:
196                current_json = self.api_client.sanitize_for_serialization(current)
197            except AttributeError:
198                current_json = asdict(current)
199            try:
200                if apply_patch(current_json, patch) != current_json:
201                    raise NeedsUpdate()
202            except (JsonPatchException, JsonPatchConflict, JsonPatchTestFailed) as exc:
203                raise ControllerException(f"JSON Patch failed: {exc}") from exc

Check what operations should be done, should be raised as ReconcileTrigger

def create(self, reference: T):
205    def create(self, reference: T):
206        """API Wrapper to create object"""
207        raise NotImplementedError

API Wrapper to create object

def retrieve(self) -> T:
209    def retrieve(self) -> T:
210        """API Wrapper to retrieve object"""
211        raise NotImplementedError

API Wrapper to retrieve object

def delete(self, reference: T):
213    def delete(self, reference: T):
214        """API Wrapper to delete object"""
215        raise NotImplementedError

API Wrapper to delete object

def update(self, current: T, reference: T):
217    def update(self, current: T, reference: T):
218        """API Wrapper to update object"""
219        raise NotImplementedError

API Wrapper to update object

def get_object_meta(self, **kwargs) -> kubernetes.client.models.v1_object_meta.V1ObjectMeta:
221    def get_object_meta(self, **kwargs) -> V1ObjectMeta:
222        """Get common object metadata"""
223        return V1ObjectMeta(
224            namespace=self.namespace,
225            labels={
226                "app.kubernetes.io/instance": slugify(self.controller.outpost.name),
227                "app.kubernetes.io/managed-by": "goauthentik.io",
228                "app.kubernetes.io/name": f"authentik-{self.controller.outpost.type.lower()}",
229                "app.kubernetes.io/version": get_version().replace("+", "-"),
230                "goauthentik.io/outpost-name": slugify(self.controller.outpost.name),
231                "goauthentik.io/outpost-type": str(self.controller.outpost.type),
232                "goauthentik.io/outpost-uuid": self.controller.outpost.uuid.hex,
233            },
234            **kwargs,
235        )

Get common object metadata

def k8s_client(self) -> kubernetes.client.api_client.ApiClient:
237    def k8s_client(self) -> ApiClient:
238        """Get Kubernetes API client"""
239        api_client = ApiClient()
240        if self.kubernetes_disable_x509_strict:
241            self.logger.warning("Disabling strict X.509 certificate verification")
242            # Relax OpenSSL TLS validation to support legacy root CA certificates
243            # (e.g. from Kubernetes <= 1.16) which may not satisfy the stricter
244            # VERIFY_X509_STRICT flags enforced by default in Python 3.13+.
245            # See https://github.com/kubernetes-client/python/issues/2394
246            ctx = ssl.create_default_context()
247            ctx.verify_flags = ctx.verify_flags & ~ssl.VERIFY_X509_STRICT
248
249            # We need to recreate the pool manager with the new SSL context
250            # We try to preserve existing pool manager arguments
251            pool_args = api_client.rest_client.pool_manager.connection_pool_kw
252
253            api_client.rest_client.pool_manager = urllib3.PoolManager(
254                num_pools=4,
255                ssl_context=ctx,
256                **pool_args,
257            )
258        return api_client

Get Kubernetes API client