outposts: Create separate metrics service in Kubernetes (#21229)

* outposts: create separate metrics service

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix service monitor plumbing

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* update docs

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* format

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add some static tests

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* make metrics service ClusterIP

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* update service monitor when labels mismatch

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
Jens L.
2026-03-29 22:51:10 +01:00
committed by GitHub
parent 416dd0cf86
commit 1848c6c380
8 changed files with 90 additions and 7 deletions
+3
View File
@@ -58,6 +58,9 @@ class BaseController:
self.connection = connection self.connection = connection
self.logger = get_logger() self.logger = get_logger()
self.deployment_ports = [] self.deployment_ports = []
self.metrics_ports = [
DeploymentPort(9300, "http-metrics", "tcp"),
]
def up(self): def up(self):
"""Called by scheduled task to reconcile deployment/service/etc""" """Called by scheduled task to reconcile deployment/service/etc"""
+45 -1
View File
@@ -2,7 +2,7 @@
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from kubernetes.client import CoreV1Api, V1Service, V1ServicePort, V1ServiceSpec from kubernetes.client import CoreV1Api, V1ObjectMeta, V1Service, V1ServicePort, V1ServiceSpec
from authentik.outposts.controllers.base import FIELD_MANAGER from authentik.outposts.controllers.base import FIELD_MANAGER
from authentik.outposts.controllers.k8s.base import KubernetesObjectReconciler from authentik.outposts.controllers.k8s.base import KubernetesObjectReconciler
@@ -84,3 +84,47 @@ class ServiceReconciler(KubernetesObjectReconciler[V1Service]):
reference, reference,
field_manager=FIELD_MANAGER, field_manager=FIELD_MANAGER,
) )
class MetricsServiceReconciler(ServiceReconciler):
@property
def noop(self) -> bool:
return self.is_embedded
@staticmethod
def reconciler_name() -> str:
return "service-metrics"
@property
def name(self):
name_suffix = "-metrics"
name = super().name
return name[: 63 - len(name_suffix)] + name_suffix
def get_object_meta(self, **kwargs) -> V1ObjectMeta:
meta: V1ObjectMeta = super().get_object_meta(**kwargs)
meta.labels["goauthentik.io/service-type"] = "metrics"
return meta
def get_reference_object(self) -> V1Service:
"""Get deployment object for outpost"""
meta = self.get_object_meta(name=self.name)
ports = []
for port in self.controller.metrics_ports:
ports.append(
V1ServicePort(
name=port.name,
port=port.port,
protocol=port.protocol.upper(),
target_port=port.inner_port or port.port,
)
)
selector_labels = DeploymentReconciler(self.controller).get_pod_meta()
return V1Service(
metadata=meta,
spec=V1ServiceSpec(
ports=ports,
selector=selector_labels,
type="ClusterIP",
),
)
@@ -8,6 +8,8 @@ from kubernetes.client import ApiextensionsV1Api, CustomObjectsApi
from authentik.outposts.controllers.base import FIELD_MANAGER from authentik.outposts.controllers.base import FIELD_MANAGER
from authentik.outposts.controllers.k8s.base import KubernetesObjectReconciler from authentik.outposts.controllers.k8s.base import KubernetesObjectReconciler
from authentik.outposts.controllers.k8s.service import MetricsServiceReconciler
from authentik.outposts.controllers.k8s.triggers import NeedsUpdate
if TYPE_CHECKING: if TYPE_CHECKING:
from authentik.outposts.controllers.kubernetes import KubernetesController from authentik.outposts.controllers.kubernetes import KubernetesController
@@ -55,6 +57,10 @@ class PrometheusServiceMonitor:
metadata: PrometheusServiceMonitorMetadata metadata: PrometheusServiceMonitorMetadata
spec: PrometheusServiceMonitorSpec spec: PrometheusServiceMonitorSpec
def to_dict(self):
"""`to_dict` to conform to how the kubernetes client converts objects to dicts"""
return asdict(self)
CRD_NAME = "servicemonitors.monitoring.coreos.com" CRD_NAME = "servicemonitors.monitoring.coreos.com"
CRD_GROUP = "monitoring.coreos.com" CRD_GROUP = "monitoring.coreos.com"
@@ -74,6 +80,11 @@ class PrometheusServiceMonitorReconciler(KubernetesObjectReconciler[PrometheusSe
def reconciler_name() -> str: def reconciler_name() -> str:
return "prometheus servicemonitor" return "prometheus servicemonitor"
def reconcile(self, current: PrometheusServiceMonitor, reference: PrometheusServiceMonitor):
if current.spec.selector.matchLabels != reference.spec.selector.matchLabels:
raise NeedsUpdate()
super().reconcile(current, reference)
@property @property
def noop(self) -> bool: def noop(self) -> bool:
if not self._crd_exists(): if not self._crd_exists():
@@ -108,7 +119,9 @@ class PrometheusServiceMonitorReconciler(KubernetesObjectReconciler[PrometheusSe
) )
], ],
selector=PrometheusServiceMonitorSpecSelector( selector=PrometheusServiceMonitorSpecSelector(
matchLabels=self.get_object_meta(name=self.name).labels, matchLabels=MetricsServiceReconciler(self.controller)
.get_object_meta(name=self.name)
.labels,
), ),
), ),
) )
+3 -1
View File
@@ -18,7 +18,7 @@ from authentik.outposts.controllers.base import BaseClient, BaseController, Cont
from authentik.outposts.controllers.k8s.base import KubernetesObjectReconciler from authentik.outposts.controllers.k8s.base import KubernetesObjectReconciler
from authentik.outposts.controllers.k8s.deployment import DeploymentReconciler from authentik.outposts.controllers.k8s.deployment import DeploymentReconciler
from authentik.outposts.controllers.k8s.secret import SecretReconciler from authentik.outposts.controllers.k8s.secret import SecretReconciler
from authentik.outposts.controllers.k8s.service import ServiceReconciler from authentik.outposts.controllers.k8s.service import MetricsServiceReconciler, ServiceReconciler
from authentik.outposts.controllers.k8s.service_monitor import PrometheusServiceMonitorReconciler from authentik.outposts.controllers.k8s.service_monitor import PrometheusServiceMonitorReconciler
from authentik.outposts.models import ( from authentik.outposts.models import (
KubernetesServiceConnection, KubernetesServiceConnection,
@@ -74,6 +74,7 @@ class KubernetesController(BaseController):
SecretReconciler.reconciler_name(): SecretReconciler, SecretReconciler.reconciler_name(): SecretReconciler,
DeploymentReconciler.reconciler_name(): DeploymentReconciler, DeploymentReconciler.reconciler_name(): DeploymentReconciler,
ServiceReconciler.reconciler_name(): ServiceReconciler, ServiceReconciler.reconciler_name(): ServiceReconciler,
MetricsServiceReconciler.reconciler_name(): MetricsServiceReconciler,
PrometheusServiceMonitorReconciler.reconciler_name(): ( PrometheusServiceMonitorReconciler.reconciler_name(): (
PrometheusServiceMonitorReconciler PrometheusServiceMonitorReconciler
), ),
@@ -82,6 +83,7 @@ class KubernetesController(BaseController):
SecretReconciler.reconciler_name(), SecretReconciler.reconciler_name(),
DeploymentReconciler.reconciler_name(), DeploymentReconciler.reconciler_name(),
ServiceReconciler.reconciler_name(), ServiceReconciler.reconciler_name(),
MetricsServiceReconciler.reconciler_name(),
PrometheusServiceMonitorReconciler.reconciler_name(), PrometheusServiceMonitorReconciler.reconciler_name(),
] ]
@@ -1,11 +1,16 @@
"""Kubernetes controller tests""" """Kubernetes controller tests"""
from unittest.mock import MagicMock, patch
from django.test import TestCase from django.test import TestCase
from kubernetes.client import ApiClient
from yaml import SafeLoader, load_all
from authentik.blueprints.tests import reconcile_app from authentik.blueprints.tests import reconcile_app
from authentik.lib.generators import generate_id from authentik.lib.generators import generate_id
from authentik.outposts.apps import MANAGED_OUTPOST from authentik.outposts.apps import MANAGED_OUTPOST
from authentik.outposts.controllers.k8s.deployment import DeploymentReconciler from authentik.outposts.controllers.k8s.deployment import DeploymentReconciler
from authentik.outposts.controllers.k8s.service_monitor import PrometheusServiceMonitorReconciler
from authentik.outposts.controllers.kubernetes import KubernetesController from authentik.outposts.controllers.kubernetes import KubernetesController
from authentik.outposts.models import KubernetesServiceConnection, Outpost, OutpostType from authentik.outposts.models import KubernetesServiceConnection, Outpost, OutpostType
@@ -28,7 +33,7 @@ class KubernetesControllerTests(TestCase):
self.integration, self.integration,
# Pass something not-none as client so we don't # Pass something not-none as client so we don't
# attempt to connect to K8s as that's not needed # attempt to connect to K8s as that's not needed
client=self, client=ApiClient(),
) )
rec = DeploymentReconciler(controller) rec = DeploymentReconciler(controller)
self.assertEqual(rec.name, "ak-outpost-authentik-embedded-outpost") self.assertEqual(rec.name, "ak-outpost-authentik-embedded-outpost")
@@ -42,3 +47,18 @@ class KubernetesControllerTests(TestCase):
controller.outpost.config = _cfg controller.outpost.config = _cfg
self.assertEqual(rec.name, f"outpost-{controller.outpost.uuid.hex}") self.assertEqual(rec.name, f"outpost-{controller.outpost.uuid.hex}")
self.assertLess(len(rec.name), 64) self.assertLess(len(rec.name), 64)
def test_static(self):
self.controller = KubernetesController(
self.outpost,
self.integration,
# Pass something not-none as client so we don't
# attempt to connect to K8s as that's not needed
client=ApiClient(),
)
with patch.object(
PrometheusServiceMonitorReconciler, "_crd_exists", MagicMock(return_value=True)
):
manifest = self.controller.get_static_deployment()
manifests = list(load_all(manifest, Loader=SafeLoader))
self.assertEqual(len(manifests), 5)
@@ -15,7 +15,6 @@ class ProxyKubernetesController(KubernetesController):
super().__init__(outpost, connection) super().__init__(outpost, connection)
self.deployment_ports = [ self.deployment_ports = [
DeploymentPort(9000, "http", "tcp"), DeploymentPort(9000, "http", "tcp"),
DeploymentPort(9300, "http-metrics", "tcp"),
DeploymentPort(9443, "https", "tcp"), DeploymentPort(9443, "https", "tcp"),
] ]
self.reconcilers[IngressReconciler.reconciler_name()] = IngressReconciler self.reconcilers[IngressReconciler.reconciler_name()] = IngressReconciler
+1 -1
View File
@@ -46,7 +46,7 @@ class TestProxyKubernetes(TestCase):
self.controller = ProxyKubernetesController(outpost, service_connection) self.controller = ProxyKubernetesController(outpost, service_connection)
manifest = self.controller.get_static_deployment() manifest = self.controller.get_static_deployment()
self.assertEqual(len(list(yaml.load_all(manifest, Loader=yaml.SafeLoader))), 4) self.assertEqual(len(list(yaml.load_all(manifest, Loader=yaml.SafeLoader))), 5)
@pytest.mark.timeout(120, func_only=True) @pytest.mark.timeout(120, func_only=True)
def test_kubernetes_controller_ingress(self): def test_kubernetes_controller_ingress(self):
@@ -9,7 +9,8 @@ This integration has the advantage over manual deployments of automatic updates
This integration creates the following objects: This integration creates the following objects:
- Deployment for the outpost container - Deployment for the outpost container
- Service - Service for protocol access
- Service for metrics access
- Secret to store the token - Secret to store the token
- Prometheus ServiceMonitor (if the Prometheus Operator is installed in the target cluster) - Prometheus ServiceMonitor (if the Prometheus Operator is installed in the target cluster)
- Ingress (only Proxy outposts) - Ingress (only Proxy outposts)
@@ -32,6 +33,7 @@ The following outpost settings are used:
- 'secret' - 'secret'
- 'deployment' - 'deployment'
- 'service' - 'service'
- 'service-metrics'
- 'prometheus servicemonitor' - 'prometheus servicemonitor'
- 'ingress' - 'ingress'
- 'traefik middleware' - 'traefik middleware'