Compare commits

...

4 commits

Author SHA1 Message Date
83533129bd Cache resource schemas for a full day
All checks were successful
Tests / test (push) Successful in 23s
2025-03-26 15:40:33 +01:00
70acf2c381 Add and document database cache 2025-03-26 15:40:33 +01:00
ebf88527fe Simplify CRD and schema retrieval 2025-03-26 15:40:33 +01:00
eaa0614839 Fix fallback values in model generation 2025-03-26 15:40:33 +01:00
5 changed files with 34 additions and 72 deletions

View file

@ -30,6 +30,7 @@ Then use ``uv`` to install the project and run its commands while youre devel
```bash
uv sync --dev
uv run --env-file=.env src/manage.py migrate
uv run --env-file=.env src/manage.py createcachetable
uv run --env-file=.env src/manage.py runserver
```

View file

@ -8,6 +8,7 @@ export XDG_CONFIG_HOME="/app/config"
echo "Applying database migrations"
uv run src/manage.py migrate
uv run src/manage.py createcachetable
echo "Starting Caddy"
exec caddy run --config /app/config/caddy/Caddyfile --adapter caddyfile 2>&1 &

View file

@ -26,8 +26,8 @@ def generate_django_model(schema, group, version, kind):
def build_object_fields(schema, name, verbose_name_prefix=None):
required_fields = schema.get("required", [])
properties = schema.get("properties", {})
required_fields = schema.get("required") or []
properties = schema.get("properties") or {}
fields = {}
for field_name, field_schema in properties.items():

View file

@ -1,4 +1,5 @@
import kubernetes
from django.core.cache import cache
from django.core.exceptions import ValidationError
from django.db import models
from django.utils.functional import cached_property
@ -318,81 +319,33 @@ class ServiceOfferingControlPlane(models.Model):
version = self.service_definition.api_definition["version"]
client = self.control_plane.get_kubernetes_client()
# We have to determine the ``name`` first, via the discovery API
# The official kubernetes client APIs only include the regular discovery API,
# but not the APIGroupDiscoveryList that we actually need, and passing it as
# response_type leads to the client receiving the data, but refusing to return
# it since the model is not defined. Instead, we have to manually
# construct the API call:
content_type = "application/json"
response_type = "APIGroupDiscoveryList"
accept_header = ",".join(
[
f"{content_type};g=apidiscovery.k8s.io;v={api_version};as={response_type}"
for api_version in ("v2", "v2beta1")
]
)
accept_header = f"{accept_header},{content_type}"
groups = client.call_api(
"/apis",
"GET",
header_params={"Accept": accept_header},
auth_settings=["BearerToken"],
_return_http_data_only=True,
_preload_content=False,
).json()
api_group = [
g for g in groups.get("items", []) if g["metadata"]["name"] == group
][0]
api_version = [v for v in api_group["versions"] if v["version"] == version][0]
resource = [
r for r in api_version["resources"] if r["responseKind"]["kind"] == kind
][0]
# This is the thing we went to all that effort for!
name = f"{resource['resource']}.{group}"
extensions_api = kubernetes.client.ApiextensionsV1Api(client)
crd = extensions_api.read_custom_resource_definition(name)
return crd
crds = extensions_api.list_custom_resource_definition()
matching_crd = None
for crd in crds.items:
if matching_crd:
break
if crd.spec.group == group:
for served_version in crd.spec.versions:
if served_version.name == version and served_version.served:
if crd.spec.names.kind == kind:
matching_crd = crd
break
return matching_crd
@cached_property
def resource_schema(self):
# We extract the schema directly from the API. As an alternative, you can
# also get it using self.resource_definition, though that requires some
# extra serialization to remove the API client types:
# for version in self.resource_definition.spec.versions:
# if self.service_definition.api_definition["version"] != version.name:
# continue
# 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
kind = self.service_definition.api_definition["kind"]
group = self.service_definition.api_definition["group"]
cache_key = f"servala:crd:schema:{self.pk}"
if result := cache.get(cache_key):
return result
version = self.service_definition.api_definition["version"]
client = self.control_plane.get_kubernetes_client()
response = client.call_api(
f"/openapi/v3/apis/{group}/{version}",
"GET",
header_params={"Accept": "application/json"},
auth_settings=["BearerToken"],
_return_http_data_only=True,
_preload_content=False,
).json()
for schema in response["components"]["schemas"].values():
gkvs = schema.get("x-kubernetes-group-version-kind")
if not gkvs or not isinstance(gkvs, list):
continue
if any(
[
gkv["group"] == group
and gkv["version"] == version
and gkv["kind"] == kind
for gkv in gkvs
]
):
return schema
for v in self.resource_definition.spec.versions:
if v.name == version:
result = v.schema.open_apiv3_schema.to_dict()
timeout_seconds = 60 * 60 * 24
cache.set(cache_key, result, timeout=timeout_seconds)
return result
@cached_property
def django_model(self):

View file

@ -132,6 +132,13 @@ STATIC_URL = "static/" # CSS, JavaScript, etc.
STATIC_ROOT = BASE_DIR / "static.dist"
MEDIA_URL = "media/" # User uploads, e.g. images
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.db.DatabaseCache",
"LOCATION": "servala_cache",
}
}
# Additional locations of static files
STATICFILES_FINDERS = (
"django.contrib.staticfiles.finders.FileSystemFinder",