From 6a521f3d0a8198eb97742e557e1e750a4b975848 Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Wed, 26 Mar 2025 13:31:39 +0100 Subject: [PATCH] Implement full crd discovery and schema retrieval --- src/servala/core/models/service.py | 86 +++++++++++++++++++++++++----- 1 file changed, 74 insertions(+), 12 deletions(-) diff --git a/src/servala/core/models/service.py b/src/servala/core/models/service.py index ca70e03..0b28a7a 100644 --- a/src/servala/core/models/service.py +++ b/src/servala/core/models/service.py @@ -313,24 +313,86 @@ class ServiceOfferingControlPlane(models.Model): @cached_property def resource_definition(self): - client = self.control_plane.get_kubernetes_client() - api_instance = kubernetes.client.ApiextensionsV1Api(client) - kind = self.service_definition.api_definition["kind"].lower() + kind = self.service_definition.api_definition["kind"] group = self.service_definition.api_definition["group"] - name = f"{kind}.{group}" - crd = api_instance.read_custom_resource_definition(name) + 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 @cached_property def resource_schema(self): - for version in self.resource_definition.spec.versions: - if self.service_definition.api_definition["version"] != version.name: + # 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"] + 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 version.schema and version.schema.open_apiv3_schema: - schema_dict = kubernetes.client.ApiClient().sanitize_for_serialization( - version.schema.open_apiv3_schema - ) - return schema_dict + 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):