Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix schema definitions #215

Merged
merged 1 commit into from
Jun 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 {
tomleb marked this conversation as resolved.
Show resolved Hide resolved
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
309 changes: 309 additions & 0 deletions pkg/schema/definitions/converter.go
MbolotSuse marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,309 @@
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, error) {
path := proto.NewPath(modelName)

definitions, err := convertJSONSchemaPropsToDefinition(*jsonSchemaProps, path)
if err != nil {
return schemaDefinition{}, err
}

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

// 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) (map[string]definition, error) {
definitions := make(map[string]definition)
_, err := convertJSONSchemaPropsObject(&props, path, definitions)
if err != nil {
return definitions, err
}
return definitions, nil
}

func convertJSONSchemaProps(props *apiextv1.JSONSchemaProps, path proto.Path, definitions map[string]definition) (definitionField, error) {
if props.Type != "object" && props.Type != "array" {
return convertJSONSchemaPropsPrimitive(props), nil
}

if props.Type == "array" {
MbolotSuse marked this conversation as resolved.
Show resolved Hide resolved
return convertJSONSchemaPropsArray(props, path, definitions)
}

if len(props.Properties) > 0 {
return convertJSONSchemaPropsObject(props, path, definitions)
}

return convertJSONSchemaPropsMap(props, path, definitions)
}

func convertJSONSchemaPropsObject(props *apiextv1.JSONSchemaProps, path proto.Path, definitions map[string]definition) (definitionField, error) {
field := definitionField{
Description: props.Description,
Type: path.String(),
}

// CRDs don't support references yet, but we guard against recursive
// lookups to be safe
if _, ok := definitions[path.String()]; ok {
return field, nil
}

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 {
subField, err := convertJSONSchemaProps(&prop, path.FieldPath(name), definitions)
if err != nil {
return definitionField{}, err
}

_, required := requiredSet[name]
subField.Required = required
def.ResourceFields[name] = subField
}

definitions[path.String()] = def
MbolotSuse marked this conversation as resolved.
Show resolved Hide resolved

return field, nil
}

func convertJSONSchemaPropsPrimitive(props *apiextv1.JSONSchemaProps) definitionField {
return definitionField{
Description: props.Description,
Type: getPrimitiveType(props.Type),
}
}

func convertJSONSchemaPropsArray(props *apiextv1.JSONSchemaProps, path proto.Path, definitions map[string]definition) (definitionField, error) {
field := definitionField{
Description: props.Description,
Type: "array",
}
item := getItemsSchema(props)
if item == nil {
return definitionField{}, fmt.Errorf("array %q must have at least one item", path.String())
}

subField, err := convertJSONSchemaProps(item, path, definitions)
if err != nil {
return definitionField{}, err
}

field.SubType = subField.Type

return field, nil
}

func convertJSONSchemaPropsMap(props *apiextv1.JSONSchemaProps, path proto.Path, definitions map[string]definition) (definitionField, error) {
field := definitionField{
Description: props.Description,
Type: "map",
}
if props.AdditionalProperties != nil && props.AdditionalProperties.Schema != nil {
subField, err := convertJSONSchemaProps(props.AdditionalProperties.Schema, path, definitions)
if err != nil {
return definitionField{}, err
}
field.SubType = subField.Type
} else {
// Create the object in the definitions (won't recurse because
// by this point, we know props doesn't have any properties)
subField, err := convertJSONSchemaPropsObject(props, path, definitions)
if err != nil {
return definitionField{}, err
}
field.SubType = subField.Type
}
return field, nil

}

// typ is a OpenAPI V2 or V3 type
func getPrimitiveType(typ string) string {
MbolotSuse marked this conversation as resolved.
Show resolved Hide resolved
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]
MbolotSuse marked this conversation as resolved.
Show resolved Hide resolved
}
return nil
}

// proto.Ref has unexported fields so we must implement our own proto.Reference
// type.
var _ proto.Reference = (*openAPIV2Reference)(nil)
var _ proto.Schema = (*openAPIV2Reference)(nil)
MbolotSuse marked this conversation as resolved.
Show resolved Hide resolved

// openAPIV2Reference will be visited by proto.Schema.Accept() as a
// proto.Reference
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)
}

// 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, error) {
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",
}
objectMetaPath := "io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta"
objectMetaModel := models.LookupModel(objectMetaPath)
if objectMetaModel == nil {
return nil, fmt.Errorf("OpenAPI V2 model %q not found", objectMetaPath)
}
metadata := &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: protoMap.Path.FieldPath("metadata"),
},
reference: objectMetaPath,
subSchema: objectMetaModel,
}
return &proto.Kind{
BaseSchema: protoMap.BaseSchema,
Fields: map[string]proto.Schema{
"apiVersion": apiVersion,
"kind": kind,
"metadata": metadata,
},
}, nil
}

// 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
var err error
protoSchema, err = mapToKind(m, models)
if err != nil {
return schemaDefinition{}, fmt.Errorf("convert map to kind: %w", err)
}
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
Loading