From 234ff8e1d657f5d4773c326a8df3ebf5a34e9c19 Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Tue, 25 Mar 2025 17:13:43 +0100 Subject: [PATCH] Build dynamic model and modelform generation --- src/servala/core/crd.py | 92 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 src/servala/core/crd.py diff --git a/src/servala/core/crd.py b/src/servala/core/crd.py new file mode 100644 index 0000000..5a3803c --- /dev/null +++ b/src/servala/core/crd.py @@ -0,0 +1,92 @@ +from django.core.validators import MaxValueValidator, MinValueValidator, RegexValidator +from django.db import models +from django.forms.models import ModelForm, ModelFormMetaclass + + +def generate_django_model(crd, group, version, kind): + """ + Generates a virtual Django model from a Kubernetes CRD's OpenAPI v3 schema. + """ + schema = crd.spec.versions[0].schema.open_apiv3_schema + properties = schema.properties + required_fields = schema.required + model_fields = {"__module__": "crd_models"} + + defaults = {"apiVersion": f"{group}/{version}", "kind": kind} + + for prop_name, prop_schema in properties.items(): + is_required = prop_name in required_fields + field = get_django_field( + prop_schema, is_required, default=defaults.get(prop_name) + ) + model_fields[prop_name] = field + + meta_class = type("Meta", (), {"app_label": "crd_models"}) + model_fields["Meta"] = meta_class + + # create the model class + model_name = crd.spec.names.kind + model_class = type(model_name, (models.Model,), model_fields) + return model_class + + +def get_django_field(prop_schema, is_required=False, default=None): + field_type = prop_schema.type or "string" + format = prop_schema.format + + kwargs = { + "blank": not is_required, + "null": not is_required, + "help_text": prop_schema.description or "", + "validators": [], + "default": default, + # TODO: verbose_name? + } + + if prop_schema.minimum: + kwargs["validators"].append(MinValueValidator(prop_schema.minimum)) + if prop_schema.maximum: + kwargs["validators"].append(MaxValueValidator(prop_schema.maximum)) + + if field_type == "string": + if format == "date-time": + return models.DateTimeField(**kwargs) + elif format == "date": + return models.DateField(**kwargs) + else: + max_length = prop_schema.max_length or 255 + if prop_schema.pattern: + kwargs["validators"].append(RegexValidator(regex=prop_schema.pattern)) + return models.CharField(max_length=max_length, **kwargs) + elif field_type == "integer": + return models.IntegerField(**kwargs) + elif field_type == "number": + return models.FloatField(**kwargs) + elif field_type == "boolean": + return models.BooleanField(**kwargs) + elif field_type == "object": + kwargs["help_text"] += " (JSON object)" + return models.JSONField(**kwargs) + elif field_type == "array": + kwargs["help_text"] += " (JSON array)" + return models.JSONField(**kwargs) + return models.CharField(max_length=255, **kwargs) + + +class CrdModelFormMixin: + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields["apiVersion"].disabled = True + + +def generate_model_form_class(model): + meta_attrs = { + "model": model, + "fields": "__all__", + } + fields = { + "Meta": type("Meta", (object,), meta_attrs), + "__module__": "crd_models", + } + class_name = f"{model.__name__}ModelForm" + return ModelFormMetaclass(class_name, (CrdModelFormMixin, ModelForm), fields)