/* Copyright 2017 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package schemaconv import ( "fmt" "sort" "sigs.k8s.io/structured-merge-diff/v4/schema" ) const ( quantityResource = "io.k8s.apimachinery.pkg.api.resource.Quantity" rawExtensionResource = "io.k8s.apimachinery.pkg.runtime.RawExtension" ) type convert struct { preserveUnknownFields bool output *schema.Schema currentName string current *schema.Atom errorMessages []string } func (c *convert) push(name string, a *schema.Atom) *convert { return &convert{ preserveUnknownFields: c.preserveUnknownFields, output: c.output, currentName: name, current: a, } } func (c *convert) top() *schema.Atom { return c.current } func (c *convert) pop(c2 *convert) { c.errorMessages = append(c.errorMessages, c2.errorMessages...) } func (c *convert) reportError(format string, args ...interface{}) { c.errorMessages = append(c.errorMessages, c.currentName+": "+fmt.Sprintf(format, args...), ) } func (c *convert) insertTypeDef(name string, atom schema.Atom) { def := schema.TypeDef{ Name: name, Atom: atom, } if def.Atom == (schema.Atom{}) { // This could happen if there were a top-level reference. return } c.output.Types = append(c.output.Types, def) } func (c *convert) addCommonTypes() { c.output.Types = append(c.output.Types, untypedDef) c.output.Types = append(c.output.Types, deducedDef) } var untypedName string = "__untyped_atomic_" var untypedDef schema.TypeDef = schema.TypeDef{ Name: untypedName, Atom: schema.Atom{ Scalar: ptr(schema.Scalar("untyped")), List: &schema.List{ ElementType: schema.TypeRef{ NamedType: &untypedName, }, ElementRelationship: schema.Atomic, }, Map: &schema.Map{ ElementType: schema.TypeRef{ NamedType: &untypedName, }, ElementRelationship: schema.Atomic, }, }, } var deducedName string = "__untyped_deduced_" var deducedDef schema.TypeDef = schema.TypeDef{ Name: deducedName, Atom: schema.Atom{ Scalar: ptr(schema.Scalar("untyped")), List: &schema.List{ ElementType: schema.TypeRef{ NamedType: &untypedName, }, ElementRelationship: schema.Atomic, }, Map: &schema.Map{ ElementType: schema.TypeRef{ NamedType: &deducedName, }, ElementRelationship: schema.Separable, }, }, } func makeUnions(extensions map[string]interface{}) ([]schema.Union, error) { schemaUnions := []schema.Union{} if iunions, ok := extensions["x-kubernetes-unions"]; ok { unions, ok := iunions.([]interface{}) if !ok { return nil, fmt.Errorf(`"x-kubernetes-unions" should be a list, got %#v`, unions) } for _, iunion := range unions { union, ok := iunion.(map[interface{}]interface{}) if !ok { return nil, fmt.Errorf(`"x-kubernetes-unions" items should be a map of string to unions, got %#v`, iunion) } unionMap := map[string]interface{}{} for k, v := range union { key, ok := k.(string) if !ok { return nil, fmt.Errorf(`"x-kubernetes-unions" has non-string key: %#v`, k) } unionMap[key] = v } schemaUnion, err := makeUnion(unionMap) if err != nil { return nil, err } schemaUnions = append(schemaUnions, schemaUnion) } } // Make sure we have no overlap between unions fs := map[string]struct{}{} for _, u := range schemaUnions { if u.Discriminator != nil { if _, ok := fs[*u.Discriminator]; ok { return nil, fmt.Errorf("%v field appears multiple times in unions", *u.Discriminator) } fs[*u.Discriminator] = struct{}{} } for _, f := range u.Fields { if _, ok := fs[f.FieldName]; ok { return nil, fmt.Errorf("%v field appears multiple times in unions", f.FieldName) } fs[f.FieldName] = struct{}{} } } return schemaUnions, nil } func makeUnion(extensions map[string]interface{}) (schema.Union, error) { union := schema.Union{ Fields: []schema.UnionField{}, } if idiscriminator, ok := extensions["discriminator"]; ok { discriminator, ok := idiscriminator.(string) if !ok { return schema.Union{}, fmt.Errorf(`"discriminator" must be a string, got: %#v`, idiscriminator) } union.Discriminator = &discriminator } if ifields, ok := extensions["fields-to-discriminateBy"]; ok { fields, ok := ifields.(map[interface{}]interface{}) if !ok { return schema.Union{}, fmt.Errorf(`"fields-to-discriminateBy" must be a map[string]string, got: %#v`, ifields) } // Needs sorted keys by field. keys := []string{} for ifield := range fields { field, ok := ifield.(string) if !ok { return schema.Union{}, fmt.Errorf(`"fields-to-discriminateBy": field must be a string, got: %#v`, ifield) } keys = append(keys, field) } sort.Strings(keys) reverseMap := map[string]struct{}{} for _, field := range keys { value := fields[field] discriminated, ok := value.(string) if !ok { return schema.Union{}, fmt.Errorf(`"fields-to-discriminateBy"/%v: value must be a string, got: %#v`, field, value) } union.Fields = append(union.Fields, schema.UnionField{ FieldName: field, DiscriminatorValue: discriminated, }) // Check that we don't have the same discriminateBy multiple times. if _, ok := reverseMap[discriminated]; ok { return schema.Union{}, fmt.Errorf("Multiple fields have the same discriminated name: %v", discriminated) } reverseMap[discriminated] = struct{}{} } } if union.Discriminator != nil && len(union.Fields) == 0 { return schema.Union{}, fmt.Errorf("discriminator set to %v, but no fields in union", *union.Discriminator) } return union, nil } func toStringSlice(o interface{}) (out []string, ok bool) { switch t := o.(type) { case []interface{}: for _, v := range t { switch vt := v.(type) { case string: out = append(out, vt) } } return out, true case []string: return t, true } return nil, false } func ptr(s schema.Scalar) *schema.Scalar { return &s } // Basic conversion functions to convert OpenAPI schema definitions to // SMD Schema atoms func convertPrimitive(typ string, format string) (a schema.Atom) { switch typ { case "integer": a.Scalar = ptr(schema.Numeric) case "number": a.Scalar = ptr(schema.Numeric) case "string": switch format { case "": a.Scalar = ptr(schema.String) case "byte": // byte really means []byte and is encoded as a string. a.Scalar = ptr(schema.String) case "int-or-string": a.Scalar = ptr(schema.Scalar("untyped")) case "date-time": a.Scalar = ptr(schema.Scalar("untyped")) default: a.Scalar = ptr(schema.Scalar("untyped")) } case "boolean": a.Scalar = ptr(schema.Boolean) default: a.Scalar = ptr(schema.Scalar("untyped")) } return a } func getListElementRelationship(ext map[string]any) (schema.ElementRelationship, []string, error) { if val, ok := ext["x-kubernetes-list-type"]; ok { switch val { case "atomic": return schema.Atomic, nil, nil case "set": return schema.Associative, nil, nil case "map": keys, ok := ext["x-kubernetes-list-map-keys"] if !ok { return schema.Associative, nil, fmt.Errorf("missing map keys") } keyNames, ok := toStringSlice(keys) if !ok { return schema.Associative, nil, fmt.Errorf("uninterpreted map keys: %#v", keys) } return schema.Associative, keyNames, nil default: return schema.Atomic, nil, fmt.Errorf("unknown list type %v", val) } } else if val, ok := ext["x-kubernetes-patch-strategy"]; ok { switch val { case "merge", "merge,retainKeys": if key, ok := ext["x-kubernetes-patch-merge-key"]; ok { keyName, ok := key.(string) if !ok { return schema.Associative, nil, fmt.Errorf("uninterpreted merge key: %#v", key) } return schema.Associative, []string{keyName}, nil } // It's not an error for x-kubernetes-patch-merge-key to be absent, // it means it's a set return schema.Associative, nil, nil case "retainKeys": return schema.Atomic, nil, nil default: return schema.Atomic, nil, fmt.Errorf("unknown patch strategy %v", val) } } // Treat as atomic by default return schema.Atomic, nil, nil } // Returns map element relationship if specified, or empty string if unspecified func getMapElementRelationship(ext map[string]any) (schema.ElementRelationship, error) { val, ok := ext["x-kubernetes-map-type"] if !ok { // unset Map element relationship return "", nil } switch val { case "atomic": return schema.Atomic, nil case "granular": return schema.Separable, nil default: return "", fmt.Errorf("unknown map type %v", val) } }