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  }