Skip to content

Commit

Permalink
Fix missing fields in schema definition endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
tomleb committed Jun 12, 2024
1 parent d0f58fc commit ca9ad29
Show file tree
Hide file tree
Showing 10 changed files with 1,467 additions and 209 deletions.
16 changes: 12 additions & 4 deletions pkg/schema/converter/k8stonorman.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,12 @@ 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)
// GetGVKForProto attempts to retrieve a GVK for a given OpenAPI V2 schema
// object.
// The GVK is defined in an extension. It is possible that the protoSchema does
// not have the GVK extension set - in that case, we return nil.
func GetGVKForProtoSchema(protoSchema proto.Schema) *schema.GroupVersionKind {
extensions, ok := protoSchema.GetExtensions()[gvkExtensionName].([]any)
if !ok {
return nil
}
Expand All @@ -69,6 +71,12 @@ func GetGVKForKind(kind *proto.Kind) *schema.GroupVersionKind {
return nil
}

// 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 {
return GetGVKForProtoSchema(kind)
}

// 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) {
Expand Down
248 changes: 248 additions & 0 deletions pkg/schema/definitions/converter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
package definitions

import (
"fmt"

"github.com/rancher/apiserver/pkg/types"
wranglerDefinition "github.com/rancher/wrangler/v3/pkg/schemas/definition"
apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
"k8s.io/kube-openapi/pkg/util/proto"
)

// crdToDefinition builds a schemaDefinition for a CustomResourceDefinition
func crdToDefinition(jsonSchemaProps *apiextv1.JSONSchemaProps, modelName string) schemaDefinition {
path := proto.NewPath(modelName)

definitions := make(map[string]definition)
convertJSONSchemaPropsToDefinition(*jsonSchemaProps, path, definitions)

return schemaDefinition{
DefinitionType: modelName,
Definitions: definitions,
}
}

// convertJSONSchemaPropsToDefinition recurses through the given schema props of
// type object and adds each definition found to the map of definitions
//
// This supports all OpenAPI V3 types: boolean, number, integer, string, object and array
// as defined here: https://swagger.io/specification/v3/
func convertJSONSchemaPropsToDefinition(props apiextv1.JSONSchemaProps, path proto.Path, definitions map[string]definition) {
def := definition{
Type: path.String(),
Description: props.Description,
ResourceFields: map[string]definitionField{},
}

requiredSet := make(map[string]struct{})
for _, name := range props.Required {
requiredSet[name] = struct{}{}
}

for name, prop := range props.Properties {
_, required := requiredSet[name]
field := convertJSONSchemaPropsToDefinitionField(prop, path.FieldPath(name), required)
def.ResourceFields[name] = field

if prop.Type != "object" && prop.Type != "array" {
continue
}

schema := prop
// Get the properties of the items inside the array
if prop.Type == "array" {
items := getItemsSchema(prop)
if items == nil {
continue
}
schema = *items
}

if schema.Type == "object" {
convertJSONSchemaPropsToDefinition(schema, path.FieldPath(name), definitions)
}
}
definitions[path.String()] = def
}

func convertJSONSchemaPropsToDefinitionField(props apiextv1.JSONSchemaProps, path proto.Path, required bool) definitionField {
field := definitionField{
Description: props.Description,
Required: required,
Type: getPrimitiveType(props.Type),
}
switch props.Type {
case "array":
field.Type = "array"
if item := getItemsSchema(props); item != nil {
if item.Type == "object" || item.Type == "array" {
field.SubType = path.String()
} else {
field.SubType = getPrimitiveType(item.Type)
}
}
case "object":
field.Type = path.String()
}
return field
}

// typ is a OpenAPI V2 or V3 type
func getPrimitiveType(typ string) string {
switch typ {
case "integer", "number":
return "int"
default:
return typ
}
}

func getItemsSchema(props apiextv1.JSONSchemaProps) *apiextv1.JSONSchemaProps {
if props.Items == nil {
return nil
}

if props.Items.Schema != nil {
return props.Items.Schema
} else if len(props.Items.JSONSchemas) > 0 {
// Copied from previous code in steve. Unclear if this path is
// ever taken because it seems to be unused even in k8s
// libraries and explicitly forbidden in CRDs
return &props.Items.JSONSchemas[0]
}
return nil
}

var _ proto.Reference = (*openAPIV2Reference)(nil)
var _ proto.Schema = (*openAPIV2Reference)(nil)

type openAPIV2Reference struct {
proto.BaseSchema
reference string
subSchema proto.Schema
}

func (r *openAPIV2Reference) Accept(v proto.SchemaVisitor) {
v.VisitReference(r)
}

func (r *openAPIV2Reference) Reference() string {
return r.reference
}

func (r *openAPIV2Reference) SubSchema() proto.Schema {
return r.subSchema
}

func (r *openAPIV2Reference) GetName() string {
return fmt.Sprintf("Reference to %q", r.reference)
}

// newObjectMetaReference creates an OpenAPI V2 reference for the .metadata
// field.
// path is expected to be the path of the object without a .metadata suffix
func newObjectMetaReference(path proto.Path, models proto.Models) *openAPIV2Reference {
objectMetaModel := models.LookupModel("io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta")
ref := &openAPIV2Reference{
BaseSchema: proto.BaseSchema{
Description: "Standard object's metadata. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata",
Path: path.FieldPath("metadata"),
},
reference: "io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta",
subSchema: objectMetaModel,
}
return ref
}

// mapToKind converts a *proto.Map to a *proto.Kind by keeping the same
// description, etc but also adding the 3 minimum fields - apiVersion, kind and
// metadata.
// This function assumes that the protoMap given is a top-level object (eg: a CRD).
func mapToKind(protoMap *proto.Map, models proto.Models) *proto.Kind {
apiVersion := &proto.Primitive{
BaseSchema: proto.BaseSchema{
Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
Path: protoMap.Path.FieldPath("apiVersion"),
},
Type: "string",
}
kind := &proto.Primitive{
BaseSchema: proto.BaseSchema{
Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
Path: protoMap.Path.FieldPath("kind"),
},
Type: "string",
}
metadata := newObjectMetaReference(protoMap.Path, models)
return &proto.Kind{
BaseSchema: protoMap.BaseSchema,
Fields: map[string]proto.Schema{
"apiVersion": apiVersion,
"kind": kind,
"metadata": metadata,
},
}
}

// openAPIV2ToDefinition builds a schemaDefinition for the given schemaID based on
// Resource information from OpenAPI v2 endpoint
func openAPIV2ToDefinition(protoSchema proto.Schema, models proto.Models, modelName string) (schemaDefinition, error) {
switch m := protoSchema.(type) {
case *proto.Map:
// If the schema is a *proto.Map, it will not have any Fields associated with it
// even though all Kubernetes resources have at least apiVersion, kind and metadata.
//
// We transform this Map to a Kind and inject these fields
protoSchema = mapToKind(m, models)
case *proto.Kind:
default:
return schemaDefinition{}, fmt.Errorf("model for %s was type %T, not a *proto.Kind nor *proto.Map", modelName, protoSchema)
}
definitions := map[string]definition{}
visitor := schemaFieldVisitor{
definitions: definitions,
}
protoSchema.Accept(&visitor)

return schemaDefinition{
DefinitionType: modelName,
Definitions: definitions,
}, nil
}

// baseSchemaToDefinition converts a given schema to the definition map. This should only be used with baseSchemas, whose definitions
// are expected to be set by another application and may not be k8s resources.
func baseSchemaToDefinition(schema types.APISchema) map[string]definition {
definitions := map[string]definition{}
def := definition{
Description: schema.Description,
Type: schema.ID,
ResourceFields: map[string]definitionField{},
}
for fieldName, field := range schema.ResourceFields {
fieldType, subType := parseFieldType(field.Type)
def.ResourceFields[fieldName] = definitionField{
Type: fieldType,
SubType: subType,
Description: field.Description,
Required: field.Required,
}
}
definitions[schema.ID] = def
return definitions
}

// parseFieldType parses a schemas.Field's type to a type (first return) and subType (second return)
func parseFieldType(fieldType string) (string, string) {
subType := wranglerDefinition.SubType(fieldType)
if wranglerDefinition.IsMapType(fieldType) {
return "map", subType
}
if wranglerDefinition.IsArrayType(fieldType) {
return "array", subType
}
if wranglerDefinition.IsReferenceType(fieldType) {
return "reference", subType
}
return fieldType, ""
}
Loading

0 comments on commit ca9ad29

Please sign in to comment.