authentik.providers.proxy.controllers.k8s.httproute

  1from dataclasses import asdict, dataclass, field
  2from typing import TYPE_CHECKING
  3from urllib.parse import urlparse
  4
  5from dacite.core import from_dict
  6from kubernetes.client import ApiextensionsV1Api, CustomObjectsApi, V1ObjectMeta
  7
  8from authentik.outposts.controllers.base import FIELD_MANAGER
  9from authentik.outposts.controllers.k8s.base import KubernetesObjectReconciler
 10from authentik.outposts.controllers.k8s.triggers import NeedsUpdate
 11from authentik.outposts.controllers.kubernetes import KubernetesController
 12from authentik.providers.proxy.models import ProxyMode, ProxyProvider
 13
 14if TYPE_CHECKING:
 15    from authentik.outposts.controllers.kubernetes import KubernetesController
 16
 17
 18@dataclass(slots=True)
 19class RouteBackendRef:
 20    name: str
 21    port: int
 22
 23
 24@dataclass(slots=True)
 25class RouteSpecParentRefs:
 26    name: str
 27    sectionName: str | None = None
 28    port: int | None = None
 29    namespace: str | None = None
 30    kind: str = "Gateway"
 31    group: str = "gateway.networking.k8s.io"
 32
 33
 34@dataclass(slots=True)
 35class HTTPRouteSpecRuleMatchPath:
 36    type: str
 37    value: str
 38
 39
 40@dataclass(slots=True)
 41class HTTPRouteSpecRuleMatchHeader:
 42    name: str
 43    value: str
 44    type: str = "Exact"
 45
 46
 47@dataclass(slots=True)
 48class HTTPRouteSpecRuleMatch:
 49    path: HTTPRouteSpecRuleMatchPath
 50    headers: list[HTTPRouteSpecRuleMatchHeader]
 51
 52
 53@dataclass(slots=True)
 54class HTTPRouteSpecRule:
 55    backendRefs: list[RouteBackendRef]
 56    matches: list[HTTPRouteSpecRuleMatch]
 57
 58
 59@dataclass(slots=True)
 60class HTTPRouteSpec:
 61    parentRefs: list[RouteSpecParentRefs]
 62    hostnames: list[str]
 63    rules: list[HTTPRouteSpecRule]
 64
 65
 66@dataclass(slots=True)
 67class HTTPRouteMetadata:
 68    name: str
 69    namespace: str
 70    annotations: dict = field(default_factory=dict)
 71    labels: dict = field(default_factory=dict)
 72
 73
 74@dataclass(slots=True)
 75class HTTPRoute:
 76    apiVersion: str
 77    kind: str
 78    metadata: HTTPRouteMetadata
 79    spec: HTTPRouteSpec
 80
 81
 82class HTTPRouteReconciler(KubernetesObjectReconciler):
 83    """Kubernetes Gateway API HTTPRoute Reconciler"""
 84
 85    def __init__(self, controller: KubernetesController) -> None:
 86        super().__init__(controller)
 87        self.api_ex = ApiextensionsV1Api(controller.client)
 88        self.api = CustomObjectsApi(controller.client)
 89        self.crd_group = "gateway.networking.k8s.io"
 90        self.crd_version = "v1"
 91        self.crd_plural = "httproutes"
 92
 93    @staticmethod
 94    def reconciler_name() -> str:
 95        return "httproute"
 96
 97    @property
 98    def noop(self) -> bool:
 99        if not self.crd_exists():
100            self.logger.debug("CRD doesn't exist")
101            return True
102        if not self.controller.outpost.config.kubernetes_httproute_parent_refs:
103            self.logger.debug("HTTPRoute parentRefs not set.")
104            return True
105        return False
106
107    def crd_exists(self) -> bool:
108        """Check if the Gateway API resources exists"""
109        return bool(
110            len(
111                self.api_ex.list_custom_resource_definition(
112                    field_selector=f"metadata.name={self.crd_plural}.{self.crd_group}"
113                ).items
114            )
115        )
116
117    def reconcile(self, current: HTTPRoute, reference: HTTPRoute):
118        super().reconcile(current, reference)
119        if current.metadata.annotations != reference.metadata.annotations:
120            raise NeedsUpdate()
121        if current.spec.parentRefs != reference.spec.parentRefs:
122            raise NeedsUpdate()
123        if current.spec.hostnames != reference.spec.hostnames:
124            raise NeedsUpdate()
125        if current.spec.rules != reference.spec.rules:
126            raise NeedsUpdate()
127
128    def get_object_meta(self, **kwargs) -> V1ObjectMeta:
129        return super().get_object_meta(
130            **kwargs,
131        )
132
133    def get_reference_object(self) -> HTTPRoute:
134        hostnames = []
135        rules = []
136
137        for proxy_provider in ProxyProvider.objects.filter(outpost__in=[self.controller.outpost]):
138            proxy_provider: ProxyProvider
139            external_host_name = urlparse(proxy_provider.external_host)
140            if proxy_provider.mode in [ProxyMode.FORWARD_SINGLE, ProxyMode.FORWARD_DOMAIN]:
141                rule = HTTPRouteSpecRule(
142                    backendRefs=[RouteBackendRef(name=self.name, port=9000)],
143                    matches=[
144                        HTTPRouteSpecRuleMatch(
145                            headers=[
146                                HTTPRouteSpecRuleMatchHeader(
147                                    name="Host",
148                                    value=external_host_name.hostname,
149                                )
150                            ],
151                            path=HTTPRouteSpecRuleMatchPath(
152                                type="PathPrefix", value="/outpost.goauthentik.io"
153                            ),
154                        )
155                    ],
156                )
157            else:
158                rule = HTTPRouteSpecRule(
159                    backendRefs=[RouteBackendRef(name=self.name, port=9000)],
160                    matches=[
161                        HTTPRouteSpecRuleMatch(
162                            headers=[
163                                HTTPRouteSpecRuleMatchHeader(
164                                    name="Host",
165                                    value=external_host_name.hostname,
166                                )
167                            ],
168                            path=HTTPRouteSpecRuleMatchPath(type="PathPrefix", value="/"),
169                        )
170                    ],
171                )
172            hostnames.append(external_host_name.hostname)
173            rules.append(rule)
174
175        return HTTPRoute(
176            apiVersion=f"{self.crd_group}/{self.crd_version}",
177            kind="HTTPRoute",
178            metadata=HTTPRouteMetadata(
179                name=self.name,
180                namespace=self.namespace,
181                annotations=self.controller.outpost.config.kubernetes_httproute_annotations,
182                labels=self.get_object_meta().labels,
183            ),
184            spec=HTTPRouteSpec(
185                parentRefs=[
186                    from_dict(RouteSpecParentRefs, spec)
187                    for spec in self.controller.outpost.config.kubernetes_httproute_parent_refs
188                ],
189                hostnames=hostnames,
190                rules=rules,
191            ),
192        )
193
194    def create(self, reference: HTTPRoute):
195        return self.api.create_namespaced_custom_object(
196            group=self.crd_group,
197            version=self.crd_version,
198            plural=self.crd_plural,
199            namespace=self.namespace,
200            body=asdict(reference),
201            field_manager=FIELD_MANAGER,
202        )
203
204    def delete(self, reference: HTTPRoute):
205        return self.api.delete_namespaced_custom_object(
206            group=self.crd_group,
207            version=self.crd_version,
208            plural=self.crd_plural,
209            namespace=self.namespace,
210            name=self.name,
211        )
212
213    def retrieve(self) -> HTTPRoute:
214        return from_dict(
215            HTTPRoute,
216            self.api.get_namespaced_custom_object(
217                group=self.crd_group,
218                version=self.crd_version,
219                plural=self.crd_plural,
220                namespace=self.namespace,
221                name=self.name,
222            ),
223        )
224
225    def update(self, current: HTTPRoute, reference: HTTPRoute):
226        return self.api.patch_namespaced_custom_object(
227            group=self.crd_group,
228            version=self.crd_version,
229            plural=self.crd_plural,
230            namespace=self.namespace,
231            name=self.name,
232            body=asdict(reference),
233            field_manager=FIELD_MANAGER,
234        )
@dataclass(slots=True)
class RouteBackendRef:
19@dataclass(slots=True)
20class RouteBackendRef:
21    name: str
22    port: int
RouteBackendRef(name: str, port: int)
name: str
port: int
@dataclass(slots=True)
class RouteSpecParentRefs:
25@dataclass(slots=True)
26class RouteSpecParentRefs:
27    name: str
28    sectionName: str | None = None
29    port: int | None = None
30    namespace: str | None = None
31    kind: str = "Gateway"
32    group: str = "gateway.networking.k8s.io"
RouteSpecParentRefs( name: str, sectionName: str | None = None, port: int | None = None, namespace: str | None = None, kind: str = 'Gateway', group: str = 'gateway.networking.k8s.io')
name: str
sectionName: str | None
port: int | None
namespace: str | None
kind: str
group: str
@dataclass(slots=True)
class HTTPRouteSpecRuleMatchPath:
35@dataclass(slots=True)
36class HTTPRouteSpecRuleMatchPath:
37    type: str
38    value: str
HTTPRouteSpecRuleMatchPath(type: str, value: str)
type: str
value: str
@dataclass(slots=True)
class HTTPRouteSpecRuleMatchHeader:
41@dataclass(slots=True)
42class HTTPRouteSpecRuleMatchHeader:
43    name: str
44    value: str
45    type: str = "Exact"
HTTPRouteSpecRuleMatchHeader(name: str, value: str, type: str = 'Exact')
name: str
value: str
type: str
@dataclass(slots=True)
class HTTPRouteSpecRuleMatch:
48@dataclass(slots=True)
49class HTTPRouteSpecRuleMatch:
50    path: HTTPRouteSpecRuleMatchPath
51    headers: list[HTTPRouteSpecRuleMatchHeader]
HTTPRouteSpecRuleMatch( path: HTTPRouteSpecRuleMatchPath, headers: list[HTTPRouteSpecRuleMatchHeader])
@dataclass(slots=True)
class HTTPRouteSpecRule:
54@dataclass(slots=True)
55class HTTPRouteSpecRule:
56    backendRefs: list[RouteBackendRef]
57    matches: list[HTTPRouteSpecRuleMatch]
HTTPRouteSpecRule( backendRefs: list[RouteBackendRef], matches: list[HTTPRouteSpecRuleMatch])
backendRefs: list[RouteBackendRef]
matches: list[HTTPRouteSpecRuleMatch]
@dataclass(slots=True)
class HTTPRouteSpec:
60@dataclass(slots=True)
61class HTTPRouteSpec:
62    parentRefs: list[RouteSpecParentRefs]
63    hostnames: list[str]
64    rules: list[HTTPRouteSpecRule]
HTTPRouteSpec( parentRefs: list[RouteSpecParentRefs], hostnames: list[str], rules: list[HTTPRouteSpecRule])
parentRefs: list[RouteSpecParentRefs]
hostnames: list[str]
rules: list[HTTPRouteSpecRule]
@dataclass(slots=True)
class HTTPRouteMetadata:
67@dataclass(slots=True)
68class HTTPRouteMetadata:
69    name: str
70    namespace: str
71    annotations: dict = field(default_factory=dict)
72    labels: dict = field(default_factory=dict)
HTTPRouteMetadata( name: str, namespace: str, annotations: dict = <factory>, labels: dict = <factory>)
name: str
namespace: str
annotations: dict
labels: dict
@dataclass(slots=True)
class HTTPRoute:
75@dataclass(slots=True)
76class HTTPRoute:
77    apiVersion: str
78    kind: str
79    metadata: HTTPRouteMetadata
80    spec: HTTPRouteSpec
HTTPRoute( apiVersion: str, kind: str, metadata: HTTPRouteMetadata, spec: HTTPRouteSpec)
apiVersion: str
kind: str
metadata: HTTPRouteMetadata
class HTTPRouteReconciler(typing.Generic[T]):
 83class HTTPRouteReconciler(KubernetesObjectReconciler):
 84    """Kubernetes Gateway API HTTPRoute Reconciler"""
 85
 86    def __init__(self, controller: KubernetesController) -> None:
 87        super().__init__(controller)
 88        self.api_ex = ApiextensionsV1Api(controller.client)
 89        self.api = CustomObjectsApi(controller.client)
 90        self.crd_group = "gateway.networking.k8s.io"
 91        self.crd_version = "v1"
 92        self.crd_plural = "httproutes"
 93
 94    @staticmethod
 95    def reconciler_name() -> str:
 96        return "httproute"
 97
 98    @property
 99    def noop(self) -> bool:
100        if not self.crd_exists():
101            self.logger.debug("CRD doesn't exist")
102            return True
103        if not self.controller.outpost.config.kubernetes_httproute_parent_refs:
104            self.logger.debug("HTTPRoute parentRefs not set.")
105            return True
106        return False
107
108    def crd_exists(self) -> bool:
109        """Check if the Gateway API resources exists"""
110        return bool(
111            len(
112                self.api_ex.list_custom_resource_definition(
113                    field_selector=f"metadata.name={self.crd_plural}.{self.crd_group}"
114                ).items
115            )
116        )
117
118    def reconcile(self, current: HTTPRoute, reference: HTTPRoute):
119        super().reconcile(current, reference)
120        if current.metadata.annotations != reference.metadata.annotations:
121            raise NeedsUpdate()
122        if current.spec.parentRefs != reference.spec.parentRefs:
123            raise NeedsUpdate()
124        if current.spec.hostnames != reference.spec.hostnames:
125            raise NeedsUpdate()
126        if current.spec.rules != reference.spec.rules:
127            raise NeedsUpdate()
128
129    def get_object_meta(self, **kwargs) -> V1ObjectMeta:
130        return super().get_object_meta(
131            **kwargs,
132        )
133
134    def get_reference_object(self) -> HTTPRoute:
135        hostnames = []
136        rules = []
137
138        for proxy_provider in ProxyProvider.objects.filter(outpost__in=[self.controller.outpost]):
139            proxy_provider: ProxyProvider
140            external_host_name = urlparse(proxy_provider.external_host)
141            if proxy_provider.mode in [ProxyMode.FORWARD_SINGLE, ProxyMode.FORWARD_DOMAIN]:
142                rule = HTTPRouteSpecRule(
143                    backendRefs=[RouteBackendRef(name=self.name, port=9000)],
144                    matches=[
145                        HTTPRouteSpecRuleMatch(
146                            headers=[
147                                HTTPRouteSpecRuleMatchHeader(
148                                    name="Host",
149                                    value=external_host_name.hostname,
150                                )
151                            ],
152                            path=HTTPRouteSpecRuleMatchPath(
153                                type="PathPrefix", value="/outpost.goauthentik.io"
154                            ),
155                        )
156                    ],
157                )
158            else:
159                rule = HTTPRouteSpecRule(
160                    backendRefs=[RouteBackendRef(name=self.name, port=9000)],
161                    matches=[
162                        HTTPRouteSpecRuleMatch(
163                            headers=[
164                                HTTPRouteSpecRuleMatchHeader(
165                                    name="Host",
166                                    value=external_host_name.hostname,
167                                )
168                            ],
169                            path=HTTPRouteSpecRuleMatchPath(type="PathPrefix", value="/"),
170                        )
171                    ],
172                )
173            hostnames.append(external_host_name.hostname)
174            rules.append(rule)
175
176        return HTTPRoute(
177            apiVersion=f"{self.crd_group}/{self.crd_version}",
178            kind="HTTPRoute",
179            metadata=HTTPRouteMetadata(
180                name=self.name,
181                namespace=self.namespace,
182                annotations=self.controller.outpost.config.kubernetes_httproute_annotations,
183                labels=self.get_object_meta().labels,
184            ),
185            spec=HTTPRouteSpec(
186                parentRefs=[
187                    from_dict(RouteSpecParentRefs, spec)
188                    for spec in self.controller.outpost.config.kubernetes_httproute_parent_refs
189                ],
190                hostnames=hostnames,
191                rules=rules,
192            ),
193        )
194
195    def create(self, reference: HTTPRoute):
196        return self.api.create_namespaced_custom_object(
197            group=self.crd_group,
198            version=self.crd_version,
199            plural=self.crd_plural,
200            namespace=self.namespace,
201            body=asdict(reference),
202            field_manager=FIELD_MANAGER,
203        )
204
205    def delete(self, reference: HTTPRoute):
206        return self.api.delete_namespaced_custom_object(
207            group=self.crd_group,
208            version=self.crd_version,
209            plural=self.crd_plural,
210            namespace=self.namespace,
211            name=self.name,
212        )
213
214    def retrieve(self) -> HTTPRoute:
215        return from_dict(
216            HTTPRoute,
217            self.api.get_namespaced_custom_object(
218                group=self.crd_group,
219                version=self.crd_version,
220                plural=self.crd_plural,
221                namespace=self.namespace,
222                name=self.name,
223            ),
224        )
225
226    def update(self, current: HTTPRoute, reference: HTTPRoute):
227        return self.api.patch_namespaced_custom_object(
228            group=self.crd_group,
229            version=self.crd_version,
230            plural=self.crd_plural,
231            namespace=self.namespace,
232            name=self.name,
233            body=asdict(reference),
234            field_manager=FIELD_MANAGER,
235        )

Kubernetes Gateway API HTTPRoute Reconciler

HTTPRouteReconciler( controller: authentik.outposts.controllers.kubernetes.KubernetesController)
86    def __init__(self, controller: KubernetesController) -> None:
87        super().__init__(controller)
88        self.api_ex = ApiextensionsV1Api(controller.client)
89        self.api = CustomObjectsApi(controller.client)
90        self.crd_group = "gateway.networking.k8s.io"
91        self.crd_version = "v1"
92        self.crd_plural = "httproutes"
api_ex
api
crd_group
crd_version
crd_plural
@staticmethod
def reconciler_name() -> str:
94    @staticmethod
95    def reconciler_name() -> str:
96        return "httproute"

A name this reconciler is identified by in the configuration

noop: bool
 98    @property
 99    def noop(self) -> bool:
100        if not self.crd_exists():
101            self.logger.debug("CRD doesn't exist")
102            return True
103        if not self.controller.outpost.config.kubernetes_httproute_parent_refs:
104            self.logger.debug("HTTPRoute parentRefs not set.")
105            return True
106        return False

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

def crd_exists(self) -> bool:
108    def crd_exists(self) -> bool:
109        """Check if the Gateway API resources exists"""
110        return bool(
111            len(
112                self.api_ex.list_custom_resource_definition(
113                    field_selector=f"metadata.name={self.crd_plural}.{self.crd_group}"
114                ).items
115            )
116        )

Check if the Gateway API resources exists

def reconcile( self, current: HTTPRoute, reference: HTTPRoute):
118    def reconcile(self, current: HTTPRoute, reference: HTTPRoute):
119        super().reconcile(current, reference)
120        if current.metadata.annotations != reference.metadata.annotations:
121            raise NeedsUpdate()
122        if current.spec.parentRefs != reference.spec.parentRefs:
123            raise NeedsUpdate()
124        if current.spec.hostnames != reference.spec.hostnames:
125            raise NeedsUpdate()
126        if current.spec.rules != reference.spec.rules:
127            raise NeedsUpdate()

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

def get_object_meta(self, **kwargs) -> kubernetes.client.models.v1_object_meta.V1ObjectMeta:
129    def get_object_meta(self, **kwargs) -> V1ObjectMeta:
130        return super().get_object_meta(
131            **kwargs,
132        )

Get common object metadata

def get_reference_object(self) -> HTTPRoute:
134    def get_reference_object(self) -> HTTPRoute:
135        hostnames = []
136        rules = []
137
138        for proxy_provider in ProxyProvider.objects.filter(outpost__in=[self.controller.outpost]):
139            proxy_provider: ProxyProvider
140            external_host_name = urlparse(proxy_provider.external_host)
141            if proxy_provider.mode in [ProxyMode.FORWARD_SINGLE, ProxyMode.FORWARD_DOMAIN]:
142                rule = HTTPRouteSpecRule(
143                    backendRefs=[RouteBackendRef(name=self.name, port=9000)],
144                    matches=[
145                        HTTPRouteSpecRuleMatch(
146                            headers=[
147                                HTTPRouteSpecRuleMatchHeader(
148                                    name="Host",
149                                    value=external_host_name.hostname,
150                                )
151                            ],
152                            path=HTTPRouteSpecRuleMatchPath(
153                                type="PathPrefix", value="/outpost.goauthentik.io"
154                            ),
155                        )
156                    ],
157                )
158            else:
159                rule = HTTPRouteSpecRule(
160                    backendRefs=[RouteBackendRef(name=self.name, port=9000)],
161                    matches=[
162                        HTTPRouteSpecRuleMatch(
163                            headers=[
164                                HTTPRouteSpecRuleMatchHeader(
165                                    name="Host",
166                                    value=external_host_name.hostname,
167                                )
168                            ],
169                            path=HTTPRouteSpecRuleMatchPath(type="PathPrefix", value="/"),
170                        )
171                    ],
172                )
173            hostnames.append(external_host_name.hostname)
174            rules.append(rule)
175
176        return HTTPRoute(
177            apiVersion=f"{self.crd_group}/{self.crd_version}",
178            kind="HTTPRoute",
179            metadata=HTTPRouteMetadata(
180                name=self.name,
181                namespace=self.namespace,
182                annotations=self.controller.outpost.config.kubernetes_httproute_annotations,
183                labels=self.get_object_meta().labels,
184            ),
185            spec=HTTPRouteSpec(
186                parentRefs=[
187                    from_dict(RouteSpecParentRefs, spec)
188                    for spec in self.controller.outpost.config.kubernetes_httproute_parent_refs
189                ],
190                hostnames=hostnames,
191                rules=rules,
192            ),
193        )

Return object as it should be

def create( self, reference: HTTPRoute):
195    def create(self, reference: HTTPRoute):
196        return self.api.create_namespaced_custom_object(
197            group=self.crd_group,
198            version=self.crd_version,
199            plural=self.crd_plural,
200            namespace=self.namespace,
201            body=asdict(reference),
202            field_manager=FIELD_MANAGER,
203        )

API Wrapper to create object

def delete( self, reference: HTTPRoute):
205    def delete(self, reference: HTTPRoute):
206        return self.api.delete_namespaced_custom_object(
207            group=self.crd_group,
208            version=self.crd_version,
209            plural=self.crd_plural,
210            namespace=self.namespace,
211            name=self.name,
212        )

API Wrapper to delete object

def retrieve(self) -> HTTPRoute:
214    def retrieve(self) -> HTTPRoute:
215        return from_dict(
216            HTTPRoute,
217            self.api.get_namespaced_custom_object(
218                group=self.crd_group,
219                version=self.crd_version,
220                plural=self.crd_plural,
221                namespace=self.namespace,
222                name=self.name,
223            ),
224        )

API Wrapper to retrieve object

def update( self, current: HTTPRoute, reference: HTTPRoute):
226    def update(self, current: HTTPRoute, reference: HTTPRoute):
227        return self.api.patch_namespaced_custom_object(
228            group=self.crd_group,
229            version=self.crd_version,
230            plural=self.crd_plural,
231            namespace=self.namespace,
232            name=self.name,
233            body=asdict(reference),
234            field_manager=FIELD_MANAGER,
235        )

API Wrapper to update object