diff --git a/staging/src/k8s.io/apimachinery/pkg/api/validate/content/errors.go b/staging/src/k8s.io/apimachinery/pkg/api/validate/content/errors.go index 2cd871d2ca3..3370df48b9c 100644 --- a/staging/src/k8s.io/apimachinery/pkg/api/validate/content/errors.go +++ b/staging/src/k8s.io/apimachinery/pkg/api/validate/content/errors.go @@ -18,6 +18,7 @@ package content import ( "fmt" + "reflect" "k8s.io/apimachinery/pkg/api/validate/constraints" ) @@ -27,3 +28,12 @@ import ( func MinError[T constraints.Integer](min T) string { return fmt.Sprintf("must be greater than or equal to %d", min) } + +// NEQError returns a string explanation of a "must not be equal to" validation failure. +func NEQError[T any](disallowed T) string { + format := "%v" + if reflect.ValueOf(disallowed).Kind() == reflect.String { + format = "%q" + } + return fmt.Sprintf("must not be equal to "+format, disallowed) +} diff --git a/staging/src/k8s.io/apimachinery/pkg/api/validate/equality.go b/staging/src/k8s.io/apimachinery/pkg/api/validate/equality.go new file mode 100644 index 00000000000..12e99d0ec49 --- /dev/null +++ b/staging/src/k8s.io/apimachinery/pkg/api/validate/equality.go @@ -0,0 +1,38 @@ +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package validate + +import ( + "context" + + "k8s.io/apimachinery/pkg/api/operation" + "k8s.io/apimachinery/pkg/api/validate/content" + "k8s.io/apimachinery/pkg/util/validation/field" +) + +// NEQ validates that the specified comparable value is not equal to the disallowed value. +func NEQ[T comparable](_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *T, disallowed T) field.ErrorList { + if value == nil { + return nil + } + if *value == disallowed { + return field.ErrorList{ + field.Invalid(fldPath, *value, content.NEQError(disallowed)).WithOrigin("neq"), + } + } + return nil +} diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/equality.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/equality.go new file mode 100644 index 00000000000..30d1b99c2e1 --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/equality.go @@ -0,0 +1,110 @@ +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package validators + +import ( + "fmt" + "strconv" + + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/code-generator/cmd/validation-gen/util" + "k8s.io/gengo/v2/codetags" + "k8s.io/gengo/v2/types" +) + +const ( + neqTagName = "k8s:neq" +) + +func init() { + RegisterTagValidator(neqTagValidator{}) +} + +type neqTagValidator struct{} + +func (neqTagValidator) Init(_ Config) {} + +func (neqTagValidator) TagName() string { + return neqTagName +} + +var neqTagValidScopes = sets.New(ScopeAny) + +func (neqTagValidator) ValidScopes() sets.Set[Scope] { + return neqTagValidScopes +} + +var ( + neqValidator = types.Name{Package: libValidationPkg, Name: "NEQ"} +) + +func (v neqTagValidator) GetValidations(context Context, tag codetags.Tag) (Validations, error) { + t := util.NonPointer(util.NativeType(context.Type)) + if !util.IsDirectComparable(t) { + return Validations{}, fmt.Errorf("can only be used on comparable types (e.g. string, int, bool), but got %s", rootTypeString(context.Type, t)) + } + + if tag.ValueType == codetags.ValueTypeNone { + return Validations{}, fmt.Errorf("missing required payload") + } + + var disallowedValue any + var err error + + switch { + case t == types.String: + if tag.ValueType != codetags.ValueTypeString { + return Validations{}, fmt.Errorf("type mismatch: field is a string, but payload is of type %s", tag.ValueType) + } + disallowedValue = tag.Value + case t == types.Bool: + if tag.ValueType != codetags.ValueTypeBool { + return Validations{}, fmt.Errorf("type mismatch: field is a bool, but payload is of type %s", tag.ValueType) + } + disallowedValue, err = strconv.ParseBool(tag.Value) + if err != nil { + return Validations{}, fmt.Errorf("invalid bool value for payload: %w", err) + } + case types.IsInteger(t): + if tag.ValueType != codetags.ValueTypeInt { + return Validations{}, fmt.Errorf("type mismatch: field is an integer, but payload is of type %s", tag.ValueType) + } + disallowedValue, err = strconv.Atoi(tag.Value) + if err != nil { + return Validations{}, fmt.Errorf("invalid integer value for payload: %w", err) + } + default: + return Validations{}, fmt.Errorf("unsupported type for 'neq' tag: %s", t.Name) + } + + fn := Function(v.TagName(), DefaultFlags, neqValidator, disallowedValue) + return Validations{Functions: []FunctionGen{fn}}, nil +} + +func (v neqTagValidator) Docs() TagDoc { + return TagDoc{ + Tag: v.TagName(), + Scopes: v.ValidScopes().UnsortedList(), + Description: "Verifies the field's value is not equal to a specific disallowed value. Supports string, integer, and boolean types.", + PayloadsRequired: true, + PayloadsType: codetags.ValueTypeRaw, + Payloads: []TagPayloadDoc{{ + Description: "", + Docs: "The disallowed value. The parser will infer the type (string, int, bool).", + }}, + } +}