diff --git a/e2e/test_provider_proxy.py b/e2e/test_provider_proxy.py index 7269923d3..aefedb710 100644 --- a/e2e/test_provider_proxy.py +++ b/e2e/test_provider_proxy.py @@ -16,9 +16,9 @@ from passbook import __version__ from passbook.core.models import Application from passbook.flows.models import Flow from passbook.outposts.models import ( + DockerServiceConnection, Outpost, OutpostConfig, - OutpostDeploymentType, OutpostType, ) from passbook.providers.proxy.models import ProxyProvider @@ -76,7 +76,6 @@ class TestProviderProxy(SeleniumTestCase): outpost: Outpost = Outpost.objects.create( name="proxy_outpost", type=OutpostType.PROXY, - deployment_type=OutpostDeploymentType.CUSTOM, ) outpost.providers.add(proxy) outpost.save() @@ -128,10 +127,11 @@ class TestProviderProxyConnect(ChannelsLiveServerTestCase): proxy.save() # we need to create an application to actually access the proxy Application.objects.create(name="proxy", slug="proxy", provider=proxy) + service_connection = DockerServiceConnection.objects.get(local=True) outpost: Outpost = Outpost.objects.create( name="proxy_outpost", type=OutpostType.PROXY, - deployment_type=OutpostDeploymentType.DOCKER, + service_connection=service_connection, _config=asdict( OutpostConfig(passbook_host=self.live_server_url, log_level="debug") ), diff --git a/passbook/outposts/apps.py b/passbook/outposts/apps.py index 8621a3df5..e8cca5e66 100644 --- a/passbook/outposts/apps.py +++ b/passbook/outposts/apps.py @@ -28,11 +28,11 @@ class PassbookOutpostConfig(AppConfig): import_module("passbook.outposts.signals") try: self.init_local_connection() - except (ProgrammingError): + except ProgrammingError: pass def init_local_connection(self): - # Check if local kubernetes or docker connections should be created + """Check if local kubernetes or docker connections should be created""" from passbook.outposts.models import ( KubernetesServiceConnection, DockerServiceConnection, diff --git a/passbook/outposts/controllers/base.py b/passbook/outposts/controllers/base.py index b5c3a9c31..a15aaad8d 100644 --- a/passbook/outposts/controllers/base.py +++ b/passbook/outposts/controllers/base.py @@ -20,8 +20,9 @@ class BaseController: outpost: Outpost connection: OutpostServiceConnection - def __init__(self, outpost: Outpost): + def __init__(self, outpost: Outpost, connection: OutpostServiceConnection): self.outpost = outpost + self.connection = connection self.logger = get_logger() self.deployment_ports = {} diff --git a/passbook/outposts/controllers/docker.py b/passbook/outposts/controllers/docker.py index 328487241..b5d3e36cc 100644 --- a/passbook/outposts/controllers/docker.py +++ b/passbook/outposts/controllers/docker.py @@ -23,8 +23,8 @@ class DockerController(BaseController): image_base = "beryju/passbook" - def __init__(self, outpost: Outpost) -> None: - super().__init__(outpost) + def __init__(self, outpost: Outpost, connection: DockerServiceConnection) -> None: + super().__init__(outpost, connection) try: if self.connection.local: self.client = DockerClient.from_env() diff --git a/passbook/outposts/controllers/kubernetes.py b/passbook/outposts/controllers/kubernetes.py index fc80751de..3ccbde6f5 100644 --- a/passbook/outposts/controllers/kubernetes.py +++ b/passbook/outposts/controllers/kubernetes.py @@ -25,8 +25,10 @@ class KubernetesController(BaseController): connection: KubernetesServiceConnection - def __init__(self, outpost: Outpost) -> None: - super().__init__(outpost) + def __init__( + self, outpost: Outpost, connection: KubernetesServiceConnection + ) -> None: + super().__init__(outpost, connection) try: if self.connection.local: load_incluster_config() diff --git a/passbook/outposts/migrations/0009_fix_missing_token_identifier.py b/passbook/outposts/migrations/0009_fix_missing_token_identifier.py index 98d537dc1..d614f1e5e 100644 --- a/passbook/outposts/migrations/0009_fix_missing_token_identifier.py +++ b/passbook/outposts/migrations/0009_fix_missing_token_identifier.py @@ -10,7 +10,9 @@ def fix_missing_token_identifier(apps: Apps, schema_editor: BaseDatabaseSchemaEd Token = apps.get_model("passbook_core", "Token") from passbook.outposts.models import Outpost - for outpost in Outpost.objects.using(schema_editor.connection.alias).all().only('pk'): + for outpost in ( + Outpost.objects.using(schema_editor.connection.alias).all().only("pk") + ): user_identifier = outpost.user_identifier user = User.objects.get(username=user_identifier) tokens = Token.objects.filter(user=user) diff --git a/passbook/outposts/migrations/0010_service_connection.py b/passbook/outposts/migrations/0010_service_connection.py index f367cad91..d35c96e7a 100644 --- a/passbook/outposts/migrations/0010_service_connection.py +++ b/passbook/outposts/migrations/0010_service_connection.py @@ -11,16 +11,23 @@ from django.db.backends.base.schema import BaseDatabaseSchemaEditor def migrate_to_service_connection(apps: Apps, schema_editor: BaseDatabaseSchemaEditor): db_alias = schema_editor.connection.alias Outpost = apps.get_model("passbook_outposts", "Outpost") - DockerServiceConnection = apps.get_model("passbook_outposts", "DockerServiceConnection") - KubernetesServiceConnection = apps.get_model("passbook_outposts", "KubernetesServiceConnection") + DockerServiceConnection = apps.get_model( + "passbook_outposts", "DockerServiceConnection" + ) + KubernetesServiceConnection = apps.get_model( + "passbook_outposts", "KubernetesServiceConnection" + ) from passbook.outposts.apps import PassbookOutpostConfig + # Ensure that local connection have been created PassbookOutpostConfig.init_local_connection(None) docker = DockerServiceConnection.objects.filter(local=True) k8s = KubernetesServiceConnection.objects.filter(local=True) - for outpost in Outpost.objects.using(db_alias).all().exclude(deployment_type="custom"): + for outpost in ( + Outpost.objects.using(db_alias).all().exclude(deployment_type="custom") + ): if outpost.deployment_type == "kubernetes": outpost.service_connection = k8s elif outpost.deployment_type == "docker": @@ -31,43 +38,85 @@ def migrate_to_service_connection(apps: Apps, schema_editor: BaseDatabaseSchemaE class Migration(migrations.Migration): dependencies = [ - ('passbook_outposts', '0009_fix_missing_token_identifier'), + ("passbook_outposts", "0009_fix_missing_token_identifier"), ] operations = [ migrations.CreateModel( - name='OutpostServiceConnection', + name="OutpostServiceConnection", fields=[ - ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), - ('name', models.TextField()), - ('local', models.BooleanField(default=False, help_text='If enabled, use the local connection. Required Docker socket/Kubernetes Integration', unique=True)), + ( + "uuid", + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + ), + ), + ("name", models.TextField()), + ( + "local", + models.BooleanField( + default=False, + help_text="If enabled, use the local connection. Required Docker socket/Kubernetes Integration", + unique=True, + ), + ), ], ), migrations.CreateModel( - name='DockerServiceConnection', + name="DockerServiceConnection", fields=[ - ('outpostserviceconnection_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_outposts.outpostserviceconnection')), - ('url', models.TextField()), - ('tls', models.BooleanField()), + ( + "outpostserviceconnection_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="passbook_outposts.outpostserviceconnection", + ), + ), + ("url", models.TextField()), + ("tls", models.BooleanField()), ], - bases=('passbook_outposts.outpostserviceconnection',), + bases=("passbook_outposts.outpostserviceconnection",), ), migrations.CreateModel( - name='KubernetesServiceConnection', + name="KubernetesServiceConnection", fields=[ - ('outpostserviceconnection_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_outposts.outpostserviceconnection')), - ('config', models.JSONField()), + ( + "outpostserviceconnection_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="passbook_outposts.outpostserviceconnection", + ), + ), + ("config", models.JSONField()), ], - bases=('passbook_outposts.outpostserviceconnection',), + bases=("passbook_outposts.outpostserviceconnection",), ), migrations.AddField( - model_name='outpost', - name='service_connection', - field=models.ForeignKey(blank=True, default=None, help_text='Select Service-Connection passbook should use to manage this outpost. Leave empty if passbook should not handle the deployment.', null=True, on_delete=django.db.models.deletion.SET_DEFAULT, to='passbook_outposts.outpostserviceconnection'), + model_name="outpost", + name="service_connection", + field=models.ForeignKey( + blank=True, + default=None, + help_text="Select Service-Connection passbook should use to manage this outpost. Leave empty if passbook should not handle the deployment.", + null=True, + on_delete=django.db.models.deletion.SET_DEFAULT, + to="passbook_outposts.outpostserviceconnection", + ), ), migrations.RunPython(migrate_to_service_connection), migrations.RemoveField( - model_name='outpost', - name='deployment_type', + model_name="outpost", + name="deployment_type", ), ] diff --git a/passbook/outposts/models.py b/passbook/outposts/models.py index 7b6b13bd1..1ad756ab5 100644 --- a/passbook/outposts/models.py +++ b/passbook/outposts/models.py @@ -18,6 +18,7 @@ from packaging.version import LegacyVersion, Version, parse from passbook import __version__ from passbook.core.models import Provider, Token, TokenIntents, User from passbook.lib.config import CONFIG +from passbook.lib.models import InheritanceForeignKey from passbook.lib.utils.template import render_to_string OUR_VERSION = parse(__version__) @@ -106,7 +107,7 @@ class Outpost(models.Model): name = models.TextField() type = models.TextField(choices=OutpostType.choices, default=OutpostType.PROXY) - service_connection = models.ForeignKey( + service_connection = InheritanceForeignKey( OutpostServiceConnection, default=None, null=True, diff --git a/passbook/outposts/tasks.py b/passbook/outposts/tasks.py index c5942a644..8a368d626 100644 --- a/passbook/outposts/tasks.py +++ b/passbook/outposts/tasks.py @@ -10,7 +10,14 @@ from structlog import get_logger from passbook.lib.tasks import MonitoredTask, TaskResult, TaskResultStatus from passbook.lib.utils.reflection import path_to_class from passbook.outposts.controllers.base import ControllerException -from passbook.outposts.models import Outpost, OutpostModel, OutpostState, OutpostType +from passbook.outposts.models import ( + DockerServiceConnection, + KubernetesServiceConnection, + Outpost, + OutpostModel, + OutpostState, + OutpostType, +) from passbook.providers.proxy.controllers.docker import ProxyDockerController from passbook.providers.proxy.controllers.kubernetes import ProxyKubernetesController from passbook.root.celery import CELERY_APP @@ -33,10 +40,13 @@ def outpost_controller(self: MonitoredTask, outpost_pk: str): self.set_uid(slugify(outpost.name)) try: if outpost.type == OutpostType.PROXY: - if outpost.deployment_type == OutpostDeploymentType.KUBERNETES: - logs = ProxyKubernetesController(outpost).up_with_logs() - if outpost.deployment_type == OutpostDeploymentType.DOCKER: - logs = ProxyDockerController(outpost).up_with_logs() + service_connection = outpost.service_connection + if isinstance(service_connection, DockerServiceConnection): + logs = ProxyDockerController(outpost, service_connection).up_with_logs() + if isinstance(service_connection, KubernetesServiceConnection): + logs = ProxyKubernetesController( + outpost, service_connection + ).up_with_logs() except ControllerException as exc: self.set_status(TaskResult(TaskResultStatus.ERROR).with_error(exc)) else: @@ -48,10 +58,11 @@ def outpost_pre_delete(outpost_pk: str): """Delete outpost objects before deleting the DB Object""" outpost = Outpost.objects.get(pk=outpost_pk) if outpost.type == OutpostType.PROXY: - if outpost.deployment_type == OutpostDeploymentType.KUBERNETES: - ProxyKubernetesController(outpost).down() - if outpost.deployment_type == OutpostDeploymentType.DOCKER: - ProxyDockerController(outpost).down() + service_connection = outpost.service_connection + if isinstance(service_connection, DockerServiceConnection): + ProxyDockerController(outpost, service_connection).down() + if isinstance(service_connection, KubernetesServiceConnection): + ProxyKubernetesController(outpost, service_connection).down() @CELERY_APP.task() diff --git a/passbook/outposts/tests.py b/passbook/outposts/tests.py index de7454165..6b3d519e6 100644 --- a/passbook/outposts/tests.py +++ b/passbook/outposts/tests.py @@ -11,7 +11,7 @@ from passbook.flows.models import Flow from passbook.outposts.controllers.k8s.base import NeedsUpdate from passbook.outposts.controllers.k8s.deployment import DeploymentReconciler from passbook.outposts.controllers.kubernetes import KubernetesController -from passbook.outposts.models import Outpost, OutpostDeploymentType, OutpostType +from passbook.outposts.models import KubernetesServiceConnection, Outpost, OutpostType from passbook.providers.proxy.models import ProxyProvider @@ -29,7 +29,6 @@ class OutpostTests(TestCase): outpost: Outpost = Outpost.objects.create( name="test", type=OutpostType.PROXY, - deployment_type=OutpostDeploymentType.CUSTOM, ) # Before we add a provider, the user should only have access to the outpost @@ -79,17 +78,18 @@ class OutpostKubernetesTests(TestCase): external_host="http://localhost", authorization_flow=Flow.objects.first(), ) + self.service_connection = KubernetesServiceConnection.objects.get(local=True) self.outpost: Outpost = Outpost.objects.create( name="test", type=OutpostType.PROXY, - deployment_type=OutpostDeploymentType.KUBERNETES, + service_connection=self.service_connection, ) self.outpost.providers.add(self.provider) self.outpost.save() def test_deployment_reconciler(self): """test that deployment requires update""" - controller = KubernetesController(self.outpost) + controller = KubernetesController(self.outpost, self.service_connection) deployment_reconciler = DeploymentReconciler(controller) self.assertIsNotNone(deployment_reconciler.retrieve()) diff --git a/passbook/outposts/views.py b/passbook/outposts/views.py index 3fab9055d..6b71a1e97 100644 --- a/passbook/outposts/views.py +++ b/passbook/outposts/views.py @@ -12,7 +12,12 @@ from structlog import get_logger from passbook.core.models import User from passbook.outposts.controllers.docker import DockerController -from passbook.outposts.models import Outpost, OutpostType +from passbook.outposts.models import ( + DockerServiceConnection, + KubernetesServiceConnection, + Outpost, + OutpostType, +) from passbook.providers.proxy.controllers.kubernetes import ProxyKubernetesController LOGGER = get_logger() @@ -35,7 +40,7 @@ class DockerComposeView(LoginRequiredMixin, View): ) manifest = "" if outpost.type == OutpostType.PROXY: - controller = DockerController(outpost) + controller = DockerController(outpost, DockerServiceConnection()) manifest = controller.get_static_deployment() return HttpResponse(manifest, content_type="text/vnd.yaml") @@ -53,7 +58,9 @@ class KubernetesManifestView(LoginRequiredMixin, View): ) manifest = "" if outpost.type == OutpostType.PROXY: - controller = ProxyKubernetesController(outpost) + controller = ProxyKubernetesController( + outpost, KubernetesServiceConnection() + ) 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 index cbcf06cdc..465b4f8c4 100644 --- a/passbook/providers/proxy/controllers/docker.py +++ b/passbook/providers/proxy/controllers/docker.py @@ -3,15 +3,15 @@ from typing import Dict from urllib.parse import urlparse from passbook.outposts.controllers.docker import DockerController -from passbook.outposts.models import Outpost +from passbook.outposts.models import DockerServiceConnection, Outpost from passbook.providers.proxy.models import ProxyProvider class ProxyDockerController(DockerController): """Proxy Provider Docker Contoller""" - def __init__(self, outpost: Outpost): - super().__init__(outpost) + def __init__(self, outpost: Outpost, connection: DockerServiceConnection): + super().__init__(outpost, connection) self.deployment_ports = { "http": 4180, "https": 4443, diff --git a/passbook/providers/proxy/controllers/kubernetes.py b/passbook/providers/proxy/controllers/kubernetes.py index 4443730db..851938353 100644 --- a/passbook/providers/proxy/controllers/kubernetes.py +++ b/passbook/providers/proxy/controllers/kubernetes.py @@ -1,14 +1,14 @@ """Proxy Provider Kubernetes Contoller""" from passbook.outposts.controllers.kubernetes import KubernetesController -from passbook.outposts.models import Outpost +from passbook.outposts.models import KubernetesServiceConnection, Outpost from passbook.providers.proxy.controllers.k8s.ingress import IngressReconciler class ProxyKubernetesController(KubernetesController): """Proxy Provider Kubernetes Contoller""" - def __init__(self, outpost: Outpost): - super().__init__(outpost) + def __init__(self, outpost: Outpost, connection: KubernetesServiceConnection): + super().__init__(outpost, connection) self.deployment_ports = { "http": 4180, "https": 4443, diff --git a/passbook/providers/proxy/tests.py b/passbook/providers/proxy/tests.py index a4413f6d5..7ccc32751 100644 --- a/passbook/providers/proxy/tests.py +++ b/passbook/providers/proxy/tests.py @@ -6,7 +6,7 @@ import yaml from django.test import TestCase from passbook.flows.models import Flow -from passbook.outposts.models import Outpost, OutpostDeploymentType, OutpostType +from passbook.outposts.models import KubernetesServiceConnection, Outpost, OutpostType from passbook.providers.proxy.controllers.kubernetes import ProxyKubernetesController from passbook.providers.proxy.models import ProxyProvider @@ -23,15 +23,16 @@ class TestControllers(TestCase): external_host="http://localhost", authorization_flow=Flow.objects.first(), ) + service_connection = KubernetesServiceConnection.objects.get(local=True) outpost: Outpost = Outpost.objects.create( name="test", type=OutpostType.PROXY, - deployment_type=OutpostDeploymentType.KUBERNETES, + service_connection=service_connection, ) outpost.providers.add(provider) outpost.save() - controller = ProxyKubernetesController(outpost) + controller = ProxyKubernetesController(outpost, service_connection) manifest = controller.get_static_deployment() self.assertEqual(len(list(yaml.load_all(manifest, Loader=yaml.SafeLoader))), 4) @@ -43,14 +44,15 @@ class TestControllers(TestCase): external_host="http://localhost", authorization_flow=Flow.objects.first(), ) + service_connection = KubernetesServiceConnection.objects.get(local=True) outpost: Outpost = Outpost.objects.create( name="test", type=OutpostType.PROXY, - deployment_type=OutpostDeploymentType.KUBERNETES, + service_connection=service_connection, ) outpost.providers.add(provider) outpost.save() - controller = ProxyKubernetesController(outpost) + controller = ProxyKubernetesController(outpost, service_connection) controller.up() controller.down()