diff --git a/README.md b/README.md index ba6fdcd..07e1764 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,6 @@ Then use ``uv`` to install the project and run its commands while you’re 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 ``` diff --git a/docker/run.sh b/docker/run.sh index bab8a08..f3c7f9a 100644 --- a/docker/run.sh +++ b/docker/run.sh @@ -8,7 +8,6 @@ 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 & diff --git a/src/servala/core/crd.py b/src/servala/core/crd.py index 6fc4219..1c2bcef 100644 --- a/src/servala/core/crd.py +++ b/src/servala/core/crd.py @@ -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") or [] - properties = schema.get("properties") or {} + required_fields = schema.get("required", []) + properties = schema.get("properties", {}) fields = {} for field_name, field_schema in properties.items(): diff --git a/src/servala/core/models/service.py b/src/servala/core/models/service.py index c4d2b79..0b28a7a 100644 --- a/src/servala/core/models/service.py +++ b/src/servala/core/models/service.py @@ -1,5 +1,4 @@ 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 @@ -319,33 +318,81 @@ 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) - 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 + crd = extensions_api.read_custom_resource_definition(name) + return crd @cached_property def resource_schema(self): - cache_key = f"servala:crd:schema:{self.pk}" - if result := cache.get(cache_key): - return result - + # 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"] version = self.service_definition.api_definition["version"] - 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 + 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 @cached_property def django_model(self): diff --git a/src/servala/settings.py b/src/servala/settings.py index 3e27e35..5d670ae 100644 --- a/src/servala/settings.py +++ b/src/servala/settings.py @@ -132,13 +132,6 @@ 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",