outposts: fix defaults and tests for outposts

This commit is contained in:
Jens Langhammer 2020-11-04 10:54:44 +01:00
parent 706448dc14
commit 3b76af4eaa
14 changed files with 136 additions and 61 deletions

View file

@ -16,9 +16,9 @@ from passbook import __version__
from passbook.core.models import Application from passbook.core.models import Application
from passbook.flows.models import Flow from passbook.flows.models import Flow
from passbook.outposts.models import ( from passbook.outposts.models import (
DockerServiceConnection,
Outpost, Outpost,
OutpostConfig, OutpostConfig,
OutpostDeploymentType,
OutpostType, OutpostType,
) )
from passbook.providers.proxy.models import ProxyProvider from passbook.providers.proxy.models import ProxyProvider
@ -76,7 +76,6 @@ class TestProviderProxy(SeleniumTestCase):
outpost: Outpost = Outpost.objects.create( outpost: Outpost = Outpost.objects.create(
name="proxy_outpost", name="proxy_outpost",
type=OutpostType.PROXY, type=OutpostType.PROXY,
deployment_type=OutpostDeploymentType.CUSTOM,
) )
outpost.providers.add(proxy) outpost.providers.add(proxy)
outpost.save() outpost.save()
@ -128,10 +127,11 @@ class TestProviderProxyConnect(ChannelsLiveServerTestCase):
proxy.save() proxy.save()
# we need to create an application to actually access the proxy # we need to create an application to actually access the proxy
Application.objects.create(name="proxy", slug="proxy", provider=proxy) Application.objects.create(name="proxy", slug="proxy", provider=proxy)
service_connection = DockerServiceConnection.objects.get(local=True)
outpost: Outpost = Outpost.objects.create( outpost: Outpost = Outpost.objects.create(
name="proxy_outpost", name="proxy_outpost",
type=OutpostType.PROXY, type=OutpostType.PROXY,
deployment_type=OutpostDeploymentType.DOCKER, service_connection=service_connection,
_config=asdict( _config=asdict(
OutpostConfig(passbook_host=self.live_server_url, log_level="debug") OutpostConfig(passbook_host=self.live_server_url, log_level="debug")
), ),

View file

@ -28,11 +28,11 @@ class PassbookOutpostConfig(AppConfig):
import_module("passbook.outposts.signals") import_module("passbook.outposts.signals")
try: try:
self.init_local_connection() self.init_local_connection()
except (ProgrammingError): except ProgrammingError:
pass pass
def init_local_connection(self): 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 ( from passbook.outposts.models import (
KubernetesServiceConnection, KubernetesServiceConnection,
DockerServiceConnection, DockerServiceConnection,

View file

@ -20,8 +20,9 @@ class BaseController:
outpost: Outpost outpost: Outpost
connection: OutpostServiceConnection connection: OutpostServiceConnection
def __init__(self, outpost: Outpost): def __init__(self, outpost: Outpost, connection: OutpostServiceConnection):
self.outpost = outpost self.outpost = outpost
self.connection = connection
self.logger = get_logger() self.logger = get_logger()
self.deployment_ports = {} self.deployment_ports = {}

View file

@ -23,8 +23,8 @@ class DockerController(BaseController):
image_base = "beryju/passbook" image_base = "beryju/passbook"
def __init__(self, outpost: Outpost) -> None: def __init__(self, outpost: Outpost, connection: DockerServiceConnection) -> None:
super().__init__(outpost) super().__init__(outpost, connection)
try: try:
if self.connection.local: if self.connection.local:
self.client = DockerClient.from_env() self.client = DockerClient.from_env()

View file

@ -25,8 +25,10 @@ class KubernetesController(BaseController):
connection: KubernetesServiceConnection connection: KubernetesServiceConnection
def __init__(self, outpost: Outpost) -> None: def __init__(
super().__init__(outpost) self, outpost: Outpost, connection: KubernetesServiceConnection
) -> None:
super().__init__(outpost, connection)
try: try:
if self.connection.local: if self.connection.local:
load_incluster_config() load_incluster_config()

View file

@ -10,7 +10,9 @@ def fix_missing_token_identifier(apps: Apps, schema_editor: BaseDatabaseSchemaEd
Token = apps.get_model("passbook_core", "Token") Token = apps.get_model("passbook_core", "Token")
from passbook.outposts.models import Outpost 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_identifier = outpost.user_identifier
user = User.objects.get(username=user_identifier) user = User.objects.get(username=user_identifier)
tokens = Token.objects.filter(user=user) tokens = Token.objects.filter(user=user)

View file

@ -11,16 +11,23 @@ from django.db.backends.base.schema import BaseDatabaseSchemaEditor
def migrate_to_service_connection(apps: Apps, schema_editor: BaseDatabaseSchemaEditor): def migrate_to_service_connection(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
db_alias = schema_editor.connection.alias db_alias = schema_editor.connection.alias
Outpost = apps.get_model("passbook_outposts", "Outpost") Outpost = apps.get_model("passbook_outposts", "Outpost")
DockerServiceConnection = apps.get_model("passbook_outposts", "DockerServiceConnection") DockerServiceConnection = apps.get_model(
KubernetesServiceConnection = apps.get_model("passbook_outposts", "KubernetesServiceConnection") "passbook_outposts", "DockerServiceConnection"
)
KubernetesServiceConnection = apps.get_model(
"passbook_outposts", "KubernetesServiceConnection"
)
from passbook.outposts.apps import PassbookOutpostConfig from passbook.outposts.apps import PassbookOutpostConfig
# Ensure that local connection have been created # Ensure that local connection have been created
PassbookOutpostConfig.init_local_connection(None) PassbookOutpostConfig.init_local_connection(None)
docker = DockerServiceConnection.objects.filter(local=True) docker = DockerServiceConnection.objects.filter(local=True)
k8s = KubernetesServiceConnection.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": if outpost.deployment_type == "kubernetes":
outpost.service_connection = k8s outpost.service_connection = k8s
elif outpost.deployment_type == "docker": elif outpost.deployment_type == "docker":
@ -31,43 +38,85 @@ def migrate_to_service_connection(apps: Apps, schema_editor: BaseDatabaseSchemaE
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('passbook_outposts', '0009_fix_missing_token_identifier'), ("passbook_outposts", "0009_fix_missing_token_identifier"),
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='OutpostServiceConnection', name="OutpostServiceConnection",
fields=[ fields=[
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), (
('name', models.TextField()), "uuid",
('local', models.BooleanField(default=False, help_text='If enabled, use the local connection. Required Docker socket/Kubernetes Integration', unique=True)), 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( migrations.CreateModel(
name='DockerServiceConnection', name="DockerServiceConnection",
fields=[ 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()), "outpostserviceconnection_ptr",
('tls', models.BooleanField()), 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( migrations.CreateModel(
name='KubernetesServiceConnection', name="KubernetesServiceConnection",
fields=[ 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( migrations.AddField(
model_name='outpost', model_name="outpost",
name='service_connection', 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'), 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.RunPython(migrate_to_service_connection),
migrations.RemoveField( migrations.RemoveField(
model_name='outpost', model_name="outpost",
name='deployment_type', name="deployment_type",
), ),
] ]

View file

@ -18,6 +18,7 @@ from packaging.version import LegacyVersion, Version, parse
from passbook import __version__ 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.utils.template import render_to_string from passbook.lib.utils.template import render_to_string
OUR_VERSION = parse(__version__) OUR_VERSION = parse(__version__)
@ -106,7 +107,7 @@ class Outpost(models.Model):
name = models.TextField() name = models.TextField()
type = models.TextField(choices=OutpostType.choices, default=OutpostType.PROXY) type = models.TextField(choices=OutpostType.choices, default=OutpostType.PROXY)
service_connection = models.ForeignKey( service_connection = InheritanceForeignKey(
OutpostServiceConnection, OutpostServiceConnection,
default=None, default=None,
null=True, null=True,

View file

@ -10,7 +10,14 @@ from structlog import get_logger
from passbook.lib.tasks import MonitoredTask, TaskResult, TaskResultStatus from passbook.lib.tasks import MonitoredTask, TaskResult, TaskResultStatus
from passbook.lib.utils.reflection import path_to_class from passbook.lib.utils.reflection import path_to_class
from passbook.outposts.controllers.base import ControllerException 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.docker import ProxyDockerController
from passbook.providers.proxy.controllers.kubernetes import ProxyKubernetesController from passbook.providers.proxy.controllers.kubernetes import ProxyKubernetesController
from passbook.root.celery import CELERY_APP 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)) self.set_uid(slugify(outpost.name))
try: try:
if outpost.type == OutpostType.PROXY: if outpost.type == OutpostType.PROXY:
if outpost.deployment_type == OutpostDeploymentType.KUBERNETES: service_connection = outpost.service_connection
logs = ProxyKubernetesController(outpost).up_with_logs() if isinstance(service_connection, DockerServiceConnection):
if outpost.deployment_type == OutpostDeploymentType.DOCKER: logs = ProxyDockerController(outpost, service_connection).up_with_logs()
logs = ProxyDockerController(outpost).up_with_logs() if isinstance(service_connection, KubernetesServiceConnection):
logs = ProxyKubernetesController(
outpost, service_connection
).up_with_logs()
except ControllerException as exc: except ControllerException as exc:
self.set_status(TaskResult(TaskResultStatus.ERROR).with_error(exc)) self.set_status(TaskResult(TaskResultStatus.ERROR).with_error(exc))
else: else:
@ -48,10 +58,11 @@ def outpost_pre_delete(outpost_pk: str):
"""Delete outpost objects before deleting the DB Object""" """Delete outpost objects before deleting the DB Object"""
outpost = Outpost.objects.get(pk=outpost_pk) outpost = Outpost.objects.get(pk=outpost_pk)
if outpost.type == OutpostType.PROXY: if outpost.type == OutpostType.PROXY:
if outpost.deployment_type == OutpostDeploymentType.KUBERNETES: service_connection = outpost.service_connection
ProxyKubernetesController(outpost).down() if isinstance(service_connection, DockerServiceConnection):
if outpost.deployment_type == OutpostDeploymentType.DOCKER: ProxyDockerController(outpost, service_connection).down()
ProxyDockerController(outpost).down() if isinstance(service_connection, KubernetesServiceConnection):
ProxyKubernetesController(outpost, service_connection).down()
@CELERY_APP.task() @CELERY_APP.task()

View file

@ -11,7 +11,7 @@ from passbook.flows.models import Flow
from passbook.outposts.controllers.k8s.base import NeedsUpdate from passbook.outposts.controllers.k8s.base import NeedsUpdate
from passbook.outposts.controllers.k8s.deployment import DeploymentReconciler from passbook.outposts.controllers.k8s.deployment import DeploymentReconciler
from passbook.outposts.controllers.kubernetes import KubernetesController 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 from passbook.providers.proxy.models import ProxyProvider
@ -29,7 +29,6 @@ class OutpostTests(TestCase):
outpost: Outpost = Outpost.objects.create( outpost: Outpost = Outpost.objects.create(
name="test", name="test",
type=OutpostType.PROXY, type=OutpostType.PROXY,
deployment_type=OutpostDeploymentType.CUSTOM,
) )
# Before we add a provider, the user should only have access to the outpost # 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", external_host="http://localhost",
authorization_flow=Flow.objects.first(), authorization_flow=Flow.objects.first(),
) )
self.service_connection = KubernetesServiceConnection.objects.get(local=True)
self.outpost: Outpost = Outpost.objects.create( self.outpost: Outpost = Outpost.objects.create(
name="test", name="test",
type=OutpostType.PROXY, type=OutpostType.PROXY,
deployment_type=OutpostDeploymentType.KUBERNETES, service_connection=self.service_connection,
) )
self.outpost.providers.add(self.provider) self.outpost.providers.add(self.provider)
self.outpost.save() self.outpost.save()
def test_deployment_reconciler(self): def test_deployment_reconciler(self):
"""test that deployment requires update""" """test that deployment requires update"""
controller = KubernetesController(self.outpost) controller = KubernetesController(self.outpost, self.service_connection)
deployment_reconciler = DeploymentReconciler(controller) deployment_reconciler = DeploymentReconciler(controller)
self.assertIsNotNone(deployment_reconciler.retrieve()) self.assertIsNotNone(deployment_reconciler.retrieve())

View file

@ -12,7 +12,12 @@ from structlog import get_logger
from passbook.core.models import User from passbook.core.models import User
from passbook.outposts.controllers.docker import DockerController 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 from passbook.providers.proxy.controllers.kubernetes import ProxyKubernetesController
LOGGER = get_logger() LOGGER = get_logger()
@ -35,7 +40,7 @@ class DockerComposeView(LoginRequiredMixin, View):
) )
manifest = "" manifest = ""
if outpost.type == OutpostType.PROXY: if outpost.type == OutpostType.PROXY:
controller = DockerController(outpost) controller = DockerController(outpost, DockerServiceConnection())
manifest = controller.get_static_deployment() manifest = controller.get_static_deployment()
return HttpResponse(manifest, content_type="text/vnd.yaml") return HttpResponse(manifest, content_type="text/vnd.yaml")
@ -53,7 +58,9 @@ class KubernetesManifestView(LoginRequiredMixin, View):
) )
manifest = "" manifest = ""
if outpost.type == OutpostType.PROXY: if outpost.type == OutpostType.PROXY:
controller = ProxyKubernetesController(outpost) controller = ProxyKubernetesController(
outpost, KubernetesServiceConnection()
)
manifest = controller.get_static_deployment() manifest = controller.get_static_deployment()
return HttpResponse(manifest, content_type="text/vnd.yaml") return HttpResponse(manifest, content_type="text/vnd.yaml")

View file

@ -3,15 +3,15 @@ from typing import Dict
from urllib.parse import urlparse from urllib.parse import urlparse
from passbook.outposts.controllers.docker import DockerController 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 from passbook.providers.proxy.models import ProxyProvider
class ProxyDockerController(DockerController): class ProxyDockerController(DockerController):
"""Proxy Provider Docker Contoller""" """Proxy Provider Docker Contoller"""
def __init__(self, outpost: Outpost): def __init__(self, outpost: Outpost, connection: DockerServiceConnection):
super().__init__(outpost) super().__init__(outpost, connection)
self.deployment_ports = { self.deployment_ports = {
"http": 4180, "http": 4180,
"https": 4443, "https": 4443,

View file

@ -1,14 +1,14 @@
"""Proxy Provider Kubernetes Contoller""" """Proxy Provider Kubernetes Contoller"""
from passbook.outposts.controllers.kubernetes import KubernetesController 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 from passbook.providers.proxy.controllers.k8s.ingress import IngressReconciler
class ProxyKubernetesController(KubernetesController): class ProxyKubernetesController(KubernetesController):
"""Proxy Provider Kubernetes Contoller""" """Proxy Provider Kubernetes Contoller"""
def __init__(self, outpost: Outpost): def __init__(self, outpost: Outpost, connection: KubernetesServiceConnection):
super().__init__(outpost) super().__init__(outpost, connection)
self.deployment_ports = { self.deployment_ports = {
"http": 4180, "http": 4180,
"https": 4443, "https": 4443,

View file

@ -6,7 +6,7 @@ import yaml
from django.test import TestCase from django.test import TestCase
from passbook.flows.models import Flow 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.controllers.kubernetes import ProxyKubernetesController
from passbook.providers.proxy.models import ProxyProvider from passbook.providers.proxy.models import ProxyProvider
@ -23,15 +23,16 @@ class TestControllers(TestCase):
external_host="http://localhost", external_host="http://localhost",
authorization_flow=Flow.objects.first(), authorization_flow=Flow.objects.first(),
) )
service_connection = KubernetesServiceConnection.objects.get(local=True)
outpost: Outpost = Outpost.objects.create( outpost: Outpost = Outpost.objects.create(
name="test", name="test",
type=OutpostType.PROXY, type=OutpostType.PROXY,
deployment_type=OutpostDeploymentType.KUBERNETES, service_connection=service_connection,
) )
outpost.providers.add(provider) outpost.providers.add(provider)
outpost.save() outpost.save()
controller = ProxyKubernetesController(outpost) controller = ProxyKubernetesController(outpost, service_connection)
manifest = controller.get_static_deployment() manifest = controller.get_static_deployment()
self.assertEqual(len(list(yaml.load_all(manifest, Loader=yaml.SafeLoader))), 4) self.assertEqual(len(list(yaml.load_all(manifest, Loader=yaml.SafeLoader))), 4)
@ -43,14 +44,15 @@ class TestControllers(TestCase):
external_host="http://localhost", external_host="http://localhost",
authorization_flow=Flow.objects.first(), authorization_flow=Flow.objects.first(),
) )
service_connection = KubernetesServiceConnection.objects.get(local=True)
outpost: Outpost = Outpost.objects.create( outpost: Outpost = Outpost.objects.create(
name="test", name="test",
type=OutpostType.PROXY, type=OutpostType.PROXY,
deployment_type=OutpostDeploymentType.KUBERNETES, service_connection=service_connection,
) )
outpost.providers.add(provider) outpost.providers.add(provider)
outpost.save() outpost.save()
controller = ProxyKubernetesController(outpost) controller = ProxyKubernetesController(outpost, service_connection)
controller.up() controller.up()
controller.down() controller.down()