159 lines
6.1 KiB
Python
159 lines
6.1 KiB
Python
"""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 passbook import __version__
|
|
from passbook.outposts.controllers.base import BaseController, ControllerException
|
|
from passbook.outposts.models import DockerServiceConnection, Outpost
|
|
|
|
|
|
class DockerController(BaseController):
|
|
"""Docker controller"""
|
|
|
|
client: DockerClient
|
|
|
|
container: Container
|
|
connection: DockerServiceConnection
|
|
|
|
image_base = "beryju/passbook"
|
|
|
|
def __init__(self, outpost: Outpost, connection: DockerServiceConnection) -> None:
|
|
super().__init__(outpost, connection)
|
|
try:
|
|
if self.connection.local:
|
|
self.client = DockerClient.from_env()
|
|
else:
|
|
self.client = DockerClient(
|
|
base_url=self.connection.url,
|
|
tls=self.connection.tls,
|
|
)
|
|
except DockerException as exc:
|
|
raise ControllerException from exc
|
|
|
|
def _get_labels(self) -> Dict[str, str]:
|
|
return {}
|
|
|
|
def _get_env(self) -> Dict[str, str]:
|
|
return {
|
|
"PASSBOOK_HOST": self.outpost.config.passbook_host,
|
|
"PASSBOOK_INSECURE": str(self.outpost.config.passbook_host_insecure),
|
|
"PASSBOOK_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"passbook-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_name = f"{self.image_base}-{self.outpost.type}:{__version__}"
|
|
self.client.images.pull(image_name)
|
|
return (
|
|
self.client.containers.create(
|
|
image=image_name,
|
|
name=f"passbook-proxy-{self.outpost.uuid.hex}",
|
|
detach=True,
|
|
ports={x: x for _, x in self.deployment_ports.items()},
|
|
environment=self._get_env(),
|
|
network_mode="host" if settings.TEST else "bridge",
|
|
labels=self._get_labels(),
|
|
),
|
|
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()]
|
|
compose = {
|
|
"version": "3.5",
|
|
"services": {
|
|
f"passbook_{self.outpost.type}": {
|
|
"image": f"{self.image_base}-{self.outpost.type}:{__version__}",
|
|
"ports": ports,
|
|
"environment": {
|
|
"PASSBOOK_HOST": self.outpost.config.passbook_host,
|
|
"PASSBOOK_INSECURE": str(
|
|
self.outpost.config.passbook_host_insecure
|
|
),
|
|
"PASSBOOK_TOKEN": self.outpost.token.key,
|
|
},
|
|
}
|
|
},
|
|
}
|
|
return safe_dump(compose, default_flow_style=False)
|