From 605e74c97a34a6598a359e072f6dbe49a95053e6 Mon Sep 17 00:00:00 2001 From: Michael Bolot Date: Mon, 23 Oct 2023 13:22:47 -0500 Subject: [PATCH] Reducing number of generated schemas Changes steve to only return top level schema definitions rather than a full defined schema for every field. Also adds basic docs and fixes a bug with schema generation on versioned crds --- pkg/schema/converter/crd.go | 22 ++---- pkg/schema/converter/description.go | 45 +++++++++++ pkg/schema/converter/discovery.go | 4 +- pkg/schema/converter/k8stonorman.go | 39 +++++++++- pkg/schema/converter/openapi.go | 117 ---------------------------- pkg/schema/converter/openapiv3.go | 92 ---------------------- 6 files changed, 90 insertions(+), 229 deletions(-) create mode 100644 pkg/schema/converter/description.go delete mode 100644 pkg/schema/converter/openapi.go delete mode 100644 pkg/schema/converter/openapiv3.go diff --git a/pkg/schema/converter/crd.go b/pkg/schema/converter/crd.go index 5aa06f21..db5e031a 100644 --- a/pkg/schema/converter/crd.go +++ b/pkg/schema/converter/crd.go @@ -27,7 +27,9 @@ var ( } ) -func AddCustomResources(crd apiextv1.CustomResourceDefinitionClient, schemas map[string]*types.APISchema) error { +// addCustomResources uses the openAPISchema defined on CRDs to provide field definitions to previously discovered schemas. +// Note that this function does not create new schemas - it only adds details to resources already present in the schemas map. +func addCustomResources(crd apiextv1.CustomResourceDefinitionClient, schemas map[string]*types.APISchema) error { crds, err := crd.List(metav1.ListOptions{}) if err != nil { return nil @@ -41,14 +43,14 @@ func AddCustomResources(crd apiextv1.CustomResourceDefinitionClient, schemas map group, kind := crd.Spec.Group, crd.Status.AcceptedNames.Kind for _, version := range crd.Spec.Versions { - forVersion(&crd, group, kind, version, schemas) + forVersion(group, kind, version, schemas) } } return nil } -func forVersion(crd *v1.CustomResourceDefinition, group, kind string, version v1.CustomResourceDefinitionVersion, schemasMap map[string]*types.APISchema) { +func forVersion(group, kind string, version v1.CustomResourceDefinitionVersion, schemasMap map[string]*types.APISchema) { var versionColumns []table.Column for _, col := range version.AdditionalPrinterColumns { versionColumns = append(versionColumns, table.Column{ @@ -73,18 +75,6 @@ func forVersion(crd *v1.CustomResourceDefinition, group, kind string, version v1 attributes.SetColumns(schema, versionColumns) } if version.Schema != nil && version.Schema.OpenAPIV3Schema != nil { - if fieldsSchema := modelV3ToSchema(id, crd.Spec.Versions[0].Schema.OpenAPIV3Schema, schemasMap); fieldsSchema != nil { - for k, v := range staticFields { - fieldsSchema.ResourceFields[k] = v - } - for k, v := range fieldsSchema.ResourceFields { - if schema.ResourceFields == nil { - schema.ResourceFields = map[string]schemas.Field{} - } - if _, ok := schema.ResourceFields[k]; !ok { - schema.ResourceFields[k] = v - } - } - } + schema.Description = version.Schema.OpenAPIV3Schema.Description } } diff --git a/pkg/schema/converter/description.go b/pkg/schema/converter/description.go new file mode 100644 index 00000000..11d784b3 --- /dev/null +++ b/pkg/schema/converter/description.go @@ -0,0 +1,45 @@ +package converter + +import ( + "github.com/rancher/apiserver/pkg/types" + "github.com/sirupsen/logrus" + "k8s.io/client-go/discovery" + "k8s.io/kube-openapi/pkg/util/proto" +) + +// addDescription adds a description to all schemas in schemas using the openapi v2 definitions from k8s. +// Will not add new schemas, only mutate existing ones. Returns an error if the definitions could not be retrieved. +func addDescription(client discovery.DiscoveryInterface, schemas map[string]*types.APISchema) error { + openapi, err := client.OpenAPISchema() + if err != nil { + return err + } + + models, err := proto.NewOpenAPIData(openapi) + if err != nil { + return err + } + + for _, modelName := range models.ListModels() { + model := models.LookupModel(modelName) + if k, ok := model.(*proto.Kind); ok { + gvk := GetGVKForKind(k) + if gvk == nil { + // kind was not for top level gvk, we can skip this resource + logrus.Tracef("when adding schema descriptions, will not add description for kind %s, which is not a top level resource", k.Path.String()) + continue + } + schemaID := GVKToVersionedSchemaID(*gvk) + schema, ok := schemas[schemaID] + // some kinds have a gvk but don't correspond to a schema (like a podList). We can + // skip these resources as well + if !ok { + logrus.Tracef("when adding schema descriptions, will not add description for ID %s, which is not in schemas", schemaID) + continue + } + schema.Description = k.GetDescription() + } + } + + return nil +} diff --git a/pkg/schema/converter/discovery.go b/pkg/schema/converter/discovery.go index e2337a26..b73e4983 100644 --- a/pkg/schema/converter/discovery.go +++ b/pkg/schema/converter/discovery.go @@ -22,7 +22,9 @@ var ( } ) -func AddDiscovery(client discovery.DiscoveryInterface, schemasMap map[string]*types.APISchema) error { +// addDiscovery uses a k8s discovery client to create very basic schemas for all registered groups/resources. Other +// functions, such as AddCustomResources are used to add more details to these schemas later on. +func addDiscovery(client discovery.DiscoveryInterface, schemasMap map[string]*types.APISchema) error { groups, resourceLists, err := client.ServerGroupsAndResources() if gd, ok := err.(*discovery.ErrGroupDiscoveryFailed); ok { logrus.Errorf("Failed to read API for groups %v", gd.Groups) diff --git a/pkg/schema/converter/k8stonorman.go b/pkg/schema/converter/k8stonorman.go index ee6e47ed..1827e4d6 100644 --- a/pkg/schema/converter/k8stonorman.go +++ b/pkg/schema/converter/k8stonorman.go @@ -1,3 +1,5 @@ +// Package converter is responsible for converting the types registered with a k8s server to schemas which can be used +// by the UI (and other consumers) to discover the resources available and the current user's permissions. package converter import ( @@ -5,9 +7,18 @@ import ( "strings" "github.com/rancher/apiserver/pkg/types" + "github.com/rancher/norman/types/convert" v1 "github.com/rancher/wrangler/v2/pkg/generated/controllers/apiextensions.k8s.io/v1" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/discovery" + "k8s.io/kube-openapi/pkg/util/proto" +) + +const ( + gvkExtensionName = "x-kubernetes-group-version-kind" + gvkExtensionGroup = "group" + gvkExtensionVersion = "version" + gvkExtensionKind = "kind" ) func GVKToVersionedSchemaID(gvk schema.GroupVersionKind) string { @@ -38,18 +49,40 @@ func GVRToPluralName(gvr schema.GroupVersionResource) string { return fmt.Sprintf("%s.%s", gvr.Group, gvr.Resource) } +// GetGVKForKind attempts to retrieve a GVK for a given Kind. Not all kind represent top level resources, +// so this function may return nil if the kind did not have a gvk extension +func GetGVKForKind(kind *proto.Kind) *schema.GroupVersionKind { + extensions, ok := kind.Extensions[gvkExtensionName].([]any) + if !ok { + return nil + } + for _, extension := range extensions { + if gvkExtension, ok := extension.(map[any]any); ok { + gvk := schema.GroupVersionKind{ + Group: convert.ToString(gvkExtension[gvkExtensionGroup]), + Version: convert.ToString(gvkExtension[gvkExtensionVersion]), + Kind: convert.ToString(gvkExtension[gvkExtensionKind]), + } + return &gvk + } + } + return nil +} + +// ToSchemas creates the schemas for a K8s server, using client to discover groups/resources, and crd to potentially +// add additional information about new fields/resources. Mostly ties together AddDiscovery and AddCustomResources. func ToSchemas(crd v1.CustomResourceDefinitionClient, client discovery.DiscoveryInterface) (map[string]*types.APISchema, error) { result := map[string]*types.APISchema{} - if err := AddOpenAPI(client, result); err != nil { + if err := addDiscovery(client, result); err != nil { return nil, err } - if err := AddDiscovery(client, result); err != nil { + if err := addCustomResources(crd, result); err != nil { return nil, err } - if err := AddCustomResources(crd, result); err != nil { + if err := addDescription(client, result); err != nil { return nil, err } diff --git a/pkg/schema/converter/openapi.go b/pkg/schema/converter/openapi.go deleted file mode 100644 index 6a1d4563..00000000 --- a/pkg/schema/converter/openapi.go +++ /dev/null @@ -1,117 +0,0 @@ -package converter - -import ( - "github.com/rancher/apiserver/pkg/types" - "github.com/rancher/steve/pkg/attributes" - "github.com/rancher/wrangler/v2/pkg/data/convert" - "github.com/rancher/wrangler/v2/pkg/schemas" - "github.com/sirupsen/logrus" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/client-go/discovery" - "k8s.io/kube-openapi/pkg/util/proto" -) - -func modelToSchema(modelName string, k *proto.Kind) *types.APISchema { - s := types.APISchema{ - Schema: &schemas.Schema{ - ID: modelName, - ResourceFields: map[string]schemas.Field{}, - Attributes: map[string]interface{}{}, - Description: k.GetDescription(), - }, - } - - for fieldName, schemaField := range k.Fields { - s.ResourceFields[fieldName] = toField(schemaField) - } - - for _, fieldName := range k.RequiredFields { - if f, ok := s.ResourceFields[fieldName]; ok { - f.Required = true - s.ResourceFields[fieldName] = f - } - } - - if ms, ok := k.Extensions["x-kubernetes-group-version-kind"].([]interface{}); ok { - for _, mv := range ms { - if m, ok := mv.(map[interface{}]interface{}); ok { - gvk := schema.GroupVersionKind{ - Group: convert.ToString(m["group"]), - Version: convert.ToString(m["version"]), - Kind: convert.ToString(m["kind"]), - } - - s.ID = GVKToVersionedSchemaID(gvk) - attributes.SetGVK(&s, gvk) - } - } - } - - for k, v := range s.ResourceFields { - if types.ReservedFields[k] { - s.ResourceFields["_"+k] = v - delete(s.ResourceFields, k) - } - } - - return &s -} - -func AddOpenAPI(client discovery.DiscoveryInterface, schemas map[string]*types.APISchema) error { - openapi, err := client.OpenAPISchema() - if err != nil { - return err - } - - models, err := proto.NewOpenAPIData(openapi) - if err != nil { - return err - } - - for _, modelName := range models.ListModels() { - model := models.LookupModel(modelName) - if k, ok := model.(*proto.Kind); ok { - schema := modelToSchema(modelName, k) - schemas[schema.ID] = schema - } - } - - return nil -} - -func toField(schema proto.Schema) schemas.Field { - f := schemas.Field{ - Description: schema.GetDescription(), - Create: true, - Update: true, - } - switch v := schema.(type) { - case *proto.Array: - f.Type = "array[" + toField(v.SubType).Type + "]" - case *proto.Primitive: - if v.Type == "number" || v.Type == "integer" { - f.Type = "int" - } else { - f.Type = v.Type - } - case *proto.Map: - f.Type = "map[" + toField(v.SubType).Type + "]" - case *proto.Kind: - f.Type = v.Path.String() - case proto.Reference: - sub := v.SubSchema() - if p, ok := sub.(*proto.Primitive); ok { - f.Type = p.Type - } else { - f.Type = sub.GetPath().String() - } - case *proto.Arbitrary: - logrus.Debugf("arbitrary type: %v", schema) - f.Type = "json" - default: - logrus.Errorf("unknown type: %v", schema) - f.Type = "json" - } - - return f -} diff --git a/pkg/schema/converter/openapiv3.go b/pkg/schema/converter/openapiv3.go deleted file mode 100644 index ee7bc78d..00000000 --- a/pkg/schema/converter/openapiv3.go +++ /dev/null @@ -1,92 +0,0 @@ -package converter - -import ( - "github.com/rancher/apiserver/pkg/types" - "github.com/rancher/wrangler/v2/pkg/schemas" - v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" -) - -func modelV3ToSchema(name string, k *v1.JSONSchemaProps, schemasMap map[string]*types.APISchema) *types.APISchema { - s := types.APISchema{ - Schema: &schemas.Schema{ - ID: name, - ResourceFields: map[string]schemas.Field{}, - Attributes: map[string]interface{}{}, - Description: k.Description, - }, - } - - for fieldName, schemaField := range k.Properties { - s.ResourceFields[fieldName] = toResourceField(name+"."+fieldName, schemaField, schemasMap) - } - - for _, fieldName := range k.Required { - if f, ok := s.ResourceFields[fieldName]; ok { - f.Required = true - s.ResourceFields[fieldName] = f - } - } - - if existing, ok := schemasMap[s.ID]; ok && len(existing.Attributes) > 0 { - s.Attributes = existing.Attributes - } - schemasMap[s.ID] = &s - - for k, v := range s.ResourceFields { - if types.ReservedFields[k] { - s.ResourceFields["_"+k] = v - delete(s.ResourceFields, k) - } - } - - return &s -} - -func toResourceField(name string, schema v1.JSONSchemaProps, schemasMap map[string]*types.APISchema) schemas.Field { - f := schemas.Field{ - Description: schema.Description, - Nullable: true, - Create: true, - Update: true, - } - var itemSchema *v1.JSONSchemaProps - if schema.Items != nil { - if schema.Items.Schema != nil { - itemSchema = schema.Items.Schema - } else if len(schema.Items.JSONSchemas) > 0 { - itemSchema = &schema.Items.JSONSchemas[0] - } - } - - switch schema.Type { - case "array": - if itemSchema == nil { - f.Type = "array[json]" - } else if itemSchema.Type == "object" { - f.Type = "array[" + name + "]" - modelV3ToSchema(name, itemSchema, schemasMap) - } else { - f.Type = "array[" + itemSchema.Type + "]" - } - case "object": - if schema.AdditionalProperties != nil && schema.AdditionalProperties.Schema != nil && schema.AdditionalProperties.Schema.Type == "object" { - f.Type = "map[" + name + "]" - modelV3ToSchema(name, schema.AdditionalProperties.Schema, schemasMap) - } else if schema.AdditionalProperties != nil && schema.AdditionalProperties.Schema != nil { - f.Type = "map[" + schema.AdditionalProperties.Schema.Type + "]" - } else { - f.Type = name - modelV3ToSchema(name, &schema, schemasMap) - } - case "number": - f.Type = "int" - default: - f.Type = schema.Type - } - - if f.Type == "" { - f.Type = "json" - } - - return f -}