wip: rename to authentik (#361)
* root: initial rename * web: rename custom element prefix * root: rename external functions with pb_ prefix * root: fix formatting * root: replace domain with goauthentik.io * proxy: update path * root: rename remaining prefixes * flows: rename file extension * root: pbadmin -> akadmin * docs: fix image filenames * lifecycle: ignore migration files * ci: copy default config from current source before loading last tagged * *: new sentry dsn * tests: fix missing python3.9-dev package * root: add additional migrations for service accounts created by outposts * core: mark system-created service accounts with attribute * policies/expression: fix pb_ replacement not working * web: fix last linting errors, add lit-analyse * policies/expressions: fix lint errors * web: fix sidebar display on screens where not all items fit * proxy: attempt to fix proxy pipeline * proxy: use go env GOPATH to get gopath * lib: fix user_default naming inconsistency * docs: add upgrade docs * docs: update screenshots to use authentik * admin: fix create button on empty-state of outpost * web: fix modal submit not refreshing SiteShell and Table * web: fix height of app-card and height of generic icon * web: fix rendering of subtext * admin: fix version check error not being caught * web: fix worker count not being shown * docs: update screenshots * root: new icon * web: fix lint error * admin: fix linting error * root: migrate coverage config to pyproject
This commit is contained in:
0
authentik/outposts/controllers/__init__.py
Normal file
0
authentik/outposts/controllers/__init__.py
Normal file
46
authentik/outposts/controllers/base.py
Normal file
46
authentik/outposts/controllers/base.py
Normal file
@ -0,0 +1,46 @@
|
||||
"""Base Controller"""
|
||||
from typing import Dict, List
|
||||
|
||||
from structlog import get_logger
|
||||
from structlog.testing import capture_logs
|
||||
|
||||
from authentik.lib.sentry import SentryIgnoredException
|
||||
from authentik.outposts.models import Outpost, OutpostServiceConnection
|
||||
|
||||
|
||||
class ControllerException(SentryIgnoredException):
|
||||
"""Exception raised when anything fails during controller run"""
|
||||
|
||||
|
||||
class BaseController:
|
||||
"""Base Outpost deployment controller"""
|
||||
|
||||
deployment_ports: Dict[str, int]
|
||||
|
||||
outpost: Outpost
|
||||
connection: OutpostServiceConnection
|
||||
|
||||
def __init__(self, outpost: Outpost, connection: OutpostServiceConnection):
|
||||
self.outpost = outpost
|
||||
self.connection = connection
|
||||
self.logger = get_logger()
|
||||
self.deployment_ports = {}
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
def up(self):
|
||||
"""Called by scheduled task to reconcile deployment/service/etc"""
|
||||
raise NotImplementedError
|
||||
|
||||
def up_with_logs(self) -> List[str]:
|
||||
"""Call .up() but capture all log output and return it."""
|
||||
with capture_logs() as logs:
|
||||
self.up()
|
||||
return [x["event"] for x in logs]
|
||||
|
||||
def down(self):
|
||||
"""Handler to delete everything we've created"""
|
||||
raise NotImplementedError
|
||||
|
||||
def get_static_deployment(self) -> str:
|
||||
"""Return a static deployment configuration"""
|
||||
raise NotImplementedError
|
||||
160
authentik/outposts/controllers/docker.py
Normal file
160
authentik/outposts/controllers/docker.py
Normal file
@ -0,0 +1,160 @@
|
||||
"""Docker controller"""
|
||||
from time import sleep
|
||||
from typing import Dict, Tuple
|
||||
|
||||
from django.conf import settings
|
||||
from docker import DockerClient
|
||||
from docker.errors import DockerException, NotFound
|
||||
from docker.models.containers import Container
|
||||
from yaml import safe_dump
|
||||
|
||||
from authentik import __version__
|
||||
from authentik.lib.config import CONFIG
|
||||
from authentik.outposts.controllers.base import BaseController, ControllerException
|
||||
from authentik.outposts.models import (
|
||||
DockerServiceConnection,
|
||||
Outpost,
|
||||
ServiceConnectionInvalid,
|
||||
)
|
||||
|
||||
|
||||
class DockerController(BaseController):
|
||||
"""Docker controller"""
|
||||
|
||||
client: DockerClient
|
||||
|
||||
container: Container
|
||||
connection: DockerServiceConnection
|
||||
|
||||
def __init__(self, outpost: Outpost, connection: DockerServiceConnection) -> None:
|
||||
super().__init__(outpost, connection)
|
||||
try:
|
||||
self.client = connection.client()
|
||||
except ServiceConnectionInvalid as exc:
|
||||
raise ControllerException from exc
|
||||
|
||||
def _get_labels(self) -> Dict[str, str]:
|
||||
return {}
|
||||
|
||||
def _get_env(self) -> Dict[str, str]:
|
||||
return {
|
||||
"AUTHENTIK_HOST": self.outpost.config.authentik_host,
|
||||
"AUTHENTIK_INSECURE": str(self.outpost.config.authentik_host_insecure),
|
||||
"AUTHENTIK_TOKEN": self.outpost.token.key,
|
||||
}
|
||||
|
||||
def _comp_env(self, container: Container) -> bool:
|
||||
"""Check if container's env is equal to what we would set. Return true if container needs
|
||||
to be rebuilt."""
|
||||
should_be = self._get_env()
|
||||
container_env = container.attrs.get("Config", {}).get("Env", {})
|
||||
for key, expected_value in should_be.items():
|
||||
if key not in container_env:
|
||||
continue
|
||||
if container_env[key] != expected_value:
|
||||
return True
|
||||
return False
|
||||
|
||||
def _get_container(self) -> Tuple[Container, bool]:
|
||||
container_name = f"authentik-proxy-{self.outpost.uuid.hex}"
|
||||
try:
|
||||
return self.client.containers.get(container_name), False
|
||||
except NotFound:
|
||||
self.logger.info("Container does not exist, creating")
|
||||
image_prefix = CONFIG.y("outposts.docker_image_base")
|
||||
image_name = f"{image_prefix}-{self.outpost.type}:{__version__}"
|
||||
self.client.images.pull(image_name)
|
||||
container_args = {
|
||||
"image": image_name,
|
||||
"name": f"authentik-proxy-{self.outpost.uuid.hex}",
|
||||
"detach": True,
|
||||
"ports": {x: x for _, x in self.deployment_ports.items()},
|
||||
"environment": self._get_env(),
|
||||
"labels": self._get_labels(),
|
||||
}
|
||||
if settings.TEST:
|
||||
del container_args["ports"]
|
||||
container_args["network_mode"] = "host"
|
||||
return (
|
||||
self.client.containers.create(**container_args),
|
||||
True,
|
||||
)
|
||||
|
||||
def up(self):
|
||||
try:
|
||||
container, has_been_created = self._get_container()
|
||||
# Check if the container is out of date, delete it and retry
|
||||
if len(container.image.tags) > 0:
|
||||
tag: str = container.image.tags[0]
|
||||
_, _, version = tag.partition(":")
|
||||
if version != __version__:
|
||||
self.logger.info(
|
||||
"Container has mismatched version, re-creating...",
|
||||
has=version,
|
||||
should=__version__,
|
||||
)
|
||||
container.kill()
|
||||
container.remove(force=True)
|
||||
return self.up()
|
||||
# Check that container values match our values
|
||||
if self._comp_env(container):
|
||||
self.logger.info("Container has outdated config, re-creating...")
|
||||
container.kill()
|
||||
container.remove(force=True)
|
||||
return self.up()
|
||||
# Check that container is healthy
|
||||
if (
|
||||
container.status == "running"
|
||||
and container.attrs.get("State", {}).get("Health", {}).get("Status", "")
|
||||
!= "healthy"
|
||||
):
|
||||
# At this point we know the config is correct, but the container isn't healthy,
|
||||
# so we just restart it with the same config
|
||||
if has_been_created:
|
||||
# Since we've just created the container, give it some time to start.
|
||||
# If its still not up by then, restart it
|
||||
self.logger.info(
|
||||
"Container is unhealthy and new, giving it time to boot."
|
||||
)
|
||||
sleep(60)
|
||||
self.logger.info("Container is unhealthy, restarting...")
|
||||
container.restart()
|
||||
return None
|
||||
# Check that container is running
|
||||
if container.status != "running":
|
||||
self.logger.info("Container is not running, restarting...")
|
||||
container.start()
|
||||
return None
|
||||
return None
|
||||
except DockerException as exc:
|
||||
raise ControllerException from exc
|
||||
|
||||
def down(self):
|
||||
try:
|
||||
container, _ = self._get_container()
|
||||
container.kill()
|
||||
container.remove()
|
||||
except DockerException as exc:
|
||||
raise ControllerException from exc
|
||||
|
||||
def get_static_deployment(self) -> str:
|
||||
"""Generate docker-compose yaml for proxy, version 3.5"""
|
||||
ports = [f"{x}:{x}" for _, x in self.deployment_ports.items()]
|
||||
image_prefix = CONFIG.y("outposts.docker_image_base")
|
||||
compose = {
|
||||
"version": "3.5",
|
||||
"services": {
|
||||
f"authentik_{self.outpost.type}": {
|
||||
"image": f"{image_prefix}-{self.outpost.type}:{__version__}",
|
||||
"ports": ports,
|
||||
"environment": {
|
||||
"AUTHENTIK_HOST": self.outpost.config.authentik_host,
|
||||
"AUTHENTIK_INSECURE": str(
|
||||
self.outpost.config.authentik_host_insecure
|
||||
),
|
||||
"AUTHENTIK_TOKEN": self.outpost.token.key,
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
return safe_dump(compose, default_flow_style=False)
|
||||
0
authentik/outposts/controllers/k8s/__init__.py
Normal file
0
authentik/outposts/controllers/k8s/__init__.py
Normal file
126
authentik/outposts/controllers/k8s/base.py
Normal file
126
authentik/outposts/controllers/k8s/base.py
Normal file
@ -0,0 +1,126 @@
|
||||
"""Base Kubernetes Reconciler"""
|
||||
from typing import TYPE_CHECKING, Generic, TypeVar
|
||||
|
||||
from kubernetes.client import V1ObjectMeta
|
||||
from kubernetes.client.rest import ApiException
|
||||
from structlog import get_logger
|
||||
|
||||
from authentik import __version__
|
||||
from authentik.lib.sentry import SentryIgnoredException
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from authentik.outposts.controllers.kubernetes import KubernetesController
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
class ReconcileTrigger(SentryIgnoredException):
|
||||
"""Base trigger raised by child classes to notify us"""
|
||||
|
||||
|
||||
class NeedsRecreate(ReconcileTrigger):
|
||||
"""Exception to trigger a complete recreate of the Kubernetes Object"""
|
||||
|
||||
|
||||
class NeedsUpdate(ReconcileTrigger):
|
||||
"""Exception to trigger an update to the Kubernetes Object"""
|
||||
|
||||
|
||||
class KubernetesObjectReconciler(Generic[T]):
|
||||
"""Base Kubernetes Reconciler, handles the basic logic."""
|
||||
|
||||
controller: "KubernetesController"
|
||||
|
||||
def __init__(self, controller: "KubernetesController"):
|
||||
self.controller = controller
|
||||
self.namespace = controller.outpost.config.kubernetes_namespace
|
||||
self.logger = get_logger()
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Get the name of the object this reconciler manages"""
|
||||
raise NotImplementedError
|
||||
|
||||
def up(self):
|
||||
"""Create object if it doesn't exist, update if needed or recreate if needed."""
|
||||
current = None
|
||||
reference = self.get_reference_object()
|
||||
try:
|
||||
try:
|
||||
current = self.retrieve()
|
||||
except ApiException as exc:
|
||||
if exc.status == 404:
|
||||
self.logger.debug("Failed to get current, triggering recreate")
|
||||
raise NeedsRecreate from exc
|
||||
self.logger.debug("Other unhandled error", exc=exc)
|
||||
raise exc
|
||||
else:
|
||||
self.logger.debug("Got current, running reconcile")
|
||||
self.reconcile(current, reference)
|
||||
except NeedsRecreate:
|
||||
self.logger.debug("Recreate requested")
|
||||
if current:
|
||||
self.logger.debug("Deleted old")
|
||||
self.delete(current)
|
||||
else:
|
||||
self.logger.debug("No old found, creating")
|
||||
self.logger.debug("Created")
|
||||
self.create(reference)
|
||||
except NeedsUpdate:
|
||||
self.logger.debug("Updating")
|
||||
self.update(current, reference)
|
||||
else:
|
||||
self.logger.debug("Nothing to do...")
|
||||
|
||||
def down(self):
|
||||
"""Delete object if found"""
|
||||
try:
|
||||
current = self.retrieve()
|
||||
self.delete(current)
|
||||
self.logger.debug("Removing")
|
||||
except ApiException as exc:
|
||||
if exc.status == 404:
|
||||
self.logger.debug("Failed to get current, assuming non-existant")
|
||||
return
|
||||
self.logger.debug("Other unhandled error", exc=exc)
|
||||
raise exc
|
||||
|
||||
def get_reference_object(self) -> T:
|
||||
"""Return object as it should be"""
|
||||
raise NotImplementedError
|
||||
|
||||
def reconcile(self, current: T, reference: T):
|
||||
"""Check what operations should be done, should be raised as
|
||||
ReconcileTrigger"""
|
||||
raise NotImplementedError
|
||||
|
||||
def create(self, reference: T):
|
||||
"""API Wrapper to create object"""
|
||||
raise NotImplementedError
|
||||
|
||||
def retrieve(self) -> T:
|
||||
"""API Wrapper to retrive object"""
|
||||
raise NotImplementedError
|
||||
|
||||
def delete(self, reference: T):
|
||||
"""API Wrapper to delete object"""
|
||||
raise NotImplementedError
|
||||
|
||||
def update(self, current: T, reference: T):
|
||||
"""API Wrapper to update object"""
|
||||
raise NotImplementedError
|
||||
|
||||
def get_object_meta(self, **kwargs) -> V1ObjectMeta:
|
||||
"""Get common object metadata"""
|
||||
return V1ObjectMeta(
|
||||
namespace=self.namespace,
|
||||
labels={
|
||||
"app.kubernetes.io/name": f"authentik-{self.controller.outpost.type.lower()}",
|
||||
"app.kubernetes.io/instance": self.controller.outpost.name,
|
||||
"app.kubernetes.io/version": __version__,
|
||||
"app.kubernetes.io/managed-by": "goauthentik.io",
|
||||
"goauthentik.io/outpost-uuid": self.controller.outpost.uuid.hex,
|
||||
},
|
||||
**kwargs,
|
||||
)
|
||||
134
authentik/outposts/controllers/k8s/deployment.py
Normal file
134
authentik/outposts/controllers/k8s/deployment.py
Normal file
@ -0,0 +1,134 @@
|
||||
"""Kubernetes Deployment Reconciler"""
|
||||
from typing import TYPE_CHECKING, Dict
|
||||
|
||||
from kubernetes.client import (
|
||||
AppsV1Api,
|
||||
V1Container,
|
||||
V1ContainerPort,
|
||||
V1Deployment,
|
||||
V1DeploymentSpec,
|
||||
V1EnvVar,
|
||||
V1EnvVarSource,
|
||||
V1LabelSelector,
|
||||
V1ObjectMeta,
|
||||
V1PodSpec,
|
||||
V1PodTemplateSpec,
|
||||
V1SecretKeySelector,
|
||||
)
|
||||
|
||||
from authentik import __version__
|
||||
from authentik.lib.config import CONFIG
|
||||
from authentik.outposts.controllers.k8s.base import (
|
||||
KubernetesObjectReconciler,
|
||||
NeedsUpdate,
|
||||
)
|
||||
from authentik.outposts.models import Outpost
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from authentik.outposts.controllers.kubernetes import KubernetesController
|
||||
|
||||
|
||||
class DeploymentReconciler(KubernetesObjectReconciler[V1Deployment]):
|
||||
"""Kubernetes Deployment Reconciler"""
|
||||
|
||||
outpost: Outpost
|
||||
|
||||
def __init__(self, controller: "KubernetesController") -> None:
|
||||
super().__init__(controller)
|
||||
self.api = AppsV1Api(controller.client)
|
||||
self.outpost = self.controller.outpost
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return f"authentik-outpost-{self.controller.outpost.uuid.hex}"
|
||||
|
||||
def reconcile(self, current: V1Deployment, reference: V1Deployment):
|
||||
if current.spec.replicas != reference.spec.replicas:
|
||||
raise NeedsUpdate()
|
||||
if (
|
||||
current.spec.template.spec.containers[0].image
|
||||
!= reference.spec.template.spec.containers[0].image
|
||||
):
|
||||
raise NeedsUpdate()
|
||||
|
||||
def get_pod_meta(self) -> Dict[str, str]:
|
||||
"""Get common object metadata"""
|
||||
return {
|
||||
"app.kubernetes.io/name": "authentik-outpost",
|
||||
"app.kubernetes.io/managed-by": "goauthentik.io",
|
||||
"goauthentik.io/outpost-uuid": self.controller.outpost.uuid.hex,
|
||||
}
|
||||
|
||||
def get_reference_object(self) -> V1Deployment:
|
||||
"""Get deployment object for outpost"""
|
||||
# Generate V1ContainerPort objects
|
||||
container_ports = []
|
||||
for port_name, port in self.controller.deployment_ports.items():
|
||||
container_ports.append(V1ContainerPort(container_port=port, name=port_name))
|
||||
meta = self.get_object_meta(name=self.name)
|
||||
secret_name = f"authentik-outpost-{self.controller.outpost.uuid.hex}-api"
|
||||
image_prefix = CONFIG.y("outposts.docker_image_base")
|
||||
return V1Deployment(
|
||||
metadata=meta,
|
||||
spec=V1DeploymentSpec(
|
||||
replicas=self.outpost.config.kubernetes_replicas,
|
||||
selector=V1LabelSelector(match_labels=self.get_pod_meta()),
|
||||
template=V1PodTemplateSpec(
|
||||
metadata=V1ObjectMeta(labels=self.get_pod_meta()),
|
||||
spec=V1PodSpec(
|
||||
containers=[
|
||||
V1Container(
|
||||
name=str(self.outpost.type),
|
||||
image=f"{image_prefix}-{self.outpost.type}:{__version__}",
|
||||
ports=container_ports,
|
||||
env=[
|
||||
V1EnvVar(
|
||||
name="AUTHENTIK_HOST",
|
||||
value_from=V1EnvVarSource(
|
||||
secret_key_ref=V1SecretKeySelector(
|
||||
name=secret_name,
|
||||
key="authentik_host",
|
||||
)
|
||||
),
|
||||
),
|
||||
V1EnvVar(
|
||||
name="AUTHENTIK_TOKEN",
|
||||
value_from=V1EnvVarSource(
|
||||
secret_key_ref=V1SecretKeySelector(
|
||||
name=secret_name,
|
||||
key="token",
|
||||
)
|
||||
),
|
||||
),
|
||||
V1EnvVar(
|
||||
name="AUTHENTIK_INSECURE",
|
||||
value_from=V1EnvVarSource(
|
||||
secret_key_ref=V1SecretKeySelector(
|
||||
name=secret_name,
|
||||
key="authentik_host_insecure",
|
||||
)
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
]
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
def create(self, reference: V1Deployment):
|
||||
return self.api.create_namespaced_deployment(self.namespace, reference)
|
||||
|
||||
def delete(self, reference: V1Deployment):
|
||||
return self.api.delete_namespaced_deployment(
|
||||
reference.metadata.name, self.namespace
|
||||
)
|
||||
|
||||
def retrieve(self) -> V1Deployment:
|
||||
return self.api.read_namespaced_deployment(self.name, self.namespace)
|
||||
|
||||
def update(self, current: V1Deployment, reference: V1Deployment):
|
||||
return self.api.patch_namespaced_deployment(
|
||||
current.metadata.name, self.namespace, reference
|
||||
)
|
||||
67
authentik/outposts/controllers/k8s/secret.py
Normal file
67
authentik/outposts/controllers/k8s/secret.py
Normal file
@ -0,0 +1,67 @@
|
||||
"""Kubernetes Secret Reconciler"""
|
||||
from base64 import b64encode
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from kubernetes.client import CoreV1Api, V1Secret
|
||||
|
||||
from authentik.outposts.controllers.k8s.base import (
|
||||
KubernetesObjectReconciler,
|
||||
NeedsUpdate,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from authentik.outposts.controllers.kubernetes import KubernetesController
|
||||
|
||||
|
||||
def b64string(source: str) -> str:
|
||||
"""Base64 Encode string"""
|
||||
return b64encode(source.encode()).decode("utf-8")
|
||||
|
||||
|
||||
class SecretReconciler(KubernetesObjectReconciler[V1Secret]):
|
||||
"""Kubernetes Secret Reconciler"""
|
||||
|
||||
def __init__(self, controller: "KubernetesController") -> None:
|
||||
super().__init__(controller)
|
||||
self.api = CoreV1Api(controller.client)
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return f"authentik-outpost-{self.controller.outpost.uuid.hex}-api"
|
||||
|
||||
def reconcile(self, current: V1Secret, reference: V1Secret):
|
||||
for key in reference.data.keys():
|
||||
if current.data[key] != reference.data[key]:
|
||||
raise NeedsUpdate()
|
||||
|
||||
def get_reference_object(self) -> V1Secret:
|
||||
"""Get deployment object for outpost"""
|
||||
meta = self.get_object_meta(name=self.name)
|
||||
return V1Secret(
|
||||
metadata=meta,
|
||||
data={
|
||||
"authentik_host": b64string(
|
||||
self.controller.outpost.config.authentik_host
|
||||
),
|
||||
"authentik_host_insecure": b64string(
|
||||
str(self.controller.outpost.config.authentik_host_insecure)
|
||||
),
|
||||
"token": b64string(self.controller.outpost.token.token_uuid.hex),
|
||||
},
|
||||
)
|
||||
|
||||
def create(self, reference: V1Secret):
|
||||
return self.api.create_namespaced_secret(self.namespace, reference)
|
||||
|
||||
def delete(self, reference: V1Secret):
|
||||
return self.api.delete_namespaced_secret(
|
||||
reference.metadata.name, self.namespace
|
||||
)
|
||||
|
||||
def retrieve(self) -> V1Secret:
|
||||
return self.api.read_namespaced_secret(self.name, self.namespace)
|
||||
|
||||
def update(self, current: V1Secret, reference: V1Secret):
|
||||
return self.api.patch_namespaced_secret(
|
||||
current.metadata.name, self.namespace, reference
|
||||
)
|
||||
60
authentik/outposts/controllers/k8s/service.py
Normal file
60
authentik/outposts/controllers/k8s/service.py
Normal file
@ -0,0 +1,60 @@
|
||||
"""Kubernetes Service Reconciler"""
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from kubernetes.client import CoreV1Api, V1Service, V1ServicePort, V1ServiceSpec
|
||||
|
||||
from authentik.outposts.controllers.k8s.base import (
|
||||
KubernetesObjectReconciler,
|
||||
NeedsUpdate,
|
||||
)
|
||||
from authentik.outposts.controllers.k8s.deployment import DeploymentReconciler
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from authentik.outposts.controllers.kubernetes import KubernetesController
|
||||
|
||||
|
||||
class ServiceReconciler(KubernetesObjectReconciler[V1Service]):
|
||||
"""Kubernetes Service Reconciler"""
|
||||
|
||||
def __init__(self, controller: "KubernetesController") -> None:
|
||||
super().__init__(controller)
|
||||
self.api = CoreV1Api(controller.client)
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return f"authentik-outpost-{self.controller.outpost.uuid.hex}"
|
||||
|
||||
def reconcile(self, current: V1Service, reference: V1Service):
|
||||
if len(current.spec.ports) != len(reference.spec.ports):
|
||||
raise NeedsUpdate()
|
||||
for port in reference.spec.ports:
|
||||
if port not in current.spec.ports:
|
||||
raise NeedsUpdate()
|
||||
|
||||
def get_reference_object(self) -> V1Service:
|
||||
"""Get deployment object for outpost"""
|
||||
meta = self.get_object_meta(name=self.name)
|
||||
ports = []
|
||||
for port_name, port in self.controller.deployment_ports.items():
|
||||
ports.append(V1ServicePort(name=port_name, port=port))
|
||||
selector_labels = DeploymentReconciler(self.controller).get_pod_meta()
|
||||
return V1Service(
|
||||
metadata=meta,
|
||||
spec=V1ServiceSpec(ports=ports, selector=selector_labels, type="ClusterIP"),
|
||||
)
|
||||
|
||||
def create(self, reference: V1Service):
|
||||
return self.api.create_namespaced_service(self.namespace, reference)
|
||||
|
||||
def delete(self, reference: V1Service):
|
||||
return self.api.delete_namespaced_service(
|
||||
reference.metadata.name, self.namespace
|
||||
)
|
||||
|
||||
def retrieve(self) -> V1Service:
|
||||
return self.api.read_namespaced_service(self.name, self.namespace)
|
||||
|
||||
def update(self, current: V1Service, reference: V1Service):
|
||||
return self.api.patch_namespaced_service(
|
||||
current.metadata.name, self.namespace, reference
|
||||
)
|
||||
81
authentik/outposts/controllers/kubernetes.py
Normal file
81
authentik/outposts/controllers/kubernetes.py
Normal file
@ -0,0 +1,81 @@
|
||||
"""Kubernetes deployment controller"""
|
||||
from io import StringIO
|
||||
from typing import Dict, List, Type
|
||||
|
||||
from kubernetes.client import OpenApiException
|
||||
from kubernetes.client.api_client import ApiClient
|
||||
from structlog.testing import capture_logs
|
||||
from yaml import dump_all
|
||||
|
||||
from authentik.outposts.controllers.base import BaseController, ControllerException
|
||||
from authentik.outposts.controllers.k8s.base import KubernetesObjectReconciler
|
||||
from authentik.outposts.controllers.k8s.deployment import DeploymentReconciler
|
||||
from authentik.outposts.controllers.k8s.secret import SecretReconciler
|
||||
from authentik.outposts.controllers.k8s.service import ServiceReconciler
|
||||
from authentik.outposts.models import KubernetesServiceConnection, Outpost
|
||||
|
||||
|
||||
class KubernetesController(BaseController):
|
||||
"""Manage deployment of outpost in kubernetes"""
|
||||
|
||||
reconcilers: Dict[str, Type[KubernetesObjectReconciler]]
|
||||
reconcile_order: List[str]
|
||||
|
||||
client: ApiClient
|
||||
connection: KubernetesServiceConnection
|
||||
|
||||
def __init__(
|
||||
self, outpost: Outpost, connection: KubernetesServiceConnection
|
||||
) -> None:
|
||||
super().__init__(outpost, connection)
|
||||
self.client = connection.client()
|
||||
self.reconcilers = {
|
||||
"secret": SecretReconciler,
|
||||
"deployment": DeploymentReconciler,
|
||||
"service": ServiceReconciler,
|
||||
}
|
||||
self.reconcile_order = ["secret", "deployment", "service"]
|
||||
|
||||
def up(self):
|
||||
try:
|
||||
for reconcile_key in self.reconcile_order:
|
||||
reconciler = self.reconcilers[reconcile_key](self)
|
||||
reconciler.up()
|
||||
|
||||
except OpenApiException as exc:
|
||||
raise ControllerException from exc
|
||||
|
||||
def up_with_logs(self) -> List[str]:
|
||||
try:
|
||||
all_logs = []
|
||||
for reconcile_key in self.reconcile_order:
|
||||
with capture_logs() as logs:
|
||||
reconciler = self.reconcilers[reconcile_key](self)
|
||||
reconciler.up()
|
||||
all_logs += [f"{reconcile_key.title()}: {x['event']}" for x in logs]
|
||||
return all_logs
|
||||
except OpenApiException as exc:
|
||||
raise ControllerException from exc
|
||||
|
||||
def down(self):
|
||||
try:
|
||||
for reconcile_key in self.reconcile_order:
|
||||
reconciler = self.reconcilers[reconcile_key](self)
|
||||
reconciler.down()
|
||||
|
||||
except OpenApiException as exc:
|
||||
raise ControllerException from exc
|
||||
|
||||
def get_static_deployment(self) -> str:
|
||||
documents = []
|
||||
for reconcile_key in self.reconcile_order:
|
||||
reconciler = self.reconcilers[reconcile_key](self)
|
||||
documents.append(reconciler.get_reference_object().to_dict())
|
||||
|
||||
with StringIO() as _str:
|
||||
dump_all(
|
||||
documents,
|
||||
stream=_str,
|
||||
default_flow_style=False,
|
||||
)
|
||||
return _str.getvalue()
|
||||
Reference in New Issue
Block a user