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:
@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"
@dataclass(slots=True)
class
HTTPRouteSpecRuleMatchPath:
@dataclass(slots=True)
class
HTTPRouteSpecRuleMatchHeader:
@dataclass(slots=True)
class
HTTPRouteSpecRuleMatch:
48@dataclass(slots=True) 49class HTTPRouteSpecRuleMatch: 50 path: HTTPRouteSpecRuleMatchPath 51 headers: list[HTTPRouteSpecRuleMatchHeader]
HTTPRouteSpecRuleMatch( path: HTTPRouteSpecRuleMatchPath, headers: list[HTTPRouteSpecRuleMatchHeader])
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]
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)
@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)
metadata: HTTPRouteMetadata
spec: HTTPRouteSpec
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"
@staticmethod
def
reconciler_name() -> str:
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
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
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
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
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
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
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