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 }