1
0
mirror of https://github.com/rancher/steve.git synced 2025-06-22 21:17:32 +00:00
steve/pkg/schema/definitions/converter.go

310 lines
9.8 KiB
Go

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" {
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
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 {
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
}
// proto.Ref has unexported fields so we must implement our own proto.Reference
// type.
var _ proto.Reference = (*openAPIV2Reference)(nil)
var _ proto.Schema = (*openAPIV2Reference)(nil)
// 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, ""
}