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"""
from typing import Generic, TypeVar
from typing import TYPE_CHECKING, Generic, TypeVar
from kubernetes.client import V1ObjectMeta
from kubernetes.client.rest import ApiException
@ -7,7 +7,9 @@ from structlog import get_logger
from passbook import __version__
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
T = TypeVar("T")
@ -28,10 +30,19 @@ class NeedsUpdate(ReconcileTrigger):
class KubernetesObjectReconciler(Generic[T]):
"""Base Kubernetes Reconciler, handles the basic logic."""
def __init__(self, outpost: Outpost):
self.outpost = outpost
self.namespace = ""
self.logger = get_logger(controller=self.__class__.__name__, outpost=outpost)
controller: "KubernetesController"
def __init__(self, controller: "KubernetesController"):
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):
"""Create object if it doesn't exist, update if needed or recreate if needed."""
@ -107,11 +118,11 @@ class KubernetesObjectReconciler(Generic[T]):
return V1ObjectMeta(
namespace=self.namespace,
labels={
"app.kubernetes.io/name": f"passbook-{self.outpost.type.lower()}",
"app.kubernetes.io/instance": self.outpost.name,
"app.kubernetes.io/name": f"passbook-{self.controller.outpost.type.lower()}",
"app.kubernetes.io/instance": self.controller.outpost.name,
"app.kubernetes.io/version": __version__,
"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,
)

View File

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

View File

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

View File

@ -1,5 +1,5 @@
"""Kubernetes Service Reconciler"""
from typing import Dict
from typing import TYPE_CHECKING
from kubernetes.client import CoreV1Api, V1Service, V1ServicePort, V1ServiceSpec
@ -7,18 +7,21 @@ from passbook.outposts.controllers.k8s.base import (
KubernetesObjectReconciler,
NeedsUpdate,
)
from passbook.outposts.models import Outpost
if TYPE_CHECKING:
from passbook.outposts.controllers.kubernetes import KubernetesController
class ServiceReconciler(KubernetesObjectReconciler[V1Service]):
"""Kubernetes Service Reconciler"""
deployment_ports: Dict[str, int]
def __init__(self, outpost: Outpost) -> None:
super().__init__(outpost)
def __init__(self, controller: "KubernetesController") -> None:
super().__init__(controller)
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):
if len(current.spec.ports) != len(reference.spec.ports):
@ -29,9 +32,9 @@ class ServiceReconciler(KubernetesObjectReconciler[V1Service]):
def get_reference_object(self) -> V1Service:
"""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 = []
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))
return V1Service(
metadata=meta,
@ -48,7 +51,7 @@ class ServiceReconciler(KubernetesObjectReconciler[V1Service]):
def retrieve(self) -> V1Service:
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):

View File

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

View File

@ -1,7 +1,7 @@
"""Outpost models"""
from dataclasses import asdict, dataclass, field
from datetime import datetime
from typing import Iterable, List, Optional, Union
from typing import Dict, Iterable, List, Optional, Union
from uuid import uuid4
from dacite import from_dict
@ -38,6 +38,8 @@ class OutpostConfig:
kubernetes_replicas: int = field(default=1)
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):