knative.dev/pkg@v0.0.0-20260602142205-ac97e43f6622/apis/field_error.go (about) 1 /* 2 Copyright 2017 The Knative 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 apis 18 19 import ( 20 "fmt" 21 "sort" 22 "strings" 23 24 "knative.dev/pkg/kmp" 25 ) 26 27 // CurrentField is a constant to supply as a fieldPath for when there is 28 // a problem with the current field itself. 29 const CurrentField = "" 30 31 // DiagnosticLevel is used to signal the severity of a particular diagnostic 32 // in the form of a FieldError. 33 type DiagnosticLevel int 34 35 const ( 36 // ErrorLevel is used to signify fatal/blocking diagnostics, e.g. those 37 // that should block admission in a validating admission webhook. 38 ErrorLevel DiagnosticLevel = iota 39 40 // WarningLevel is used to signify information/non-blocking diagnostics, 41 // e.g. those that should be surfaced as warnings in a validating admission 42 // webhook. 43 WarningLevel 44 ) 45 46 func (dl DiagnosticLevel) String() string { 47 switch dl { 48 case ErrorLevel: 49 return "Error" 50 case WarningLevel: 51 return "Warning" 52 53 default: 54 return fmt.Sprintf("<UNKNOWN: %d>", dl) 55 } 56 } 57 58 // FieldError is used to propagate the context of errors pertaining to 59 // specific fields in a manner suitable for use in a recursive walk, so 60 // that errors contain the appropriate field context. 61 // FieldError methods are non-mutating. 62 // +k8s:deepcopy-gen=true 63 type FieldError struct { 64 // Message holds the main diagnostic message carried by this FieldError 65 Message string 66 67 // Paths holds a list of paths to which this diagnostic pertains 68 Paths []string 69 70 // Level holds the severity of the diagnostic. 71 // If empty, this defaults to ErrorLevel. 72 Level DiagnosticLevel 73 74 // Details contains an optional longer payload. 75 // +optional 76 Details string 77 78 errors []FieldError 79 } 80 81 // FieldError implements error 82 var _ error = (*FieldError)(nil) 83 84 // ViaField is used to propagate a validation error along a field access. 85 // For example, if a type recursively validates its "spec" via: 86 // 87 // if err := foo.Spec.Validate(); err != nil { 88 // // Augment any field paths with the context that they were accessed 89 // // via "spec". 90 // return err.ViaField("spec") 91 // } 92 func (fe *FieldError) ViaField(prefix ...string) *FieldError { 93 if fe == nil { 94 return nil 95 } 96 // Copy over message and details, paths will be updated and errors come 97 // along using .Also(). 98 newErr := &FieldError{ 99 Message: fe.Message, 100 Level: fe.Level, 101 Details: fe.Details, 102 } 103 104 // Prepend the Prefix to existing errors. 105 newPaths := make([]string, 0, len(fe.Paths)) 106 for _, oldPath := range fe.Paths { 107 newPaths = append(newPaths, flatten(append(prefix, oldPath))) 108 } 109 newErr.Paths = newPaths 110 for _, e := range fe.errors { 111 newErr = newErr.Also(e.ViaField(prefix...)) 112 } 113 return newErr 114 } 115 116 // ViaIndex is used to attach an index to the next ViaField provided. 117 // For example, if a type recursively validates a parameter that has a collection: 118 // 119 // for i, c := range spec.Collection { 120 // if err := doValidation(c); err != nil { 121 // return err.ViaIndex(i).ViaField("collection") 122 // } 123 // } 124 func (fe *FieldError) ViaIndex(index int) *FieldError { 125 return fe.ViaField(asIndex(index)) 126 } 127 128 // ViaFieldIndex is the short way to chain: err.ViaIndex(bar).ViaField(foo) 129 func (fe *FieldError) ViaFieldIndex(field string, index int) *FieldError { 130 return fe.ViaIndex(index).ViaField(field) 131 } 132 133 // ViaKey is used to attach a key to the next ViaField provided. 134 // For example, if a type recursively validates a parameter that has a collection: 135 // 136 // for k, v := range spec.Bag { 137 // if err := doValidation(v); err != nil { 138 // return err.ViaKey(k).ViaField("bag") 139 // } 140 // } 141 func (fe *FieldError) ViaKey(key string) *FieldError { 142 return fe.ViaField(asKey(key)) 143 } 144 145 // ViaFieldKey is the short way to chain: err.ViaKey(bar).ViaField(foo) 146 func (fe *FieldError) ViaFieldKey(field, key string) *FieldError { 147 return fe.ViaKey(key).ViaField(field) 148 } 149 150 // At is a way to alter the level of the diagnostics held in this FieldError. 151 // 152 // ErrMissingField("foo").At(WarningLevel) 153 func (fe *FieldError) At(l DiagnosticLevel) *FieldError { 154 if fe == nil { 155 return nil 156 } 157 // Copy over message and details, paths will be updated and errors come 158 // along using .Also(). 159 newErr := &FieldError{ 160 Message: fe.Message, 161 Level: l, 162 Details: fe.Details, 163 Paths: fe.Paths, 164 } 165 166 for _, e := range fe.errors { 167 newErr = newErr.Also(e.At(l)) 168 } 169 return newErr 170 } 171 172 // Filter is a way to access the set of diagnostics having a particular level. 173 // 174 // if err := x.Validate(ctx).Filter(ErrorLevel); err != nil { 175 // return err 176 // } 177 func (fe *FieldError) Filter(l DiagnosticLevel) *FieldError { 178 if fe == nil { 179 return nil 180 } 181 var newErr *FieldError 182 if l == fe.Level { 183 newErr = &FieldError{ 184 Message: fe.Message, 185 Level: fe.Level, 186 Details: fe.Details, 187 Paths: fe.Paths, 188 } 189 } 190 191 for _, e := range fe.errors { 192 newErr = newErr.Also(e.Filter(l)) 193 } 194 if newErr.isEmpty() { 195 return nil 196 } 197 return newErr 198 } 199 200 // Also collects errors, returns a new collection of existing errors and new errors. 201 func (fe *FieldError) Also(errs ...*FieldError) *FieldError { 202 // Avoid doing any work, if we don't have to. 203 if l := len(errs); l == 0 || l == 1 && errs[0].isEmpty() { 204 return fe 205 } 206 207 var newErr *FieldError 208 // collect the current objects errors, if it has any 209 if !fe.isEmpty() { 210 newErr = fe.DeepCopy() 211 } else { 212 newErr = &FieldError{} 213 } 214 // and then collect the passed in errors 215 for _, e := range errs { 216 if !e.isEmpty() { 217 newErr.errors = append(newErr.errors, *e) 218 } 219 } 220 if newErr.isEmpty() { 221 return nil 222 } 223 return newErr 224 } 225 226 func (fe *FieldError) isEmpty() bool { 227 if fe == nil { 228 return true 229 } 230 return fe.Message == "" && fe.Details == "" && len(fe.errors) == 0 && len(fe.Paths) == 0 231 } 232 233 // normalized returns a flattened copy of all the errors. 234 func (fe *FieldError) normalized() []*FieldError { 235 // In case we call normalized on a nil object, return just an empty 236 // list. This can happen when .Error() is called on a nil object. 237 if fe == nil { 238 return []*FieldError(nil) 239 } 240 241 // Allocate errors with at least as many objects as we'll get on the first pass. 242 errors := make([]*FieldError, 0, len(fe.errors)+1) 243 // If this FieldError is a leaf, add it. 244 if fe.Message != "" { 245 errors = append(errors, &FieldError{ 246 Message: fe.Message, 247 Level: fe.Level, 248 Paths: fe.Paths, 249 Details: fe.Details, 250 }) 251 } 252 // And then collect all other errors recursively. 253 for _, e := range fe.errors { 254 errors = append(errors, e.normalized()...) 255 } 256 return errors 257 } 258 259 // WrappedErrors returns the value of the errors after normalizing and deduping using merge(). 260 func (fe *FieldError) WrappedErrors() []*FieldError { 261 return merge(fe.normalized()) 262 } 263 264 // Error implements error 265 func (fe *FieldError) Error() string { 266 // Get the list of errors as a flat merged list. 267 normedErrors := fe.WrappedErrors() 268 errs := make([]string, 0, len(normedErrors)) 269 for _, e := range normedErrors { 270 if e.Details == "" { 271 errs = append(errs, fmt.Sprintf("%v: %v", e.Message, strings.Join(e.Paths, ", "))) 272 } else { 273 errs = append(errs, fmt.Sprintf("%v: %v\n%v", e.Message, strings.Join(e.Paths, ", "), e.Details)) 274 } 275 } 276 return strings.Join(errs, "\n") 277 } 278 279 // Helpers --- 280 281 func asIndex(index int) string { 282 return fmt.Sprintf("[%d]", index) 283 } 284 285 func isIndex(part string) bool { 286 return strings.HasPrefix(part, "[") && strings.HasSuffix(part, "]") 287 } 288 289 func asKey(key string) string { 290 return fmt.Sprintf("[%s]", key) 291 } 292 293 // flatten takes in a array of path components and looks for chances to flatten 294 // objects that have index prefixes, examples: 295 // 296 // err([0]).ViaField(bar).ViaField(foo) -> foo.bar.[0] converts to foo.bar[0] 297 // err(bar).ViaIndex(0).ViaField(foo) -> foo.[0].bar converts to foo[0].bar 298 // err(bar).ViaField(foo).ViaIndex(0) -> [0].foo.bar converts to [0].foo.bar 299 // err(bar).ViaIndex(0).ViaIndex(1).ViaField(foo) -> foo.[1].[0].bar converts to foo[1][0].bar 300 func flatten(path []string) string { 301 var newPath []string 302 for _, part := range path { 303 for _, p := range strings.Split(part, ".") { 304 switch { 305 case p == CurrentField: 306 continue 307 case len(newPath) > 0 && isIndex(p): 308 newPath[len(newPath)-1] += p 309 default: 310 newPath = append(newPath, p) 311 } 312 } 313 } 314 return strings.Join(newPath, ".") 315 } 316 317 // mergePaths takes in two string slices and returns the combination of them 318 // without any duplicate entries. 319 func mergePaths(a, b []string) []string { 320 newPaths := make([]string, 0, len(a)+len(b)) 321 newPaths = append(newPaths, a...) 322 for _, bi := range b { 323 if !containsString(newPaths, bi) { 324 newPaths = append(newPaths, bi) 325 } 326 } 327 return newPaths 328 } 329 330 // containsString takes in a string slice and looks for the provided string 331 // within the slice. 332 func containsString(slice []string, s string) bool { 333 for _, item := range slice { 334 if item == s { 335 return true 336 } 337 } 338 return false 339 } 340 341 // merge takes in a flat list of FieldErrors and returns back a merged list of 342 // FieldErrors. FieldErrors have their Paths combined (and de-duped) if their 343 // Message and Details are the same. Merge will not inspect FieldError.errors. 344 // Merge will also sort the .Path slice, and the errors slice before returning. 345 func merge(errs []*FieldError) []*FieldError { 346 // make a map big enough for all the errors. 347 m := make(map[string]*FieldError, len(errs)) 348 349 // Convert errs to a map where the key is <message>-<details> and the value 350 // is the error. If an error already exists in the map with the same key, 351 // then the paths will be merged. 352 for _, e := range errs { 353 k := key(e) 354 if v, ok := m[k]; ok { 355 // Found a match, merge the keys. 356 v.Paths = mergePaths(v.Paths, e.Paths) 357 } else { 358 // Does not exist in the map, save the error. 359 m[k] = e 360 } 361 } 362 363 // Take the map made previously and flatten it back out again. 364 newErrs := make([]*FieldError, 0, len(m)) 365 for _, v := range m { 366 // While we have access to the merged paths, sort them too. 367 sort.Slice(v.Paths, func(i, j int) bool { return v.Paths[i] < v.Paths[j] }) 368 newErrs = append(newErrs, v) 369 } 370 371 // Sort the flattened map. 372 sort.Slice(newErrs, func(i, j int) bool { 373 if newErrs[i].Message == newErrs[j].Message { 374 if newErrs[i].Details == newErrs[j].Details { 375 return newErrs[i].Level < newErrs[j].Level 376 } 377 return newErrs[i].Details < newErrs[j].Details 378 } 379 return newErrs[i].Message < newErrs[j].Message 380 }) 381 382 // return back the merged list of sorted errors. 383 return newErrs 384 } 385 386 // key returns the key using the fields .Message and .Details. 387 func key(err *FieldError) string { 388 return fmt.Sprintf("%s-%s-%s", err.Level, err.Message, err.Details) 389 } 390 391 // Public helpers --- 392 393 // ErrMissingField is a variadic helper method for constructing a FieldError for 394 // a set of missing fields. 395 func ErrMissingField(fieldPaths ...string) *FieldError { 396 return &FieldError{ 397 Message: "missing field(s)", 398 Paths: fieldPaths, 399 } 400 } 401 402 // ErrDisallowedFields is a variadic helper method for constructing a FieldError 403 // for a set of disallowed fields. 404 func ErrDisallowedFields(fieldPaths ...string) *FieldError { 405 return &FieldError{ 406 Message: "must not set the field(s)", 407 Paths: fieldPaths, 408 } 409 } 410 411 // ErrDisallowedUpdateDeprecatedFields is a variadic helper method for 412 // constructing a FieldError for updating of deprecated fields. 413 func ErrDisallowedUpdateDeprecatedFields(fieldPaths ...string) *FieldError { 414 return &FieldError{ 415 Message: "must not update deprecated field(s)", 416 Paths: fieldPaths, 417 } 418 } 419 420 // ErrInvalidArrayValue constructs a FieldError for a repetitive `field` 421 // at `index` that has received an invalid value. 422 func ErrInvalidArrayValue(value interface{}, field string, index int) *FieldError { 423 return ErrInvalidValue(value, CurrentField).ViaFieldIndex(field, index) 424 } 425 426 // ErrInvalidValue is a variadic helper method for constructing a FieldError 427 // for a field that has received an invalid value. 428 func ErrInvalidValue(value interface{}, fieldPath string, details ...string) *FieldError { 429 return &FieldError{ 430 Message: fmt.Sprint("invalid value: ", value), 431 Paths: []string{fieldPath}, 432 Details: strings.Join(details, ", "), 433 } 434 } 435 436 // ErrGeneric constructs a FieldError to allow for the different error strings for the 437 // the different cases. 438 func ErrGeneric(diagnostic string, fieldPaths ...string) *FieldError { 439 return &FieldError{ 440 Message: diagnostic, 441 Paths: fieldPaths, 442 } 443 } 444 445 // ErrMissingOneOf is a variadic helper method for constructing a FieldError for 446 // not having at least one field in a mutually exclusive field group. 447 func ErrMissingOneOf(fieldPaths ...string) *FieldError { 448 return &FieldError{ 449 Message: "expected exactly one, got neither", 450 Paths: fieldPaths, 451 } 452 } 453 454 // ErrMultipleOneOf is a variadic helper method for constructing a FieldError 455 // for having more than one field set in a mutually exclusive field group. 456 func ErrMultipleOneOf(fieldPaths ...string) *FieldError { 457 return &FieldError{ 458 Message: "expected exactly one, got both", 459 Paths: fieldPaths, 460 } 461 } 462 463 // ErrInvalidKeyName is a variadic helper method for constructing a FieldError 464 // that specifies a key name that is invalid. 465 func ErrInvalidKeyName(key, fieldPath string, details ...string) *FieldError { 466 return &FieldError{ 467 Message: fmt.Sprintf("invalid key name %q", key), 468 Paths: []string{fieldPath}, 469 Details: strings.Join(details, ", "), 470 } 471 } 472 473 // ErrOutOfBoundsValue constructs a FieldError for a field that has received an 474 // out of bound value. 475 func ErrOutOfBoundsValue(value, lower, upper interface{}, fieldPath string) *FieldError { 476 return &FieldError{ 477 Message: fmt.Sprintf("expected %v <= %v <= %v", lower, value, upper), 478 Paths: []string{fieldPath}, 479 } 480 } 481 482 // CheckDisallowedFields compares the request object against a masked request object. Fields 483 // that are set in the request object that are unset in the mask are reported back as disallowed fields. If 484 // there is an error comparing the two objects FieldError of "Internal Error" is returned. 485 func CheckDisallowedFields(request, maskedRequest interface{}) *FieldError { 486 if disallowed, err := kmp.CompareSetFields(request, maskedRequest); err != nil { 487 return &FieldError{ 488 Message: "Internal Error", 489 Paths: []string{CurrentField}, 490 } 491 } else if len(disallowed) > 0 { 492 return ErrDisallowedFields(disallowed...) 493 } 494 return nil 495 }