github.com/alex123012/deckhouse-controller-tools@v0.0.0-20230510090815-d594daf1af8c/pkg/crd/flatten.go (about) 1 /* 2 Copyright 2019 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 crd 18 19 import ( 20 "fmt" 21 "reflect" 22 "sort" 23 "strings" 24 "sync" 25 26 apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 27 28 "sigs.k8s.io/controller-tools/pkg/loader" 29 ) 30 31 // ErrorRecorder knows how to record errors. It wraps the part of 32 // pkg/loader.Package that we need to record errors in places were it might not 33 // make sense to have a loader.Package 34 type ErrorRecorder interface { 35 // AddError records that the given error occurred. 36 // See the documentation on loader.Package.AddError for more information. 37 AddError(error) 38 } 39 40 // isOrNil checks if val is nil if val is of a nillable type, otherwise, 41 // it compares val to valInt (which should probably be the zero value). 42 func isOrNil(val reflect.Value, valInt interface{}, zeroInt interface{}) bool { 43 switch valKind := val.Kind(); valKind { 44 case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice: 45 return val.IsNil() 46 default: 47 return valInt == zeroInt 48 } 49 } 50 51 // flattenAllOfInto copies properties from src to dst, then copies the properties 52 // of each item in src's allOf to dst's properties as well. 53 func flattenAllOfInto(dst *apiext.JSONSchemaProps, src apiext.JSONSchemaProps, errRec ErrorRecorder) { 54 if len(src.AllOf) > 0 { 55 for _, embedded := range src.AllOf { 56 flattenAllOfInto(dst, embedded, errRec) 57 } 58 } 59 60 dstVal := reflect.Indirect(reflect.ValueOf(dst)) 61 srcVal := reflect.ValueOf(src) 62 typ := dstVal.Type() 63 64 srcRemainder := apiext.JSONSchemaProps{} 65 srcRemVal := reflect.Indirect(reflect.ValueOf(&srcRemainder)) 66 dstRemainder := apiext.JSONSchemaProps{} 67 dstRemVal := reflect.Indirect(reflect.ValueOf(&dstRemainder)) 68 hoisted := false 69 70 for i := 0; i < srcVal.NumField(); i++ { 71 fieldName := typ.Field(i).Name 72 switch fieldName { 73 case "AllOf": 74 // don't merge because we deal with it above 75 continue 76 case "Title", "Description", "Example", "ExternalDocs": 77 // don't merge because we pre-merge to properly preserve field docs 78 continue 79 } 80 srcField := srcVal.Field(i) 81 fldTyp := srcField.Type() 82 zeroVal := reflect.Zero(fldTyp) 83 zeroInt := zeroVal.Interface() 84 srcInt := srcField.Interface() 85 86 if isOrNil(srcField, srcInt, zeroInt) { 87 // nothing to copy from src, continue 88 continue 89 } 90 91 dstField := dstVal.Field(i) 92 dstInt := dstField.Interface() 93 if isOrNil(dstField, dstInt, zeroInt) { 94 // dst is empty, continue 95 dstField.Set(srcField) 96 continue 97 } 98 99 if fldTyp.Comparable() && srcInt == dstInt { 100 // same value, continue 101 continue 102 } 103 104 // resolve conflict 105 switch fieldName { 106 case "Properties": 107 // merge if possible, use all of otherwise 108 srcMap := srcInt.(map[string]apiext.JSONSchemaProps) 109 dstMap := dstInt.(map[string]apiext.JSONSchemaProps) 110 111 for k, v := range srcMap { 112 dstProp, exists := dstMap[k] 113 if !exists { 114 dstMap[k] = v 115 continue 116 } 117 flattenAllOfInto(&dstProp, v, errRec) 118 dstMap[k] = dstProp 119 } 120 case "Required": 121 // merge 122 dstField.Set(reflect.AppendSlice(dstField, srcField)) 123 case "Type": 124 if srcInt != dstInt { 125 // TODO(directxman12): figure out how to attach this back to a useful point in the Go source or in the schema 126 errRec.AddError(fmt.Errorf("conflicting types in allOf branches in schema: %s vs %s", dstInt, srcInt)) 127 } 128 // keep the destination value, for now 129 // TODO(directxman12): Default -- use field? 130 // TODO(directxman12): 131 // - Dependencies: if field x is present, then either schema validates or all props are present 132 // - AdditionalItems: like AdditionalProperties 133 // - Definitions: common named validation sets that can be references (merge, bail if duplicate) 134 case "AdditionalProperties": 135 // as of the time of writing, `allows: false` is not allowed, so we don't have to handle it 136 srcProps := srcInt.(*apiext.JSONSchemaPropsOrBool) 137 if srcProps.Schema == nil { 138 // nothing to merge 139 continue 140 } 141 dstProps := dstInt.(*apiext.JSONSchemaPropsOrBool) 142 if dstProps.Schema == nil { 143 dstProps.Schema = &apiext.JSONSchemaProps{} 144 } 145 flattenAllOfInto(dstProps.Schema, *srcProps.Schema, errRec) 146 case "XPreserveUnknownFields": 147 dstField.Set(srcField) 148 case "XMapType": 149 dstField.Set(srcField) 150 // NB(directxman12): no need to explicitly handle nullable -- false is considered to be the zero value 151 // TODO(directxman12): src isn't necessarily the field value -- it's just the most recent allOf entry 152 default: 153 // hoist into allOf... 154 hoisted = true 155 156 srcRemVal.Field(i).Set(srcField) 157 dstRemVal.Field(i).Set(dstField) 158 // ...and clear the original 159 dstField.Set(zeroVal) 160 } 161 } 162 163 if hoisted { 164 dst.AllOf = append(dst.AllOf, dstRemainder, srcRemainder) 165 } 166 167 // dedup required 168 if len(dst.Required) > 0 { 169 reqUniq := make(map[string]struct{}) 170 for _, req := range dst.Required { 171 reqUniq[req] = struct{}{} 172 } 173 dst.Required = make([]string, 0, len(reqUniq)) 174 for req := range reqUniq { 175 dst.Required = append(dst.Required, req) 176 } 177 // be deterministic 178 sort.Strings(dst.Required) 179 } 180 } 181 182 // allOfVisitor recursively visits allOf fields in the schema, 183 // merging nested allOf properties into the root schema. 184 type allOfVisitor struct { 185 // errRec is used to record errors while flattening (like two conflicting 186 // field values used in an allOf) 187 errRec ErrorRecorder 188 } 189 190 func (v *allOfVisitor) Visit(schema *apiext.JSONSchemaProps) SchemaVisitor { 191 if schema == nil { 192 return v 193 } 194 195 // clear this now so that we can safely preserve edits made my flattenAllOfInto 196 origAllOf := schema.AllOf 197 schema.AllOf = nil 198 199 for _, embedded := range origAllOf { 200 flattenAllOfInto(schema, embedded, v.errRec) 201 } 202 return v 203 } 204 205 // NB(directxman12): FlattenEmbedded is separate from Flattener because 206 // some tooling wants to flatten out embedded fields, but only actually 207 // flatten a few specific types first. 208 209 // FlattenEmbedded flattens embedded fields (represented via AllOf) which have 210 // already had their references resolved into simple properties in the containing 211 // schema. 212 func FlattenEmbedded(schema *apiext.JSONSchemaProps, errRec ErrorRecorder) *apiext.JSONSchemaProps { 213 outSchema := schema.DeepCopy() 214 EditSchema(outSchema, &allOfVisitor{errRec: errRec}) 215 return outSchema 216 } 217 218 // Flattener knows how to take a root type, and flatten all references in it 219 // into a single, flat type. Flattened types are cached, so it's relatively 220 // cheap to make repeated calls with the same type. 221 type Flattener struct { 222 // Parser is used to lookup package and type details, and parse in new packages. 223 Parser *Parser 224 225 LookupReference func(ref string, contextPkg *loader.Package) (TypeIdent, error) 226 227 // flattenedTypes hold the flattened version of each seen type for later reuse. 228 flattenedTypes map[TypeIdent]apiext.JSONSchemaProps 229 initOnce sync.Once 230 } 231 232 func (f *Flattener) init() { 233 f.initOnce.Do(func() { 234 f.flattenedTypes = make(map[TypeIdent]apiext.JSONSchemaProps) 235 if f.LookupReference == nil { 236 f.LookupReference = identFromRef 237 } 238 }) 239 } 240 241 // cacheType saves the flattened version of the given type for later reuse 242 func (f *Flattener) cacheType(typ TypeIdent, schema apiext.JSONSchemaProps) { 243 f.init() 244 f.flattenedTypes[typ] = schema 245 } 246 247 // loadUnflattenedSchema fetches a fresh, unflattened schema from the parser. 248 func (f *Flattener) loadUnflattenedSchema(typ TypeIdent) (*apiext.JSONSchemaProps, error) { 249 f.Parser.NeedSchemaFor(typ) 250 251 baseSchema, found := f.Parser.Schemata[typ] 252 if !found { 253 return nil, fmt.Errorf("unable to locate schema for type %s", typ) 254 } 255 return &baseSchema, nil 256 } 257 258 // FlattenType flattens the given pre-loaded type, removing any references from it. 259 // It deep-copies the schema first, so it won't affect the parser's version of the schema. 260 func (f *Flattener) FlattenType(typ TypeIdent) *apiext.JSONSchemaProps { 261 f.init() 262 if cachedSchema, isCached := f.flattenedTypes[typ]; isCached { 263 return &cachedSchema 264 } 265 baseSchema, err := f.loadUnflattenedSchema(typ) 266 if err != nil { 267 typ.Package.AddError(err) 268 return nil 269 } 270 resSchema := f.FlattenSchema(*baseSchema, typ.Package) 271 f.cacheType(typ, *resSchema) 272 return resSchema 273 } 274 275 // FlattenSchema flattens the given schema, removing any references. 276 // It deep-copies the schema first, so the input schema won't be affected. 277 func (f *Flattener) FlattenSchema(baseSchema apiext.JSONSchemaProps, currentPackage *loader.Package) *apiext.JSONSchemaProps { 278 resSchema := baseSchema.DeepCopy() 279 EditSchema(resSchema, &flattenVisitor{ 280 Flattener: f, 281 currentPackage: currentPackage, 282 }) 283 284 return resSchema 285 } 286 287 // RefParts splits a reference produced by the schema generator into its component 288 // type name and package name (if it's a cross-package reference). Note that 289 // referenced packages *must* be looked up relative to the current package. 290 func RefParts(ref string) (typ string, pkgName string, err error) { 291 if !strings.HasPrefix(ref, defPrefix) { 292 return "", "", fmt.Errorf("non-standard reference link %q", ref) 293 } 294 ref = ref[len(defPrefix):] 295 // decode the json pointer encodings 296 ref = strings.Replace(ref, "~1", "/", -1) 297 ref = strings.Replace(ref, "~0", "~", -1) 298 nameParts := strings.SplitN(ref, "~", 2) 299 300 if len(nameParts) == 1 { 301 // local reference 302 return nameParts[0], "", nil 303 } 304 // cross-package reference 305 return nameParts[1], nameParts[0], nil 306 } 307 308 // identFromRef converts the given schema ref from the given package back 309 // into the TypeIdent that it represents. 310 func identFromRef(ref string, contextPkg *loader.Package) (TypeIdent, error) { 311 typ, pkgName, err := RefParts(ref) 312 if err != nil { 313 return TypeIdent{}, err 314 } 315 316 if pkgName == "" { 317 // a local reference 318 return TypeIdent{ 319 Name: typ, 320 Package: contextPkg, 321 }, nil 322 } 323 324 // an external reference 325 return TypeIdent{ 326 Name: typ, 327 Package: contextPkg.Imports()[pkgName], 328 }, nil 329 } 330 331 // preserveFields copies documentation fields from src into dst, preserving 332 // field-level documentation when flattening, and preserving field-level validation 333 // as allOf entries. 334 func preserveFields(dst *apiext.JSONSchemaProps, src apiext.JSONSchemaProps) { 335 srcDesc := src.Description 336 srcTitle := src.Title 337 srcExDoc := src.ExternalDocs 338 srcEx := src.Example 339 340 src.Description, src.Title, src.ExternalDocs, src.Example = "", "", nil, nil 341 342 src.Ref = nil 343 *dst = apiext.JSONSchemaProps{ 344 AllOf: []apiext.JSONSchemaProps{*dst, src}, 345 346 // keep these, in case the source field doesn't specify anything useful 347 Description: dst.Description, 348 Title: dst.Title, 349 ExternalDocs: dst.ExternalDocs, 350 Example: dst.Example, 351 } 352 353 if srcDesc != "" { 354 dst.Description = srcDesc 355 } 356 if srcTitle != "" { 357 dst.Title = srcTitle 358 } 359 if srcExDoc != nil { 360 dst.ExternalDocs = srcExDoc 361 } 362 if srcEx != nil { 363 dst.Example = srcEx 364 } 365 } 366 367 // flattenVisitor visits each node in the schema, recursively flattening references. 368 type flattenVisitor struct { 369 *Flattener 370 371 currentPackage *loader.Package 372 currentType *TypeIdent 373 currentSchema *apiext.JSONSchemaProps 374 originalField apiext.JSONSchemaProps 375 } 376 377 func (f *flattenVisitor) Visit(baseSchema *apiext.JSONSchemaProps) SchemaVisitor { 378 if baseSchema == nil { 379 // end-of-node marker, cache the results 380 if f.currentType != nil { 381 f.cacheType(*f.currentType, *f.currentSchema) 382 // preserve field information *after* caching so that we don't 383 // accidentally cache field-level information onto the schema for 384 // the type in general. 385 preserveFields(f.currentSchema, f.originalField) 386 } 387 return f 388 } 389 390 // if we get a type that's just a ref, resolve it 391 if baseSchema.Ref != nil && len(*baseSchema.Ref) > 0 { 392 // resolve this ref 393 refIdent, err := f.LookupReference(*baseSchema.Ref, f.currentPackage) 394 if err != nil { 395 f.currentPackage.AddError(err) 396 return nil 397 } 398 399 // load and potentially flatten the schema 400 401 // check the cache first... 402 if refSchemaCached, isCached := f.flattenedTypes[refIdent]; isCached { 403 // shallow copy is fine, it's just to avoid overwriting the doc fields 404 preserveFields(&refSchemaCached, *baseSchema) 405 *baseSchema = refSchemaCached 406 return nil // don't recurse, we're done 407 } 408 409 // ...otherwise, we need to flatten 410 refSchema, err := f.loadUnflattenedSchema(refIdent) 411 if err != nil { 412 f.currentPackage.AddError(err) 413 return nil 414 } 415 refSchema = refSchema.DeepCopy() 416 417 // keep field around to preserve field-level validation, docs, etc 418 origField := *baseSchema 419 *baseSchema = *refSchema 420 421 // avoid loops (which shouldn't exist, but just in case) 422 // by marking a nil cached pointer before we start recursing 423 f.cacheType(refIdent, apiext.JSONSchemaProps{}) 424 425 return &flattenVisitor{ 426 Flattener: f.Flattener, 427 428 currentPackage: refIdent.Package, 429 currentType: &refIdent, 430 currentSchema: baseSchema, 431 originalField: origField, 432 } 433 } 434 435 // otherwise, continue recursing... 436 if f.currentType != nil { 437 // ...but don't accidentally end this node early (for caching purposes) 438 return &flattenVisitor{ 439 Flattener: f.Flattener, 440 currentPackage: f.currentPackage, 441 } 442 } 443 444 return f 445 }