Use serialized data for schema
This commit is contained in:
parent
234ff8e1d6
commit
ee8fba07ef
2 changed files with 93 additions and 32 deletions
|
@ -1,52 +1,80 @@
|
||||||
|
import re
|
||||||
|
|
||||||
from django.core.validators import MaxValueValidator, MinValueValidator, RegexValidator
|
from django.core.validators import MaxValueValidator, MinValueValidator, RegexValidator
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.forms.models import ModelForm, ModelFormMetaclass
|
from django.forms.models import ModelForm, ModelFormMetaclass
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
def generate_django_model(crd, group, version, kind):
|
def generate_django_model(schema, group, version, kind):
|
||||||
"""
|
"""
|
||||||
Generates a virtual Django model from a Kubernetes CRD's OpenAPI v3 schema.
|
Generates a virtual Django model from a Kubernetes CRD's OpenAPI v3 schema.
|
||||||
"""
|
"""
|
||||||
schema = crd.spec.versions[0].schema.open_apiv3_schema
|
spec = schema["properties"].get("spec") or {}
|
||||||
properties = schema.properties
|
# defaults = {"apiVersion": f"{group}/{version}", "kind": kind}
|
||||||
required_fields = schema.required
|
|
||||||
model_fields = {"__module__": "crd_models"}
|
model_fields = {"__module__": "crd_models"}
|
||||||
|
model_fields.update(build_object_fields(spec, "spec"))
|
||||||
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"})
|
meta_class = type("Meta", (), {"app_label": "crd_models"})
|
||||||
model_fields["Meta"] = meta_class
|
model_fields["Meta"] = meta_class
|
||||||
|
|
||||||
# create the model class
|
# create the model class
|
||||||
model_name = crd.spec.names.kind
|
model_name = kind
|
||||||
model_class = type(model_name, (models.Model,), model_fields)
|
model_class = type(model_name, (models.Model,), model_fields)
|
||||||
return model_class
|
return model_class
|
||||||
|
|
||||||
|
|
||||||
def get_django_field(prop_schema, is_required=False, default=None):
|
def build_object_fields(schema, name, verbose_name_prefix=None):
|
||||||
field_type = prop_schema.type or "string"
|
required_fields = schema.get("required", [])
|
||||||
format = prop_schema.format
|
properties = schema.get("properties", {})
|
||||||
|
fields = {}
|
||||||
|
|
||||||
|
for field_name, field_schema in properties.items():
|
||||||
|
is_required = field_name in required_fields
|
||||||
|
full_name = f"{name}.{field_name}"
|
||||||
|
result = get_django_field(
|
||||||
|
field_schema,
|
||||||
|
is_required,
|
||||||
|
field_name,
|
||||||
|
full_name,
|
||||||
|
verbose_name_prefix=verbose_name_prefix,
|
||||||
|
)
|
||||||
|
if isinstance(result, dict):
|
||||||
|
fields.update(result)
|
||||||
|
else:
|
||||||
|
fields[full_name] = result
|
||||||
|
return fields
|
||||||
|
|
||||||
|
|
||||||
|
def deslugify(title):
|
||||||
|
if "_" in title:
|
||||||
|
title.replace("_", " ")
|
||||||
|
return title.title()
|
||||||
|
return re.sub(r"(?<!^)(?=[A-Z])", "_", title)
|
||||||
|
|
||||||
|
|
||||||
|
def get_django_field(
|
||||||
|
field_schema, is_required, field_name, full_name, verbose_name_prefix=None
|
||||||
|
):
|
||||||
|
field_type = field_schema.get("type") or "string"
|
||||||
|
format = field_schema.get("format")
|
||||||
|
verbose_name_prefix = verbose_name_prefix or ""
|
||||||
|
verbose_name = f"{verbose_name_prefix} {deslugify(field_name)}".strip()
|
||||||
|
|
||||||
kwargs = {
|
kwargs = {
|
||||||
"blank": not is_required,
|
"blank": not is_required,
|
||||||
"null": not is_required,
|
"null": not is_required,
|
||||||
"help_text": prop_schema.description or "",
|
"help_text": field_schema.get("description"),
|
||||||
"validators": [],
|
"validators": [],
|
||||||
"default": default,
|
"verbose_name": verbose_name,
|
||||||
# TODO: verbose_name?
|
"default": field_schema.get("default"),
|
||||||
}
|
}
|
||||||
|
|
||||||
if prop_schema.minimum:
|
if minimum := field_schema.get("minimum"):
|
||||||
kwargs["validators"].append(MinValueValidator(prop_schema.minimum))
|
kwargs["validators"].append(MinValueValidator(minimum))
|
||||||
if prop_schema.maximum:
|
if maximum := field_schema.get("maximum"):
|
||||||
kwargs["validators"].append(MaxValueValidator(prop_schema.maximum))
|
kwargs["validators"].append(MaxValueValidator(maximum))
|
||||||
|
|
||||||
if field_type == "string":
|
if field_type == "string":
|
||||||
if format == "date-time":
|
if format == "date-time":
|
||||||
|
@ -54,9 +82,9 @@ def get_django_field(prop_schema, is_required=False, default=None):
|
||||||
elif format == "date":
|
elif format == "date":
|
||||||
return models.DateField(**kwargs)
|
return models.DateField(**kwargs)
|
||||||
else:
|
else:
|
||||||
max_length = prop_schema.max_length or 255
|
max_length = field_schema.get("max_length") or 255
|
||||||
if prop_schema.pattern:
|
if pattern := field_schema.get("pattern"):
|
||||||
kwargs["validators"].append(RegexValidator(regex=prop_schema.pattern))
|
kwargs["validators"].append(RegexValidator(regex=pattern))
|
||||||
return models.CharField(max_length=max_length, **kwargs)
|
return models.CharField(max_length=max_length, **kwargs)
|
||||||
elif field_type == "integer":
|
elif field_type == "integer":
|
||||||
return models.IntegerField(**kwargs)
|
return models.IntegerField(**kwargs)
|
||||||
|
@ -65,10 +93,14 @@ def get_django_field(prop_schema, is_required=False, default=None):
|
||||||
elif field_type == "boolean":
|
elif field_type == "boolean":
|
||||||
return models.BooleanField(**kwargs)
|
return models.BooleanField(**kwargs)
|
||||||
elif field_type == "object":
|
elif field_type == "object":
|
||||||
kwargs["help_text"] += " (JSON object)"
|
return build_object_fields(
|
||||||
return models.JSONField(**kwargs)
|
field_schema, full_name, verbose_name_prefix=f"{verbose_name}:"
|
||||||
|
)
|
||||||
elif field_type == "array":
|
elif field_type == "array":
|
||||||
kwargs["help_text"] += " (JSON array)"
|
# TODO: handle items / validate items, build multi-select input
|
||||||
|
# if field_schema.get("items") and (choices := field_schema["items"].get("enum")):
|
||||||
|
# choices = [c, c for c in choices]
|
||||||
|
kwargs["help_text"] = _("JSON field (array)")
|
||||||
return models.JSONField(**kwargs)
|
return models.JSONField(**kwargs)
|
||||||
return models.CharField(max_length=255, **kwargs)
|
return models.CharField(max_length=255, **kwargs)
|
||||||
|
|
||||||
|
@ -76,7 +108,8 @@ def get_django_field(prop_schema, is_required=False, default=None):
|
||||||
class CrdModelFormMixin:
|
class CrdModelFormMixin:
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.fields["apiVersion"].disabled = True
|
# self.fields["apiVersion"].disabled = True
|
||||||
|
# self.fields["kind"].disabled = True
|
||||||
|
|
||||||
|
|
||||||
def generate_model_form_class(model):
|
def generate_model_form_class(model):
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import kubernetes
|
import kubernetes
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.utils.functional import cached_property
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from encrypted_fields.fields import EncryptedJSONField
|
from encrypted_fields.fields import EncryptedJSONField
|
||||||
from kubernetes import config
|
from kubernetes import config
|
||||||
|
@ -310,7 +311,8 @@ class ServiceOfferingControlPlane(models.Model):
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.service_offering} on {self.control_plane} with {self.service_definition}"
|
return f"{self.service_offering} on {self.control_plane} with {self.service_definition}"
|
||||||
|
|
||||||
def get_resource_definition(self):
|
@cached_property
|
||||||
|
def resource_definition(self):
|
||||||
client = self.control_plane.get_kubernetes_client()
|
client = self.control_plane.get_kubernetes_client()
|
||||||
api_instance = kubernetes.client.ApiextensionsV1Api(client)
|
api_instance = kubernetes.client.ApiextensionsV1Api(client)
|
||||||
kind = self.service_definition.api_definition["kind"].lower()
|
kind = self.service_definition.api_definition["kind"].lower()
|
||||||
|
@ -319,6 +321,32 @@ class ServiceOfferingControlPlane(models.Model):
|
||||||
crd = api_instance.read_custom_resource_definition(name)
|
crd = api_instance.read_custom_resource_definition(name)
|
||||||
return crd
|
return crd
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def resource_schema(self):
|
||||||
|
for version in self.resource_definition.spec.versions:
|
||||||
|
if version.schema and version.schema.open_apiv3_schema:
|
||||||
|
schema_dict = kubernetes.client.ApiClient().sanitize_for_serialization(
|
||||||
|
version.schema.open_apiv3_schema
|
||||||
|
)
|
||||||
|
return schema_dict
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def django_model(self):
|
||||||
|
from servala.core.crd import generate_django_model
|
||||||
|
|
||||||
|
kwargs = {
|
||||||
|
key: value
|
||||||
|
for key, value in self.service_definition.api_definition.items()
|
||||||
|
if key in ("group", "version", "kind")
|
||||||
|
}
|
||||||
|
return generate_django_model(self.resource_schema, **kwargs)
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def model_form_class(self):
|
||||||
|
from servala.core.crd import generate_model_form_class
|
||||||
|
|
||||||
|
return generate_model_form_class(self.django_model)
|
||||||
|
|
||||||
|
|
||||||
class ServiceOffering(models.Model):
|
class ServiceOffering(models.Model):
|
||||||
"""
|
"""
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue