diff --git a/src/servala/core/admin.py b/src/servala/core/admin.py index dad3d6e..7d110d1 100644 --- a/src/servala/core/admin.py +++ b/src/servala/core/admin.py @@ -131,7 +131,7 @@ class ControlPlaneAdmin(admin.ModelAdmin): fieldsets = ( ( None, - {"fields": ("name", "description", "cloud_provider")}, + {"fields": ("name", "description", "cloud_provider", "required_label")}, ), ( _("API Credentials"), diff --git a/src/servala/core/migrations/0002_alter_controlplanecrd_options_and_more.py b/src/servala/core/migrations/0002_alter_controlplanecrd_options_and_more.py new file mode 100644 index 0000000..63cab45 --- /dev/null +++ b/src/servala/core/migrations/0002_alter_controlplanecrd_options_and_more.py @@ -0,0 +1,31 @@ +# Generated by Django 5.2b1 on 2025-04-03 15:24 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("core", "0001_initial"), + ] + + operations = [ + migrations.AlterModelOptions( + name="controlplanecrd", + options={ + "verbose_name": "ControlPlane CRD", + "verbose_name_plural": "ControlPlane CRDs", + }, + ), + migrations.AddField( + model_name="controlplane", + name="required_label", + field=models.CharField( + blank=True, + help_text="Label value for the 'appcat.vshn.io/provider-config' added to every instance on this plane.", + max_length=100, + null=True, + verbose_name="Required Label", + ), + ), + ] diff --git a/src/servala/core/models/service.py b/src/servala/core/models/service.py index 9e282a6..f60d57c 100644 --- a/src/servala/core/models/service.py +++ b/src/servala/core/models/service.py @@ -1,12 +1,13 @@ import kubernetes import urlman +from django.conf import settings from django.core.cache import cache from django.core.exceptions import ValidationError from django.db import models from django.utils.functional import cached_property from django.utils.translation import gettext_lazy as _ from encrypted_fields.fields import EncryptedJSONField -from kubernetes import config +from kubernetes import client, config from kubernetes.client.rest import ApiException from servala.core.models.mixins import ServalaModelMixin @@ -106,6 +107,15 @@ class ControlPlane(ServalaModelMixin, models.Model): help_text="Required fields: certificate-authority-data, server (URL), token", validators=[validate_api_credentials], ) + required_label = models.CharField( + max_length=100, + blank=True, + null=True, + verbose_name=_("Required Label"), + help_text=_( + "Label value for the 'appcat.vshn.io/provider-config' added to every instance on this plane." + ), + ) cloud_provider = models.ForeignKey( to="CloudProvider", @@ -137,7 +147,6 @@ class ControlPlane(ServalaModelMixin, models.Model): "clusters": [ { "cluster": { - "insecure-skip-tls-verify": True, "certificate-authority-data": self.api_credentials[ "certificate-authority-data" ], @@ -484,6 +493,35 @@ class ServiceInstance(ServalaModelMixin, models.Model): # Ensure the namespace exists context.control_plane.get_or_create_namespace(organization.namespace) + group = context.service_definition.api_definition["group"] + version = context.service_definition.api_definition["version"] + kind = context.service_definition.api_definition["kind"] + create_data = { + "apiVersion": f"{group}/{version}", + "kind": kind, + "metadata": { + "name": name, + "namespace": organization.namespace, + }, + "spec": spec_data or {}, + } + if label := context.control_plane.required_label: + create_data["metadata"]["labels"] = {settings.DEFAULT_LABEL_KEY: label} + api_instance = client.CustomObjectsApi( + context.control_plane.get_kubernetes_client() + ) + plural = kind.lower() + if not plural.endswith("s"): + plural = f"{plural}s" + + api_instance.create_namespaced_custom_object( + group=group, + version=version, + namespace=organization.namespace, + plural=plural, + body=create_data, + ) + return cls.objects.create( name=name, organization=organization, diff --git a/src/servala/frontend/views/service.py b/src/servala/frontend/views/service.py index 1803dd0..c19aff5 100644 --- a/src/servala/frontend/views/service.py +++ b/src/servala/frontend/views/service.py @@ -131,7 +131,7 @@ class ServiceOfferingDetailView(OrganizationViewMixin, HtmxViewMixin, DetailView name=form.cleaned_data["name"], context=self.context_object, created_by=request.user, - spec_data=form.get_nested_data(), + spec_data=form.get_nested_data().get("spec"), ) return redirect(service_instance.urls.base) except Exception as e: diff --git a/src/servala/settings.py b/src/servala/settings.py index 3e27e35..50b0c5e 100644 --- a/src/servala/settings.py +++ b/src/servala/settings.py @@ -210,6 +210,7 @@ LANGUAGE_COOKIE_NAME = "servala_lang" SESSION_COOKIE_NAME = "servala_sess" SESSION_COOKIE_SECURE = not DEBUG +DEFAULT_LABEL_KEY = "appcat.vshn.io/provider-config" DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" # TODO diff --git a/src/tests/conftest.py b/src/tests/conftest.py index d88870e..a69e50c 100644 --- a/src/tests/conftest.py +++ b/src/tests/conftest.py @@ -15,12 +15,16 @@ def origin(): @pytest.fixture def organization(origin): - return Organization.objects.create(name="Test Org", origin=origin) + return Organization.objects.create( + name="Test Org", namespace="test-org", origin=origin + ) @pytest.fixture def other_organization(origin): - return Organization.objects.create(name="Test Org Alternate", origin=origin) + return Organization.objects.create( + name="Test Org Alternate", namespace="test-org-alternate", origin=origin + ) @pytest.fixture