outposts: simplify k8s controller add more extensibility

This commit is contained in:
Jens Langhammer 2020-10-18 17:07:11 +02:00
parent c698ba37d9
commit ad29d54bbf
6 changed files with 91 additions and 78 deletions

View File

@ -1,5 +1,5 @@
"""Base Kubernetes Reconciler""" """Base Kubernetes Reconciler"""
from typing import Generic, TypeVar from typing import TYPE_CHECKING, Generic, TypeVar
from kubernetes.client import V1ObjectMeta from kubernetes.client import V1ObjectMeta
from kubernetes.client.rest import ApiException from kubernetes.client.rest import ApiException
@ -7,7 +7,9 @@ from structlog import get_logger
from passbook import __version__ from passbook import __version__
from passbook.lib.sentry import SentryIgnoredException from passbook.lib.sentry import SentryIgnoredException
from passbook.outposts.models import Outpost
if TYPE_CHECKING:
from passbook.outposts.controllers.kubernetes import KubernetesController
# pylint: disable=invalid-name # pylint: disable=invalid-name
T = TypeVar("T") T = TypeVar("T")
@ -28,10 +30,19 @@ class NeedsUpdate(ReconcileTrigger):
class KubernetesObjectReconciler(Generic[T]): class KubernetesObjectReconciler(Generic[T]):
"""Base Kubernetes Reconciler, handles the basic logic.""" """Base Kubernetes Reconciler, handles the basic logic."""
def __init__(self, outpost: Outpost): controller: "KubernetesController"
self.outpost = outpost
self.namespace = "" def __init__(self, controller: "KubernetesController"):
self.logger = get_logger(controller=self.__class__.__name__, outpost=outpost) self.controller = controller
self.namespace = controller.outpost.config.kubernetes_namespace
self.logger = get_logger(
controller=self.__class__.__name__, outpost=controller.outpost
)
@property
def name(self) -> str:
"""Get the name of the object this reconciler manages"""
raise NotImplementedError
def up(self): def up(self):
"""Create object if it doesn't exist, update if needed or recreate if needed.""" """Create object if it doesn't exist, update if needed or recreate if needed."""
@ -107,11 +118,11 @@ class KubernetesObjectReconciler(Generic[T]):
return V1ObjectMeta( return V1ObjectMeta(
namespace=self.namespace, namespace=self.namespace,
labels={ labels={
"app.kubernetes.io/name": f"passbook-{self.outpost.type.lower()}", "app.kubernetes.io/name": f"passbook-{self.controller.outpost.type.lower()}",
"app.kubernetes.io/instance": self.outpost.name, "app.kubernetes.io/instance": self.controller.outpost.name,
"app.kubernetes.io/version": __version__, "app.kubernetes.io/version": __version__,
"app.kubernetes.io/managed-by": "passbook.beryju.org", "app.kubernetes.io/managed-by": "passbook.beryju.org",
"passbook.beryju.org/outpost-uuid": self.outpost.uuid.hex, "passbook.beryju.org/outpost-uuid": self.controller.outpost.uuid.hex,
}, },
**kwargs, **kwargs,
) )

View File

@ -1,5 +1,5 @@
"""Kubernetes Deployment Reconciler""" """Kubernetes Deployment Reconciler"""
from typing import Dict from typing import TYPE_CHECKING
from kubernetes.client import ( from kubernetes.client import (
AppsV1Api, AppsV1Api,
@ -23,18 +23,25 @@ from passbook.outposts.controllers.k8s.base import (
) )
from passbook.outposts.models import Outpost from passbook.outposts.models import Outpost
if TYPE_CHECKING:
from passbook.outposts.controllers.kubernetes import KubernetesController
class DeploymentReconciler(KubernetesObjectReconciler[V1Deployment]): class DeploymentReconciler(KubernetesObjectReconciler[V1Deployment]):
"""Kubernetes Deployment Reconciler""" """Kubernetes Deployment Reconciler"""
image_base = "beryju/passbook" image_base = "beryju/passbook"
deployment_ports: Dict[str, int] outpost: Outpost
def __init__(self, outpost: Outpost) -> None: def __init__(self, controller: "KubernetesController") -> None:
super().__init__(outpost) super().__init__(controller)
self.api = AppsV1Api() self.api = AppsV1Api()
self.deployment_ports = {} self.outpost = self.controller.outpost
@property
def name(self) -> str:
return f"passbook-outpost-{self.outpost.name}"
def reconcile(self, current: V1Deployment, reference: V1Deployment): def reconcile(self, current: V1Deployment, reference: V1Deployment):
if current.spec.replicas != reference.spec.replicas: if current.spec.replicas != reference.spec.replicas:
@ -49,9 +56,9 @@ class DeploymentReconciler(KubernetesObjectReconciler[V1Deployment]):
"""Get deployment object for outpost""" """Get deployment object for outpost"""
# Generate V1ContainerPort objects # Generate V1ContainerPort objects
container_ports = [] container_ports = []
for port_name, port in self.deployment_ports.items(): for port_name, port in self.controller.deployment_ports.items():
container_ports.append(V1ContainerPort(container_port=port, name=port_name)) container_ports.append(V1ContainerPort(container_port=port, name=port_name))
meta = self.get_object_meta(name=f"passbook-outpost-{self.outpost.name}") meta = self.get_object_meta(name=self.name)
return V1Deployment( return V1Deployment(
metadata=meta, metadata=meta,
spec=V1DeploymentSpec( spec=V1DeploymentSpec(

View File

@ -1,5 +1,6 @@
"""Kubernetes Secret Reconciler""" """Kubernetes Secret Reconciler"""
from base64 import b64encode from base64 import b64encode
from typing import TYPE_CHECKING
from kubernetes.client import CoreV1Api, V1Secret from kubernetes.client import CoreV1Api, V1Secret
@ -7,7 +8,9 @@ from passbook.outposts.controllers.k8s.base import (
KubernetesObjectReconciler, KubernetesObjectReconciler,
NeedsUpdate, NeedsUpdate,
) )
from passbook.outposts.models import Outpost
if TYPE_CHECKING:
from passbook.outposts.controllers.kubernetes import KubernetesController
def b64string(source: str) -> str: def b64string(source: str) -> str:
@ -18,10 +21,14 @@ def b64string(source: str) -> str:
class SecretReconciler(KubernetesObjectReconciler[V1Secret]): class SecretReconciler(KubernetesObjectReconciler[V1Secret]):
"""Kubernetes Secret Reconciler""" """Kubernetes Secret Reconciler"""
def __init__(self, outpost: Outpost) -> None: def __init__(self, controller: "KubernetesController") -> None:
super().__init__(outpost) super().__init__(controller)
self.api = CoreV1Api() self.api = CoreV1Api()
@property
def name(self) -> str:
return f"passbook-outpost-{self.controller.outpost.name}-api"
def reconcile(self, current: V1Secret, reference: V1Secret): def reconcile(self, current: V1Secret, reference: V1Secret):
for key in reference.data.keys(): for key in reference.data.keys():
if current.data[key] != reference.data[key]: if current.data[key] != reference.data[key]:
@ -29,15 +36,17 @@ class SecretReconciler(KubernetesObjectReconciler[V1Secret]):
def get_reference_object(self) -> V1Secret: def get_reference_object(self) -> V1Secret:
"""Get deployment object for outpost""" """Get deployment object for outpost"""
meta = self.get_object_meta(name=f"passbook-outpost-{self.outpost.name}-api") meta = self.get_object_meta(name=self.name)
return V1Secret( return V1Secret(
metadata=meta, metadata=meta,
data={ data={
"passbook_host": b64string(self.outpost.config.passbook_host), "passbook_host": b64string(
"passbook_host_insecure": b64string( self.controller.outpost.config.passbook_host
str(self.outpost.config.passbook_host_insecure)
), ),
"token": b64string(self.outpost.token.token_uuid.hex), "passbook_host_insecure": b64string(
str(self.controller.outpost.config.passbook_host_insecure)
),
"token": b64string(self.controller.outpost.token.token_uuid.hex),
}, },
) )
@ -51,7 +60,7 @@ class SecretReconciler(KubernetesObjectReconciler[V1Secret]):
def retrieve(self) -> V1Secret: def retrieve(self) -> V1Secret:
return self.api.read_namespaced_secret( return self.api.read_namespaced_secret(
f"passbook-outpost-{self.outpost.name}-api", self.namespace f"passbook-outpost-{self.controller.outpost.name}-api", self.namespace
) )
def update(self, current: V1Secret, reference: V1Secret): def update(self, current: V1Secret, reference: V1Secret):

View File

@ -1,5 +1,5 @@
"""Kubernetes Service Reconciler""" """Kubernetes Service Reconciler"""
from typing import Dict from typing import TYPE_CHECKING
from kubernetes.client import CoreV1Api, V1Service, V1ServicePort, V1ServiceSpec from kubernetes.client import CoreV1Api, V1Service, V1ServicePort, V1ServiceSpec
@ -7,18 +7,21 @@ from passbook.outposts.controllers.k8s.base import (
KubernetesObjectReconciler, KubernetesObjectReconciler,
NeedsUpdate, NeedsUpdate,
) )
from passbook.outposts.models import Outpost
if TYPE_CHECKING:
from passbook.outposts.controllers.kubernetes import KubernetesController
class ServiceReconciler(KubernetesObjectReconciler[V1Service]): class ServiceReconciler(KubernetesObjectReconciler[V1Service]):
"""Kubernetes Service Reconciler""" """Kubernetes Service Reconciler"""
deployment_ports: Dict[str, int] def __init__(self, controller: "KubernetesController") -> None:
super().__init__(controller)
def __init__(self, outpost: Outpost) -> None:
super().__init__(outpost)
self.api = CoreV1Api() self.api = CoreV1Api()
self.deployment_ports = {}
@property
def name(self) -> str:
return f"passbook-outpost-{self.controller.outpost.name}"
def reconcile(self, current: V1Service, reference: V1Service): def reconcile(self, current: V1Service, reference: V1Service):
if len(current.spec.ports) != len(reference.spec.ports): if len(current.spec.ports) != len(reference.spec.ports):
@ -29,9 +32,9 @@ class ServiceReconciler(KubernetesObjectReconciler[V1Service]):
def get_reference_object(self) -> V1Service: def get_reference_object(self) -> V1Service:
"""Get deployment object for outpost""" """Get deployment object for outpost"""
meta = self.get_object_meta(name=f"passbook-outpost-{self.outpost.name}") meta = self.get_object_meta(name=self.name)
ports = [] ports = []
for port_name, port in self.deployment_ports.items(): for port_name, port in self.controller.deployment_ports.items():
ports.append(V1ServicePort(name=port_name, port=port)) ports.append(V1ServicePort(name=port_name, port=port))
return V1Service( return V1Service(
metadata=meta, metadata=meta,
@ -48,7 +51,7 @@ class ServiceReconciler(KubernetesObjectReconciler[V1Service]):
def retrieve(self) -> V1Service: def retrieve(self) -> V1Service:
return self.api.read_namespaced_service( return self.api.read_namespaced_service(
f"passbook-outpost-{self.outpost.name}", self.namespace f"passbook-outpost-{self.controller.outpost.name}", self.namespace
) )
def update(self, current: V1Service, reference: V1Service): def update(self, current: V1Service, reference: V1Service):

View File

@ -1,5 +1,6 @@
"""Kubernetes deployment controller""" """Kubernetes deployment controller"""
from io import StringIO from io import StringIO
from typing import Dict, List, Type
from kubernetes.client import OpenApiException from kubernetes.client import OpenApiException
from kubernetes.config import load_incluster_config, load_kube_config from kubernetes.config import load_incluster_config, load_kube_config
@ -7,6 +8,7 @@ from kubernetes.config.config_exception import ConfigException
from yaml import dump_all from yaml import dump_all
from passbook.outposts.controllers.base import BaseController, ControllerException from passbook.outposts.controllers.base import BaseController, ControllerException
from passbook.outposts.controllers.k8s.base import KubernetesObjectReconciler
from passbook.outposts.controllers.k8s.deployment import DeploymentReconciler from passbook.outposts.controllers.k8s.deployment import DeploymentReconciler
from passbook.outposts.controllers.k8s.secret import SecretReconciler from passbook.outposts.controllers.k8s.secret import SecretReconciler
from passbook.outposts.controllers.k8s.service import ServiceReconciler from passbook.outposts.controllers.k8s.service import ServiceReconciler
@ -16,71 +18,50 @@ from passbook.outposts.models import Outpost
class KubernetesController(BaseController): class KubernetesController(BaseController):
"""Manage deployment of outpost in kubernetes""" """Manage deployment of outpost in kubernetes"""
reconcilers: Dict[str, Type[KubernetesObjectReconciler]]
reconcile_order: List[str]
def __init__(self, outpost: Outpost) -> None: def __init__(self, outpost: Outpost) -> None:
super().__init__(outpost) super().__init__(outpost)
try: try:
load_incluster_config() load_incluster_config()
except ConfigException: except ConfigException:
load_kube_config() load_kube_config()
self.reconcilers = {
"secret": SecretReconciler,
"deployment": DeploymentReconciler,
"service": ServiceReconciler,
}
self.reconcile_order = ["secret", "deployment", "service"]
def up(self): def up(self):
try: try:
namespace = self.outpost.config.kubernetes_namespace for reconcile_key in self.reconcile_order:
reconciler = self.reconcilers[reconcile_key](self)
reconciler.up()
secret_reconciler = SecretReconciler(self.outpost)
secret_reconciler.namespace = namespace
secret_reconciler.up()
deployment_reconciler = DeploymentReconciler(self.outpost)
deployment_reconciler.namespace = namespace
deployment_reconciler.deployment_ports = self.deployment_ports
deployment_reconciler.up()
service_reconciler = ServiceReconciler(self.outpost)
service_reconciler.namespace = namespace
service_reconciler.deployment_ports = self.deployment_ports
service_reconciler.up()
except OpenApiException as exc: except OpenApiException as exc:
raise ControllerException from exc raise ControllerException from exc
def down(self): def down(self):
try: try:
namespace = self.outpost.config.kubernetes_namespace for reconcile_key in self.reconcile_order:
reconciler = self.reconcilers[reconcile_key](self)
reconciler.down()
secret_reconciler = SecretReconciler(self.outpost)
secret_reconciler.namespace = namespace
secret_reconciler.down()
deployment_reconciler = DeploymentReconciler(self.outpost)
deployment_reconciler.namespace = namespace
deployment_reconciler.deployment_ports = self.deployment_ports
deployment_reconciler.down()
service_reconciler = ServiceReconciler(self.outpost)
service_reconciler.namespace = namespace
service_reconciler.deployment_ports = self.deployment_ports
service_reconciler.down()
except OpenApiException as exc: except OpenApiException as exc:
raise ControllerException from exc raise ControllerException from exc
def get_static_deployment(self) -> str: def get_static_deployment(self) -> str:
secret_reconciler = SecretReconciler(self.outpost) documents = []
secret_reconciler.namespace = "" for reconcile_key in self.reconcile_order:
reconciler = self.reconcilers[reconcile_key](self)
reconciler.up()
documents.append(reconciler.get_reference_object().to_dict())
deployment_reconciler = DeploymentReconciler(self.outpost)
deployment_reconciler.namespace = ""
deployment_reconciler.deployment_ports = self.deployment_ports
service_reconciler = ServiceReconciler(self.outpost)
service_reconciler.namespace = ""
service_reconciler.deployment_ports = self.deployment_ports
with StringIO() as _str: with StringIO() as _str:
dump_all( dump_all(
[ documents,
secret_reconciler.get_reference_object().to_dict(),
deployment_reconciler.get_reference_object().to_dict(),
service_reconciler.get_reference_object().to_dict(),
],
stream=_str, stream=_str,
default_flow_style=False, default_flow_style=False,
) )

View File

@ -1,7 +1,7 @@
"""Outpost models""" """Outpost models"""
from dataclasses import asdict, dataclass, field from dataclasses import asdict, dataclass, field
from datetime import datetime from datetime import datetime
from typing import Iterable, List, Optional, Union from typing import Dict, Iterable, List, Optional, Union
from uuid import uuid4 from uuid import uuid4
from dacite import from_dict from dacite import from_dict
@ -38,6 +38,8 @@ class OutpostConfig:
kubernetes_replicas: int = field(default=1) kubernetes_replicas: int = field(default=1)
kubernetes_namespace: str = field(default="default") kubernetes_namespace: str = field(default="default")
kubernetes_ingress_annotations: Dict[str, str] = field(default_factory=dict)
kubernetes_ingress_secret_name: str = field(default="passbook-outpost")
class OutpostModel(Model): class OutpostModel(Model):