k8s.io/kube-openapi@v0.0.0-20240826222958-65a50c78dec5/pkg/schemaconv/smd.go (about)

     1  /*
     2  Copyright 2017 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package schemaconv
    18  
    19  import (
    20  	"fmt"
    21  	"sort"
    22  
    23  	"sigs.k8s.io/structured-merge-diff/v4/schema"
    24  )
    25  
    26  const (
    27  	quantityResource     = "io.k8s.apimachinery.pkg.api.resource.Quantity"
    28  	rawExtensionResource = "io.k8s.apimachinery.pkg.runtime.RawExtension"
    29  )
    30  
    31  type convert struct {
    32  	preserveUnknownFields bool
    33  	output                *schema.Schema
    34  
    35  	currentName   string
    36  	current       *schema.Atom
    37  	errorMessages []string
    38  }
    39  
    40  func (c *convert) push(name string, a *schema.Atom) *convert {
    41  	return &convert{
    42  		preserveUnknownFields: c.preserveUnknownFields,
    43  		output:                c.output,
    44  		currentName:           name,
    45  		current:               a,
    46  	}
    47  }
    48  
    49  func (c *convert) top() *schema.Atom { return c.current }
    50  
    51  func (c *convert) pop(c2 *convert) {
    52  	c.errorMessages = append(c.errorMessages, c2.errorMessages...)
    53  }
    54  
    55  func (c *convert) reportError(format string, args ...interface{}) {
    56  	c.errorMessages = append(c.errorMessages,
    57  		c.currentName+": "+fmt.Sprintf(format, args...),
    58  	)
    59  }
    60  
    61  func (c *convert) insertTypeDef(name string, atom schema.Atom) {
    62  	def := schema.TypeDef{
    63  		Name: name,
    64  		Atom: atom,
    65  	}
    66  	if def.Atom == (schema.Atom{}) {
    67  		// This could happen if there were a top-level reference.
    68  		return
    69  	}
    70  	c.output.Types = append(c.output.Types, def)
    71  }
    72  
    73  func (c *convert) addCommonTypes() {
    74  	c.output.Types = append(c.output.Types, untypedDef)
    75  	c.output.Types = append(c.output.Types, deducedDef)
    76  }
    77  
    78  var untypedName string = "__untyped_atomic_"
    79  
    80  var untypedDef schema.TypeDef = schema.TypeDef{
    81  	Name: untypedName,
    82  	Atom: schema.Atom{
    83  		Scalar: ptr(schema.Scalar("untyped")),
    84  		List: &schema.List{
    85  			ElementType: schema.TypeRef{
    86  				NamedType: &untypedName,
    87  			},
    88  			ElementRelationship: schema.Atomic,
    89  		},
    90  		Map: &schema.Map{
    91  			ElementType: schema.TypeRef{
    92  				NamedType: &untypedName,
    93  			},
    94  			ElementRelationship: schema.Atomic,
    95  		},
    96  	},
    97  }
    98  
    99  var deducedName string = "__untyped_deduced_"
   100  
   101  var deducedDef schema.TypeDef = schema.TypeDef{
   102  	Name: deducedName,
   103  	Atom: schema.Atom{
   104  		Scalar: ptr(schema.Scalar("untyped")),
   105  		List: &schema.List{
   106  			ElementType: schema.TypeRef{
   107  				NamedType: &untypedName,
   108  			},
   109  			ElementRelationship: schema.Atomic,
   110  		},
   111  		Map: &schema.Map{
   112  			ElementType: schema.TypeRef{
   113  				NamedType: &deducedName,
   114  			},
   115  			ElementRelationship: schema.Separable,
   116  		},
   117  	},
   118  }
   119  
   120  func makeUnions(extensions map[string]interface{}) ([]schema.Union, error) {
   121  	schemaUnions := []schema.Union{}
   122  	if iunions, ok := extensions["x-kubernetes-unions"]; ok {
   123  		unions, ok := iunions.([]interface{})
   124  		if !ok {
   125  			return nil, fmt.Errorf(`"x-kubernetes-unions" should be a list, got %#v`, unions)
   126  		}
   127  		for _, iunion := range unions {
   128  			union, ok := iunion.(map[interface{}]interface{})
   129  			if !ok {
   130  				return nil, fmt.Errorf(`"x-kubernetes-unions" items should be a map of string to unions, got %#v`, iunion)
   131  			}
   132  			unionMap := map[string]interface{}{}
   133  			for k, v := range union {
   134  				key, ok := k.(string)
   135  				if !ok {
   136  					return nil, fmt.Errorf(`"x-kubernetes-unions" has non-string key: %#v`, k)
   137  				}
   138  				unionMap[key] = v
   139  			}
   140  			schemaUnion, err := makeUnion(unionMap)
   141  			if err != nil {
   142  				return nil, err
   143  			}
   144  			schemaUnions = append(schemaUnions, schemaUnion)
   145  		}
   146  	}
   147  
   148  	// Make sure we have no overlap between unions
   149  	fs := map[string]struct{}{}
   150  	for _, u := range schemaUnions {
   151  		if u.Discriminator != nil {
   152  			if _, ok := fs[*u.Discriminator]; ok {
   153  				return nil, fmt.Errorf("%v field appears multiple times in unions", *u.Discriminator)
   154  			}
   155  			fs[*u.Discriminator] = struct{}{}
   156  		}
   157  		for _, f := range u.Fields {
   158  			if _, ok := fs[f.FieldName]; ok {
   159  				return nil, fmt.Errorf("%v field appears multiple times in unions", f.FieldName)
   160  			}
   161  			fs[f.FieldName] = struct{}{}
   162  		}
   163  	}
   164  
   165  	return schemaUnions, nil
   166  }
   167  
   168  func makeUnion(extensions map[string]interface{}) (schema.Union, error) {
   169  	union := schema.Union{
   170  		Fields: []schema.UnionField{},
   171  	}
   172  
   173  	if idiscriminator, ok := extensions["discriminator"]; ok {
   174  		discriminator, ok := idiscriminator.(string)
   175  		if !ok {
   176  			return schema.Union{}, fmt.Errorf(`"discriminator" must be a string, got: %#v`, idiscriminator)
   177  		}
   178  		union.Discriminator = &discriminator
   179  	}
   180  
   181  	if ifields, ok := extensions["fields-to-discriminateBy"]; ok {
   182  		fields, ok := ifields.(map[interface{}]interface{})
   183  		if !ok {
   184  			return schema.Union{}, fmt.Errorf(`"fields-to-discriminateBy" must be a map[string]string, got: %#v`, ifields)
   185  		}
   186  		// Needs sorted keys by field.
   187  		keys := []string{}
   188  		for ifield := range fields {
   189  			field, ok := ifield.(string)
   190  			if !ok {
   191  				return schema.Union{}, fmt.Errorf(`"fields-to-discriminateBy": field must be a string, got: %#v`, ifield)
   192  			}
   193  			keys = append(keys, field)
   194  
   195  		}
   196  		sort.Strings(keys)
   197  		reverseMap := map[string]struct{}{}
   198  		for _, field := range keys {
   199  			value := fields[field]
   200  			discriminated, ok := value.(string)
   201  			if !ok {
   202  				return schema.Union{}, fmt.Errorf(`"fields-to-discriminateBy"/%v: value must be a string, got: %#v`, field, value)
   203  			}
   204  			union.Fields = append(union.Fields, schema.UnionField{
   205  				FieldName:          field,
   206  				DiscriminatorValue: discriminated,
   207  			})
   208  
   209  			// Check that we don't have the same discriminateBy multiple times.
   210  			if _, ok := reverseMap[discriminated]; ok {
   211  				return schema.Union{}, fmt.Errorf("Multiple fields have the same discriminated name: %v", discriminated)
   212  			}
   213  			reverseMap[discriminated] = struct{}{}
   214  		}
   215  	}
   216  
   217  	return union, nil
   218  }
   219  
   220  func toStringSlice(o interface{}) (out []string, ok bool) {
   221  	switch t := o.(type) {
   222  	case []interface{}:
   223  		for _, v := range t {
   224  			switch vt := v.(type) {
   225  			case string:
   226  				out = append(out, vt)
   227  			}
   228  		}
   229  		return out, true
   230  	case []string:
   231  		return t, true
   232  	}
   233  	return nil, false
   234  }
   235  
   236  func ptr(s schema.Scalar) *schema.Scalar { return &s }
   237  
   238  // Basic conversion functions to convert OpenAPI schema definitions to
   239  // SMD Schema atoms
   240  func convertPrimitive(typ string, format string) (a schema.Atom) {
   241  	switch typ {
   242  	case "integer":
   243  		a.Scalar = ptr(schema.Numeric)
   244  	case "number":
   245  		a.Scalar = ptr(schema.Numeric)
   246  	case "string":
   247  		switch format {
   248  		case "":
   249  			a.Scalar = ptr(schema.String)
   250  		case "byte":
   251  			// byte really means []byte and is encoded as a string.
   252  			a.Scalar = ptr(schema.String)
   253  		case "int-or-string":
   254  			a.Scalar = ptr(schema.Scalar("untyped"))
   255  		case "date-time":
   256  			a.Scalar = ptr(schema.Scalar("untyped"))
   257  		default:
   258  			a.Scalar = ptr(schema.Scalar("untyped"))
   259  		}
   260  	case "boolean":
   261  		a.Scalar = ptr(schema.Boolean)
   262  	default:
   263  		a.Scalar = ptr(schema.Scalar("untyped"))
   264  	}
   265  
   266  	return a
   267  }
   268  
   269  func getListElementRelationship(ext map[string]any) (schema.ElementRelationship, []string, error) {
   270  	if val, ok := ext["x-kubernetes-list-type"]; ok {
   271  		switch val {
   272  		case "atomic":
   273  			return schema.Atomic, nil, nil
   274  		case "set":
   275  			return schema.Associative, nil, nil
   276  		case "map":
   277  			keys, ok := ext["x-kubernetes-list-map-keys"]
   278  
   279  			if !ok {
   280  				return schema.Associative, nil, fmt.Errorf("missing map keys")
   281  			}
   282  
   283  			keyNames, ok := toStringSlice(keys)
   284  			if !ok {
   285  				return schema.Associative, nil, fmt.Errorf("uninterpreted map keys: %#v", keys)
   286  			}
   287  
   288  			return schema.Associative, keyNames, nil
   289  		default:
   290  			return schema.Atomic, nil, fmt.Errorf("unknown list type %v", val)
   291  		}
   292  	} else if val, ok := ext["x-kubernetes-patch-strategy"]; ok {
   293  		switch val {
   294  		case "merge", "merge,retainKeys":
   295  			if key, ok := ext["x-kubernetes-patch-merge-key"]; ok {
   296  				keyName, ok := key.(string)
   297  
   298  				if !ok {
   299  					return schema.Associative, nil, fmt.Errorf("uninterpreted merge key: %#v", key)
   300  				}
   301  
   302  				return schema.Associative, []string{keyName}, nil
   303  			}
   304  			// It's not an error for x-kubernetes-patch-merge-key to be absent,
   305  			// it means it's a set
   306  			return schema.Associative, nil, nil
   307  		case "retainKeys":
   308  			return schema.Atomic, nil, nil
   309  		default:
   310  			return schema.Atomic, nil, fmt.Errorf("unknown patch strategy %v", val)
   311  		}
   312  	}
   313  
   314  	// Treat as atomic by default
   315  	return schema.Atomic, nil, nil
   316  }
   317  
   318  // Returns map element relationship if specified, or empty string if unspecified
   319  func getMapElementRelationship(ext map[string]any) (schema.ElementRelationship, error) {
   320  	val, ok := ext["x-kubernetes-map-type"]
   321  	if !ok {
   322  		// unset Map element relationship
   323  		return "", nil
   324  	}
   325  
   326  	switch val {
   327  	case "atomic":
   328  		return schema.Atomic, nil
   329  	case "granular":
   330  		return schema.Separable, nil
   331  	default:
   332  		return "", fmt.Errorf("unknown map type %v", val)
   333  	}
   334  }