From d506e8f1a3a60074654665457b5d2d6719d0086f Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Sun, 4 Oct 2020 00:36:12 +0200 Subject: [PATCH] outposts: implement docker controller --- passbook/outposts/controllers/compose.py | 36 --------- passbook/outposts/controllers/docker.py | 76 +++++++++++++++++++ .../migrations/0006_auto_20201003_2239.py | 26 +++++++ passbook/outposts/models.py | 3 +- passbook/outposts/tasks.py | 24 ++++-- passbook/outposts/views.py | 4 +- .../providers/proxy/controllers/docker.py | 13 ++++ 7 files changed, 135 insertions(+), 47 deletions(-) delete mode 100644 passbook/outposts/controllers/compose.py create mode 100644 passbook/outposts/controllers/docker.py create mode 100644 passbook/outposts/migrations/0006_auto_20201003_2239.py create mode 100644 passbook/providers/proxy/controllers/docker.py diff --git a/passbook/outposts/controllers/compose.py b/passbook/outposts/controllers/compose.py deleted file mode 100644 index e6706be5d..000000000 --- a/passbook/outposts/controllers/compose.py +++ /dev/null @@ -1,36 +0,0 @@ -"""Docker Compose controller""" -from yaml import safe_dump - -from passbook import __version__ -from passbook.outposts.controllers.base import BaseController - - -class DockerComposeController(BaseController): - """Docker Compose controller""" - - image_base = "beryju/passbook" - - def run(self): - self.logger.warning("DockerComposeController does not implement run") - raise NotImplementedError - - 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.token_uuid.hex, - }, - } - }, - } - return safe_dump(compose, default_flow_style=False) diff --git a/passbook/outposts/controllers/docker.py b/passbook/outposts/controllers/docker.py new file mode 100644 index 000000000..3f1ab8ecc --- /dev/null +++ b/passbook/outposts/controllers/docker.py @@ -0,0 +1,76 @@ +"""Docker controller""" +from docker import DockerClient, from_env +from docker.errors import NotFound +from docker.models.containers import Container +from yaml import safe_dump + +from passbook import __version__ +from passbook.outposts.controllers.base import BaseController + + +class DockerController(BaseController): + """Docker controller""" + + client: DockerClient + + container: Container + + image_base = "beryju/passbook" + + def __init__(self, outpost_pk: str) -> None: + super().__init__(outpost_pk) + self.client = from_env() + + def _get_container(self) -> Container: + container_name = f"passbook-proxy-{self.outpost.uuid.hex}" + try: + return self.client.containers.get(container_name) + except NotFound: + return self.client.containers.create( + image=f"{self.image_base}-{self.outpost.type}:{__version__}", + name=f"passbook-proxy-{self.outpost.uuid.hex}", + detach=True, + ports={x: x for _, x in self.deployment_ports.items()}, + environment={ + "PASSBOOK_HOST": self.outpost.config.passbook_host, + "PASSBOOK_INSECURE": str( + self.outpost.config.passbook_host_insecure + ), + "PASSBOOK_TOKEN": self.outpost.token.token_uuid.hex, + }, + ) + + def run(self): + container = self._get_container() + # Check if the container is out of date, delete it and retry + if len(container.image.tags) > 0: + tag: str = container.iamge.tags[0] + _, _, version = tag.partition(":") + if version != __version__: + container.kill() + container.remove(force=True) + return self.run() + if container.status != "running": + container.start() + return None + + 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.token_uuid.hex, + }, + } + }, + } + return safe_dump(compose, default_flow_style=False) diff --git a/passbook/outposts/migrations/0006_auto_20201003_2239.py b/passbook/outposts/migrations/0006_auto_20201003_2239.py new file mode 100644 index 000000000..55effdc3e --- /dev/null +++ b/passbook/outposts/migrations/0006_auto_20201003_2239.py @@ -0,0 +1,26 @@ +# Generated by Django 3.1.2 on 2020-10-03 22:39 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("passbook_outposts", "0005_auto_20200909_1733"), + ] + + operations = [ + migrations.AlterField( + model_name="outpost", + name="deployment_type", + field=models.TextField( + choices=[ + ("kubernetes", "Kubernetes"), + ("docker", "Docker"), + ("custom", "Custom"), + ], + default="custom", + help_text="Select between passbook-managed deployment types or a custom deployment.", + ), + ), + ] diff --git a/passbook/outposts/models.py b/passbook/outposts/models.py index afabdbcbf..2ec2147f7 100644 --- a/passbook/outposts/models.py +++ b/passbook/outposts/models.py @@ -59,7 +59,8 @@ class OutpostType(models.TextChoices): class OutpostDeploymentType(models.TextChoices): """Deployment types that are managed through passbook""" - # KUBERNETES = "kubernetes" + KUBERNETES = "kubernetes" + DOCKER = "docker" CUSTOM = "custom" diff --git a/passbook/outposts/tasks.py b/passbook/outposts/tasks.py index c28dfd4a3..9b88cec36 100644 --- a/passbook/outposts/tasks.py +++ b/passbook/outposts/tasks.py @@ -12,6 +12,7 @@ from passbook.outposts.models import ( OutpostModel, OutpostType, ) +from passbook.providers.proxy.controllers.docker import ProxyDockerController from passbook.providers.proxy.controllers.kubernetes import ProxyKubernetesController from passbook.root.celery import CELERY_APP @@ -20,20 +21,27 @@ LOGGER = get_logger() @CELERY_APP.task(bind=True) # pylint: disable=unused-argument -def outpost_k8s_controller(self): - """Launch Kubernetes Controller for all Outposts which are deployed in kubernetes""" - for outpost in Outpost.objects.filter( - deployment_type=OutpostDeploymentType.KUBERNETES +def outpost_controller(self): + """Launch Controller for all Outposts which support it""" + for outpost in Outpost.objects.exclude( + deployment_type=OutpostDeploymentType.CUSTOM ): - outpost_k8s_controller_single.delay(outpost.pk.hex, outpost.type) + outpost_controller_single.delay( + outpost.pk.hex, outpost.deployment_type, outpost.type + ) @CELERY_APP.task(bind=True) # pylint: disable=unused-argument -def outpost_k8s_controller_single(self, outpost: str, outpost_type: str): - """Launch Kubernetes manager and reconcile deployment/service/etc""" +def outpost_controller_single( + self, outpost: str, deployment_type: str, outpost_type: str +): + """Launch controller and reconcile deployment/service/etc""" if outpost_type == OutpostType.PROXY: - ProxyKubernetesController(outpost).run() + if deployment_type == OutpostDeploymentType.KUBERNETES: + ProxyKubernetesController(outpost).run() + if deployment_type == OutpostDeploymentType.DOCKER: + ProxyDockerController(outpost).run() @CELERY_APP.task() diff --git a/passbook/outposts/views.py b/passbook/outposts/views.py index 32e18a495..d5ddf58f7 100644 --- a/passbook/outposts/views.py +++ b/passbook/outposts/views.py @@ -11,7 +11,7 @@ from guardian.shortcuts import get_objects_for_user from structlog import get_logger from passbook.core.models import User -from passbook.outposts.controllers.compose import DockerComposeController +from passbook.outposts.controllers.docker import DockerController from passbook.outposts.models import Outpost, OutpostType from passbook.providers.proxy.controllers.kubernetes import ProxyKubernetesController @@ -35,7 +35,7 @@ class DockerComposeView(LoginRequiredMixin, View): ) manifest = "" if outpost.type == OutpostType.PROXY: - controller = DockerComposeController(outpost_pk) + controller = DockerController(outpost_pk) manifest = controller.get_static_deployment() return HttpResponse(manifest, content_type="text/vnd.yaml") diff --git a/passbook/providers/proxy/controllers/docker.py b/passbook/providers/proxy/controllers/docker.py new file mode 100644 index 000000000..252319a6f --- /dev/null +++ b/passbook/providers/proxy/controllers/docker.py @@ -0,0 +1,13 @@ +"""Proxy Provider Docker Contoller""" +from passbook.outposts.controllers.docker import DockerController + + +class ProxyDockerController(DockerController): + """Proxy Provider Docker Contoller""" + + def __init__(self, outpost_pk: str): + super().__init__(outpost_pk) + self.deployment_ports = { + "http": 4180, + "https": 4443, + }