mirror of
https://github.com/rancher/steve.git
synced 2025-04-28 19:24:42 +00:00
272 lines
9.7 KiB
Go
272 lines
9.7 KiB
Go
package definitions
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"sync"
|
|
|
|
"github.com/rancher/apiserver/pkg/apierror"
|
|
"github.com/rancher/apiserver/pkg/types"
|
|
"github.com/rancher/steve/pkg/schema/converter"
|
|
wapiextv1 "github.com/rancher/wrangler/v3/pkg/generated/controllers/apiextensions.k8s.io/v1"
|
|
"github.com/rancher/wrangler/v3/pkg/schemas/validation"
|
|
"github.com/sirupsen/logrus"
|
|
apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/labels"
|
|
"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",
|
|
}
|
|
)
|
|
|
|
type gvkModel struct {
|
|
// ModelName is the name of the OpenAPI V2 model.
|
|
// For example, the GVK Group=management.cattle.io/v2, Kind=UserAttribute will have
|
|
// the following model name: io.cattle.management.v2.UserAttribute.
|
|
ModelName string
|
|
// Schema is the OpenAPI V2 schema for this GVK
|
|
Schema proto.Schema
|
|
// CRD is the schema from the CRD for this GVK, if there exists a CRD
|
|
// for this group/kind
|
|
CRD *apiextv1.JSONSchemaProps
|
|
}
|
|
|
|
// SchemaDefinitionHandler provides a schema definition for the schema ID provided. The schema definition is built
|
|
// using the following information, in order:
|
|
//
|
|
// 1. If the schema ID refers to a BaseSchema (a schema that doesn't exist in Kubernetes), then we use the
|
|
// schema to return the schema definition - we return early. Otherwise:
|
|
// 2. We build a schema definition from the OpenAPI V2 info.
|
|
// 3. If the schemaID refers to a CRD, then we also build a schema definition from the CRD.
|
|
// 4. We merge both the OpenAPI V2 and the CRD schema definition. CRD will ALWAYS override whatever is
|
|
// in OpenAPI V2. This makes sense because CRD is defined by OpenAPI V3, so has more information. This
|
|
// merged schema definition is returned.
|
|
//
|
|
// Note: SchemaDefinitionHandler only implements a ByID handler. It does not implement any method allowing a caller
|
|
// to list definitions for all schemas.
|
|
type SchemaDefinitionHandler struct {
|
|
// gvkModels maps a schema ID (eg: management.cattle.io.userattributes) to
|
|
// the computed and cached gvkModel. It is recomputed on `Refresh()`.
|
|
gvkModels map[string]gvkModel
|
|
// models are the cached models from the last response from kubernetes.
|
|
models proto.Models
|
|
// lock protects gvkModels and models which are updated in Refresh
|
|
lock sync.RWMutex
|
|
|
|
// baseSchema are the schemas (which may not represent a real CRD) added to the server
|
|
baseSchema *types.APISchemas
|
|
|
|
// crdCache is used to add more information to a schema definition by getting information
|
|
// from the CRD of the resource being accessed (if said resource is a CRD)
|
|
crdCache wapiextv1.CustomResourceDefinitionCache
|
|
|
|
// client is the discovery client used to get the groups/resources/fields from kubernetes
|
|
client discovery.DiscoveryInterface
|
|
}
|
|
|
|
func NewSchemaDefinitionHandler(
|
|
baseSchema *types.APISchemas,
|
|
crdCache wapiextv1.CustomResourceDefinitionCache,
|
|
client discovery.DiscoveryInterface,
|
|
) *SchemaDefinitionHandler {
|
|
handler := &SchemaDefinitionHandler{
|
|
baseSchema: baseSchema,
|
|
crdCache: crdCache,
|
|
client: client,
|
|
}
|
|
return handler
|
|
}
|
|
|
|
// 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 {
|
|
openapi, err := s.client.OpenAPISchema()
|
|
if err != nil {
|
|
return fmt.Errorf("unable to fetch openapi v2 definition: %w", err)
|
|
}
|
|
models, err := proto.NewOpenAPIData(openapi)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to parse openapi definition into models: %w", err)
|
|
}
|
|
groups, err := s.client.ServerGroups()
|
|
if err != nil {
|
|
return fmt.Errorf("unable to retrieve groups: %w", err)
|
|
}
|
|
|
|
gvkModels, err := listGVKModels(models, groups, s.crdCache)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
s.lock.Lock()
|
|
defer s.lock.Unlock()
|
|
s.gvkModels = gvkModels
|
|
s.models = models
|
|
return nil
|
|
}
|
|
|
|
// byIDHandler is the Handler method for a request to get the schema definition for a specific 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 baseSchema := s.baseSchema.LookupSchema(requestSchema.ID); baseSchema != nil {
|
|
// if this schema is a base schema it won't be in the model cache. In this case, and only this case, we process
|
|
// the fields independently
|
|
definitions := baseSchemaToDefinition(*requestSchema)
|
|
return types.APIObject{
|
|
ID: request.Name,
|
|
Type: "schemaDefinition",
|
|
Object: schemaDefinition{
|
|
DefinitionType: requestSchema.ID,
|
|
Definitions: definitions,
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
s.lock.RLock()
|
|
gvkModels := s.gvkModels
|
|
protoModels := s.models
|
|
s.lock.RUnlock()
|
|
|
|
if gvkModels == nil || protoModels == nil {
|
|
return types.APIObject{}, apierror.NewAPIError(notRefreshedErrorCode, "schema definitions not yet refreshed")
|
|
}
|
|
|
|
model, ok := gvkModels[requestSchema.ID]
|
|
if !ok {
|
|
return types.APIObject{}, apierror.NewAPIError(notRefreshedErrorCode, "no model found for schema, try again after refresh")
|
|
}
|
|
|
|
schemaDef, err := buildSchemaDefinitionForModel(protoModels, model)
|
|
if err != nil {
|
|
logrus.Errorf("failed building schema definition for model %s: %s", model.ModelName, err)
|
|
return types.APIObject{}, apierror.NewAPIError(internalServerErrorCode, "failed building schema definition")
|
|
}
|
|
|
|
return types.APIObject{
|
|
ID: request.Name,
|
|
Type: "schemaDefinition",
|
|
Object: schemaDef,
|
|
}, nil
|
|
}
|
|
|
|
func buildSchemaDefinitionForModel(models proto.Models, gvk gvkModel) (schemaDefinition, error) {
|
|
definitions, err := openAPIV2ToDefinition(gvk.Schema, models, gvk.ModelName)
|
|
if err != nil {
|
|
return schemaDefinition{}, fmt.Errorf("OpenAPI V2 to definition error: %w", err)
|
|
}
|
|
|
|
// CRDs don't always exists (eg: Pods, Deployments, etc)
|
|
if gvk.CRD != nil {
|
|
// CRD definitions generally has more information than the OpenAPI V2
|
|
// because it embeds an OpenAPI V3 document. However, these 3 fields
|
|
// are the exception where the Open API V2 endpoint has more
|
|
// information.
|
|
props := gvk.CRD.DeepCopy()
|
|
delete(props.Properties, "apiVersion")
|
|
delete(props.Properties, "kind")
|
|
delete(props.Properties, "metadata")
|
|
|
|
// We want to merge the OpenAPI V2 information with the CRD information
|
|
// whenever possible because the CRD is defined by OpenAPI V3 which
|
|
// _generally_ ends up with more information than OpenAPI V2
|
|
// (eg: Optional fields wrongly ends up as type string in V2)
|
|
crdDefinitions, err := crdToDefinition(props, gvk.ModelName)
|
|
if err != nil {
|
|
return schemaDefinition{}, fmt.Errorf("failed converting CRD to schema definition: %w", err)
|
|
}
|
|
|
|
if err := definitions.Merge(crdDefinitions); err != nil {
|
|
return schemaDefinition{}, fmt.Errorf("merging V2 and CRD definition: %w", err)
|
|
}
|
|
}
|
|
|
|
return definitions, nil
|
|
}
|
|
|
|
// listGVKModels returns a map of schemaID to the gvkModel. Will use the preferred version of a
|
|
// resource if possible.
|
|
func listGVKModels(models proto.Models, groups *metav1.APIGroupList, crdCache wapiextv1.CustomResourceDefinitionCache) (map[string]gvkModel, error) {
|
|
groupToPreferredVersion := make(map[string]string)
|
|
if groups != nil {
|
|
for _, group := range groups.Groups {
|
|
groupToPreferredVersion[group.Name] = group.PreferredVersion.Version
|
|
}
|
|
}
|
|
|
|
gvkToCRD := make(map[schema.GroupVersionKind]*apiextv1.JSONSchemaProps)
|
|
crds, err := crdCache.List(labels.Everything())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, crd := range crds {
|
|
for _, version := range crd.Spec.Versions {
|
|
gvk := schema.GroupVersionKind{
|
|
Group: crd.Spec.Group,
|
|
Version: version.Name,
|
|
Kind: crd.Spec.Names.Kind,
|
|
}
|
|
if version.Schema != nil {
|
|
gvkToCRD[gvk] = version.Schema.OpenAPIV3Schema
|
|
}
|
|
}
|
|
}
|
|
|
|
schemaToGVKModel := map[string]gvkModel{}
|
|
for _, modelName := range models.ListModels() {
|
|
protoSchema := models.LookupModel(modelName)
|
|
switch protoSchema.(type) {
|
|
// It is possible that a Kubernetes resources ends up being treated as
|
|
// a *proto.Map instead of *proto.Kind when it doesn't have any fields
|
|
// defined. (eg: management.cattle.io.v1.UserAttributes)
|
|
//
|
|
// For that reason, we accept both *proto.Kind and *proto.Map
|
|
// as long as they have a GVK assigned
|
|
case *proto.Kind, *proto.Map:
|
|
default:
|
|
// no need to process models that aren't kind or map
|
|
continue
|
|
}
|
|
|
|
// Makes sure the schema has a GVK (whether it's a Map or a Kind)
|
|
gvk := converter.GetGVKForProtoSchema(protoSchema)
|
|
if gvk == nil {
|
|
continue
|
|
}
|
|
|
|
schemaID := converter.GVKToSchemaID(*gvk)
|
|
|
|
prefVersion := groupToPreferredVersion[gvk.Group]
|
|
_, ok := schemaToGVKModel[schemaID]
|
|
// we always add the preferred version to the map. However, if this isn't the preferred version the preferred group could
|
|
// be missing this resource (e.x. v1alpha1 has a resource, it's removed in v1). In those cases, we add the model name
|
|
// only if we don't already have an entry. This way we always choose the preferred, if possible, but still have 1 version
|
|
// for everything
|
|
if !ok || prefVersion == gvk.Version {
|
|
schemaToGVKModel[schemaID] = gvkModel{
|
|
ModelName: modelName,
|
|
Schema: protoSchema,
|
|
CRD: gvkToCRD[*gvk],
|
|
}
|
|
}
|
|
}
|
|
return schemaToGVKModel, nil
|
|
}
|