Repository: goauthentik/authentik

Issue:
Support for new Traefik API group 'traefik.io'
**Is your feature request related to a problem? Please describe.**
Traefik 2.10 has introduced their new API group traefik.io, in addition to deprecating traefik.containo.us.
They have stated that traefik.containo.us will be removed in traefik 3.
Please see https://traefik.io/blog/announcing-traefik-proxy-2-10/

It's been stated that this migration prior to v3 is to potentially support a webhook that may be provided by traefik later on to support updating from traefik.io/v1alpha1 to traefik.io/v1

**Describe the solution you'd like**
Consider supporting both API groups, either by a configuration option in the outpost config, or by detecting the presence of the newer group and preferring that.

**Describe alternatives you've considered**
Could always keep using the legacy one until it gets completely removed in traefik v3.

**Additional context**
Add any other context or screenshots about the feature request here.



Discussion:

Resolving patch:
diff --git a/authentik/providers/proxy/controllers/k8s/traefik.py b/authentik/providers/proxy/controllers/k8s/traefik.py
--- a/authentik/providers/proxy/controllers/k8s/traefik.py
+++ b/authentik/providers/proxy/controllers/k8s/traefik.py
@@ -1,185 +1,40 @@
 """Kubernetes Traefik Middleware Reconciler"""
-from dataclasses import asdict, dataclass, field
-from typing import TYPE_CHECKING
-
-from dacite.core import from_dict
-from kubernetes.client import ApiextensionsV1Api, CustomObjectsApi
-
-from authentik.outposts.controllers.base import FIELD_MANAGER
 from authentik.outposts.controllers.k8s.base import KubernetesObjectReconciler
-from authentik.outposts.controllers.k8s.triggers import NeedsUpdate
-from authentik.providers.proxy.models import ProxyMode, ProxyProvider
-
-if TYPE_CHECKING:
-    from authentik.outposts.controllers.kubernetes import KubernetesController
-
-
-@dataclass
-class TraefikMiddlewareSpecForwardAuth:
-    """traefik middleware forwardAuth spec"""
-
-    address: str
-    # pylint: disable=invalid-name
-    authResponseHeadersRegex: str = field(default="")
-    # pylint: disable=invalid-name
-    authResponseHeaders: list[str] = field(default_factory=list)
-    # pylint: disable=invalid-name
-    trustForwardHeader: bool = field(default=True)
-
-
-@dataclass
-class TraefikMiddlewareSpec:
-    """Traefik middleware spec"""
-
-    # pylint: disable=invalid-name
-    forwardAuth: TraefikMiddlewareSpecForwardAuth
+from authentik.outposts.controllers.kubernetes import KubernetesController
+from authentik.providers.proxy.controllers.k8s.traefik_2 import Traefik2MiddlewareReconciler
+from authentik.providers.proxy.controllers.k8s.traefik_3 import (
+    Traefik3MiddlewareReconciler,
+    TraefikMiddleware,
+)
 
 
-@dataclass
-class TraefikMiddlewareMetadata:
-    """Traefik Middleware metadata"""
-
-    name: str
-    namespace: str
-    labels: dict = field(default_factory=dict)
-
-
-@dataclass
-class TraefikMiddleware:
-    """Traefik Middleware"""
-
-    # pylint: disable=invalid-name
-    apiVersion: str
-    kind: str
-    metadata: TraefikMiddlewareMetadata
-    spec: TraefikMiddlewareSpec
-
-
-CRD_NAME = "middlewares.traefik.containo.us"
-CRD_GROUP = "traefik.containo.us"
-CRD_VERSION = "v1alpha1"
-CRD_PLURAL = "middlewares"
-
-
-class TraefikMiddlewareReconciler(KubernetesObjectReconciler[TraefikMiddleware]):
+class TraefikMiddlewareReconciler(KubernetesObjectReconciler):
     """Kubernetes Traefik Middleware Reconciler"""
 
     def __init__(self, controller: "KubernetesController") -> None:
         super().__init__(controller)
-        self.api_ex = ApiextensionsV1Api(controller.client)
-        self.api = CustomObjectsApi(controller.client)
+        self.reconciler = Traefik3MiddlewareReconciler(controller)
+        if not self.reconciler.crd_exists():
+            self.reconciler = Traefik2MiddlewareReconciler(controller)
 
     @property
     def noop(self) -> bool:
-        if not ProxyProvider.objects.filter(
-            outpost__in=[self.controller.outpost],
-            mode__in=[ProxyMode.FORWARD_SINGLE, ProxyMode.FORWARD_DOMAIN],
-        ).exists():
-            self.logger.debug("No providers with forward auth enabled.")
-            return True
-        if not self._crd_exists():
-            self.logger.debug("CRD doesn't exist")
-            return True
-        return False
-
-    def _crd_exists(self) -> bool:
-        """Check if the traefik middleware exists"""
-        return bool(
-            len(
-                self.api_ex.list_custom_resource_definition(
-                    field_selector=f"metadata.name={CRD_NAME}"
-                ).items
-            )
-        )
+        return self.reconciler.noop
 
     def reconcile(self, current: TraefikMiddleware, reference: TraefikMiddleware):
-        super().reconcile(current, reference)
-        if current.spec.forwardAuth.address != reference.spec.forwardAuth.address:
-            raise NeedsUpdate()
-        if (
-            current.spec.forwardAuth.authResponseHeadersRegex
-            != reference.spec.forwardAuth.authResponseHeadersRegex
-        ):
-            raise NeedsUpdate()
-        # Ensure all of our headers are set, others can be added by the user.
-        if not set(current.spec.forwardAuth.authResponseHeaders).issubset(
-            reference.spec.forwardAuth.authResponseHeaders
-        ):
-            raise NeedsUpdate()
+        return self.reconcile(current, reference)
 
     def get_reference_object(self) -> TraefikMiddleware:
-        """Get deployment object for outpost"""
-        return TraefikMiddleware(
-            apiVersion=f"{CRD_GROUP}/{CRD_VERSION}",
-            kind="Middleware",
-            metadata=TraefikMiddlewareMetadata(
-                name=self.name,
-                namespace=self.namespace,
-                labels=self.get_object_meta().labels,
-            ),
-            spec=TraefikMiddlewareSpec(
-                forwardAuth=TraefikMiddlewareSpecForwardAuth(
-                    address=(
-                        f"http://{self.name}.{self.namespace}:9000/"
-                        "outpost.goauthentik.io/auth/traefik"
-                    ),
-                    authResponseHeaders=[
-                        "X-authentik-username",
-                        "X-authentik-groups",
-                        "X-authentik-email",
-                        "X-authentik-name",
-                        "X-authentik-uid",
-                        "X-authentik-jwt",
-                        "X-authentik-meta-jwks",
-                        "X-authentik-meta-outpost",
-                        "X-authentik-meta-provider",
-                        "X-authentik-meta-app",
-                        "X-authentik-meta-version",
-                    ],
-                    authResponseHeadersRegex="",
-                    trustForwardHeader=True,
-                )
-            ),
-        )
+        return self.get_reference_object()
 
     def create(self, reference: TraefikMiddleware):
-        return self.api.create_namespaced_custom_object(
-            group=CRD_GROUP,
-            version=CRD_VERSION,
-            plural=CRD_PLURAL,
-            namespace=self.namespace,
-            body=asdict(reference),
-            field_manager=FIELD_MANAGER,
-        )
+        return self.create(reference)
 
     def delete(self, reference: TraefikMiddleware):
-        return self.api.delete_namespaced_custom_object(
-            group=CRD_GROUP,
-            version=CRD_VERSION,
-            namespace=self.namespace,
-            plural=CRD_PLURAL,
-            name=self.name,
-        )
+        return self.delete(reference)
 
     def retrieve(self) -> TraefikMiddleware:
-        return from_dict(
-            TraefikMiddleware,
-            self.api.get_namespaced_custom_object(
-                group=CRD_GROUP,
-                version=CRD_VERSION,
-                namespace=self.namespace,
-                plural=CRD_PLURAL,
-                name=self.name,
-            ),
-        )
+        return self.retrieve()
 
     def update(self, current: TraefikMiddleware, reference: TraefikMiddleware):
-        return self.api.patch_namespaced_custom_object(
-            group=CRD_GROUP,
-            version=CRD_VERSION,
-            namespace=self.namespace,
-            plural=CRD_PLURAL,
-            name=self.name,
-            body=asdict(reference),
-            field_manager=FIELD_MANAGER,
-        )
+        return self.update(current, reference)
diff --git a/authentik/providers/proxy/controllers/k8s/traefik_2.py b/authentik/providers/proxy/controllers/k8s/traefik_2.py
new file mode 100644
--- /dev/null
+++ b/authentik/providers/proxy/controllers/k8s/traefik_2.py
@@ -0,0 +1,18 @@
+"""Kubernetes Traefik Middleware Reconciler"""
+from typing import TYPE_CHECKING
+
+from authentik.providers.proxy.controllers.k8s.traefik_3 import Traefik3MiddlewareReconciler
+
+if TYPE_CHECKING:
+    from authentik.outposts.controllers.kubernetes import KubernetesController
+
+
+class Traefik2MiddlewareReconciler(Traefik3MiddlewareReconciler):
+    """Kubernetes Traefik Middleware Reconciler"""
+
+    def __init__(self, controller: "KubernetesController") -> None:
+        super().__init__(controller)
+        self.crd_name = "middlewares.traefik.containo.us"
+        self.crd_group = "traefik.containo.us"
+        self.crd_version = "v1alpha1"
+        self.crd_plural = "middlewares"
diff --git a/authentik/providers/proxy/controllers/k8s/traefik_3.py b/authentik/providers/proxy/controllers/k8s/traefik_3.py
new file mode 100644
--- /dev/null
+++ b/authentik/providers/proxy/controllers/k8s/traefik_3.py
@@ -0,0 +1,183 @@
+"""Kubernetes Traefik Middleware Reconciler"""
+from dataclasses import asdict, dataclass, field
+from typing import TYPE_CHECKING
+
+from dacite.core import from_dict
+from kubernetes.client import ApiextensionsV1Api, CustomObjectsApi
+
+from authentik.outposts.controllers.base import FIELD_MANAGER
+from authentik.outposts.controllers.k8s.base import KubernetesObjectReconciler
+from authentik.outposts.controllers.k8s.triggers import NeedsUpdate
+from authentik.providers.proxy.models import ProxyMode, ProxyProvider
+
+if TYPE_CHECKING:
+    from authentik.outposts.controllers.kubernetes import KubernetesController
+
+
+@dataclass
+class TraefikMiddlewareSpecForwardAuth:
+    """traefik middleware forwardAuth spec"""
+
+    address: str
+    # pylint: disable=invalid-name
+    authResponseHeadersRegex: str = field(default="")
+    # pylint: disable=invalid-name
+    authResponseHeaders: list[str] = field(default_factory=list)
+    # pylint: disable=invalid-name
+    trustForwardHeader: bool = field(default=True)
+
+
+@dataclass
+class TraefikMiddlewareSpec:
+    """Traefik middleware spec"""
+
+    # pylint: disable=invalid-name
+    forwardAuth: TraefikMiddlewareSpecForwardAuth
+
+
+@dataclass
+class TraefikMiddlewareMetadata:
+    """Traefik Middleware metadata"""
+
+    name: str
+    namespace: str
+    labels: dict = field(default_factory=dict)
+
+
+@dataclass
+class TraefikMiddleware:
+    """Traefik Middleware"""
+
+    # pylint: disable=invalid-name
+    apiVersion: str
+    kind: str
+    metadata: TraefikMiddlewareMetadata
+    spec: TraefikMiddlewareSpec
+
+
+class Traefik3MiddlewareReconciler(KubernetesObjectReconciler[TraefikMiddleware]):
+    """Kubernetes Traefik Middleware Reconciler"""
+
+    def __init__(self, controller: "KubernetesController") -> None:
+        super().__init__(controller)
+        self.api_ex = ApiextensionsV1Api(controller.client)
+        self.api = CustomObjectsApi(controller.client)
+        self.crd_name = "middlewares.traefik.io"
+        self.crd_group = "traefik.io"
+        self.crd_version = "v1alpha1"
+        self.crd_plural = "middlewares"
+
+    @property
+    def noop(self) -> bool:
+        if not ProxyProvider.objects.filter(
+            outpost__in=[self.controller.outpost],
+            mode__in=[ProxyMode.FORWARD_SINGLE, ProxyMode.FORWARD_DOMAIN],
+        ).exists():
+            self.logger.debug("No providers with forward auth enabled.")
+            return True
+        if not self.crd_exists():
+            self.logger.debug("CRD doesn't exist")
+            return True
+        return False
+
+    def crd_exists(self) -> bool:
+        """Check if the traefik middleware exists"""
+        return bool(
+            len(
+                self.api_ex.list_custom_resource_definition(
+                    field_selector=f"metadata.name={self.crd_name}"
+                ).items
+            )
+        )
+
+    def reconcile(self, current: TraefikMiddleware, reference: TraefikMiddleware):
+        super().reconcile(current, reference)
+        if current.spec.forwardAuth.address != reference.spec.forwardAuth.address:
+            raise NeedsUpdate()
+        if (
+            current.spec.forwardAuth.authResponseHeadersRegex
+            != reference.spec.forwardAuth.authResponseHeadersRegex
+        ):
+            raise NeedsUpdate()
+        # Ensure all of our headers are set, others can be added by the user.
+        if not set(current.spec.forwardAuth.authResponseHeaders).issubset(
+            reference.spec.forwardAuth.authResponseHeaders
+        ):
+            raise NeedsUpdate()
+
+    def get_reference_object(self) -> TraefikMiddleware:
+        """Get deployment object for outpost"""
+        return TraefikMiddleware(
+            apiVersion=f"{self.crd_group}/{self.crd_version}",
+            kind="Middleware",
+            metadata=TraefikMiddlewareMetadata(
+                name=self.name,
+                namespace=self.namespace,
+                labels=self.get_object_meta().labels,
+            ),
+            spec=TraefikMiddlewareSpec(
+                forwardAuth=TraefikMiddlewareSpecForwardAuth(
+                    address=(
+                        f"http://{self.name}.{self.namespace}:9000/"
+                        "outpost.goauthentik.io/auth/traefik"
+                    ),
+                    authResponseHeaders=[
+                        "X-authentik-username",
+                        "X-authentik-groups",
+                        "X-authentik-email",
+                        "X-authentik-name",
+                        "X-authentik-uid",
+                        "X-authentik-jwt",
+                        "X-authentik-meta-jwks",
+                        "X-authentik-meta-outpost",
+                        "X-authentik-meta-provider",
+                        "X-authentik-meta-app",
+                        "X-authentik-meta-version",
+                    ],
+                    authResponseHeadersRegex="",
+                    trustForwardHeader=True,
+                )
+            ),
+        )
+
+    def create(self, reference: TraefikMiddleware):
+        return self.api.create_namespaced_custom_object(
+            group=self.crd_group,
+            version=self.crd_version,
+            plural=self.crd_plural,
+            namespace=self.namespace,
+            body=asdict(reference),
+            field_manager=FIELD_MANAGER,
+        )
+
+    def delete(self, reference: TraefikMiddleware):
+        return self.api.delete_namespaced_custom_object(
+            group=self.crd_group,
+            version=self.crd_version,
+            plural=self.crd_plural,
+            namespace=self.namespace,
+            name=self.name,
+        )
+
+    def retrieve(self) -> TraefikMiddleware:
+        return from_dict(
+            TraefikMiddleware,
+            self.api.get_namespaced_custom_object(
+                group=self.crd_group,
+                version=self.crd_version,
+                plural=self.crd_plural,
+                namespace=self.namespace,
+                name=self.name,
+            ),
+        )
+
+    def update(self, current: TraefikMiddleware, reference: TraefikMiddleware):
+        return self.api.patch_namespaced_custom_object(
+            group=self.crd_group,
+            version=self.crd_version,
+            plural=self.crd_plural,
+            namespace=self.namespace,
+            name=self.name,
+            body=asdict(reference),
+            field_manager=FIELD_MANAGER,
+        )


Test patch: