From 48b5a1e3e42f9680f4ee78f277abdd565602ead9 Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Mon, 24 Mar 2025 15:21:06 +0100 Subject: [PATCH] Add ServiceDefinition model --- ...serviceoffering_control_planes_and_more.py | 81 +++++++++++++++++++ src/servala/core/models/__init__.py | 2 + src/servala/core/models/service.py | 71 +++++++++++++--- 3 files changed, 142 insertions(+), 12 deletions(-) create mode 100644 src/servala/core/migrations/0006_rename_control_plane_serviceoffering_control_planes_and_more.py diff --git a/src/servala/core/migrations/0006_rename_control_plane_serviceoffering_control_planes_and_more.py b/src/servala/core/migrations/0006_rename_control_plane_serviceoffering_control_planes_and_more.py new file mode 100644 index 0000000..a5f9340 --- /dev/null +++ b/src/servala/core/migrations/0006_rename_control_plane_serviceoffering_control_planes_and_more.py @@ -0,0 +1,81 @@ +# Generated by Django 5.2b1 on 2025-03-24 14:20 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("core", "0005_remove_controlplane_k8s_api_endpoint"), + ] + + operations = [ + migrations.RenameField( + model_name="serviceoffering", + old_name="control_plane", + new_name="control_planes", + ), + migrations.CreateModel( + name="ServiceDefinition", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=100, verbose_name="Name")), + ( + "description", + models.TextField(blank=True, verbose_name="Description"), + ), + ( + "api_definition", + models.JSONField( + blank=True, + help_text="Contains group, version, and kind information", + null=True, + verbose_name="API Definition", + ), + ), + ( + "control_plane", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="service_definitions", + to="core.controlplane", + verbose_name="Control Plane", + ), + ), + ( + "service", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="service_definitions", + to="core.service", + verbose_name="Service", + ), + ), + ], + options={ + "verbose_name": "Service definition", + "verbose_name_plural": "Service definitions", + }, + ), + migrations.AddField( + model_name="serviceoffering", + name="service_definition", + field=models.ForeignKey( + default=1, + on_delete=django.db.models.deletion.PROTECT, + related_name="offerings", + to="core.servicedefinition", + verbose_name="Service definition", + ), + preserve_default=False, + ), + ] diff --git a/src/servala/core/models/__init__.py b/src/servala/core/models/__init__.py index f637b57..b4ddf21 100644 --- a/src/servala/core/models/__init__.py +++ b/src/servala/core/models/__init__.py @@ -11,6 +11,7 @@ from .service import ( Plan, Service, ServiceCategory, + ServiceDefinition, ServiceOffering, ) from .user import User @@ -26,6 +27,7 @@ __all__ = [ "Plan", "Service", "ServiceCategory", + "ServiceDefinition", "ServiceOffering", "User", ] diff --git a/src/servala/core/models/service.py b/src/servala/core/models/service.py index 5ab1e02..770ac40 100644 --- a/src/servala/core/models/service.py +++ b/src/servala/core/models/service.py @@ -67,18 +67,13 @@ class Service(models.Model): return self.name -def validate_api_credentials(value): - """ - Validates that api_credentials either contains all required fields or is empty. - """ - # If empty dict, that's valid - if not value: - return - - # Check for required fields - required_fields = ("certificate-authority-data", "server", "token") - missing_fields = required_fields - set(value) +def validate_dict(data, required_fields=None, allow_empty=True): + if not data: + if allow_empty: + return + raise ValidationError(_("Data may not be empty!")) + missing_fields = required_fields - set(data) if missing_fields: raise ValidationError( _("Missing required fields in API credentials: %(fields)s"), @@ -86,6 +81,11 @@ def validate_api_credentials(value): ) +def validate_api_credentials(value): + required_fields = ("certificate-authority-data", "server", "token") + return validate_dict(value, required_fields) + + class ControlPlane(models.Model): name = models.CharField(max_length=100, verbose_name=_("Name")) description = models.TextField(blank=True, verbose_name=_("Description")) @@ -228,6 +228,48 @@ class Plan(models.Model): return self.name +def validate_api_definition(value): + required_fields = ("group", "version", "kind") + return validate_dict(value, required_fields) + + +class ServiceDefinition(models.Model): + """ + Configuration/service implementation: contains information on which + CompositeResourceDefinition (aka XRD) implements a service on a ControlPlane. + + Is required in order to query the OpenAPI spec for dynamic form generation. + """ + + name = models.CharField(max_length=100, verbose_name=_("Name")) + description = models.TextField(blank=True, verbose_name=_("Description")) + api_definition = models.JSONField( + verbose_name=_("API Definition"), + help_text=_("Contains group, version, and kind information"), + null=True, + blank=True, + ) + control_plane = models.ForeignKey( + to="ControlPlane", + on_delete=models.CASCADE, + related_name="service_definitions", + verbose_name=_("Control Plane"), + ) + service = models.ForeignKey( + to="Service", + on_delete=models.CASCADE, + related_name="service_definitions", + verbose_name=_("Service"), + ) + + class Meta: + verbose_name = _("Service definition") + verbose_name_plural = _("Service definitions") + + def __str__(self): + return self.name + + class ServiceOffering(models.Model): """ A service offering, e.g. "PostgreSQL on AWS", "MinIO on GCP". @@ -250,8 +292,13 @@ class ServiceOffering(models.Model): related_name="offerings", verbose_name=_("Control planes"), ) + service_definition = models.ForeignKey( + to="ServiceDefinition", + related_name="offerings", + verbose_name=_("Service definition"), + on_delete=models.PROTECT, + ) description = models.TextField(blank=True, verbose_name=_("Description")) - gvk # group, version, kind = jsonfeld; property => gvk kombiniert, kubernetes-ding class Meta: verbose_name = _("Service offering")