initial version of osb

This commit is contained in:
Tobias Brunner 2025-01-27 17:42:40 +01:00
parent 7143234e22
commit 022f0ad60f
No known key found for this signature in database
14 changed files with 281 additions and 22 deletions

View file

@ -8,6 +8,29 @@ env.read_env()
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"verbose": {
"format": "{levelname} {asctime} {module} {process:d} {thread:d} {message}",
"style": "{",
},
},
"handlers": {
"console": {
"class": "logging.StreamHandler",
"formatter": "verbose",
}
},
"loggers": {
"odoo_api": {
"handlers": ["console"],
"level": "DEBUG",
"propagate": True,
},
},
}
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/
@ -33,8 +56,10 @@ INSTALLED_APPS = [
"django.contrib.staticfiles",
# 3rd party
"django_prose_editor",
"rest_framework",
# local
"services",
"servicebroker",
]
if DEBUG:
INSTALLED_APPS += ["django_browser_reload"]
@ -139,26 +164,13 @@ ODOO_CONFIG = {
"password": env.str("ODOO_PASSWORD"),
}
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"verbose": {
"format": "{levelname} {asctime} {module} {process:d} {thread:d} {message}",
"style": "{",
},
},
"handlers": {
"console": {
"class": "logging.StreamHandler",
"formatter": "verbose",
}
},
"loggers": {
"odoo_api": {
"handlers": ["console"],
"level": "DEBUG",
"propagate": True,
},
},
BROKER_USERNAME = env.str("BROKER_USERNAME")
BROKER_PASSWORD = env.str("BROKER_PASSWORD")
BASE_URL = "https://your-domain.com"
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": [
"servicebroker.authentication.ServiceBrokerAuthentication",
],
"UNAUTHENTICATED_USER": None,
}

View file

@ -6,6 +6,7 @@ from django.urls import path, include
urlpatterns = [
path("admin/", admin.site.urls),
path("", include("services.urls")),
path("broker/", include("servicebroker.urls")),
]
if settings.DEBUG:
urlpatterns += [

View file

View file

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

View file

@ -0,0 +1,6 @@
from django.apps import AppConfig
class ServicebrokerConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "servicebroker"

View file

@ -0,0 +1,34 @@
from django.conf import settings
from django.contrib.auth.models import User
from rest_framework import authentication
from rest_framework import exceptions
class ServiceBrokerAuthentication(authentication.BaseAuthentication):
def authenticate(self, request):
auth = request.META.get("HTTP_AUTHORIZATION")
if not auth:
return None
try:
import base64
auth_type, auth_string = auth.split(" ")
if auth_type.lower() != "basic":
return None
decoded = base64.b64decode(auth_string).decode("utf-8")
username, password = decoded.split(":")
if (
username == settings.BROKER_USERNAME
and password == settings.BROKER_PASSWORD
):
# Use a dummy user for authentication
user = User(username=username, is_staff=True)
return (user, None)
except Exception as e:
raise exceptions.AuthenticationFailed("Invalid credentials")
raise exceptions.AuthenticationFailed("Invalid credentials")

View file

View file

@ -0,0 +1,3 @@
from django.db import models
# Create your models here.

View file

@ -0,0 +1,51 @@
from rest_framework import serializers
from services.models import Service, ServiceLevel, CloudProvider
class ServicePlanSerializer(serializers.Serializer):
id = serializers.CharField()
name = serializers.CharField()
description = serializers.CharField()
metadata = serializers.DictField()
free = serializers.BooleanField(default=False)
class ServiceSerializer(serializers.Serializer):
id = serializers.CharField()
name = serializers.CharField()
description = serializers.CharField()
bindable = serializers.BooleanField(default=True)
plans = ServicePlanSerializer(many=True)
metadata = serializers.DictField()
tags = serializers.ListField(child=serializers.CharField())
class CatalogSerializer(serializers.Serializer):
services = ServiceSerializer(many=True)
class ProvisionRequestSerializer(serializers.Serializer):
service_id = serializers.CharField()
plan_id = serializers.CharField()
organization_guid = serializers.CharField(required=False)
space_guid = serializers.CharField(required=False)
parameters = serializers.DictField(required=False)
class ProvisionResponseSerializer(serializers.Serializer):
dashboard_url = serializers.URLField(required=False)
operation = serializers.CharField(required=False, allow_null=True)
class BindingRequestSerializer(serializers.Serializer):
service_id = serializers.CharField()
plan_id = serializers.CharField()
bind_resource = serializers.DictField(required=False)
parameters = serializers.DictField(required=False)
class BindingResponseSerializer(serializers.Serializer):
credentials = serializers.DictField()
syslog_drain_url = serializers.URLField(required=False, allow_null=True)
route_service_url = serializers.URLField(required=False, allow_null=True)
volume_mounts = serializers.ListField(required=False)

View file

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

18
hub/servicebroker/urls.py Normal file
View file

@ -0,0 +1,18 @@
from django.urls import path
from . import views
app_name = "servicebroker"
urlpatterns = [
path("v2/catalog", views.CatalogView.as_view(), name="catalog"),
path(
"v2/service_instances/<str:instance_id>",
views.ProvisioningView.as_view(),
name="provisioning",
),
path(
"v2/service_instances/<str:instance_id>/service_bindings/<str:binding_id>",
views.BindingView.as_view(),
name="binding",
),
]

113
hub/servicebroker/views.py Normal file
View file

@ -0,0 +1,113 @@
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from django.conf import settings
from .authentication import ServiceBrokerAuthentication
from .serializers import (
CatalogSerializer,
ProvisionRequestSerializer,
ProvisionResponseSerializer,
BindingRequestSerializer,
BindingResponseSerializer,
)
from services.models import Service, ServiceLevel
class ServiceBrokerView(APIView):
authentication_classes = [ServiceBrokerAuthentication]
def get_broker_version(self, request):
return request.META.get("HTTP_X_BROKER_API_VERSION", "2.14")
class CatalogView(ServiceBrokerView):
def get(self, request):
services = []
marketplace_services = Service.objects.all().prefetch_related(
"cloud_provider", "service_level", "categories"
)
for service in marketplace_services:
plans = []
service_levels = ServiceLevel.objects.all()
for level in service_levels:
plan = {
"id": f"plan-{service.id}-{level.id}",
"name": level.name,
"description": level.description,
"metadata": {
"costs": [{"amount": float(service.price), "unit": "MONTHLY"}],
"bullets": [level.description],
},
"free": False,
}
plans.append(plan)
service_data = {
"id": f"service-{service.id}",
"name": service.name,
"description": service.description,
"bindable": True,
"plans": plans,
"metadata": {
"displayName": service.name,
"provider": service.cloud_provider.name,
"imageUrl": service.logo.url if service.logo else None,
"categories": [cat.name for cat in service.categories.all()],
},
"tags": [cat.name for cat in service.categories.all()],
}
services.append(service_data)
catalog = {"services": services}
serializer = CatalogSerializer(data=catalog)
serializer.is_valid(raise_exception=True)
return Response(serializer.data)
class ProvisioningView(ServiceBrokerView):
def put(self, request, instance_id):
serializer = ProvisionRequestSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
service_id = serializer.validated_data["service_id"]
marketplace_service_id = service_id.replace("service-", "")
service = Service.objects.get(id=marketplace_service_id)
response_data = {
"dashboard_url": f"{settings.BASE_URL}/service/{service.slug}/",
"operation": None,
}
response_serializer = ProvisionResponseSerializer(data=response_data)
response_serializer.is_valid(raise_exception=True)
return Response(response_serializer.data, status=status.HTTP_201_CREATED)
def delete(self, request, instance_id):
return Response(status=status.HTTP_200_OK)
class BindingView(ServiceBrokerView):
def put(self, request, instance_id, binding_id):
serializer = BindingRequestSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
service_id = serializer.validated_data["service_id"]
marketplace_service_id = service_id.replace("service-", "")
service = Service.objects.get(id=marketplace_service_id)
credentials = {
"service_name": service.name,
"provider": service.cloud_provider.name,
"service_url": f"{settings.BASE_URL}/service/{service.slug}/",
}
response_data = {"credentials": credentials}
response_serializer = BindingResponseSerializer(data=response_data)
response_serializer.is_valid(raise_exception=True)
return Response(response_serializer.data, status=status.HTTP_201_CREATED)
def delete(self, request, instance_id, binding_id):
return Response(status=status.HTTP_200_OK)