From 19fa5ef5eb070f7401f958c8b28c173071b86d96 Mon Sep 17 00:00:00 2001 From: Lalit Chauhan Date: Tue, 6 Jan 2026 21:23:52 +0000 Subject: [PATCH] apimachinery/field: Add MarkDeclarativeNative support to Error and ErrorList --- .../util/validation/field/error_matcher.go | 16 ++++ .../validation/field/error_matcher_test.go | 50 +++++++++++ .../pkg/util/validation/field/errors.go | 89 ++++++++++++++++--- 3 files changed, 144 insertions(+), 11 deletions(-) diff --git a/staging/src/k8s.io/apimachinery/pkg/util/validation/field/error_matcher.go b/staging/src/k8s.io/apimachinery/pkg/util/validation/field/error_matcher.go index f0264e50c7d..0ec6c7ef2da 100644 --- a/staging/src/k8s.io/apimachinery/pkg/util/validation/field/error_matcher.go +++ b/staging/src/k8s.io/apimachinery/pkg/util/validation/field/error_matcher.go @@ -44,6 +44,7 @@ type ErrorMatcher struct { matchOrigin bool matchDetail func(want, got string) bool requireOriginWhenInvalid bool + matchDeclarativeNative bool // normalizationRules holds the pre-compiled regex patterns for path normalization. normalizationRules []NormalizationRule } @@ -86,6 +87,10 @@ func (m ErrorMatcher) Matches(want, got *Error) bool { if m.matchDetail != nil && !m.matchDetail(want.Detail, got.Detail) { return false } + if m.matchDeclarativeNative && want.DeclarativeNative != got.DeclarativeNative { + return false + } + return true } @@ -148,6 +153,10 @@ func (m ErrorMatcher) Render(e *Error) string { comma() buf.WriteString(fmt.Sprintf("Detail=%q", e.Detail)) } + if m.matchDeclarativeNative { + comma() + buf.WriteString(fmt.Sprintf("DeclarativeNative=%t", e.DeclarativeNative)) + } return "{" + buf.String() + "}" } @@ -224,6 +233,13 @@ func (m ErrorMatcher) RequireOriginWhenInvalid() ErrorMatcher { return m } +// ByDeclarativeNative returns a derived ErrorMatcher which also matches by the DeclarativeNative +// value of field errors. +func (m ErrorMatcher) ByDeclarativeNative() ErrorMatcher { + m.matchDeclarativeNative = true + return m +} + // ByDetailExact returns a derived ErrorMatcher which also matches errors by // the exact detail string. func (m ErrorMatcher) ByDetailExact() ErrorMatcher { diff --git a/staging/src/k8s.io/apimachinery/pkg/util/validation/field/error_matcher_test.go b/staging/src/k8s.io/apimachinery/pkg/util/validation/field/error_matcher_test.go index 45a477ce2a2..02e07fb8be5 100644 --- a/staging/src/k8s.io/apimachinery/pkg/util/validation/field/error_matcher_test.go +++ b/staging/src/k8s.io/apimachinery/pkg/util/validation/field/error_matcher_test.go @@ -303,6 +303,26 @@ func TestErrorMatcher_Matches(t *testing.T) { wantedErr: baseErr, actualErr: &Error{Type: ErrorTypeRequired, Field: "field", BadValue: "value", Detail: "detail", Origin: "origin"}, matches: false, + }, { + name: "ByDeclarativeNative: match", + matcher: ErrorMatcher{}.ByDeclarativeNative(), + wantedErr: func() *Error { + e := baseErr() + e.DeclarativeNative = true + return e + }, + actualErr: &Error{DeclarativeNative: true}, + matches: true, + }, { + name: "ByDeclarativeNative: no match", + matcher: ErrorMatcher{}.ByDeclarativeNative(), + wantedErr: func() *Error { + e := baseErr() + e.DeclarativeNative = true + return e + }, + actualErr: &Error{DeclarativeNative: false}, + matches: false, }, { name: "RequireOriginWhenInvalid: match", matcher: ErrorMatcher{}.ByOrigin().RequireOriginWhenInvalid(), @@ -406,6 +426,17 @@ func TestErrorMatcher_Test(t *testing.T) { want: ErrorList{Invalid(NewPath("f").Index(0).Child("x", "a"), nil, "")}, got: ErrorList{Invalid(NewPath("f").Index(1).Child("a"), "v", "d")}, expectedErrors: []string{"expected an error matching:", "unmatched error:"}, + }, { + name: "declarative native: match", + matcher: ErrorMatcher{}.ByDeclarativeNative(), + want: ErrorList{{DeclarativeNative: true}}, + got: ErrorList{{DeclarativeNative: true}}, + }, { + name: "declarative native: no match", + matcher: ErrorMatcher{}.ByDeclarativeNative(), + want: ErrorList{{DeclarativeNative: true}}, + got: ErrorList{{DeclarativeNative: false}}, + expectedErrors: []string{"expected an error matching:", "unmatched error:"}, }, { name: "with origin: single match", matcher: ErrorMatcher{}.ByField().ByOrigin(), @@ -533,6 +564,25 @@ func TestErrorMatcher_Render(t *testing.T) { expected: `{Field="f[0].x.a"}`, }, { + name: "with covered by declarative", + matcher: ErrorMatcher{}.ByDeclarativeNative(), + err: func() *Error { + e := Invalid(NewPath("field"), "value", "detail") + e.DeclarativeNative = true + return e + }(), + expected: `{DeclarativeNative=true}`, + }, + { + name: "all fields with covered by declarative", + matcher: ErrorMatcher{}.ByType().ByField().ByValue().ByOrigin().ByDetailExact().ByDeclarativeNative(), + err: func() *Error { + e := Invalid(NewPath("field"), "value", "detail").WithOrigin("origin") + e.DeclarativeNative = true + return e + }(), + expected: `{Type="Invalid value", Field="field", Value="value", Origin="origin", Detail="detail", DeclarativeNative=true}`, + }, { name: "requireOriginWhenInvalid with origin", matcher: ErrorMatcher{}.ByOrigin().RequireOriginWhenInvalid(), err: Invalid(NewPath("field"), "value", "detail").WithOrigin("origin"), diff --git a/staging/src/k8s.io/apimachinery/pkg/util/validation/field/errors.go b/staging/src/k8s.io/apimachinery/pkg/util/validation/field/errors.go index 950d8386823..4b83eed3104 100644 --- a/staging/src/k8s.io/apimachinery/pkg/util/validation/field/errors.go +++ b/staging/src/k8s.io/apimachinery/pkg/util/validation/field/errors.go @@ -42,7 +42,7 @@ type Error struct { // The value should be either: // - A simple camelCase identifier (e.g., "maximum", "maxItems") // - A structured format using "format=" for validation errors related to specific formats - // (e.g., "format=dns-label", "format=qualified-name") + // (e.g. "format=k8s-short-name") // // If the Origin corresponds to an existing declarative validation tag or JSON Schema keyword, // use that same name for consistency. @@ -55,6 +55,10 @@ type Error struct { // validation. This field is to identify errors from imperative validation // that should also be caught by declarative validation. CoveredByDeclarative bool + + // DeclarativeNative is true when this error originates from a declarative-native validation. + // This field is used to distinguish errors that are exclusively declarative and lack an imperative counterpart. + DeclarativeNative bool } var _ error = &Error{} @@ -201,32 +205,56 @@ func (t ErrorType) String() string { // TypeInvalid returns a *Error indicating "type is invalid" func TypeInvalid(field *Path, value interface{}, detail string) *Error { - return &Error{ErrorTypeTypeInvalid, field.String(), value, detail, "", false} + return &Error{ + Type: ErrorTypeTypeInvalid, + Field: field.String(), + BadValue: value, + Detail: detail, + } } // NotFound returns a *Error indicating "value not found". This is // used to report failure to find a requested value (e.g. looking up an ID). func NotFound(field *Path, value interface{}) *Error { - return &Error{ErrorTypeNotFound, field.String(), value, "", "", false} + return &Error{ + Type: ErrorTypeNotFound, + Field: field.String(), + BadValue: value, + } } // Required returns a *Error indicating "value required". This is used // to report required values that are not provided (e.g. empty strings, null // values, or empty arrays). func Required(field *Path, detail string) *Error { - return &Error{ErrorTypeRequired, field.String(), "", detail, "", false} + return &Error{ + Type: ErrorTypeRequired, + Field: field.String(), + Detail: detail, + BadValue: "", + } } // Duplicate returns a *Error indicating "duplicate value". This is // used to report collisions of values that must be unique (e.g. names or IDs). func Duplicate(field *Path, value interface{}) *Error { - return &Error{ErrorTypeDuplicate, field.String(), value, "", "", false} + return &Error{ + Type: ErrorTypeDuplicate, + Field: field.String(), + BadValue: value, + } } // Invalid returns a *Error indicating "invalid value". This is used // to report malformed values (e.g. failed regex match, too long, out of bounds). func Invalid(field *Path, value interface{}, detail string) *Error { - return &Error{ErrorTypeInvalid, field.String(), value, detail, "", false} + return &Error{ + Type: ErrorTypeInvalid, + Field: field.String(), + BadValue: value, + Detail: detail, + } + } // NotSupported returns a *Error indicating "unsupported value". @@ -241,7 +269,12 @@ func NotSupported[T ~string](field *Path, value interface{}, validValues []T) *E } detail = "supported values: " + strings.Join(quotedValues, ", ") } - return &Error{ErrorTypeNotSupported, field.String(), value, detail, "", false} + return &Error{ + Type: ErrorTypeNotSupported, + Field: field.String(), + BadValue: value, + Detail: detail, + } } // Forbidden returns a *Error indicating "forbidden". This is used to @@ -249,7 +282,12 @@ func NotSupported[T ~string](field *Path, value interface{}, validValues []T) *E // some conditions, but which are not permitted by current conditions (e.g. // security policy). func Forbidden(field *Path, detail string) *Error { - return &Error{ErrorTypeForbidden, field.String(), "", detail, "", false} + return &Error{ + Type: ErrorTypeForbidden, + Field: field.String(), + Detail: detail, + BadValue: "", + } } // TooLong returns a *Error indicating "too long". This is used to report that @@ -267,7 +305,12 @@ func TooLong(field *Path, _ interface{}, maxLength int) *Error { } else { msg = "value is too long" } - return &Error{ErrorTypeTooLong, field.String(), "", msg, "", false} + return &Error{ + Type: ErrorTypeTooLong, + Field: field.String(), + BadValue: "", + Detail: msg, + } } // TooLongMaxLength returns a *Error indicating "too long". @@ -299,14 +342,24 @@ func TooMany(field *Path, actualQuantity, maxQuantity int) *Error { actual = omitValue } - return &Error{ErrorTypeTooMany, field.String(), actual, msg, "", false} + return &Error{ + Type: ErrorTypeTooMany, + Field: field.String(), + BadValue: actual, + Detail: msg, + } } // InternalError returns a *Error indicating "internal error". This is used // to signal that an error was found that was not directly related to user // input. The err argument must be non-nil. func InternalError(field *Path, err error) *Error { - return &Error{ErrorTypeInternal, field.String(), nil, err.Error(), "", false} + return &Error{ + Type: ErrorTypeInternal, + Field: field.String(), + BadValue: err, + Detail: err.Error(), + } } // ErrorList holds a set of Errors. It is plausible that we might one day have @@ -397,6 +450,20 @@ func (list ErrorList) ExtractCoveredByDeclarative() ErrorList { return newList } +// MarkDeclarativeNative marks the error as originating from a declarative-native validation. +func (e *Error) MarkDeclarativeNative() *Error { + e.DeclarativeNative = true + return e +} + +// MarkDeclarativeNative marks all errors in the list as originating from declarative-native validations. +func (list ErrorList) MarkDeclarativeNative() ErrorList { + for _, err := range list { + err.DeclarativeNative = true + } + return list +} + // RemoveCoveredByDeclarative returns a new ErrorList containing only the errors that should not be covered by declarative validation. func (list ErrorList) RemoveCoveredByDeclarative() ErrorList { newList := ErrorList{}