diff --git a/go.mod b/go.mod index e2b6e01c..2e9f7c54 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ replace ( require ( github.com/adrg/xdg v0.4.0 github.com/golang/mock v1.6.0 - github.com/google/gnostic v0.5.7-v3refs + github.com/google/gnostic-models v0.6.8 github.com/gorilla/mux v1.8.0 github.com/gorilla/websocket v1.5.0 github.com/pborman/uuid v1.2.1 @@ -58,7 +58,6 @@ require ( github.com/go-openapi/swag v0.22.3 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/protobuf v1.5.3 // indirect - github.com/google/gnostic-models v0.6.8 // indirect github.com/google/go-cmp v0.5.9 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/uuid v1.3.0 // indirect diff --git a/go.sum b/go.sum index a8df3480..0936aeb7 100644 --- a/go.sum +++ b/go.sum @@ -73,7 +73,6 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= -github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= github.com/emicklei/go-restful/v3 v3.8.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= @@ -165,8 +164,6 @@ github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiu github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= -github.com/google/gnostic v0.5.7-v3refs h1:FhTMOKj2VhjpouxvWJAV1TL304uMlb9zcDqkl6cEI54= -github.com/google/gnostic v0.5.7-v3refs/go.mod h1:73MKFl6jIHelAJNaBGFzt3SPtZULs9dYrGFt8OiIsHQ= github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= @@ -347,7 +344,6 @@ github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTd github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -733,7 +729,6 @@ google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7Fc google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20230526161137-0005af68ea54 h1:9NWlQfY2ePejTmfwUH1OWwmznFa+0kKcHGPDvcPza9M= google.golang.org/genproto v0.0.0-20230526161137-0005af68ea54/go.mod h1:zqTuNwFlFRsw5zIts5VnzLQxSRqh+CGOTVMlYbY0Eyk= @@ -794,7 +789,6 @@ gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/pkg/resources/schema.go b/pkg/resources/schema.go index 4bb18b8f..96cdcb50 100644 --- a/pkg/resources/schema.go +++ b/pkg/resources/schema.go @@ -16,7 +16,6 @@ import ( "github.com/rancher/steve/pkg/resources/formatters" "github.com/rancher/steve/pkg/resources/userpreferences" "github.com/rancher/steve/pkg/schema" - steveschema "github.com/rancher/steve/pkg/schema" "github.com/rancher/steve/pkg/stores/proxy" "github.com/rancher/steve/pkg/summarycache" corecontrollers "github.com/rancher/wrangler/v2/pkg/generated/controllers/core/v1" @@ -25,7 +24,7 @@ import ( ) func DefaultSchemas(ctx context.Context, baseSchema *types.APISchemas, ccache clustercache.ClusterCache, - cg proxy.ClientGetter, schemaFactory steveschema.Factory, serverVersion string) error { + cg proxy.ClientGetter, schemaFactory schema.Factory, serverVersion string) error { counts.Register(baseSchema, ccache) subscribe.Register(baseSchema, func(apiOp *types.APIRequest) *types.APISchemas { user, ok := request.UserFrom(apiOp.Context()) diff --git a/pkg/schema/converter/discovery.go b/pkg/schema/converter/discovery.go index b73e4983..d6e55fd2 100644 --- a/pkg/schema/converter/discovery.go +++ b/pkg/schema/converter/discovery.go @@ -23,7 +23,7 @@ var ( ) // 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. +// 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 { diff --git a/pkg/schema/converter/k8stonorman.go b/pkg/schema/converter/k8stonorman.go index 1827e4d6..6e9097fd 100644 --- a/pkg/schema/converter/k8stonorman.go +++ b/pkg/schema/converter/k8stonorman.go @@ -70,7 +70,7 @@ func GetGVKForKind(kind *proto.Kind) *schema.GroupVersionKind { } // 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. +// 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{} diff --git a/pkg/schema/definitions/handler.go b/pkg/schema/definitions/handler.go new file mode 100644 index 00000000..094cb7ea --- /dev/null +++ b/pkg/schema/definitions/handler.go @@ -0,0 +1,197 @@ +package definitions + +import ( + "errors" + "fmt" + "net/http" + "sync" + "time" + + "github.com/rancher/apiserver/pkg/apierror" + "github.com/rancher/apiserver/pkg/types" + "github.com/rancher/steve/pkg/schema/converter" + "github.com/rancher/wrangler/v2/pkg/schemas/validation" + "github.com/sirupsen/logrus" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/discovery" + "k8s.io/kube-openapi/pkg/util/proto" +) + +var ( + internalServerErrorCode = validation.ErrorCode{ + Status: http.StatusInternalServerError, + Code: "InternalServerError", + } + notRefreshedErrorCode = validation.ErrorCode{ + Status: http.StatusServiceUnavailable, + Code: "SchemasNotRefreshed", + } +) + +// schemaDefinitionHandler is a byID handler for a specific schema, which provides field definitions for all schemas. +// Does not implement any method allowing a caller to list definitions for all schemas. +type schemaDefinitionHandler struct { + sync.RWMutex + + // lastRefresh is the last time that the handler retrieved models from kubernetes. + lastRefresh time.Time + // refreshStale is the duration between lastRefresh and the next refresh of models. + refreshStale time.Duration + // client is the discovery client used to get the groups/resources/fields from kubernetes. + client discovery.DiscoveryInterface + // models are the cached models from the last response from kubernetes. + models *proto.Models + // schemaToModel is a map of the schema name to the model for that schema. Can be used to load the + // top-level definition for a schema, which can then be processed by the schemaFieldVisitor. + schemaToModel map[string]string +} + +// byIDHandler is the Handler method for a request to get the schema definition for a specifc schema. Will use the +// cached models found during the last refresh as part of this process. +func (s *schemaDefinitionHandler) byIDHandler(request *types.APIRequest) (types.APIObject, error) { + // pseudo-access check, designed to make sure that users have access to the schema for the definition that they + // are accessing. + requestSchema := request.Schemas.LookupSchema(request.Name) + if requestSchema == nil { + return types.APIObject{}, apierror.NewAPIError(validation.NotFound, "no such schema") + } + + if s.needsRefresh() { + err := s.refresh() + if err != nil { + logrus.Errorf("error refreshing schemas %s", err.Error()) + return types.APIObject{}, apierror.NewAPIError(internalServerErrorCode, "error refreshing schemas") + } + } + + // lock only in read-mode so that we don't read while refresh writes. Only use a read-lock - using a write lock + // would make this endpoint only usable by one caller at a time + s.RLock() + defer s.RUnlock() + + if s.models == nil { + return types.APIObject{}, apierror.NewAPIError(notRefreshedErrorCode, "schema definitions not yet refreshed") + } + models := *s.models + modelName, ok := s.schemaToModel[requestSchema.ID] + if !ok { + return types.APIObject{}, apierror.NewAPIError(notRefreshedErrorCode, "no model found for schema, try again after refresh") + } + model := models.LookupModel(modelName) + protoKind, ok := model.(*proto.Kind) + if !ok { + errorMsg := fmt.Sprintf("model for %s was type %T, not a proto.Kind", modelName, model) + return types.APIObject{}, apierror.NewAPIError(internalServerErrorCode, errorMsg) + } + definitions := map[string]definition{} + visitor := schemaFieldVisitor{ + definitions: definitions, + models: models, + } + protoKind.Accept(&visitor) + + return types.APIObject{ + ID: request.Name, + Type: "schemaDefinition", + Object: schemaDefinition{ + DefinitionType: modelName, + Definitions: definitions, + }, + }, nil +} + +// needsRefresh readLocks and checks if the cache needs to be refreshed. +func (s *schemaDefinitionHandler) needsRefresh() bool { + s.RLock() + defer s.RUnlock() + if s.lastRefresh.IsZero() { + return true + } + return s.lastRefresh.Add(s.refreshStale).Before(time.Now()) +} + +// refresh writeLocks and updates the cache with new schemaDefinitions. Will result in a call to kubernetes to retrieve +// the openAPI schemas. +func (s *schemaDefinitionHandler) refresh() error { + s.Lock() + defer s.Unlock() + openapi, err := s.client.OpenAPISchema() + if err != nil { + return fmt.Errorf("unable to fetch openapi definition: %w", err) + } + models, err := proto.NewOpenAPIData(openapi) + if err != nil { + return fmt.Errorf("unable to parse openapi definition into models: %w", err) + } + s.models = &models + nameIndex, err := s.indexSchemaNames(models) + // indexSchemaNames may successfully refresh some definitions, but still return an error + // in these cases, store what we could find, but still return up an error + if nameIndex != nil { + s.schemaToModel = nameIndex + s.lastRefresh = time.Now() + } + if err != nil { + return fmt.Errorf("unable to index schema name to model name: %w", err) + } + return nil +} + +// indexSchemaNames returns a map of schemaID to the modelName for a given schema. Will use the preferred version of a +// resource if possible. May return a map and an error if it was able to index some schemas but not others. +func (s *schemaDefinitionHandler) indexSchemaNames(models proto.Models) (map[string]string, error) { + _, resourceLists, err := s.client.ServerGroupsAndResources() + // this may occasionally fail to discover certain groups, but we still can refresh the others in those cases + if _, ok := err.(*discovery.ErrGroupDiscoveryFailed); err != nil && !ok { + return nil, fmt.Errorf("unable to retrieve groups and resources: %w", err) + } + preferredResourceVersions := map[schema.GroupKind]string{} + for _, resourceList := range resourceLists { + if resourceList == nil { + continue + } + groupVersion, gvErr := schema.ParseGroupVersion(resourceList.GroupVersion) + // we may fail to parse the GV of one group, but can still parse out the others + if gvErr != nil { + err = errors.Join(err, fmt.Errorf("unable to parse group version %s: %w", resourceList.GroupVersion, gvErr)) + continue + } + for _, resource := range resourceList.APIResources { + gk := schema.GroupKind{ + Group: groupVersion.Group, + Kind: resource.Kind, + } + // per the resource docs, if the resource.Version is empty, the preferred version for + // this resource is the version of the APIResourceList it is in + if resource.Version == "" || resource.Version == groupVersion.Version { + preferredResourceVersions[gk] = groupVersion.Version + } + } + } + schemaToModel := map[string]string{} + for _, modelName := range models.ListModels() { + protoKind, ok := models.LookupModel(modelName).(*proto.Kind) + if !ok { + // no need to process models that aren't kinds + continue + } + gvk := converter.GetGVKForKind(protoKind) + if gvk == nil { + // not all kinds are for top-level resources, since these won't have a schema, + // we can safely continue + continue + } + gk := schema.GroupKind{ + Group: gvk.Group, + Kind: gvk.Kind, + } + prefVersion, ok := preferredResourceVersions[gk] + // if we don't have a known preferred version for this group or we are the preferred version + // add this as the model name for the schema + if !ok || prefVersion == gvk.Version { + schemaID := converter.GVKToSchemaID(*gvk) + schemaToModel[schemaID] = modelName + } + } + return schemaToModel, err +} diff --git a/pkg/schema/definitions/schema.go b/pkg/schema/definitions/schema.go new file mode 100644 index 00000000..b6ddf44c --- /dev/null +++ b/pkg/schema/definitions/schema.go @@ -0,0 +1,51 @@ +package definitions + +import ( + "time" + + "github.com/rancher/apiserver/pkg/types" + "github.com/rancher/wrangler/v2/pkg/schemas" + "k8s.io/client-go/discovery" +) + +const ( + gvkExtensionName = "x-kubernetes-group-version-kind" + gvkExtensionGroup = "group" + gvkExtensionVersion = "version" + gvkExtensionKind = "kind" + defaultDuration = time.Second * 5 +) + +// Register registers the schemaDefinition schema. +func Register(baseSchema *types.APISchemas, client discovery.DiscoveryInterface) { + handler := schemaDefinitionHandler{ + client: client, + refreshStale: defaultDuration, + } + baseSchema.MustAddSchema(types.APISchema{ + Schema: &schemas.Schema{ + ID: "schemaDefinition", + PluralName: "schemaDefinitions", + ResourceMethods: []string{"GET"}, + }, + ByIDHandler: handler.byIDHandler, + }) +} + +type schemaDefinition struct { + DefinitionType string `json:"definitionType"` + Definitions map[string]definition `json:"definitions"` +} + +type definition struct { + ResourceFields map[string]definitionField `json:"resourceFields"` + Type string `json:"type"` + Description string `json:"description"` +} + +type definitionField struct { + Type string `json:"type"` + SubType string `json:"subtype,omitempty"` + Description string `json:"description,omitempty"` + Required bool `json:"required,omitempty"` +} diff --git a/pkg/schema/definitions/visitor.go b/pkg/schema/definitions/visitor.go new file mode 100644 index 00000000..c112dbd0 --- /dev/null +++ b/pkg/schema/definitions/visitor.go @@ -0,0 +1,122 @@ +package definitions + +import ( + "k8s.io/kube-openapi/pkg/util/proto" +) + +// schemaFieldVisitor implements proto.SchemaVisitor and turns a given schema into a definitionField. +type schemaFieldVisitor struct { + field definitionField + definitions map[string]definition + models proto.Models +} + +// VisitArray turns an array into a definitionField (stored on the receiver). For arrays of complex types, will also +// visit the subtype. +func (s *schemaFieldVisitor) VisitArray(array *proto.Array) { + field := definitionField{ + Description: array.GetDescription(), + } + // this currently is not recursive and provides little information for nested types- while this isn't optimal, + // it was kept this way to provide backwards compat with previous endpoints. + array.SubType.Accept(s) + subField := s.field + field.Type = "array" + field.SubType = subField.Type + s.field = field +} + +// VisitMap turns a map into a definitionField (stored on the receiver). For maps of complex types, will also visit the +// subtype. +func (s *schemaFieldVisitor) VisitMap(protoMap *proto.Map) { + field := definitionField{ + Description: protoMap.GetDescription(), + } + // this currently is not recursive and provides little information for nested types- while this isn't optimal, + // it was kept this way to provide backwards compat with previous endpoints. + protoMap.SubType.Accept(s) + subField := s.field + field.Type = "map" + field.SubType = subField.Type + s.field = field +} + +// VisitPrimitive turns a primitive into a definitionField (stored on the receiver). +func (s *schemaFieldVisitor) VisitPrimitive(primitive *proto.Primitive) { + field := definitionField{ + Description: primitive.GetDescription(), + } + if primitive.Type == "number" || primitive.Type == "integer" { + field.Type = "int" + } else { + field.Type = primitive.Type + } + s.field = field +} + +// VisitKind turns a kind into a definitionField and a definition. Both are stored on the receiver. +func (s *schemaFieldVisitor) VisitKind(kind *proto.Kind) { + path := kind.Path.String() + field := definitionField{ + Description: kind.GetDescription(), + Type: path, + } + if _, ok := s.definitions[path]; ok { + // if we have already seen this kind, we don't want to re-evaluate the definition. Some kinds can be + // recursive through use of references, so this circuit-break is necessary to avoid infinite loops + s.field = field + return + } + schemaDefinition := definition{ + ResourceFields: map[string]definitionField{}, + Type: path, + Description: kind.GetDescription(), + } + // this definition may refer to itself, so we mark this as seen to not infinitely recurse + s.definitions[path] = definition{} + for fieldName, schemaField := range kind.Fields { + schemaField.Accept(s) + schemaDefinition.ResourceFields[fieldName] = s.field + } + for _, field := range kind.RequiredFields { + current, ok := schemaDefinition.ResourceFields[field] + if !ok { + // this does silently ignore inconsistent kinds that list + continue + } + current.Required = true + schemaDefinition.ResourceFields[field] = current + } + s.definitions[path] = schemaDefinition + // the visitor may have set the field multiple times while evaluating kind fields, so we only set the final + // kind-based field at the end + s.field = field +} + +// VisitReference turns a reference into a definitionField. Will also visit the referred type. +func (s *schemaFieldVisitor) VisitReference(ref proto.Reference) { + sub := ref.SubSchema() + if sub == nil { + // if we don't have a sub-schema defined, we can't extract much meaningful information + field := definitionField{ + Description: ref.GetDescription(), + Type: ref.Reference(), + } + s.field = field + return + } + sub.Accept(s) + field := s.field + field.Description = ref.GetDescription() + s.field = field +} + +// VisitArbitrary turns an abitrary (item with no type) into a definitionField (stored on the receiver). +func (s *schemaFieldVisitor) VisitArbitrary(arb *proto.Arbitrary) { + // In certain cases k8s seems to not provide a type for certain fields. We assume for the + // purposes of this visitor that all of these have a type of string. + s.field = definitionField{ + Description: arb.GetDescription(), + Type: "string", + } +} diff --git a/pkg/schema/factory_test.go b/pkg/schema/factory_test.go index bc9ac419..20dc5c0a 100644 --- a/pkg/schema/factory_test.go +++ b/pkg/schema/factory_test.go @@ -2,9 +2,10 @@ package schema import ( "context" - "github.com/stretchr/testify/assert" "testing" + "github.com/stretchr/testify/assert" + "github.com/rancher/apiserver/pkg/types" "github.com/rancher/wrangler/v2/pkg/schemas" k8sSchema "k8s.io/apimachinery/pkg/runtime/schema" diff --git a/pkg/server/server.go b/pkg/server/server.go index 06f1c0e4..88ee2da4 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -18,6 +18,7 @@ import ( "github.com/rancher/steve/pkg/resources/common" "github.com/rancher/steve/pkg/resources/schemas" "github.com/rancher/steve/pkg/schema" + "github.com/rancher/steve/pkg/schema/definitions" "github.com/rancher/steve/pkg/server/handler" "github.com/rancher/steve/pkg/server/router" "github.com/rancher/steve/pkg/summarycache" @@ -141,6 +142,7 @@ func setup(ctx context.Context, server *Server) error { if err = resources.DefaultSchemas(ctx, server.BaseSchemas, ccache, server.ClientFactory, sf, server.Version); err != nil { return err } + definitions.Register(server.BaseSchemas, server.controllers.K8s.Discovery()) summaryCache := summarycache.New(sf, ccache) summaryCache.Start(ctx)