admin: add views for outpost service-connections

This commit is contained in:
Jens Langhammer 2020-11-04 13:01:38 +01:00
parent bd74e518a7
commit c04d0a373a
14 changed files with 413 additions and 29 deletions

View File

@ -46,11 +46,28 @@
{% trans 'Providers' %} {% trans 'Providers' %}
</a> </a>
</li> </li>
<li class="pf-c-nav__item"> <li class="pf-c-nav__item pf-m-expanded">
<a href="{% url 'passbook_admin:outposts' %}" <a href="#" class="pf-c-nav__link" aria-expanded="true">{% trans 'Outposts' %}
class="pf-c-nav__link {% is_active 'passbook_admin:outposts' 'passbook_admin:outpost-create' 'passbook_admin:outpost-update' 'passbook_admin:outpost-delete' %}"> <span class="pf-c-nav__toggle">
{% trans 'Outposts' %} <i class="fas fa-angle-right" aria-hidden="true"></i>
</span>
</a> </a>
<section class="pf-c-nav__subnav">
<ul class="pf-c-nav__simple-list">
<li class="pf-c-nav__item">
<a href="{% url 'passbook_admin:outposts' %}"
class="pf-c-nav__link {% is_active 'passbook_admin:outposts' 'passbook_admin:outpost-create' 'passbook_admin:outpost-update' 'passbook_admin:outpost-delete' %}">
{% trans 'Outposts' %}
</a>
</li>
<li class="pf-c-nav__item">
<a href="{% url 'passbook_admin:outpost-service-connections' %}"
class="pf-c-nav__link {% is_active 'passbook_admin:outpost-service-connections' 'passbook_admin:outpost-service-connections-create' 'passbook_admin:outpost-service-connections-update' 'passbook_admin:outpost-service-connections-delete' %}">
{% trans 'Service Connections' %}
</a>
</li>
</ul>
</section>
</li> </li>
<li class="pf-c-nav__item"> <li class="pf-c-nav__item">
<a href="{% url 'passbook_admin:property-mappings' %}" <a href="{% url 'passbook_admin:property-mappings' %}"

View File

@ -0,0 +1,125 @@
{% extends "administration/base.html" %}
{% load i18n %}
{% load humanize %}
{% load passbook_utils %}
{% load admin_reflection %}
{% block content %}
<section class="pf-c-page__main-section pf-m-light">
<div class="pf-c-content">
<h1>
<i class="pf-icon-integration"></i>
{% trans 'Outpost Service-Connections' %}
</h1>
<p>{% trans "Outpost Service-Connections define how passbook connects to external platforms to manage and deploy Outposts." %}</p>
</div>
</section>
<section class="pf-c-page__main-section pf-m-no-padding-mobile">
<div class="pf-c-card">
{% if object_list %}
<div class="pf-c-toolbar">
<div class="pf-c-toolbar__content">
{% include 'partials/toolbar_search.html' %}
<div class="pf-c-toolbar__bulk-select">
<div class="pf-c-dropdown">
<button class="pf-m-primary pf-c-dropdown__toggle" type="button">
<span class="pf-c-dropdown__toggle-text">{% trans 'Create' %}</span>
<i class="fas fa-caret-down pf-c-dropdown__toggle-icon" aria-hidden="true"></i>
</button>
<ul class="pf-c-dropdown__menu" hidden>
{% for type, name in types.items %}
<li>
<a class="pf-c-dropdown__menu-item" href="{% url 'passbook_admin:outpost-service-connection-create' %}?type={{ type }}&back={{ request.get_full_path }}">
{{ name|verbose_name }}<br>
<small>
{{ name|doc }}
</small>
</a>
</li>
{% endfor %}
</ul>
</div>
</div>
{% include 'partials/pagination.html' %}
</div>
</div>
<table class="pf-c-table pf-m-compact pf-m-grid-xl" role="grid">
<thead>
<tr role="row">
<th role="columnheader" scope="col">{% trans 'Name' %}</th>
<th role="columnheader" scope="col">{% trans 'Type' %}</th>
<th role="columnheader" scope="col">{% trans 'Local?' %}</th>
<th role="cell"></th>
</tr>
</thead>
<tbody role="rowgroup">
{% for sc in object_list %}
<tr role="row">
<th role="columnheader">
<span>{{ sc.name }}</span>
</th>
<td role="cell">
<span>
{{ sc|verbose_name }}
</span>
</td>
<td role="cell">
<span>
{{ sc.local|yesno:"Yes,No" }}
</span>
</td>
<td>
<a class="pf-c-button pf-m-secondary" href="{% url 'passbook_admin:outpost-service-connection-update' pk=sc.pk %}?back={{ request.get_full_path }}">{% trans 'Edit' %}</a>
<a class="pf-c-button pf-m-danger" href="{% url 'passbook_admin:outpost-service-connection-delete' pk=sc.pk %}?back={{ request.get_full_path }}">{% trans 'Delete' %}</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<div class="pf-c-pagination pf-m-bottom">
{% include 'partials/pagination.html' %}
</div>
{% else %}
<div class="pf-c-toolbar">
<div class="pf-c-toolbar__content">
{% include 'partials/toolbar_search.html' %}
</div>
</div>
<div class="pf-c-empty-state">
<div class="pf-c-empty-state__content">
<i class="fas fa-map-marker pf-c-empty-state__icon" aria-hidden="true"></i>
<h1 class="pf-c-title pf-m-lg">
{% trans 'No Outpost Service Connections.' %}
</h1>
<div class="pf-c-empty-state__body">
{% if request.GET.search != "" %}
{% trans "Your search query doesn't match any outposts." %}
{% else %}
{% trans 'Currently no service connections exist. Click the button below to create one.' %}
{% endif %}
</div>
<div class="pf-c-dropdown">
<button class="pf-m-primary pf-c-dropdown__toggle" type="button">
<span class="pf-c-dropdown__toggle-text">{% trans 'Create' %}</span>
<i class="fas fa-caret-down pf-c-dropdown__toggle-icon" aria-hidden="true"></i>
</button>
<ul class="pf-c-dropdown__menu" hidden>
{% for type, name in types.items %}
<li>
<a class="pf-c-dropdown__menu-item" href="{% url 'passbook_admin:outpost-service-connection-create' %}?type={{ type }}&back={{ request.get_full_path }}">
{{ name|verbose_name }}<br>
<small>
{{ name|doc }}
</small>
</a>
</li>
{% endfor %}
</ul>
</div>
</div>
</div>
{% endif %}
</div>
</section>
{% endblock %}

View File

@ -7,10 +7,11 @@ from passbook.admin.views import (
flows, flows,
groups, groups,
outposts, outposts,
outposts_service_connections,
overview, overview,
policies, policies,
policies_bindings, policies_bindings,
property_mapping, property_mappings,
providers, providers,
sources, sources,
stages, stages,
@ -225,22 +226,22 @@ urlpatterns = [
# Property Mappings # Property Mappings
path( path(
"property-mappings/", "property-mappings/",
property_mapping.PropertyMappingListView.as_view(), property_mappings.PropertyMappingListView.as_view(),
name="property-mappings", name="property-mappings",
), ),
path( path(
"property-mappings/create/", "property-mappings/create/",
property_mapping.PropertyMappingCreateView.as_view(), property_mappings.PropertyMappingCreateView.as_view(),
name="property-mapping-create", name="property-mapping-create",
), ),
path( path(
"property-mappings/<uuid:pk>/update/", "property-mappings/<uuid:pk>/update/",
property_mapping.PropertyMappingUpdateView.as_view(), property_mappings.PropertyMappingUpdateView.as_view(),
name="property-mapping-update", name="property-mapping-update",
), ),
path( path(
"property-mappings/<uuid:pk>/delete/", "property-mappings/<uuid:pk>/delete/",
property_mapping.PropertyMappingDeleteView.as_view(), property_mappings.PropertyMappingDeleteView.as_view(),
name="property-mapping-delete", name="property-mapping-delete",
), ),
# Users # Users
@ -312,6 +313,27 @@ urlpatterns = [
outposts.OutpostDeleteView.as_view(), outposts.OutpostDeleteView.as_view(),
name="outpost-delete", name="outpost-delete",
), ),
# Outpost Service Connections
path(
"outposts/service_connections/",
outposts_service_connections.OutpostServiceConnectionListView.as_view(),
name="outpost-service-connections",
),
path(
"outposts/service_connections/create/",
outposts_service_connections.OutpostServiceConnectionCreateView.as_view(),
name="outpost-service-connection-create",
),
path(
"outposts/service_connections/<uuid:pk>/update/",
outposts_service_connections.OutpostServiceConnectionUpdateView.as_view(),
name="outpost-service-connection-update",
),
path(
"outposts/service_connections/<uuid:pk>/delete/",
outposts_service_connections.OutpostServiceConnectionDeleteView.as_view(),
name="outpost-service-connection-delete",
),
# Tasks # Tasks
path( path(
"tasks/", "tasks/",

View File

@ -0,0 +1,83 @@
"""passbook OutpostServiceConnection administration"""
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.mixins import (
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
)
from django.contrib.messages.views import SuccessMessageMixin
from django.urls import reverse_lazy
from django.utils.translation import gettext as _
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
from passbook.admin.views.utils import (
BackSuccessUrlMixin,
DeleteMessageView,
InheritanceCreateView,
InheritanceListView,
InheritanceUpdateView,
SearchListMixin,
UserPaginateListMixin,
)
from passbook.outposts.models import OutpostServiceConnection
class OutpostServiceConnectionListView(
LoginRequiredMixin,
PermissionListMixin,
UserPaginateListMixin,
SearchListMixin,
InheritanceListView,
):
"""Show list of all outpost-service-connections"""
model = OutpostServiceConnection
permission_required = "passbook_outposts.add_outpostserviceconnection"
template_name = "administration/outpost_service_connection/list.html"
ordering = "pk"
search_fields = ["pk", "name"]
class OutpostServiceConnectionCreateView(
SuccessMessageMixin,
BackSuccessUrlMixin,
LoginRequiredMixin,
DjangoPermissionRequiredMixin,
InheritanceCreateView,
):
"""Create new OutpostServiceConnection"""
model = OutpostServiceConnection
permission_required = "passbook_outposts.add_outpostserviceconnection"
template_name = "generic/create.html"
success_url = reverse_lazy("passbook_admin:outpost-service-connections")
success_message = _("Successfully created OutpostServiceConnection")
class OutpostServiceConnectionUpdateView(
SuccessMessageMixin,
BackSuccessUrlMixin,
LoginRequiredMixin,
PermissionRequiredMixin,
InheritanceUpdateView,
):
"""Update outpostserviceconnection"""
model = OutpostServiceConnection
permission_required = "passbook_outposts.change_outpostserviceconnection"
template_name = "generic/update.html"
success_url = reverse_lazy("passbook_admin:outpost-service-connections")
success_message = _("Successfully updated OutpostServiceConnection")
class OutpostServiceConnectionDeleteView(
LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageView
):
"""Delete outpostserviceconnection"""
model = OutpostServiceConnection
permission_required = "passbook_outposts.delete_outpostserviceconnection"
template_name = "generic/delete.html"
success_url = reverse_lazy("passbook_admin:outpost-service-connections")
success_message = _("Successfully deleted OutpostServiceConnection")

View File

@ -32,8 +32,8 @@ class ProviderListView(
model = Provider model = Provider
permission_required = "passbook_core.add_provider" permission_required = "passbook_core.add_provider"
template_name = "administration/provider/list.html" template_name = "administration/provider/list.html"
ordering = "id" ordering = "pk"
search_fields = ["id", "name"] search_fields = ["pk", "name"]
class ProviderCreateView( class ProviderCreateView(

View File

@ -19,7 +19,11 @@ from passbook.core.api.tokens import TokenViewSet
from passbook.core.api.users import UserViewSet from passbook.core.api.users import UserViewSet
from passbook.crypto.api import CertificateKeyPairViewSet from passbook.crypto.api import CertificateKeyPairViewSet
from passbook.flows.api import FlowStageBindingViewSet, FlowViewSet, StageViewSet from passbook.flows.api import FlowStageBindingViewSet, FlowViewSet, StageViewSet
from passbook.outposts.api import OutpostViewSet, DockerServiceConnectionViewSet, KubernetesServiceConnectionViewSet from passbook.outposts.api import (
DockerServiceConnectionViewSet,
KubernetesServiceConnectionViewSet,
OutpostViewSet,
)
from passbook.policies.api import PolicyBindingViewSet, PolicyViewSet from passbook.policies.api import PolicyBindingViewSet, PolicyViewSet
from passbook.policies.dummy.api import DummyPolicyViewSet from passbook.policies.dummy.api import DummyPolicyViewSet
from passbook.policies.expiry.api import PasswordExpiryPolicyViewSet from passbook.policies.expiry.api import PasswordExpiryPolicyViewSet
@ -67,7 +71,9 @@ router.register("core/tokens", TokenViewSet)
router.register("outposts/outposts", OutpostViewSet) router.register("outposts/outposts", OutpostViewSet)
router.register("outposts/service_connections/docker", DockerServiceConnectionViewSet) router.register("outposts/service_connections/docker", DockerServiceConnectionViewSet)
router.register("outposts/service_connections/kubernetes", KubernetesServiceConnectionViewSet) router.register(
"outposts/service_connections/kubernetes", KubernetesServiceConnectionViewSet
)
router.register("outposts/proxy", ProxyOutpostConfigViewSet) router.register("outposts/proxy", ProxyOutpostConfigViewSet)
router.register("flows/instances", FlowViewSet) router.register("flows/instances", FlowViewSet)

View File

@ -49,7 +49,7 @@ class KubernetesServiceConnectionSerializer(ModelSerializer):
class Meta: class Meta:
model = KubernetesServiceConnection model = KubernetesServiceConnection
fields = ["pk", "name", "local", "config"] fields = ["pk", "name", "local", "kubeconfig"]
class KubernetesServiceConnectionViewSet(ModelViewSet): class KubernetesServiceConnectionViewSet(ModelViewSet):

View File

@ -6,6 +6,7 @@ from pathlib import Path
from socket import gethostname from socket import gethostname
from urllib.parse import urlparse from urllib.parse import urlparse
import yaml
from django.apps import AppConfig from django.apps import AppConfig
from django.db import ProgrammingError from django.db import ProgrammingError
from docker.constants import DEFAULT_UNIX_SOCKET from docker.constants import DEFAULT_UNIX_SOCKET
@ -43,19 +44,21 @@ class PassbookOutpostConfig(AppConfig):
if not KubernetesServiceConnection.objects.filter(local=True).exists(): if not KubernetesServiceConnection.objects.filter(local=True).exists():
LOGGER.debug("Created Service Connection for in-cluster") LOGGER.debug("Created Service Connection for in-cluster")
KubernetesServiceConnection.objects.create( KubernetesServiceConnection.objects.create(
name="Local Kubernetes Cluster", local=True, config={} name="Local Kubernetes Cluster", local=True, kubeconfig={}
) )
# For development, check for the existence of a kubeconfig file # For development, check for the existence of a kubeconfig file
kubeconfig_path = expanduser(KUBE_CONFIG_DEFAULT_LOCATION) kubeconfig_path = expanduser(KUBE_CONFIG_DEFAULT_LOCATION)
if Path(kubeconfig_path).exists(): if Path(kubeconfig_path).exists():
LOGGER.debug("Detected kubeconfig") LOGGER.debug("Detected kubeconfig")
kubeconfig_local_name = f"k8s-{gethostname()}"
if not KubernetesServiceConnection.objects.filter( if not KubernetesServiceConnection.objects.filter(
name=gethostname() name=kubeconfig_local_name
).exists(): ).exists():
LOGGER.debug("Creating kubeconfig Service Connection") LOGGER.debug("Creating kubeconfig Service Connection")
with open(kubeconfig_path, "r") as _kubeconfig: with open(kubeconfig_path, "r") as _kubeconfig:
KubernetesServiceConnection.objects.create( KubernetesServiceConnection.objects.create(
name=gethostname(), config=_kubeconfig.read() name=kubeconfig_local_name,
kubeconfig=yaml.safe_load(_kubeconfig),
) )
unix_socket_path = urlparse(DEFAULT_UNIX_SOCKET).path unix_socket_path = urlparse(DEFAULT_UNIX_SOCKET).path
socket = Path(unix_socket_path) socket = Path(unix_socket_path)

View File

@ -33,7 +33,7 @@ class KubernetesController(BaseController):
if self.connection.local: if self.connection.local:
load_incluster_config() load_incluster_config()
else: else:
load_kube_config_from_dict(self.connection.config) load_kube_config_from_dict(self.connection.kubeconfig)
except ConfigException: except ConfigException:
load_kube_config() load_kube_config()
self.reconcilers = { self.reconcilers = {

View File

@ -4,7 +4,11 @@ from django import forms
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from passbook.admin.fields import CodeMirrorWidget, YAMLField from passbook.admin.fields import CodeMirrorWidget, YAMLField
from passbook.outposts.models import Outpost from passbook.outposts.models import (
DockerServiceConnection,
KubernetesServiceConnection,
Outpost,
)
from passbook.providers.proxy.models import ProxyProvider from passbook.providers.proxy.models import ProxyProvider
@ -33,3 +37,35 @@ class OutpostForm(forms.ModelForm):
"_config": YAMLField, "_config": YAMLField,
} }
labels = {"_config": _("Configuration")} labels = {"_config": _("Configuration")}
class DockerServiceConnectionForm(forms.ModelForm):
"""Docker service-connection form"""
class Meta:
model = DockerServiceConnection
fields = ["name", "local", "url", "tls"]
widgets = {
"name": forms.TextInput,
}
class KubernetesServiceConnectionForm(forms.ModelForm):
"""Kubernetes service-connection form"""
class Meta:
model = KubernetesServiceConnection
fields = [
"name",
"local",
"kubeconfig",
]
widgets = {
"name": forms.TextInput,
"kubeconfig": CodeMirrorWidget,
}
field_classes = {
"kubeconfig": YAMLField,
}

View File

@ -7,6 +7,8 @@ from django.apps.registry import Apps
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
import passbook.lib.models
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
@ -98,7 +100,7 @@ class Migration(migrations.Migration):
to="passbook_outposts.outpostserviceconnection", to="passbook_outposts.outpostserviceconnection",
), ),
), ),
("config", models.JSONField()), ("kubeconfig", models.JSONField()),
], ],
bases=("passbook_outposts.outpostserviceconnection",), bases=("passbook_outposts.outpostserviceconnection",),
), ),
@ -119,4 +121,46 @@ class Migration(migrations.Migration):
model_name="outpost", model_name="outpost",
name="deployment_type", name="deployment_type",
), ),
migrations.AlterModelOptions(
name="dockerserviceconnection",
options={
"verbose_name": "Docker Service-Connection",
"verbose_name_plural": "Docker Service-Connections",
},
),
migrations.AlterModelOptions(
name="kubernetesserviceconnection",
options={
"verbose_name": "Kubernetes Service-Connection",
"verbose_name_plural": "Kubernetes Service-Connections",
},
),
migrations.AlterField(
model_name="outpost",
name="service_connection",
field=passbook.lib.models.InheritanceForeignKey(
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.AlterModelOptions(
name="outpostserviceconnection",
options={
"verbose_name": "Outpost Service-Connection",
"verbose_name_plural": "Outpost Service-Connections",
},
),
migrations.AlterField(
model_name="kubernetesserviceconnection",
name="kubeconfig",
field=models.JSONField(
default=None,
help_text="Paste your kubeconfig here. passbook will automatically use the currently selected context.",
),
preserve_default=False,
),
] ]

View File

@ -1,13 +1,14 @@
"""Outpost models""" """Outpost models"""
from dataclasses import asdict, dataclass, field from dataclasses import asdict, dataclass, field
from datetime import datetime from datetime import datetime
from typing import Dict, Iterable, List, Optional, Union 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.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.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 guardian.models import UserObjectPermission from guardian.models import UserObjectPermission
@ -86,18 +87,63 @@ class OutpostServiceConnection(models.Model):
objects = InheritanceManager() objects = InheritanceManager()
@property
def form(self) -> Type[ModelForm]:
"""Return Form class used to edit this object"""
raise NotImplementedError
class Meta:
verbose_name = _("Outpost Service-Connection")
verbose_name_plural = _("Outpost Service-Connections")
class DockerServiceConnection(OutpostServiceConnection): class DockerServiceConnection(OutpostServiceConnection):
"""Service Connection to a docker endpoint""" """Service Connection to a Docker endpoint"""
url = models.TextField() url = models.TextField()
tls = models.BooleanField() tls = models.BooleanField()
@property
def form(self) -> Type[ModelForm]:
from passbook.outposts.forms import DockerServiceConnectionForm
return DockerServiceConnectionForm
def __str__(self) -> str:
return f"Docker Service-Connection {self.name}"
class Meta:
verbose_name = _("Docker Service-Connection")
verbose_name_plural = _("Docker Service-Connections")
class KubernetesServiceConnection(OutpostServiceConnection): class KubernetesServiceConnection(OutpostServiceConnection):
"""Service Connection to a kubernetes cluster""" """Service Connection to a Kubernetes cluster"""
config = models.JSONField() kubeconfig = models.JSONField(
help_text=_(
(
"Paste your kubeconfig here. passbook will automatically use "
"the currently selected context."
)
)
)
@property
def form(self) -> Type[ModelForm]:
from passbook.outposts.forms import KubernetesServiceConnectionForm
return KubernetesServiceConnectionForm
def __str__(self) -> str:
return f"Kubernetes Service-Connection {self.name}"
class Meta:
verbose_name = _("Kubernetes Service-Connection")
verbose_name_plural = _("Kubernetes Service-Connections")
class Outpost(models.Model): class Outpost(models.Model):

View File

@ -1476,7 +1476,7 @@ paths:
parameters: parameters:
- name: uuid - name: uuid
in: path in: path
description: A UUID string identifying this docker service connection. description: A UUID string identifying this Docker Service-Connection.
required: true required: true
type: string type: string
format: uuid format: uuid
@ -1603,7 +1603,7 @@ paths:
parameters: parameters:
- name: uuid - name: uuid
in: path in: path
description: A UUID string identifying this kubernetes service connection. description: A UUID string identifying this Kubernetes Service-Connection.
required: true required: true
type: string type: string
format: uuid format: uuid
@ -6888,7 +6888,7 @@ definitions:
description: KubernetesServiceConnection Serializer description: KubernetesServiceConnection Serializer
required: required:
- name - name
- config - kubeconfig
type: object type: object
properties: properties:
pk: pk:
@ -6905,8 +6905,10 @@ definitions:
description: If enabled, use the local connection. Required Docker socket/Kubernetes description: If enabled, use the local connection. Required Docker socket/Kubernetes
Integration Integration
type: boolean type: boolean
config: kubeconfig:
title: Config title: Kubeconfig
description: Paste your kubeconfig here. passbook will automatically use the
currently selected context.
type: object type: object
Policy: Policy:
description: Policy Serializer description: Policy Serializer