outposts: save state of outposts

This commit is contained in:
Jens Langhammer 2020-11-08 21:02:52 +01:00
parent e91e286ebc
commit 7e8e3893eb
5 changed files with 100 additions and 21 deletions

View File

@ -9,7 +9,7 @@ from passbook.outposts.models import Outpost, OutpostServiceConnection
class ControllerException(SentryIgnoredException): class ControllerException(SentryIgnoredException):
"""Exception raise when anything fails during controller run""" """Exception raised when anything fails during controller run"""
class BaseController: class BaseController:

View File

@ -10,7 +10,11 @@ from yaml import safe_dump
from passbook import __version__ from passbook import __version__
from passbook.outposts.controllers.base import BaseController, ControllerException from passbook.outposts.controllers.base import BaseController, ControllerException
from passbook.outposts.models import DockerServiceConnection, Outpost from passbook.outposts.models import (
DockerServiceConnection,
Outpost,
ServiceConnectionInvalid,
)
class DockerController(BaseController): class DockerController(BaseController):
@ -26,14 +30,8 @@ class DockerController(BaseController):
def __init__(self, outpost: Outpost, connection: DockerServiceConnection) -> None: def __init__(self, outpost: Outpost, connection: DockerServiceConnection) -> None:
super().__init__(outpost, connection) super().__init__(outpost, connection)
try: try:
if self.connection.local: self.client = connection.client()
self.client = DockerClient.from_env() except ServiceConnectionInvalid as exc:
else:
self.client = DockerClient(
base_url=self.connection.url,
tls=self.connection.tls,
)
except DockerException as exc:
raise ControllerException from exc raise ControllerException from exc
def _get_labels(self) -> Dict[str, str]: def _get_labels(self) -> Dict[str, str]:

View File

@ -3,9 +3,7 @@ from io import StringIO
from typing import Dict, List, Type 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.client.api_client import ApiClient
from kubernetes.config.config_exception import ConfigException
from kubernetes.config.kube_config import load_kube_config_from_dict
from structlog.testing import capture_logs from structlog.testing import capture_logs
from yaml import dump_all from yaml import dump_all
@ -23,19 +21,14 @@ class KubernetesController(BaseController):
reconcilers: Dict[str, Type[KubernetesObjectReconciler]] reconcilers: Dict[str, Type[KubernetesObjectReconciler]]
reconcile_order: List[str] reconcile_order: List[str]
config: ApiClient
connection: KubernetesServiceConnection connection: KubernetesServiceConnection
def __init__( def __init__(
self, outpost: Outpost, connection: KubernetesServiceConnection self, outpost: Outpost, connection: KubernetesServiceConnection
) -> None: ) -> None:
super().__init__(outpost, connection) super().__init__(outpost, connection)
try: self.client = connection.client()
if self.connection.local:
load_incluster_config()
else:
load_kube_config_from_dict(self.connection.kubeconfig)
except ConfigException:
load_kube_config()
self.reconcilers = { self.reconcilers = {
"secret": SecretReconciler, "secret": SecretReconciler,
"deployment": DeploymentReconciler, "deployment": DeploymentReconciler,

View File

@ -4,9 +4,10 @@ import uuid
import django.db.models.deletion import django.db.models.deletion
from django.apps.registry import Apps from django.apps.registry import Apps
from django.core.exceptions import FieldError
from django.db import migrations, models from django.db import migrations, models
from django.db.backends.base.schema import BaseDatabaseSchemaEditor from django.db.backends.base.schema import BaseDatabaseSchemaEditor
from django.core.exceptions import FieldError
import passbook.lib.models import passbook.lib.models

View File

@ -5,14 +5,24 @@ from typing import Dict, Iterable, List, Optional, Type, Union
from uuid import uuid4 from uuid import uuid4
from dacite import from_dict from dacite import from_dict
from django.conf import settings
from django.core.cache import cache from django.core.cache import cache
from django.db import models, transaction from django.db import models, transaction
from django.db.models.base import Model from django.db.models.base import Model
from django.forms.models import ModelForm from django.forms.models import ModelForm
from django.http import HttpRequest from django.http import HttpRequest
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from docker.client import DockerClient
from docker.errors import DockerException
from guardian.models import UserObjectPermission from guardian.models import UserObjectPermission
from guardian.shortcuts import assign_perm from guardian.shortcuts import assign_perm
from kubernetes.client import VersionApi, VersionInfo
from kubernetes.client.api_client import ApiClient
from kubernetes.client.configuration import Configuration
from kubernetes.client.exceptions import OpenApiException
from kubernetes.config.config_exception import ConfigException
from kubernetes.config.incluster_config import load_incluster_config
from kubernetes.config.kube_config import load_kube_config, load_kube_config_from_dict
from model_utils.managers import InheritanceManager from model_utils.managers import InheritanceManager
from packaging.version import LegacyVersion, Version, parse from packaging.version import LegacyVersion, Version, parse
@ -20,12 +30,17 @@ from passbook import __version__
from passbook.core.models import Provider, Token, TokenIntents, User from passbook.core.models import Provider, Token, TokenIntents, User
from passbook.lib.config import CONFIG from passbook.lib.config import CONFIG
from passbook.lib.models import InheritanceForeignKey from passbook.lib.models import InheritanceForeignKey
from passbook.lib.sentry import SentryIgnoredException
from passbook.lib.utils.template import render_to_string from passbook.lib.utils.template import render_to_string
OUR_VERSION = parse(__version__) OUR_VERSION = parse(__version__)
OUTPOST_HELLO_INTERVAL = 10 OUTPOST_HELLO_INTERVAL = 10
class ServiceConnectionInvalid(SentryIgnoredException):
""""Exception raised when a Service Connection has invalid parameters"""
@dataclass @dataclass
class OutpostConfig: class OutpostConfig:
"""Configuration an outpost uses to configure it self""" """Configuration an outpost uses to configure it self"""
@ -68,6 +83,14 @@ def default_outpost_config():
return asdict(OutpostConfig(passbook_host="")) return asdict(OutpostConfig(passbook_host=""))
@dataclass
class OutpostServiceConnectionState:
"""State of an Outpost Service Connection"""
version: str
healthy: bool
class OutpostServiceConnection(models.Model): class OutpostServiceConnection(models.Model):
"""Connection details for an Outpost Controller, like Docker or Kubernetes""" """Connection details for an Outpost Controller, like Docker or Kubernetes"""
@ -87,6 +110,19 @@ class OutpostServiceConnection(models.Model):
objects = InheritanceManager() objects = InheritanceManager()
@property
def state(self) -> OutpostServiceConnectionState:
"""Get state of service connection"""
state_key = f"outpost_service_connection_{self.pk.hex}"
state = cache.get(state_key, None)
if state:
state = self._get_state()
cache.set(state_key, state)
return state
def _get_state(self) -> OutpostServiceConnectionState:
raise NotImplementedError
@property @property
def form(self) -> Type[ModelForm]: def form(self) -> Type[ModelForm]:
"""Return Form class used to edit this object""" """Return Form class used to edit this object"""
@ -113,6 +149,31 @@ class DockerServiceConnection(OutpostServiceConnection):
def __str__(self) -> str: def __str__(self) -> str:
return f"Docker Service-Connection {self.name}" return f"Docker Service-Connection {self.name}"
def client(self) -> DockerClient:
"""Get DockerClient"""
try:
client = None
if self.local:
client = DockerClient.from_env()
else:
client = DockerClient(
base_url=self.url,
tls=self.tls,
)
client.containers.list()
except DockerException as exc:
raise ServiceConnectionInvalid from exc
return client
def _get_state(self) -> OutpostServiceConnectionState:
try:
client = self.client()
return OutpostServiceConnectionState(
version=client.info()["ServerVersion"], healthy=True
)
except ServiceConnectionInvalid:
return OutpostServiceConnectionState(version="", healthy=False)
class Meta: class Meta:
verbose_name = _("Docker Service-Connection") verbose_name = _("Docker Service-Connection")
@ -140,6 +201,32 @@ class KubernetesServiceConnection(OutpostServiceConnection):
def __str__(self) -> str: def __str__(self) -> str:
return f"Kubernetes Service-Connection {self.name}" return f"Kubernetes Service-Connection {self.name}"
def _get_state(self) -> OutpostServiceConnectionState:
try:
client = self.client()
api_instance = VersionApi(client)
version: VersionInfo = api_instance.get_code()
return OutpostServiceConnectionState(
version=version.git_version, healthy=True
)
except OpenApiException:
return OutpostServiceConnectionState(version="", healthy=False)
def client(self) -> ApiClient:
"""Get Kubernetes client configured from kubeconfig"""
config = Configuration()
try:
if self.local:
load_incluster_config(client_configuration=config)
else:
load_kube_config_from_dict(self.kubeconfig, client_configuration=config)
return ApiClient(config)
except ConfigException as exc:
if not settings.DEBUG:
raise ServiceConnectionInvalid from exc
load_kube_config(client_configuration=config)
return config
class Meta: class Meta:
verbose_name = _("Kubernetes Service-Connection") verbose_name = _("Kubernetes Service-Connection")