From 400743bdc3b68994f01e93708c5cd834bff698b0 Mon Sep 17 00:00:00 2001 From: Bryce Palmer Date: Fri, 27 Feb 2026 09:46:58 -0500 Subject: [PATCH 01/11] field/errors: add TooLongCharacters error utility Signed-off-by: Bryce Palmer --- .../pkg/util/validation/field/errors.go | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) 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 b90370c0c8e..f1f227acf73 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 @@ -346,6 +346,29 @@ func TooLong(field *Path, _ interface{}, maxLength int) *Error { } } +// TooLongCharacters returns a *Error indicating "too long". This is used to report that +// the given value is too long in characters (including multi-byte characters). +// This is similar to Invalid, but the returned error will not include the too-long value. +// If maxLength is negative, it will be included in the message. The value argument is not used. +func TooLongCharacters[T ~string](field *Path, _ T, maxLength int) *Error { + var msg string + if maxLength >= 0 { + bs := "chars" + if maxLength == 1 { + bs = "char" + } + msg = fmt.Sprintf("may not be more than %d %s", maxLength, bs) + } else { + msg = "value is too long" + } + return &Error{ + Type: ErrorTypeTooLong, + Field: field.String(), + BadValue: "", + Detail: msg, + } +} + // TooLongMaxLength returns a *Error indicating "too long". // Deprecated: Use TooLong instead. func TooLongMaxLength(field *Path, value interface{}, maxLength int) *Error { From b5dd643c95b16091a272ddd4c3a6cfaebbe2c6bb Mon Sep 17 00:00:00 2001 From: Bryce Palmer Date: Fri, 27 Feb 2026 09:48:50 -0500 Subject: [PATCH 02/11] validate/limits: update MaxLength validation to be character-based so that it aligns with the OpenAPI maxLength validation evaluation process that performs rune counting instead of a byte-based length check Signed-off-by: Bryce Palmer --- .../apimachinery/pkg/api/validate/limits.go | 37 ++++- .../pkg/api/validate/limits_test.go | 131 +++++++++++++++++- 2 files changed, 163 insertions(+), 5 deletions(-) diff --git a/staging/src/k8s.io/apimachinery/pkg/api/validate/limits.go b/staging/src/k8s.io/apimachinery/pkg/api/validate/limits.go index b6db5e08cbe..ce91a2a8624 100644 --- a/staging/src/k8s.io/apimachinery/pkg/api/validate/limits.go +++ b/staging/src/k8s.io/apimachinery/pkg/api/validate/limits.go @@ -18,6 +18,8 @@ package validate import ( "context" + "math" + "unicode/utf8" "k8s.io/apimachinery/pkg/api/operation" "k8s.io/apimachinery/pkg/api/validate/constraints" @@ -31,9 +33,40 @@ func MaxLength[T ~string](_ context.Context, _ operation.Operation, fldPath *fie if value == nil { return nil } - if len(*value) > max { - return field.ErrorList{field.TooLong(fldPath, *value, max).WithOrigin("maxLength")} + + // if the length of the value in bytes is less + // than the maximum size then we can confidently + // say that this value is within the bounds + // enforced by the maximum value regardless + // of the actual makeup of characters in the value + byteLength := len(*value) + if byteLength <= max { + return nil } + + // because runes are up to 4 byte characters, if we assume all characters + // in the input are runes, the minimum number of characters that + // are specified is len(value)/4. If the minimum multi-byte + // character count is greater than our enforced maximum, we + // can confidently say that the value is invalid without having + // to actually perform the more expensive rune counting step + minimum := int(math.Ceil(float64(byteLength) / 4.0)) + if minimum > max || utf8.RuneCountInString(string(*value)) > max { + return field.ErrorList{field.TooLongCharacters(fldPath, *value, max).WithOrigin("maxLength")} + } + return nil +} + +// MaxBytes verifies that the specified value is not longer than max bytes. +func MaxBytes[T ~string](_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *T, max int) field.ErrorList { + if value == nil { + return nil + } + + if len(*value) > max { + return field.ErrorList{field.TooLong(fldPath, *value, max).WithOrigin("maxBytes")} + } + return nil } diff --git a/staging/src/k8s.io/apimachinery/pkg/api/validate/limits_test.go b/staging/src/k8s.io/apimachinery/pkg/api/validate/limits_test.go index f17937c6913..44e866ad8db 100644 --- a/staging/src/k8s.io/apimachinery/pkg/api/validate/limits_test.go +++ b/staging/src/k8s.io/apimachinery/pkg/api/validate/limits_test.go @@ -42,7 +42,7 @@ func TestMaxLength(t *testing.T) { value: "0", max: 0, wantErrs: field.ErrorList{ - field.TooLong(field.NewPath("fldpath"), nil, 0).WithOrigin("maxLength"), + field.TooLongCharacters(field.NewPath("fldpath"), "", 0).WithOrigin("maxLength"), }, }, { name: "one character", @@ -54,14 +54,55 @@ func TestMaxLength(t *testing.T) { value: "01", max: 1, wantErrs: field.ErrorList{ - field.TooLong(field.NewPath("fldpath"), nil, 1).WithOrigin("maxLength"), + field.TooLongCharacters(field.NewPath("fldpath"), "", 1).WithOrigin("maxLength"), }, }, { value: "", max: -1, wantErrs: field.ErrorList{ - field.TooLong(field.NewPath("fldpath"), nil, -1).WithOrigin("maxLength"), + field.TooLongCharacters(field.NewPath("fldpath"), "", -1).WithOrigin("maxLength"), }, + }, { + name: "ascii-only characters, less characters than max (n-1)", + value: "abcdefghi", + max: 10, + wantErrs: nil, + }, { + name: "multi-byte characters, less characters than max (n-1)", + value: "©®©®©®©®©", + max: 10, + wantErrs: nil, + }, { + name: "ascii-only characters, more characters than max (n+1)", + value: "abcdefghijkl", + max: 10, + wantErrs: field.ErrorList{ + field.TooLongCharacters(field.NewPath("fldpath"), "", 10).WithOrigin("maxLength"), + }, + }, { + name: "multi-byte characters, more characters than max (n+1)", + value: "©®©®©®©®©®©", + max: 10, + wantErrs: field.ErrorList{ + field.TooLongCharacters(field.NewPath("fldpath"), "", 10).WithOrigin("maxLength"), + }, + }, { + name: "mixture of characters, minimum possible size of input is less than max, rune count exceed maximum", + value: "©abc®defghi", + max: 10, + wantErrs: field.ErrorList{ + field.TooLongCharacters(field.NewPath("fldpath"), "", 10).WithOrigin("maxLength"), + }, + }, { + name: "multi-byte characters, exact characters as max (n)", + value: "©®©®©®©®©®", + max: 10, + wantErrs: nil, + }, { + name: "ascii-only characters, exact characters as max (n)", + value: "abcdefghij", + max: 10, + wantErrs: nil, }} matcher := field.ErrorMatcher{}.ByOrigin().ByDetailSubstring().ByField().ByType() @@ -213,3 +254,87 @@ func doTestMinimum[T constraints.Integer](t *testing.T, cases []minimumTestCase[ } } } + +func TestMaxBytes(t *testing.T) { + cases := []struct { + name string + value string + max int + wantErrs field.ErrorList // regex + }{{ + name: "empty string", + value: "", + max: 0, + wantErrs: nil, + }, { + name: "zero length", + value: "0", + max: 0, + wantErrs: field.ErrorList{ + field.TooLong(field.NewPath("fldpath"), "", 0).WithOrigin("maxBytes"), + }, + }, { + name: "one character", + value: "0", + max: 1, + wantErrs: nil, + }, { + name: "two characters", + value: "01", + max: 1, + wantErrs: field.ErrorList{ + field.TooLong(field.NewPath("fldpath"), "", 1).WithOrigin("maxBytes"), + }, + }, { + value: "", + max: -1, + wantErrs: field.ErrorList{ + field.TooLong(field.NewPath("fldpath"), "", -1).WithOrigin("maxBytes"), + }, + }, { + name: "ascii-only characters, less bytes than max", + value: "abcdefghi", + max: 10, + wantErrs: nil, + }, { + name: "multi-byte characters, less bytes than max", + value: "©®©®", + max: 10, + wantErrs: nil, + }, { + name: "ascii-only characters, more bytes than max", + value: "abcdefghijkl", + max: 10, + wantErrs: field.ErrorList{ + field.TooLong(field.NewPath("fldpath"), "", 10).WithOrigin("maxBytes"), + }, + }, { + name: "multi-byte characters, more bytes than max", + value: "©®©®©©", + max: 10, + wantErrs: field.ErrorList{ + field.TooLong(field.NewPath("fldpath"), "", 10).WithOrigin("maxBytes"), + }, + }, { + name: "mixture of characters, less bytes than max", + value: "©abc®®", + max: 10, + wantErrs: nil, + }, { + name: "mixture of characters, more bytes than max", + value: "©abc®®abc", + max: 10, + wantErrs: field.ErrorList{ + field.TooLong(field.NewPath("fldpath"), "", 10).WithOrigin("maxBytes"), + }, + }} + + matcher := field.ErrorMatcher{}.ByOrigin().ByDetailSubstring().ByField().ByType() + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + v := tc.value + gotErrs := MaxBytes(context.Background(), operation.Operation{}, field.NewPath("fldpath"), &v, nil, tc.max) + matcher.Test(t, tc.wantErrs, gotErrs) + }) + } +} From fff6e5950d7f66012d13b699ef70b5190ed12f57 Mon Sep 17 00:00:00 2001 From: Bryce Palmer Date: Fri, 27 Feb 2026 09:49:46 -0500 Subject: [PATCH 03/11] validation-gen/util: add ParseInt utility for canonical integer parsing Signed-off-by: Bryce Palmer --- .../cmd/validation-gen/util/util.go | 23 ++++++++ .../cmd/validation-gen/util/util_test.go | 52 +++++++++++++++++++ 2 files changed, 75 insertions(+) diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/util/util.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/util/util.go index 4566699baff..208a5ea19b0 100644 --- a/staging/src/k8s.io/code-generator/cmd/validation-gen/util/util.go +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/util/util.go @@ -17,6 +17,9 @@ limitations under the License. package util import ( + "fmt" + "strconv" + "k8s.io/gengo/v2/parser/tags" "k8s.io/gengo/v2/types" ) @@ -114,3 +117,23 @@ func IsDirectComparable(t *types.Type) bool { } return false } + +// ParseInt strictly parses an int from a string input, +// ensuring that when converted back to a string, the resulting +// int and the input string have the exact same representation. +// This prevents scenarios where an input like `0100` parses +// as 100 and would be re-stringed as `100`. +func ParseInt(val string) (int, error) { + intVal, err := strconv.Atoi(val) + if err != nil { + return 0, fmt.Errorf("parsing %q as int: %w", val, err) + } + + strVal := strconv.Itoa(intVal) + if strVal != val { + err := fmt.Errorf("parsed int %d converted to a string value of %q which does not match the input string", intVal, strVal) + return 0, fmt.Errorf("parsing %q as int: %w", val, err) + } + + return intVal, nil +} diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/util/util_test.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/util/util_test.go index 5ff3ae6cb8c..554e7bc3a80 100644 --- a/staging/src/k8s.io/code-generator/cmd/validation-gen/util/util_test.go +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/util/util_test.go @@ -452,3 +452,55 @@ func TestIsDirectComparable(t *testing.T) { } } } + +func TestParseInt(t *testing.T) { + type testcase struct { + name string + in string + expectedOut int + expectedError bool + } + + testcases := []testcase{ + { + name: "valid canonical integer string", + in: "100", + expectedOut: 100, + expectedError: false, + }, + { + name: "invalid canonical integer string, not an integer at all", + in: "notanint", + expectedOut: 0, + expectedError: true, + }, + { + name: "invalid canonical integer string, spurious leading zeros", + in: "00100", + expectedOut: 0, + expectedError: true, + }, + { + name: "invalid canonical integer string, octal value", + in: "0o123", + expectedOut: 0, + expectedError: true, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + out, err := ParseInt(tc.in) + switch { + case tc.expectedError && err == nil: + t.Error("expected an error but did not receive one") + case !tc.expectedError && err != nil: + t.Errorf("received an unexpected error: %v", err) + } + + if out != tc.expectedOut { + t.Errorf("expected an output value of %d but got %d", tc.expectedOut, out) + } + }) + } +} From 91893a392933434356b8a3c049344b74acf36590 Mon Sep 17 00:00:00 2001 From: Bryce Palmer Date: Fri, 27 Feb 2026 09:51:58 -0500 Subject: [PATCH 04/11] validators/limits: minor fixes to MaxLength validator for strict canonical integer parsing and deterministic valid scope list output Signed-off-by: Bryce Palmer --- .../cmd/validation-gen/validators/limits.go | 62 ++++++++++++++++++- 1 file changed, 59 insertions(+), 3 deletions(-) diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/limits.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/limits.go index 70fd36a52a7..b7fae021064 100644 --- a/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/limits.go +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/limits.go @@ -65,7 +65,7 @@ func (maxLengthTagValidator) GetValidations(context Context, tag codetags.Tag) ( return Validations{}, fmt.Errorf("can only be used on string types (%s)", rootTypeString(context.Type, t)) } - intVal, err := strconv.Atoi(tag.Value) + intVal, err := util.ParseInt(tag.Value) if err != nil { return result, fmt.Errorf("failed to parse tag payload as int: %w", err) } @@ -80,8 +80,10 @@ func (mltv maxLengthTagValidator) Docs() TagDoc { return TagDoc{ Tag: mltv.TagName(), StabilityLevel: TagStabilityLevelBeta, - Scopes: mltv.ValidScopes().UnsortedList(), - Description: "Indicates that a string field has a limit on its length.", + Scopes: sets.List(mltv.ValidScopes()), + Description: `Indicates that a string field has a limit on its length in characters. + This could allow up to 4*N bytes if multi-byte characters are used. + If you want to limit length of bytes specifically, use maxBytes.`, Payloads: []TagPayloadDoc{{ Description: "", Docs: "This field must be no more than X characters long.", @@ -91,6 +93,60 @@ func (mltv maxLengthTagValidator) Docs() TagDoc { } } +type maxBytesTagValidator struct{} + +func (maxBytesTagValidator) Init(_ Config) {} + +func (maxBytesTagValidator) TagName() string { + return maxBytesTagName +} + +var maxBytesTagValidScopes = sets.New(ScopeType, ScopeField, ScopeListVal, ScopeMapKey, ScopeMapVal) + +func (maxBytesTagValidator) ValidScopes() sets.Set[Scope] { + return maxBytesTagValidScopes +} + +var maxBytesValidator = types.Name{Package: libValidationPkg, Name: "MaxBytes"} + +func (maxBytesTagValidator) GetValidations(context Context, tag codetags.Tag) (Validations, error) { + var result Validations + + // This tag can apply to value and pointer fields, as well as typedefs + // (which should never be pointers). We need to check the concrete type. + if t := util.NonPointer(util.NativeType(context.Type)); t != types.String { + return Validations{}, fmt.Errorf("can only be used on string types (%s)", rootTypeString(context.Type, t)) + } + + intVal, err := util.ParseInt(tag.Value) + if err != nil { + return result, fmt.Errorf("failed to parse tag payload as int: %w", err) + } + if intVal < 0 { + return result, fmt.Errorf("must be greater than or equal to zero") + } + result.AddFunction(Function(maxBytesTagName, DefaultFlags, maxBytesValidator, intVal)) + return result, nil +} + +func (mltv maxBytesTagValidator) Docs() TagDoc { + return TagDoc{ + Tag: mltv.TagName(), + StabilityLevel: TagStabilityLevelBeta, + Scopes: sets.List(mltv.ValidScopes()), + Description: `Indicates that a string field has a limit on its length in bytes. + This could only allow as few as N/4 multi-byte characters. + If you want to limit length of characters specifically, use maxLength.`, + Payloads: []TagPayloadDoc{{ + Description: "", + Docs: "This field must be no more than X bytes long.", + }}, + PayloadsType: codetags.ValueTypeInt, + PayloadsRequired: true, + } +} + +>>>>>>> d860a611ab8 (fixup! validators limits gofmt) type maxItemsTagValidator struct{} func (maxItemsTagValidator) Init(_ Config) {} From b9cebe2a6b0344150e87137882d8e895cb417316 Mon Sep 17 00:00:00 2001 From: Bryce Palmer Date: Fri, 27 Feb 2026 10:00:54 -0500 Subject: [PATCH 05/11] validation-gen/tests/maxlength: use TooLongCharacters error utility for assertions Signed-off-by: Bryce Palmer --- .../output_tests/tags/maxlength/doc_test.go | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/maxlength/doc_test.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/maxlength/doc_test.go index 6524a471425..88b489db08f 100644 --- a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/maxlength/doc_test.go +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/maxlength/doc_test.go @@ -73,18 +73,18 @@ func Test(t *testing.T) { Max10ValidatedTypedefPtrField: ptr.To(Max10Type(strings.Repeat("x", 11))), } st.Value(testVal).ExpectMatches(field.ErrorMatcher{}.ByType().ByField(), field.ErrorList{ - field.TooLong(field.NewPath("max0Field"), nil, 0), - field.TooLong(field.NewPath("max0PtrField"), nil, 0), - field.TooLong(field.NewPath("max10Field"), nil, 10), - field.TooLong(field.NewPath("max10PtrField"), nil, 10), - field.TooLong(field.NewPath("max0UnvalidatedTypedefField"), nil, 0), - field.TooLong(field.NewPath("max0UnvalidatedTypedefPtrField"), nil, 0), - field.TooLong(field.NewPath("max10UnvalidatedTypedefField"), nil, 10), - field.TooLong(field.NewPath("max10UnvalidatedTypedefPtrField"), nil, 10), - field.TooLong(field.NewPath("max0ValidatedTypedefField"), nil, 0), - field.TooLong(field.NewPath("max0ValidatedTypedefPtrField"), nil, 0), - field.TooLong(field.NewPath("max10ValidatedTypedefField"), nil, 10), - field.TooLong(field.NewPath("max10ValidatedTypedefPtrField"), nil, 10), + field.TooLongCharacters(field.NewPath("max0Field"), "", 0), + field.TooLongCharacters(field.NewPath("max0PtrField"), "", 0), + field.TooLongCharacters(field.NewPath("max10Field"), "", 10), + field.TooLongCharacters(field.NewPath("max10PtrField"), "", 10), + field.TooLongCharacters(field.NewPath("max0UnvalidatedTypedefField"), "", 0), + field.TooLongCharacters(field.NewPath("max0UnvalidatedTypedefPtrField"), "", 0), + field.TooLongCharacters(field.NewPath("max10UnvalidatedTypedefField"), "", 10), + field.TooLongCharacters(field.NewPath("max10UnvalidatedTypedefPtrField"), "", 10), + field.TooLongCharacters(field.NewPath("max0ValidatedTypedefField"), "", 0), + field.TooLongCharacters(field.NewPath("max0ValidatedTypedefPtrField"), "", 0), + field.TooLongCharacters(field.NewPath("max10ValidatedTypedefField"), "", 10), + field.TooLongCharacters(field.NewPath("max10ValidatedTypedefPtrField"), "", 10), }) // Test validation ratcheting From 100b1a6136f589868db4e5e4b70d02979650c973 Mon Sep 17 00:00:00 2001 From: Bryce Palmer Date: Fri, 27 Feb 2026 14:24:06 -0500 Subject: [PATCH 06/11] api/validate: add MaxBytes limit validation Signed-off-by: Bryce Palmer --- .../src/k8s.io/apimachinery/pkg/api/validate/limits_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/staging/src/k8s.io/apimachinery/pkg/api/validate/limits_test.go b/staging/src/k8s.io/apimachinery/pkg/api/validate/limits_test.go index 44e866ad8db..7c369f1c7d4 100644 --- a/staging/src/k8s.io/apimachinery/pkg/api/validate/limits_test.go +++ b/staging/src/k8s.io/apimachinery/pkg/api/validate/limits_test.go @@ -316,9 +316,9 @@ func TestMaxBytes(t *testing.T) { field.TooLong(field.NewPath("fldpath"), "", 10).WithOrigin("maxBytes"), }, }, { - name: "mixture of characters, less bytes than max", - value: "©abc®®", - max: 10, + name: "mixture of characters, less bytes than max", + value: "©abc®®", + max: 10, wantErrs: nil, }, { name: "mixture of characters, more bytes than max", From c9652760b537dfb8eab45f9c3f64677aaa38ccdf Mon Sep 17 00:00:00 2001 From: Bryce Palmer Date: Fri, 27 Feb 2026 14:55:24 -0500 Subject: [PATCH 07/11] validation-gen/validators: add maxBytes validator Signed-off-by: Bryce Palmer --- .../pkg/api/validate/limits_test.go | 6 +++--- .../cmd/validation-gen/validators/limits.go | 19 +++++++------------ 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/staging/src/k8s.io/apimachinery/pkg/api/validate/limits_test.go b/staging/src/k8s.io/apimachinery/pkg/api/validate/limits_test.go index 7c369f1c7d4..44e866ad8db 100644 --- a/staging/src/k8s.io/apimachinery/pkg/api/validate/limits_test.go +++ b/staging/src/k8s.io/apimachinery/pkg/api/validate/limits_test.go @@ -316,9 +316,9 @@ func TestMaxBytes(t *testing.T) { field.TooLong(field.NewPath("fldpath"), "", 10).WithOrigin("maxBytes"), }, }, { - name: "mixture of characters, less bytes than max", - value: "©abc®®", - max: 10, + name: "mixture of characters, less bytes than max", + value: "©abc®®", + max: 10, wantErrs: nil, }, { name: "mixture of characters, more bytes than max", diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/limits.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/limits.go index b7fae021064..65d146310df 100644 --- a/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/limits.go +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/limits.go @@ -30,12 +30,14 @@ const ( maxItemsTagName = "k8s:maxItems" minimumTagName = "k8s:minimum" maxLengthTagName = "k8s:maxLength" + maxBytesTagName = "k8s:maxBytes" ) func init() { RegisterTagValidator(maxItemsTagValidator{}) RegisterTagValidator(minimumTagValidator{}) RegisterTagValidator(maxLengthTagValidator{}) + RegisterTagValidator(maxBytesTagValidator{}) } type maxLengthTagValidator struct{} @@ -52,9 +54,7 @@ func (maxLengthTagValidator) ValidScopes() sets.Set[Scope] { return maxLengthTagValidScopes } -var ( - maxLengthValidator = types.Name{Package: libValidationPkg, Name: "MaxLength"} -) +var maxLengthValidator = types.Name{Package: libValidationPkg, Name: "MaxLength"} func (maxLengthTagValidator) GetValidations(context Context, tag codetags.Tag) (Validations, error) { var result Validations @@ -81,7 +81,7 @@ func (mltv maxLengthTagValidator) Docs() TagDoc { Tag: mltv.TagName(), StabilityLevel: TagStabilityLevelBeta, Scopes: sets.List(mltv.ValidScopes()), - Description: `Indicates that a string field has a limit on its length in characters. + Description: `Indicates that a string field has a limit on its length in characters. This could allow up to 4*N bytes if multi-byte characters are used. If you want to limit length of bytes specifically, use maxBytes.`, Payloads: []TagPayloadDoc{{ @@ -134,7 +134,7 @@ func (mltv maxBytesTagValidator) Docs() TagDoc { Tag: mltv.TagName(), StabilityLevel: TagStabilityLevelBeta, Scopes: sets.List(mltv.ValidScopes()), - Description: `Indicates that a string field has a limit on its length in bytes. + Description: `Indicates that a string field has a limit on its length in bytes. This could only allow as few as N/4 multi-byte characters. If you want to limit length of characters specifically, use maxLength.`, Payloads: []TagPayloadDoc{{ @@ -146,7 +146,6 @@ func (mltv maxBytesTagValidator) Docs() TagDoc { } } ->>>>>>> d860a611ab8 (fixup! validators limits gofmt) type maxItemsTagValidator struct{} func (maxItemsTagValidator) Init(_ Config) {} @@ -166,9 +165,7 @@ func (maxItemsTagValidator) ValidScopes() sets.Set[Scope] { return maxItemsTagValidScopes } -var ( - maxItemsValidator = types.Name{Package: libValidationPkg, Name: "MaxItems"} -) +var maxItemsValidator = types.Name{Package: libValidationPkg, Name: "MaxItems"} func (maxItemsTagValidator) GetValidations(context Context, tag codetags.Tag) (Validations, error) { var result Validations @@ -219,9 +216,7 @@ func (minimumTagValidator) ValidScopes() sets.Set[Scope] { return minimumTagValidScopes } -var ( - minimumValidator = types.Name{Package: libValidationPkg, Name: "Minimum"} -) +var minimumValidator = types.Name{Package: libValidationPkg, Name: "Minimum"} func (minimumTagValidator) GetValidations(context Context, tag codetags.Tag) (Validations, error) { var result Validations From 3deb264c915a47bca909fc2cd6aeeadb01c68fb1 Mon Sep 17 00:00:00 2001 From: Bryce Palmer Date: Fri, 27 Feb 2026 14:57:29 -0500 Subject: [PATCH 08/11] validation-gen/tests: add maxBytes tag tests Signed-off-by: Bryce Palmer --- .../output_tests/tags/maxbytes/doc.go | 81 +++++++ .../output_tests/tags/maxbytes/doc_test.go | 118 ++++++++++ .../tags/maxbytes/zz_generated.validations.go | 217 ++++++++++++++++++ .../cmd/validation-gen/validators/limits.go | 4 +- 4 files changed, 418 insertions(+), 2 deletions(-) create mode 100644 staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/maxbytes/doc.go create mode 100644 staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/maxbytes/doc_test.go create mode 100644 staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/maxbytes/zz_generated.validations.go diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/maxbytes/doc.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/maxbytes/doc.go new file mode 100644 index 00000000000..79873c95df8 --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/maxbytes/doc.go @@ -0,0 +1,81 @@ +/* +Copyright 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. +*/ + +// +k8s:validation-gen=TypeMeta +// +k8s:validation-gen-scheme-registry=k8s.io/code-generator/cmd/validation-gen/testscheme.Scheme + +// This is a test package. +// +k8s:validation-gen-nolint +package maxbytes + +import "k8s.io/code-generator/cmd/validation-gen/testscheme" + +var localSchemeBuilder = testscheme.New() + +type Struct struct { + TypeMeta int + + // +k8s:maxBytes=0 + Max0Field string `json:"max0Field"` + + // +k8s:maxBytes=0 + Max0PtrField *string `json:"max0PtrField"` + + // +k8s:maxBytes=10 + Max10Field string `json:"max10Field"` + + // +k8s:maxBytes=10 + Max10PtrField *string `json:"max10PtrField"` + + // +k8s:maxBytes=0 + Max0UnvalidatedTypedefField UnvalidatedStringType `json:"max0UnvalidatedTypedefField"` + + // +k8s:maxBytes=0 + Max0UnvalidatedTypedefPtrField *UnvalidatedStringType `json:"max0UnvalidatedTypedefPtrField"` + + // +k8s:maxBytes=10 + Max10UnvalidatedTypedefField UnvalidatedStringType `json:"max10UnvalidatedTypedefField"` + + // +k8s:maxBytes=10 + Max10UnvalidatedTypedefPtrField *UnvalidatedStringType `json:"max10UnvalidatedTypedefPtrField"` + + // Note: no validation here + Max0ValidatedTypedefField Max0Type `json:"max0ValidatedTypedefField"` + + // Note: no validation here + Max0ValidatedTypedefPtrField *Max0Type `json:"max0ValidatedTypedefPtrField"` + + // Note: no validation here + Max10ValidatedTypedefField Max10Type `json:"max10ValidatedTypedefField"` + + // Note: no validation here + Max10ValidatedTypedefPtrField *Max10Type `json:"max10ValidatedTypedefPtrField"` +} + +// Note: no validation here +type UnvalidatedStringType string + +// This tests that markers on type definitions +// are pulled through the validations of fields +// that use the type definition. +// +k8s:maxBytes=0 +type Max0Type string + +// This tests that markers on type definitions +// are pulled through the validations of fields +// that use the type definition. +// +k8s:maxBytes=10 +type Max10Type string diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/maxbytes/doc_test.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/maxbytes/doc_test.go new file mode 100644 index 00000000000..47bc3f416d9 --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/maxbytes/doc_test.go @@ -0,0 +1,118 @@ +/* +Copyright 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 maxbytes + +import ( + "strings" + "testing" + + "k8s.io/apimachinery/pkg/util/validation/field" + "k8s.io/utils/ptr" +) + +func Test(t *testing.T) { + st := localSchemeBuilder.Test(t) + + st.Value(&Struct{ + // All zero values + }).ExpectValid() + + st.Value(&Struct{ + Max10Field: strings.Repeat("x", 1), + Max10PtrField: ptr.To(strings.Repeat("x", 1)), + Max10UnvalidatedTypedefField: UnvalidatedStringType(strings.Repeat("x", 1)), + Max10UnvalidatedTypedefPtrField: ptr.To(UnvalidatedStringType(strings.Repeat("x", 1))), + Max10ValidatedTypedefField: Max10Type(strings.Repeat("x", 1)), + Max10ValidatedTypedefPtrField: ptr.To(Max10Type(strings.Repeat("x", 1))), + }).ExpectValid() + + st.Value(&Struct{ + Max10Field: strings.Repeat("x", 9), + Max10PtrField: ptr.To(strings.Repeat("x", 9)), + Max10UnvalidatedTypedefField: UnvalidatedStringType(strings.Repeat("x", 9)), + Max10UnvalidatedTypedefPtrField: ptr.To(UnvalidatedStringType(strings.Repeat("x", 9))), + Max10ValidatedTypedefField: Max10Type(strings.Repeat("x", 9)), + Max10ValidatedTypedefPtrField: ptr.To(Max10Type(strings.Repeat("x", 9))), + }).ExpectValid() + + st.Value(&Struct{ + Max10Field: strings.Repeat("x", 10), + Max10PtrField: ptr.To(strings.Repeat("x", 10)), + Max10UnvalidatedTypedefField: UnvalidatedStringType(strings.Repeat("x", 10)), + Max10UnvalidatedTypedefPtrField: ptr.To(UnvalidatedStringType(strings.Repeat("x", 10))), + Max10ValidatedTypedefField: Max10Type(strings.Repeat("x", 10)), + Max10ValidatedTypedefPtrField: ptr.To(Max10Type(strings.Repeat("x", 10))), + }).ExpectValid() + + testVal := &Struct{ + Max0Field: strings.Repeat("x", 1), + Max0PtrField: ptr.To(strings.Repeat("x", 1)), + Max10Field: strings.Repeat("x", 11), + Max10PtrField: ptr.To(strings.Repeat("x", 11)), + Max0UnvalidatedTypedefField: UnvalidatedStringType(strings.Repeat("x", 1)), + Max0UnvalidatedTypedefPtrField: ptr.To(UnvalidatedStringType(strings.Repeat("x", 1))), + Max10UnvalidatedTypedefField: UnvalidatedStringType(strings.Repeat("x", 11)), + Max10UnvalidatedTypedefPtrField: ptr.To(UnvalidatedStringType(strings.Repeat("x", 11))), + Max0ValidatedTypedefField: Max0Type(strings.Repeat("x", 1)), + Max0ValidatedTypedefPtrField: ptr.To(Max0Type(strings.Repeat("x", 1))), + Max10ValidatedTypedefField: Max10Type(strings.Repeat("x", 11)), + Max10ValidatedTypedefPtrField: ptr.To(Max10Type(strings.Repeat("x", 11))), + } + st.Value(testVal).ExpectMatches(field.ErrorMatcher{}.ByType().ByField(), field.ErrorList{ + field.TooLong(field.NewPath("max0Field"), "", 0), + field.TooLong(field.NewPath("max0PtrField"), "", 0), + field.TooLong(field.NewPath("max10Field"), "", 10), + field.TooLong(field.NewPath("max10PtrField"), "", 10), + field.TooLong(field.NewPath("max0UnvalidatedTypedefField"), "", 0), + field.TooLong(field.NewPath("max0UnvalidatedTypedefPtrField"), "", 0), + field.TooLong(field.NewPath("max10UnvalidatedTypedefField"), "", 10), + field.TooLong(field.NewPath("max10UnvalidatedTypedefPtrField"), "", 10), + field.TooLong(field.NewPath("max0ValidatedTypedefField"), "", 0), + field.TooLong(field.NewPath("max0ValidatedTypedefPtrField"), "", 0), + field.TooLong(field.NewPath("max10ValidatedTypedefField"), "", 10), + field.TooLong(field.NewPath("max10ValidatedTypedefPtrField"), "", 10), + }) + + // Test validation ratcheting + st.Value(&Struct{ + Max0Field: strings.Repeat("x", 1), + Max0PtrField: ptr.To(strings.Repeat("x", 1)), + Max10Field: strings.Repeat("x", 11), + Max10PtrField: ptr.To(strings.Repeat("x", 11)), + Max0UnvalidatedTypedefField: UnvalidatedStringType(strings.Repeat("x", 1)), + Max0UnvalidatedTypedefPtrField: ptr.To(UnvalidatedStringType(strings.Repeat("x", 1))), + Max10UnvalidatedTypedefField: UnvalidatedStringType(strings.Repeat("x", 11)), + Max10UnvalidatedTypedefPtrField: ptr.To(UnvalidatedStringType(strings.Repeat("x", 11))), + Max0ValidatedTypedefField: Max0Type(strings.Repeat("x", 1)), + Max0ValidatedTypedefPtrField: ptr.To(Max0Type(strings.Repeat("x", 1))), + Max10ValidatedTypedefField: Max10Type(strings.Repeat("x", 11)), + Max10ValidatedTypedefPtrField: ptr.To(Max10Type(strings.Repeat("x", 11))), + }).OldValue(&Struct{ + Max0Field: strings.Repeat("x", 1), + Max0PtrField: ptr.To(strings.Repeat("x", 1)), + Max10Field: strings.Repeat("x", 11), + Max10PtrField: ptr.To(strings.Repeat("x", 11)), + Max0UnvalidatedTypedefField: UnvalidatedStringType(strings.Repeat("x", 1)), + Max0UnvalidatedTypedefPtrField: ptr.To(UnvalidatedStringType(strings.Repeat("x", 1))), + Max10UnvalidatedTypedefField: UnvalidatedStringType(strings.Repeat("x", 11)), + Max10UnvalidatedTypedefPtrField: ptr.To(UnvalidatedStringType(strings.Repeat("x", 11))), + Max0ValidatedTypedefField: Max0Type(strings.Repeat("x", 1)), + Max0ValidatedTypedefPtrField: ptr.To(Max0Type(strings.Repeat("x", 1))), + Max10ValidatedTypedefField: Max10Type(strings.Repeat("x", 11)), + Max10ValidatedTypedefPtrField: ptr.To(Max10Type(strings.Repeat("x", 11))), + }).ExpectValid() +} diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/maxbytes/zz_generated.validations.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/maxbytes/zz_generated.validations.go new file mode 100644 index 00000000000..4f93d9945a8 --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/maxbytes/zz_generated.validations.go @@ -0,0 +1,217 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +/* +Copyright 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. +*/ + +// Code generated by validation-gen. DO NOT EDIT. + +package maxbytes + +import ( + context "context" + fmt "fmt" + + operation "k8s.io/apimachinery/pkg/api/operation" + safe "k8s.io/apimachinery/pkg/api/safe" + validate "k8s.io/apimachinery/pkg/api/validate" + field "k8s.io/apimachinery/pkg/util/validation/field" + testscheme "k8s.io/code-generator/cmd/validation-gen/testscheme" +) + +func init() { localSchemeBuilder.Register(RegisterValidations) } + +// RegisterValidations adds validation functions to the given scheme. +// Public to allow building arbitrary schemes. +func RegisterValidations(scheme *testscheme.Scheme) error { + // type Struct + scheme.AddValidationFunc((*Struct)(nil), func(ctx context.Context, op operation.Operation, obj, oldObj interface{}) field.ErrorList { + switch op.Request.SubresourcePath() { + case "/": + return Validate_Struct(ctx, op, nil /* fldPath */, obj.(*Struct), safe.Cast[*Struct](oldObj)) + } + return field.ErrorList{field.InternalError(nil, fmt.Errorf("no validation found for %T, subresource: %v", obj, op.Request.SubresourcePath()))} + }) + return nil +} + +// Validate_Max0Type validates an instance of Max0Type according +// to declarative validation rules in the API schema. +func Validate_Max0Type(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *Max0Type) (errs field.ErrorList) { + errs = append(errs, validate.MaxBytes(ctx, op, fldPath, obj, oldObj, 0)...) + + return errs +} + +// Validate_Max10Type validates an instance of Max10Type according +// to declarative validation rules in the API schema. +func Validate_Max10Type(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *Max10Type) (errs field.ErrorList) { + errs = append(errs, validate.MaxBytes(ctx, op, fldPath, obj, oldObj, 10)...) + + return errs +} + +// Validate_Struct validates an instance of Struct according +// to declarative validation rules in the API schema. +func Validate_Struct(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *Struct) (errs field.ErrorList) { + // field Struct.TypeMeta has no validation + + // field Struct.Max0Field + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *string, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) { + return nil + } + // call field-attached validations + errs = append(errs, validate.MaxBytes(ctx, op, fldPath, obj, oldObj, 0)...) + return + }(fldPath.Child("max0Field"), &obj.Max0Field, safe.Field(oldObj, func(oldObj *Struct) *string { return &oldObj.Max0Field }), oldObj != nil)...) + + // field Struct.Max0PtrField + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *string, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) { + return nil + } + // call field-attached validations + errs = append(errs, validate.MaxBytes(ctx, op, fldPath, obj, oldObj, 0)...) + return + }(fldPath.Child("max0PtrField"), obj.Max0PtrField, safe.Field(oldObj, func(oldObj *Struct) *string { return oldObj.Max0PtrField }), oldObj != nil)...) + + // field Struct.Max10Field + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *string, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) { + return nil + } + // call field-attached validations + errs = append(errs, validate.MaxBytes(ctx, op, fldPath, obj, oldObj, 10)...) + return + }(fldPath.Child("max10Field"), &obj.Max10Field, safe.Field(oldObj, func(oldObj *Struct) *string { return &oldObj.Max10Field }), oldObj != nil)...) + + // field Struct.Max10PtrField + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *string, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) { + return nil + } + // call field-attached validations + errs = append(errs, validate.MaxBytes(ctx, op, fldPath, obj, oldObj, 10)...) + return + }(fldPath.Child("max10PtrField"), obj.Max10PtrField, safe.Field(oldObj, func(oldObj *Struct) *string { return oldObj.Max10PtrField }), oldObj != nil)...) + + // field Struct.Max0UnvalidatedTypedefField + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *UnvalidatedStringType, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) { + return nil + } + // call field-attached validations + errs = append(errs, validate.MaxBytes(ctx, op, fldPath, obj, oldObj, 0)...) + return + }(fldPath.Child("max0UnvalidatedTypedefField"), &obj.Max0UnvalidatedTypedefField, safe.Field(oldObj, func(oldObj *Struct) *UnvalidatedStringType { return &oldObj.Max0UnvalidatedTypedefField }), oldObj != nil)...) + + // field Struct.Max0UnvalidatedTypedefPtrField + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *UnvalidatedStringType, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) { + return nil + } + // call field-attached validations + errs = append(errs, validate.MaxBytes(ctx, op, fldPath, obj, oldObj, 0)...) + return + }(fldPath.Child("max0UnvalidatedTypedefPtrField"), obj.Max0UnvalidatedTypedefPtrField, safe.Field(oldObj, func(oldObj *Struct) *UnvalidatedStringType { return oldObj.Max0UnvalidatedTypedefPtrField }), oldObj != nil)...) + + // field Struct.Max10UnvalidatedTypedefField + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *UnvalidatedStringType, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) { + return nil + } + // call field-attached validations + errs = append(errs, validate.MaxBytes(ctx, op, fldPath, obj, oldObj, 10)...) + return + }(fldPath.Child("max10UnvalidatedTypedefField"), &obj.Max10UnvalidatedTypedefField, safe.Field(oldObj, func(oldObj *Struct) *UnvalidatedStringType { return &oldObj.Max10UnvalidatedTypedefField }), oldObj != nil)...) + + // field Struct.Max10UnvalidatedTypedefPtrField + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *UnvalidatedStringType, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) { + return nil + } + // call field-attached validations + errs = append(errs, validate.MaxBytes(ctx, op, fldPath, obj, oldObj, 10)...) + return + }(fldPath.Child("max10UnvalidatedTypedefPtrField"), obj.Max10UnvalidatedTypedefPtrField, safe.Field(oldObj, func(oldObj *Struct) *UnvalidatedStringType { return oldObj.Max10UnvalidatedTypedefPtrField }), oldObj != nil)...) + + // field Struct.Max0ValidatedTypedefField + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *Max0Type, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) { + return nil + } + // call the type's validation function + errs = append(errs, Validate_Max0Type(ctx, op, fldPath, obj, oldObj)...) + return + }(fldPath.Child("max0ValidatedTypedefField"), &obj.Max0ValidatedTypedefField, safe.Field(oldObj, func(oldObj *Struct) *Max0Type { return &oldObj.Max0ValidatedTypedefField }), oldObj != nil)...) + + // field Struct.Max0ValidatedTypedefPtrField + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *Max0Type, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) { + return nil + } + // call the type's validation function + errs = append(errs, Validate_Max0Type(ctx, op, fldPath, obj, oldObj)...) + return + }(fldPath.Child("max0ValidatedTypedefPtrField"), obj.Max0ValidatedTypedefPtrField, safe.Field(oldObj, func(oldObj *Struct) *Max0Type { return oldObj.Max0ValidatedTypedefPtrField }), oldObj != nil)...) + + // field Struct.Max10ValidatedTypedefField + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *Max10Type, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) { + return nil + } + // call the type's validation function + errs = append(errs, Validate_Max10Type(ctx, op, fldPath, obj, oldObj)...) + return + }(fldPath.Child("max10ValidatedTypedefField"), &obj.Max10ValidatedTypedefField, safe.Field(oldObj, func(oldObj *Struct) *Max10Type { return &oldObj.Max10ValidatedTypedefField }), oldObj != nil)...) + + // field Struct.Max10ValidatedTypedefPtrField + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *Max10Type, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) { + return nil + } + // call the type's validation function + errs = append(errs, Validate_Max10Type(ctx, op, fldPath, obj, oldObj)...) + return + }(fldPath.Child("max10ValidatedTypedefPtrField"), obj.Max10ValidatedTypedefPtrField, safe.Field(oldObj, func(oldObj *Struct) *Max10Type { return oldObj.Max10ValidatedTypedefPtrField }), oldObj != nil)...) + + return errs +} diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/limits.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/limits.go index 65d146310df..573c266307e 100644 --- a/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/limits.go +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/limits.go @@ -81,7 +81,7 @@ func (mltv maxLengthTagValidator) Docs() TagDoc { Tag: mltv.TagName(), StabilityLevel: TagStabilityLevelBeta, Scopes: sets.List(mltv.ValidScopes()), - Description: `Indicates that a string field has a limit on its length in characters. + Description: `Indicates that a string field has a limit on its length in characters. This could allow up to 4*N bytes if multi-byte characters are used. If you want to limit length of bytes specifically, use maxBytes.`, Payloads: []TagPayloadDoc{{ @@ -134,7 +134,7 @@ func (mltv maxBytesTagValidator) Docs() TagDoc { Tag: mltv.TagName(), StabilityLevel: TagStabilityLevelBeta, Scopes: sets.List(mltv.ValidScopes()), - Description: `Indicates that a string field has a limit on its length in bytes. + Description: `Indicates that a string field has a limit on its length in bytes. This could only allow as few as N/4 multi-byte characters. If you want to limit length of characters specifically, use maxLength.`, Payloads: []TagPayloadDoc{{ From 430b5d884a2ae68591a18cf7eabc8707c72b4ee9 Mon Sep 17 00:00:00 2001 From: Bryce Palmer Date: Fri, 27 Feb 2026 15:10:36 -0500 Subject: [PATCH 09/11] apis/resource: migrate maxLength to maxByte tags for the InterfaceName and HardwareAddress fields so that declarative validation and handwritten validation are semantically equivalent. Signed-off-by: Bryce Palmer --- .../resource/v1/zz_generated.validations.go | 4 +- .../v1beta1/zz_generated.validations.go | 4 +- .../v1beta2/zz_generated.validations.go | 4 +- .../validation_resourceclaim_test.go | 69 +++++++++++++++++++ staging/src/k8s.io/api/resource/v1/types.go | 8 +-- .../src/k8s.io/api/resource/v1beta1/types.go | 8 +-- .../src/k8s.io/api/resource/v1beta2/types.go | 8 +-- 7 files changed, 87 insertions(+), 18 deletions(-) diff --git a/pkg/apis/resource/v1/zz_generated.validations.go b/pkg/apis/resource/v1/zz_generated.validations.go index a46ece5c3b3..78860ec8aea 100644 --- a/pkg/apis/resource/v1/zz_generated.validations.go +++ b/pkg/apis/resource/v1/zz_generated.validations.go @@ -1465,7 +1465,7 @@ func Validate_NetworkDeviceData(ctx context.Context, op operation.Operation, fld if earlyReturn { return // do not proceed } - errs = append(errs, validate.MaxLength(ctx, op, fldPath, obj, oldObj, 256).MarkAlpha()...) + errs = append(errs, validate.MaxBytes(ctx, op, fldPath, obj, oldObj, 256).MarkAlpha()...) return }(fldPath.Child("interfaceName"), &obj.InterfaceName, safe.Field(oldObj, func(oldObj *resourcev1.NetworkDeviceData) *string { return &oldObj.InterfaceName }), oldObj != nil)...) @@ -1508,7 +1508,7 @@ func Validate_NetworkDeviceData(ctx context.Context, op operation.Operation, fld if earlyReturn { return // do not proceed } - errs = append(errs, validate.MaxLength(ctx, op, fldPath, obj, oldObj, 128).MarkAlpha()...) + errs = append(errs, validate.MaxBytes(ctx, op, fldPath, obj, oldObj, 128).MarkAlpha()...) return }(fldPath.Child("hardwareAddress"), &obj.HardwareAddress, safe.Field(oldObj, func(oldObj *resourcev1.NetworkDeviceData) *string { return &oldObj.HardwareAddress }), oldObj != nil)...) diff --git a/pkg/apis/resource/v1beta1/zz_generated.validations.go b/pkg/apis/resource/v1beta1/zz_generated.validations.go index 1699f58670c..dfdfafb30e7 100644 --- a/pkg/apis/resource/v1beta1/zz_generated.validations.go +++ b/pkg/apis/resource/v1beta1/zz_generated.validations.go @@ -1496,7 +1496,7 @@ func Validate_NetworkDeviceData(ctx context.Context, op operation.Operation, fld if earlyReturn { return // do not proceed } - errs = append(errs, validate.MaxLength(ctx, op, fldPath, obj, oldObj, 256).MarkAlpha()...) + errs = append(errs, validate.MaxBytes(ctx, op, fldPath, obj, oldObj, 256).MarkAlpha()...) return }(fldPath.Child("interfaceName"), &obj.InterfaceName, safe.Field(oldObj, func(oldObj *resourcev1beta1.NetworkDeviceData) *string { return &oldObj.InterfaceName }), oldObj != nil)...) @@ -1539,7 +1539,7 @@ func Validate_NetworkDeviceData(ctx context.Context, op operation.Operation, fld if earlyReturn { return // do not proceed } - errs = append(errs, validate.MaxLength(ctx, op, fldPath, obj, oldObj, 128).MarkAlpha()...) + errs = append(errs, validate.MaxBytes(ctx, op, fldPath, obj, oldObj, 128).MarkAlpha()...) return }(fldPath.Child("hardwareAddress"), &obj.HardwareAddress, safe.Field(oldObj, func(oldObj *resourcev1beta1.NetworkDeviceData) *string { return &oldObj.HardwareAddress }), oldObj != nil)...) diff --git a/pkg/apis/resource/v1beta2/zz_generated.validations.go b/pkg/apis/resource/v1beta2/zz_generated.validations.go index 0939e739459..934c36458e9 100644 --- a/pkg/apis/resource/v1beta2/zz_generated.validations.go +++ b/pkg/apis/resource/v1beta2/zz_generated.validations.go @@ -1495,7 +1495,7 @@ func Validate_NetworkDeviceData(ctx context.Context, op operation.Operation, fld if earlyReturn { return // do not proceed } - errs = append(errs, validate.MaxLength(ctx, op, fldPath, obj, oldObj, 256).MarkAlpha()...) + errs = append(errs, validate.MaxBytes(ctx, op, fldPath, obj, oldObj, 256).MarkAlpha()...) return }(fldPath.Child("interfaceName"), &obj.InterfaceName, safe.Field(oldObj, func(oldObj *resourcev1beta2.NetworkDeviceData) *string { return &oldObj.InterfaceName }), oldObj != nil)...) @@ -1538,7 +1538,7 @@ func Validate_NetworkDeviceData(ctx context.Context, op operation.Operation, fld if earlyReturn { return // do not proceed } - errs = append(errs, validate.MaxLength(ctx, op, fldPath, obj, oldObj, 128).MarkAlpha()...) + errs = append(errs, validate.MaxBytes(ctx, op, fldPath, obj, oldObj, 128).MarkAlpha()...) return }(fldPath.Child("hardwareAddress"), &obj.HardwareAddress, safe.Field(oldObj, func(oldObj *resourcev1beta2.NetworkDeviceData) *string { return &oldObj.HardwareAddress }), oldObj != nil)...) diff --git a/pkg/apis/resource/validation/validation_resourceclaim_test.go b/pkg/apis/resource/validation/validation_resourceclaim_test.go index d3347754cba..be49d6aed6b 100644 --- a/pkg/apis/resource/validation/validation_resourceclaim_test.go +++ b/pkg/apis/resource/validation/validation_resourceclaim_test.go @@ -1394,6 +1394,45 @@ func TestValidateClaimStatusUpdate(t *testing.T) { }, deviceStatusFeatureGate: true, }, + "valid-network-device-status-with-multi-byte-interface-name-and-hardware-address": { + oldClaim: func() *resource.ResourceClaim { return validAllocatedClaim }(), + update: func(claim *resource.ResourceClaim) *resource.ResourceClaim { + claim.Status.Devices = []resource.AllocatedDeviceStatus{ + { + Driver: goodName, + Pool: goodName, + Device: goodName, + Conditions: []metav1.Condition{ + {Type: "test-0", Status: metav1.ConditionTrue, Reason: "test_reason", LastTransitionTime: metav1.Now(), ObservedGeneration: 0}, + {Type: "test-1", Status: metav1.ConditionTrue, Reason: "test_reason", LastTransitionTime: metav1.Now(), ObservedGeneration: 0}, + {Type: "test-2", Status: metav1.ConditionTrue, Reason: "test_reason", LastTransitionTime: metav1.Now(), ObservedGeneration: 0}, + {Type: "test-3", Status: metav1.ConditionTrue, Reason: "test_reason", LastTransitionTime: metav1.Now(), ObservedGeneration: 0}, + {Type: "test-4", Status: metav1.ConditionTrue, Reason: "test_reason", LastTransitionTime: metav1.Now(), ObservedGeneration: 0}, + {Type: "test-5", Status: metav1.ConditionTrue, Reason: "test_reason", LastTransitionTime: metav1.Now(), ObservedGeneration: 0}, + {Type: "test-6", Status: metav1.ConditionTrue, Reason: "test_reason", LastTransitionTime: metav1.Now(), ObservedGeneration: 0}, + {Type: "test-7", Status: metav1.ConditionTrue, Reason: "test_reason", LastTransitionTime: metav1.Now(), ObservedGeneration: 0}, + }, + Data: &runtime.RawExtension{ + Raw: []byte(`{"kind": "foo", "apiVersion": "dra.example.com/v1"}`), + }, + NetworkData: &resource.NetworkDeviceData{ + InterfaceName: strings.Repeat("𝄞", 256/4), // the G clef unicode character is exactly 4 bytes so repeating this 256/4 times means that it is exactly 256 bytes worth of length and should be valid. + HardwareAddress: strings.Repeat("𝄞", 128/4), // the G clef unicode character is exactly 4 bytes so repeating this 128/4 times means that it is exactly 128 bytes worth of length and should be valid. + IPs: []string{ + "10.9.8.0/24", + "2001:db8::/64", + "10.9.8.1/24", + "2001:db8::1/64", + "10.9.8.2/24", "10.9.8.3/24", "10.9.8.4/24", "10.9.8.5/24", "10.9.8.6/24", "10.9.8.7/24", + "10.9.8.8/24", "10.9.8.9/24", "10.9.8.10/24", "10.9.8.11/24", "10.9.8.12/24", "10.9.8.13/24", + }, + }, + }, + } + return claim + }, + deviceStatusFeatureGate: true, + }, "invalid-device-status-duplicate": { wantFailures: field.ErrorList{ field.Duplicate(field.NewPath("status", "devices").Index(0).Child("networkData", "ips").Index(1), "2001:db8::1/64").MarkCoveredByDeclarative(), @@ -1483,6 +1522,36 @@ func TestValidateClaimStatusUpdate(t *testing.T) { }, deviceStatusFeatureGate: true, }, + "invalid-network-device-status-with-multi-byte-interface-name-and-hardware-address": { + wantFailures: field.ErrorList{ + field.TooLong(field.NewPath("status", "devices").Index(0).Child("networkData", "interfaceName"), "", resource.NetworkDeviceDataInterfaceNameMaxLength).MarkCoveredByDeclarative(), + field.TooLong(field.NewPath("status", "devices").Index(0).Child("networkData", "hardwareAddress"), "", resource.NetworkDeviceDataHardwareAddressMaxLength).MarkCoveredByDeclarative(), + field.Invalid(field.NewPath("status", "devices").Index(0).Child("networkData", "ips").Index(0), "300.9.8.0/24", "must be a valid address in CIDR form, (e.g. 10.9.8.7/24 or 2001:db8::1/64)"), + field.Invalid(field.NewPath("status", "devices").Index(0).Child("networkData", "ips").Index(1), "010.009.008.000/24", "must be in canonical form (\"10.9.8.0/24\")"), + field.Invalid(field.NewPath("status", "devices").Index(0).Child("networkData", "ips").Index(2), "2001:0db8::1/64", "must be in canonical form (\"2001:db8::1/64\")"), + }, + oldClaim: func() *resource.ResourceClaim { return validAllocatedClaim }(), + update: func(claim *resource.ResourceClaim) *resource.ResourceClaim { + claim.Status.Devices = []resource.AllocatedDeviceStatus{ + { + Driver: goodName, + Pool: goodName, + Device: goodName, + NetworkData: &resource.NetworkDeviceData{ + InterfaceName: strings.Repeat("𝄞", resource.NetworkDeviceDataInterfaceNameMaxLength), // the G clef unicode character is exactly 4 bytes in length and should exceed the byte length limit for this field by a factor of 4 + HardwareAddress: strings.Repeat("𝄞", resource.NetworkDeviceDataHardwareAddressMaxLength), // the G clef unicode character is exactly 4 bytes in length and should exceed the byte length limit for this field by a factor of 4 + IPs: []string{ + "300.9.8.0/24", + "010.009.008.000/24", + "2001:0db8::1/64", + }, + }, + }, + } + return claim + }, + deviceStatusFeatureGate: true, + }, "invalid-data-device-status": { wantFailures: field.ErrorList{ field.Invalid(field.NewPath("status", "devices").Index(0).Child("data"), "", "error parsing data as JSON: invalid character 'o' in literal false (expecting 'a')"), diff --git a/staging/src/k8s.io/api/resource/v1/types.go b/staging/src/k8s.io/api/resource/v1/types.go index c7bca166c53..dee44a16137 100644 --- a/staging/src/k8s.io/api/resource/v1/types.go +++ b/staging/src/k8s.io/api/resource/v1/types.go @@ -1989,11 +1989,11 @@ type NetworkDeviceData struct { // the allocated device. This might be the name of a physical or virtual // network interface being configured in the pod. // - // Must not be longer than 256 characters. + // Must not be longer than 256 bytes. // // +optional // +k8s:alpha(since: "1.36")=+k8s:optional - // +k8s:alpha(since: "1.36")=+k8s:maxLength=256 + // +k8s:alpha(since: "1.36")=+k8s:maxBytes=256 InterfaceName string `json:"interfaceName,omitempty" protobuf:"bytes,1,opt,name=interfaceName"` // IPs lists the network addresses assigned to the device's network interface. @@ -2012,10 +2012,10 @@ type NetworkDeviceData struct { // HardwareAddress represents the hardware address (e.g. MAC Address) of the device's network interface. // - // Must not be longer than 128 characters. + // Must not be longer than 128 bytes. // // +optional // +k8s:alpha(since: "1.36")=+k8s:optional - // +k8s:alpha(since: "1.36")=+k8s:maxLength=128 + // +k8s:alpha(since: "1.36")=+k8s:maxBytes=128 HardwareAddress string `json:"hardwareAddress,omitempty" protobuf:"bytes,3,opt,name=hardwareAddress"` } diff --git a/staging/src/k8s.io/api/resource/v1beta1/types.go b/staging/src/k8s.io/api/resource/v1beta1/types.go index e532784b432..f0c2919b1d0 100644 --- a/staging/src/k8s.io/api/resource/v1beta1/types.go +++ b/staging/src/k8s.io/api/resource/v1beta1/types.go @@ -2014,11 +2014,11 @@ type NetworkDeviceData struct { // the allocated device. This might be the name of a physical or virtual // network interface being configured in the pod. // - // Must not be longer than 256 characters. + // Must not be longer than 256 bytes. // // +optional // +k8s:alpha(since: "1.36")=+k8s:optional - // +k8s:alpha(since: "1.36")=+k8s:maxLength=256 + // +k8s:alpha(since: "1.36")=+k8s:maxBytes=256 InterfaceName string `json:"interfaceName,omitempty" protobuf:"bytes,1,opt,name=interfaceName"` // IPs lists the network addresses assigned to the device's network interface. @@ -2039,10 +2039,10 @@ type NetworkDeviceData struct { // HardwareAddress represents the hardware address (e.g. MAC Address) of the device's network interface. // - // Must not be longer than 128 characters. + // Must not be longer than 128 bytes. // // +optional // +k8s:alpha(since: "1.36")=+k8s:optional - // +k8s:alpha(since: "1.36")=+k8s:maxLength=128 + // +k8s:alpha(since: "1.36")=+k8s:maxBytes=128 HardwareAddress string `json:"hardwareAddress,omitempty" protobuf:"bytes,3,opt,name=hardwareAddress"` } diff --git a/staging/src/k8s.io/api/resource/v1beta2/types.go b/staging/src/k8s.io/api/resource/v1beta2/types.go index fb8f30672c3..e5c04f58d1a 100644 --- a/staging/src/k8s.io/api/resource/v1beta2/types.go +++ b/staging/src/k8s.io/api/resource/v1beta2/types.go @@ -2001,11 +2001,11 @@ type NetworkDeviceData struct { // the allocated device. This might be the name of a physical or virtual // network interface being configured in the pod. // - // Must not be longer than 256 characters. + // Must not be longer than 256 bytes. // // +optional // +k8s:alpha(since: "1.36")=+k8s:optional - // +k8s:alpha(since: "1.36")=+k8s:maxLength=256 + // +k8s:alpha(since: "1.36")=+k8s:maxBytes=256 InterfaceName string `json:"interfaceName,omitempty" protobuf:"bytes,1,opt,name=interfaceName"` // IPs lists the network addresses assigned to the device's network interface. @@ -2024,10 +2024,10 @@ type NetworkDeviceData struct { // HardwareAddress represents the hardware address (e.g. MAC Address) of the device's network interface. // - // Must not be longer than 128 characters. + // Must not be longer than 128 bytes. // // +optional // +k8s:alpha(since: "1.36")=+k8s:optional - // +k8s:alpha(since: "1.36")=+k8s:maxLength=128 + // +k8s:alpha(since: "1.36")=+k8s:maxBytes=128 HardwareAddress string `json:"hardwareAddress,omitempty" protobuf:"bytes,3,opt,name=hardwareAddress"` } From 729792e8700780f9cfbb4a14e74621ef219444c1 Mon Sep 17 00:00:00 2001 From: Bryce Palmer Date: Fri, 27 Feb 2026 15:23:12 -0500 Subject: [PATCH 10/11] update-codegen: regenerate generated files Signed-off-by: Bryce Palmer --- api/openapi-spec/swagger.json | 12 ++++++------ .../v3/apis__resource.k8s.io__v1_openapi.json | 4 ++-- .../v3/apis__resource.k8s.io__v1beta1_openapi.json | 4 ++-- .../v3/apis__resource.k8s.io__v1beta2_openapi.json | 4 ++-- pkg/generated/openapi/zz_generated.openapi.go | 12 ++++++------ staging/src/k8s.io/api/resource/v1/generated.proto | 8 ++++---- .../api/resource/v1/types_swagger_doc_generated.go | 4 ++-- .../src/k8s.io/api/resource/v1beta1/generated.proto | 8 ++++---- .../resource/v1beta1/types_swagger_doc_generated.go | 4 ++-- .../src/k8s.io/api/resource/v1beta2/generated.proto | 8 ++++---- .../resource/v1beta2/types_swagger_doc_generated.go | 4 ++-- .../resource/v1/networkdevicedata.go | 4 ++-- .../resource/v1beta1/networkdevicedata.go | 4 ++-- .../resource/v1beta2/networkdevicedata.go | 4 ++-- 14 files changed, 42 insertions(+), 42 deletions(-) diff --git a/api/openapi-spec/swagger.json b/api/openapi-spec/swagger.json index cbb839662d6..06d9a5f1fe4 100644 --- a/api/openapi-spec/swagger.json +++ b/api/openapi-spec/swagger.json @@ -16329,11 +16329,11 @@ "description": "NetworkDeviceData provides network-related details for the allocated device. This information may be filled by drivers or other components to configure or identify the device within a network context.", "properties": { "hardwareAddress": { - "description": "HardwareAddress represents the hardware address (e.g. MAC Address) of the device's network interface.\n\nMust not be longer than 128 characters.", + "description": "HardwareAddress represents the hardware address (e.g. MAC Address) of the device's network interface.\n\nMust not be longer than 128 bytes.", "type": "string" }, "interfaceName": { - "description": "InterfaceName specifies the name of the network interface associated with the allocated device. This might be the name of a physical or virtual network interface being configured in the pod.\n\nMust not be longer than 256 characters.", + "description": "InterfaceName specifies the name of the network interface associated with the allocated device. This might be the name of a physical or virtual network interface being configured in the pod.\n\nMust not be longer than 256 bytes.", "type": "string" }, "ips": { @@ -17663,11 +17663,11 @@ "description": "NetworkDeviceData provides network-related details for the allocated device. This information may be filled by drivers or other components to configure or identify the device within a network context.", "properties": { "hardwareAddress": { - "description": "HardwareAddress represents the hardware address (e.g. MAC Address) of the device's network interface.\n\nMust not be longer than 128 characters.", + "description": "HardwareAddress represents the hardware address (e.g. MAC Address) of the device's network interface.\n\nMust not be longer than 128 bytes.", "type": "string" }, "interfaceName": { - "description": "InterfaceName specifies the name of the network interface associated with the allocated device. This might be the name of a physical or virtual network interface being configured in the pod.\n\nMust not be longer than 256 characters.", + "description": "InterfaceName specifies the name of the network interface associated with the allocated device. This might be the name of a physical or virtual network interface being configured in the pod.\n\nMust not be longer than 256 bytes.", "type": "string" }, "ips": { @@ -18849,11 +18849,11 @@ "description": "NetworkDeviceData provides network-related details for the allocated device. This information may be filled by drivers or other components to configure or identify the device within a network context.", "properties": { "hardwareAddress": { - "description": "HardwareAddress represents the hardware address (e.g. MAC Address) of the device's network interface.\n\nMust not be longer than 128 characters.", + "description": "HardwareAddress represents the hardware address (e.g. MAC Address) of the device's network interface.\n\nMust not be longer than 128 bytes.", "type": "string" }, "interfaceName": { - "description": "InterfaceName specifies the name of the network interface associated with the allocated device. This might be the name of a physical or virtual network interface being configured in the pod.\n\nMust not be longer than 256 characters.", + "description": "InterfaceName specifies the name of the network interface associated with the allocated device. This might be the name of a physical or virtual network interface being configured in the pod.\n\nMust not be longer than 256 bytes.", "type": "string" }, "ips": { diff --git a/api/openapi-spec/v3/apis__resource.k8s.io__v1_openapi.json b/api/openapi-spec/v3/apis__resource.k8s.io__v1_openapi.json index d80ad017640..b53ca635703 100644 --- a/api/openapi-spec/v3/apis__resource.k8s.io__v1_openapi.json +++ b/api/openapi-spec/v3/apis__resource.k8s.io__v1_openapi.json @@ -1100,11 +1100,11 @@ "description": "NetworkDeviceData provides network-related details for the allocated device. This information may be filled by drivers or other components to configure or identify the device within a network context.", "properties": { "hardwareAddress": { - "description": "HardwareAddress represents the hardware address (e.g. MAC Address) of the device's network interface.\n\nMust not be longer than 128 characters.", + "description": "HardwareAddress represents the hardware address (e.g. MAC Address) of the device's network interface.\n\nMust not be longer than 128 bytes.", "type": "string" }, "interfaceName": { - "description": "InterfaceName specifies the name of the network interface associated with the allocated device. This might be the name of a physical or virtual network interface being configured in the pod.\n\nMust not be longer than 256 characters.", + "description": "InterfaceName specifies the name of the network interface associated with the allocated device. This might be the name of a physical or virtual network interface being configured in the pod.\n\nMust not be longer than 256 bytes.", "type": "string" }, "ips": { diff --git a/api/openapi-spec/v3/apis__resource.k8s.io__v1beta1_openapi.json b/api/openapi-spec/v3/apis__resource.k8s.io__v1beta1_openapi.json index f2b2f27c769..c446f177d77 100644 --- a/api/openapi-spec/v3/apis__resource.k8s.io__v1beta1_openapi.json +++ b/api/openapi-spec/v3/apis__resource.k8s.io__v1beta1_openapi.json @@ -1097,11 +1097,11 @@ "description": "NetworkDeviceData provides network-related details for the allocated device. This information may be filled by drivers or other components to configure or identify the device within a network context.", "properties": { "hardwareAddress": { - "description": "HardwareAddress represents the hardware address (e.g. MAC Address) of the device's network interface.\n\nMust not be longer than 128 characters.", + "description": "HardwareAddress represents the hardware address (e.g. MAC Address) of the device's network interface.\n\nMust not be longer than 128 bytes.", "type": "string" }, "interfaceName": { - "description": "InterfaceName specifies the name of the network interface associated with the allocated device. This might be the name of a physical or virtual network interface being configured in the pod.\n\nMust not be longer than 256 characters.", + "description": "InterfaceName specifies the name of the network interface associated with the allocated device. This might be the name of a physical or virtual network interface being configured in the pod.\n\nMust not be longer than 256 bytes.", "type": "string" }, "ips": { diff --git a/api/openapi-spec/v3/apis__resource.k8s.io__v1beta2_openapi.json b/api/openapi-spec/v3/apis__resource.k8s.io__v1beta2_openapi.json index 678be7cb21b..72264f64d27 100644 --- a/api/openapi-spec/v3/apis__resource.k8s.io__v1beta2_openapi.json +++ b/api/openapi-spec/v3/apis__resource.k8s.io__v1beta2_openapi.json @@ -1100,11 +1100,11 @@ "description": "NetworkDeviceData provides network-related details for the allocated device. This information may be filled by drivers or other components to configure or identify the device within a network context.", "properties": { "hardwareAddress": { - "description": "HardwareAddress represents the hardware address (e.g. MAC Address) of the device's network interface.\n\nMust not be longer than 128 characters.", + "description": "HardwareAddress represents the hardware address (e.g. MAC Address) of the device's network interface.\n\nMust not be longer than 128 bytes.", "type": "string" }, "interfaceName": { - "description": "InterfaceName specifies the name of the network interface associated with the allocated device. This might be the name of a physical or virtual network interface being configured in the pod.\n\nMust not be longer than 256 characters.", + "description": "InterfaceName specifies the name of the network interface associated with the allocated device. This might be the name of a physical or virtual network interface being configured in the pod.\n\nMust not be longer than 256 bytes.", "type": "string" }, "ips": { diff --git a/pkg/generated/openapi/zz_generated.openapi.go b/pkg/generated/openapi/zz_generated.openapi.go index 4594decdc24..d98478a6666 100644 --- a/pkg/generated/openapi/zz_generated.openapi.go +++ b/pkg/generated/openapi/zz_generated.openapi.go @@ -48237,7 +48237,7 @@ func schema_k8sio_api_resource_v1_NetworkDeviceData(ref common.ReferenceCallback Properties: map[string]spec.Schema{ "interfaceName": { SchemaProps: spec.SchemaProps{ - Description: "InterfaceName specifies the name of the network interface associated with the allocated device. This might be the name of a physical or virtual network interface being configured in the pod.\n\nMust not be longer than 256 characters.", + Description: "InterfaceName specifies the name of the network interface associated with the allocated device. This might be the name of a physical or virtual network interface being configured in the pod.\n\nMust not be longer than 256 bytes.", Type: []string{"string"}, Format: "", }, @@ -48264,7 +48264,7 @@ func schema_k8sio_api_resource_v1_NetworkDeviceData(ref common.ReferenceCallback }, "hardwareAddress": { SchemaProps: spec.SchemaProps{ - Description: "HardwareAddress represents the hardware address (e.g. MAC Address) of the device's network interface.\n\nMust not be longer than 128 characters.", + Description: "HardwareAddress represents the hardware address (e.g. MAC Address) of the device's network interface.\n\nMust not be longer than 128 bytes.", Type: []string{"string"}, Format: "", }, @@ -50723,7 +50723,7 @@ func schema_k8sio_api_resource_v1beta1_NetworkDeviceData(ref common.ReferenceCal Properties: map[string]spec.Schema{ "interfaceName": { SchemaProps: spec.SchemaProps{ - Description: "InterfaceName specifies the name of the network interface associated with the allocated device. This might be the name of a physical or virtual network interface being configured in the pod.\n\nMust not be longer than 256 characters.", + Description: "InterfaceName specifies the name of the network interface associated with the allocated device. This might be the name of a physical or virtual network interface being configured in the pod.\n\nMust not be longer than 256 bytes.", Type: []string{"string"}, Format: "", }, @@ -50750,7 +50750,7 @@ func schema_k8sio_api_resource_v1beta1_NetworkDeviceData(ref common.ReferenceCal }, "hardwareAddress": { SchemaProps: spec.SchemaProps{ - Description: "HardwareAddress represents the hardware address (e.g. MAC Address) of the device's network interface.\n\nMust not be longer than 128 characters.", + Description: "HardwareAddress represents the hardware address (e.g. MAC Address) of the device's network interface.\n\nMust not be longer than 128 bytes.", Type: []string{"string"}, Format: "", }, @@ -52917,7 +52917,7 @@ func schema_k8sio_api_resource_v1beta2_NetworkDeviceData(ref common.ReferenceCal Properties: map[string]spec.Schema{ "interfaceName": { SchemaProps: spec.SchemaProps{ - Description: "InterfaceName specifies the name of the network interface associated with the allocated device. This might be the name of a physical or virtual network interface being configured in the pod.\n\nMust not be longer than 256 characters.", + Description: "InterfaceName specifies the name of the network interface associated with the allocated device. This might be the name of a physical or virtual network interface being configured in the pod.\n\nMust not be longer than 256 bytes.", Type: []string{"string"}, Format: "", }, @@ -52944,7 +52944,7 @@ func schema_k8sio_api_resource_v1beta2_NetworkDeviceData(ref common.ReferenceCal }, "hardwareAddress": { SchemaProps: spec.SchemaProps{ - Description: "HardwareAddress represents the hardware address (e.g. MAC Address) of the device's network interface.\n\nMust not be longer than 128 characters.", + Description: "HardwareAddress represents the hardware address (e.g. MAC Address) of the device's network interface.\n\nMust not be longer than 128 bytes.", Type: []string{"string"}, Format: "", }, diff --git a/staging/src/k8s.io/api/resource/v1/generated.proto b/staging/src/k8s.io/api/resource/v1/generated.proto index 09ace4660e6..38d8a5edb33 100644 --- a/staging/src/k8s.io/api/resource/v1/generated.proto +++ b/staging/src/k8s.io/api/resource/v1/generated.proto @@ -1310,11 +1310,11 @@ message NetworkDeviceData { // the allocated device. This might be the name of a physical or virtual // network interface being configured in the pod. // - // Must not be longer than 256 characters. + // Must not be longer than 256 bytes. // // +optional // +k8s:alpha(since: "1.36")=+k8s:optional - // +k8s:alpha(since: "1.36")=+k8s:maxLength=256 + // +k8s:alpha(since: "1.36")=+k8s:maxBytes=256 optional string interfaceName = 1; // IPs lists the network addresses assigned to the device's network interface. @@ -1333,11 +1333,11 @@ message NetworkDeviceData { // HardwareAddress represents the hardware address (e.g. MAC Address) of the device's network interface. // - // Must not be longer than 128 characters. + // Must not be longer than 128 bytes. // // +optional // +k8s:alpha(since: "1.36")=+k8s:optional - // +k8s:alpha(since: "1.36")=+k8s:maxLength=128 + // +k8s:alpha(since: "1.36")=+k8s:maxBytes=128 optional string hardwareAddress = 3; } diff --git a/staging/src/k8s.io/api/resource/v1/types_swagger_doc_generated.go b/staging/src/k8s.io/api/resource/v1/types_swagger_doc_generated.go index f1d9291fbce..10a1c175268 100644 --- a/staging/src/k8s.io/api/resource/v1/types_swagger_doc_generated.go +++ b/staging/src/k8s.io/api/resource/v1/types_swagger_doc_generated.go @@ -358,9 +358,9 @@ func (ExactDeviceRequest) SwaggerDoc() map[string]string { var map_NetworkDeviceData = map[string]string{ "": "NetworkDeviceData provides network-related details for the allocated device. This information may be filled by drivers or other components to configure or identify the device within a network context.", - "interfaceName": "InterfaceName specifies the name of the network interface associated with the allocated device. This might be the name of a physical or virtual network interface being configured in the pod.\n\nMust not be longer than 256 characters.", + "interfaceName": "InterfaceName specifies the name of the network interface associated with the allocated device. This might be the name of a physical or virtual network interface being configured in the pod.\n\nMust not be longer than 256 bytes.", "ips": "IPs lists the network addresses assigned to the device's network interface. This can include both IPv4 and IPv6 addresses. The IPs are in the CIDR notation, which includes both the address and the associated subnet mask. e.g.: \"192.0.2.5/24\" for IPv4 and \"2001:db8::5/64\" for IPv6.", - "hardwareAddress": "HardwareAddress represents the hardware address (e.g. MAC Address) of the device's network interface.\n\nMust not be longer than 128 characters.", + "hardwareAddress": "HardwareAddress represents the hardware address (e.g. MAC Address) of the device's network interface.\n\nMust not be longer than 128 bytes.", } func (NetworkDeviceData) SwaggerDoc() map[string]string { diff --git a/staging/src/k8s.io/api/resource/v1beta1/generated.proto b/staging/src/k8s.io/api/resource/v1beta1/generated.proto index 1432652b50d..5bee2688c29 100644 --- a/staging/src/k8s.io/api/resource/v1beta1/generated.proto +++ b/staging/src/k8s.io/api/resource/v1beta1/generated.proto @@ -1326,11 +1326,11 @@ message NetworkDeviceData { // the allocated device. This might be the name of a physical or virtual // network interface being configured in the pod. // - // Must not be longer than 256 characters. + // Must not be longer than 256 bytes. // // +optional // +k8s:alpha(since: "1.36")=+k8s:optional - // +k8s:alpha(since: "1.36")=+k8s:maxLength=256 + // +k8s:alpha(since: "1.36")=+k8s:maxBytes=256 optional string interfaceName = 1; // IPs lists the network addresses assigned to the device's network interface. @@ -1351,11 +1351,11 @@ message NetworkDeviceData { // HardwareAddress represents the hardware address (e.g. MAC Address) of the device's network interface. // - // Must not be longer than 128 characters. + // Must not be longer than 128 bytes. // // +optional // +k8s:alpha(since: "1.36")=+k8s:optional - // +k8s:alpha(since: "1.36")=+k8s:maxLength=128 + // +k8s:alpha(since: "1.36")=+k8s:maxBytes=128 optional string hardwareAddress = 3; } diff --git a/staging/src/k8s.io/api/resource/v1beta1/types_swagger_doc_generated.go b/staging/src/k8s.io/api/resource/v1beta1/types_swagger_doc_generated.go index 070536e67f7..61da657ff12 100644 --- a/staging/src/k8s.io/api/resource/v1beta1/types_swagger_doc_generated.go +++ b/staging/src/k8s.io/api/resource/v1beta1/types_swagger_doc_generated.go @@ -358,9 +358,9 @@ func (DeviceToleration) SwaggerDoc() map[string]string { var map_NetworkDeviceData = map[string]string{ "": "NetworkDeviceData provides network-related details for the allocated device. This information may be filled by drivers or other components to configure or identify the device within a network context.", - "interfaceName": "InterfaceName specifies the name of the network interface associated with the allocated device. This might be the name of a physical or virtual network interface being configured in the pod.\n\nMust not be longer than 256 characters.", + "interfaceName": "InterfaceName specifies the name of the network interface associated with the allocated device. This might be the name of a physical or virtual network interface being configured in the pod.\n\nMust not be longer than 256 bytes.", "ips": "IPs lists the network addresses assigned to the device's network interface. This can include both IPv4 and IPv6 addresses. The IPs are in the CIDR notation, which includes both the address and the associated subnet mask. e.g.: \"192.0.2.5/24\" for IPv4 and \"2001:db8::5/64\" for IPv6.\n\nMust not contain more than 16 entries.", - "hardwareAddress": "HardwareAddress represents the hardware address (e.g. MAC Address) of the device's network interface.\n\nMust not be longer than 128 characters.", + "hardwareAddress": "HardwareAddress represents the hardware address (e.g. MAC Address) of the device's network interface.\n\nMust not be longer than 128 bytes.", } func (NetworkDeviceData) SwaggerDoc() map[string]string { diff --git a/staging/src/k8s.io/api/resource/v1beta2/generated.proto b/staging/src/k8s.io/api/resource/v1beta2/generated.proto index c55a3d062b1..c0f60b7f79f 100644 --- a/staging/src/k8s.io/api/resource/v1beta2/generated.proto +++ b/staging/src/k8s.io/api/resource/v1beta2/generated.proto @@ -1313,11 +1313,11 @@ message NetworkDeviceData { // the allocated device. This might be the name of a physical or virtual // network interface being configured in the pod. // - // Must not be longer than 256 characters. + // Must not be longer than 256 bytes. // // +optional // +k8s:alpha(since: "1.36")=+k8s:optional - // +k8s:alpha(since: "1.36")=+k8s:maxLength=256 + // +k8s:alpha(since: "1.36")=+k8s:maxBytes=256 optional string interfaceName = 1; // IPs lists the network addresses assigned to the device's network interface. @@ -1336,11 +1336,11 @@ message NetworkDeviceData { // HardwareAddress represents the hardware address (e.g. MAC Address) of the device's network interface. // - // Must not be longer than 128 characters. + // Must not be longer than 128 bytes. // // +optional // +k8s:alpha(since: "1.36")=+k8s:optional - // +k8s:alpha(since: "1.36")=+k8s:maxLength=128 + // +k8s:alpha(since: "1.36")=+k8s:maxBytes=128 optional string hardwareAddress = 3; } diff --git a/staging/src/k8s.io/api/resource/v1beta2/types_swagger_doc_generated.go b/staging/src/k8s.io/api/resource/v1beta2/types_swagger_doc_generated.go index a086f9bf23e..2556dd91b14 100644 --- a/staging/src/k8s.io/api/resource/v1beta2/types_swagger_doc_generated.go +++ b/staging/src/k8s.io/api/resource/v1beta2/types_swagger_doc_generated.go @@ -358,9 +358,9 @@ func (ExactDeviceRequest) SwaggerDoc() map[string]string { var map_NetworkDeviceData = map[string]string{ "": "NetworkDeviceData provides network-related details for the allocated device. This information may be filled by drivers or other components to configure or identify the device within a network context.", - "interfaceName": "InterfaceName specifies the name of the network interface associated with the allocated device. This might be the name of a physical or virtual network interface being configured in the pod.\n\nMust not be longer than 256 characters.", + "interfaceName": "InterfaceName specifies the name of the network interface associated with the allocated device. This might be the name of a physical or virtual network interface being configured in the pod.\n\nMust not be longer than 256 bytes.", "ips": "IPs lists the network addresses assigned to the device's network interface. This can include both IPv4 and IPv6 addresses. The IPs are in the CIDR notation, which includes both the address and the associated subnet mask. e.g.: \"192.0.2.5/24\" for IPv4 and \"2001:db8::5/64\" for IPv6.", - "hardwareAddress": "HardwareAddress represents the hardware address (e.g. MAC Address) of the device's network interface.\n\nMust not be longer than 128 characters.", + "hardwareAddress": "HardwareAddress represents the hardware address (e.g. MAC Address) of the device's network interface.\n\nMust not be longer than 128 bytes.", } func (NetworkDeviceData) SwaggerDoc() map[string]string { diff --git a/staging/src/k8s.io/client-go/applyconfigurations/resource/v1/networkdevicedata.go b/staging/src/k8s.io/client-go/applyconfigurations/resource/v1/networkdevicedata.go index 5d0ee8fc501..9cb1b4dea5b 100644 --- a/staging/src/k8s.io/client-go/applyconfigurations/resource/v1/networkdevicedata.go +++ b/staging/src/k8s.io/client-go/applyconfigurations/resource/v1/networkdevicedata.go @@ -29,7 +29,7 @@ type NetworkDeviceDataApplyConfiguration struct { // the allocated device. This might be the name of a physical or virtual // network interface being configured in the pod. // - // Must not be longer than 256 characters. + // Must not be longer than 256 bytes. InterfaceName *string `json:"interfaceName,omitempty"` // IPs lists the network addresses assigned to the device's network interface. // This can include both IPv4 and IPv6 addresses. @@ -39,7 +39,7 @@ type NetworkDeviceDataApplyConfiguration struct { IPs []string `json:"ips,omitempty"` // HardwareAddress represents the hardware address (e.g. MAC Address) of the device's network interface. // - // Must not be longer than 128 characters. + // Must not be longer than 128 bytes. HardwareAddress *string `json:"hardwareAddress,omitempty"` } diff --git a/staging/src/k8s.io/client-go/applyconfigurations/resource/v1beta1/networkdevicedata.go b/staging/src/k8s.io/client-go/applyconfigurations/resource/v1beta1/networkdevicedata.go index 5c3edcfe886..f112f01454c 100644 --- a/staging/src/k8s.io/client-go/applyconfigurations/resource/v1beta1/networkdevicedata.go +++ b/staging/src/k8s.io/client-go/applyconfigurations/resource/v1beta1/networkdevicedata.go @@ -29,7 +29,7 @@ type NetworkDeviceDataApplyConfiguration struct { // the allocated device. This might be the name of a physical or virtual // network interface being configured in the pod. // - // Must not be longer than 256 characters. + // Must not be longer than 256 bytes. InterfaceName *string `json:"interfaceName,omitempty"` // IPs lists the network addresses assigned to the device's network interface. // This can include both IPv4 and IPv6 addresses. @@ -41,7 +41,7 @@ type NetworkDeviceDataApplyConfiguration struct { IPs []string `json:"ips,omitempty"` // HardwareAddress represents the hardware address (e.g. MAC Address) of the device's network interface. // - // Must not be longer than 128 characters. + // Must not be longer than 128 bytes. HardwareAddress *string `json:"hardwareAddress,omitempty"` } diff --git a/staging/src/k8s.io/client-go/applyconfigurations/resource/v1beta2/networkdevicedata.go b/staging/src/k8s.io/client-go/applyconfigurations/resource/v1beta2/networkdevicedata.go index 9621703268f..238e5412f13 100644 --- a/staging/src/k8s.io/client-go/applyconfigurations/resource/v1beta2/networkdevicedata.go +++ b/staging/src/k8s.io/client-go/applyconfigurations/resource/v1beta2/networkdevicedata.go @@ -29,7 +29,7 @@ type NetworkDeviceDataApplyConfiguration struct { // the allocated device. This might be the name of a physical or virtual // network interface being configured in the pod. // - // Must not be longer than 256 characters. + // Must not be longer than 256 bytes. InterfaceName *string `json:"interfaceName,omitempty"` // IPs lists the network addresses assigned to the device's network interface. // This can include both IPv4 and IPv6 addresses. @@ -39,7 +39,7 @@ type NetworkDeviceDataApplyConfiguration struct { IPs []string `json:"ips,omitempty"` // HardwareAddress represents the hardware address (e.g. MAC Address) of the device's network interface. // - // Must not be longer than 128 characters. + // Must not be longer than 128 bytes. HardwareAddress *string `json:"hardwareAddress,omitempty"` } From 82d8b65bf07ee16237f0af5704c5f89e6fec971a Mon Sep 17 00:00:00 2001 From: Bryce Palmer Date: Fri, 27 Feb 2026 15:57:01 -0500 Subject: [PATCH 11/11] validation: align handwritten and declarative validations for the InterfaceName and HardwareAddress fields and add tests to ensure that if either are changed to use character-based length counting that a difference in the validation expectations will be found Signed-off-by: Bryce Palmer --- pkg/apis/resource/validation/validation.go | 4 +- .../declarative_validation_test.go | 76 ++++++++++++++++++- 2 files changed, 76 insertions(+), 4 deletions(-) diff --git a/pkg/apis/resource/validation/validation.go b/pkg/apis/resource/validation/validation.go index b2847e98980..a3ad1513705 100644 --- a/pkg/apis/resource/validation/validation.go +++ b/pkg/apis/resource/validation/validation.go @@ -1329,11 +1329,11 @@ func validateNetworkDeviceData(networkDeviceData *resource.NetworkDeviceData, fl } if len(networkDeviceData.InterfaceName) > resource.NetworkDeviceDataInterfaceNameMaxLength { - allErrs = append(allErrs, field.TooLong(fldPath.Child("interfaceName"), "" /* unused */, resource.NetworkDeviceDataInterfaceNameMaxLength).WithOrigin("maxLength").MarkCoveredByDeclarative()) + allErrs = append(allErrs, field.TooLong(fldPath.Child("interfaceName"), "" /* unused */, resource.NetworkDeviceDataInterfaceNameMaxLength).WithOrigin("maxBytes").MarkCoveredByDeclarative()) } if len(networkDeviceData.HardwareAddress) > resource.NetworkDeviceDataHardwareAddressMaxLength { - allErrs = append(allErrs, field.TooLong(fldPath.Child("hardwareAddress"), "" /* unused */, resource.NetworkDeviceDataHardwareAddressMaxLength).WithOrigin("maxLength").MarkCoveredByDeclarative()) + allErrs = append(allErrs, field.TooLong(fldPath.Child("hardwareAddress"), "" /* unused */, resource.NetworkDeviceDataHardwareAddressMaxLength).WithOrigin("maxBytes").MarkCoveredByDeclarative()) } allErrs = append(allErrs, validateSet(networkDeviceData.IPs, resource.NetworkDeviceDataMaxIPs, diff --git a/pkg/registry/resource/resourceclaim/declarative_validation_test.go b/pkg/registry/resource/resourceclaim/declarative_validation_test.go index bc1a7ce4e7c..745113925e3 100644 --- a/pkg/registry/resource/resourceclaim/declarative_validation_test.go +++ b/pkg/registry/resource/resourceclaim/declarative_validation_test.go @@ -1257,6 +1257,21 @@ func testValidateStatusUpdateForDeclarative(t *testing.T, apiVersion string) { ), ), }, + "valid networkdevicedata interfacename with multi-byte characters": { + old: mkValidResourceClaim(), + update: mkResourceClaimWithStatus( + tweakStatusDevices( + resource.AllocatedDeviceStatus{ + Driver: "dra.example.com", + Pool: "pool-0", + Device: "device-0", + NetworkData: &resource.NetworkDeviceData{ + InterfaceName: strings.Repeat("𝄞", resource.NetworkDeviceDataInterfaceNameMaxLength/4), // the G clef unicode character is exactly 4 bytes so repeating this 256/4 times means that it is exactly 256 bytes worth of length and should be valid. + }, + }, + ), + ), + }, "invalid networkdevicedata interfacename too long": { old: mkValidResourceClaim(), update: mkResourceClaimWithStatus( @@ -1272,7 +1287,28 @@ func testValidateStatusUpdateForDeclarative(t *testing.T, apiVersion string) { ), ), expectedErrs: field.ErrorList{ - field.TooLong(field.NewPath("status", "devices").Index(0).Child("networkData", "interfaceName"), "", resource.NetworkDeviceDataInterfaceNameMaxLength).MarkCoveredByDeclarative().WithOrigin("maxLength").MarkAlpha(), + field.TooLong(field.NewPath("status", "devices").Index(0).Child("networkData", "interfaceName"), "", resource.NetworkDeviceDataInterfaceNameMaxLength).MarkCoveredByDeclarative().WithOrigin("maxBytes").MarkAlpha(), + }, + }, + "invalid networkdevicedata interfacename too long with multi-byte characters repeating max bytes length times": { + old: mkValidResourceClaim(), + update: mkResourceClaimWithStatus( + tweakStatusDevices( + resource.AllocatedDeviceStatus{ + Driver: "dra.example.com", + Pool: "pool-0", + Device: "device-0", + NetworkData: &resource.NetworkDeviceData{ + // The G clef unicode character is exactly 4 bytes in length so repeating the character + // the same number of times as the maxBytes payload means that we are guaranteed to fail + // a byte-based length check, but not a character based check. + InterfaceName: strings.Repeat("𝄞", resource.NetworkDeviceDataInterfaceNameMaxLength), + }, + }, + ), + ), + expectedErrs: field.ErrorList{ + field.TooLong(field.NewPath("status", "devices").Index(0).Child("networkData", "interfaceName"), "", resource.NetworkDeviceDataInterfaceNameMaxLength).MarkCoveredByDeclarative().WithOrigin("maxBytes").MarkAlpha(), }, }, "valid status.devices.networkData.hardwareAddress": { @@ -1290,6 +1326,21 @@ func testValidateStatusUpdateForDeclarative(t *testing.T, apiVersion string) { ), ), }, + "valid status.devices.networkData.hardwareAddress with multi-byte characters": { + old: mkValidResourceClaim(), + update: mkResourceClaimWithStatus( + tweakStatusDevices( + resource.AllocatedDeviceStatus{ + Driver: "dra.example.com", + Pool: "pool-0", + Device: "device-0", + NetworkData: &resource.NetworkDeviceData{ + HardwareAddress: strings.Repeat("𝄞", resource.NetworkDeviceDataHardwareAddressMaxLength/4), // the G clef unicode character is exactly 4 bytes so repeating this 256/4 times means that it is exactly 256 bytes worth of length and should be valid. + }, + }, + ), + ), + }, "invalid status.devices.networkData.hardwareAddress too long": { old: mkValidResourceClaim(), update: mkResourceClaimWithStatus( @@ -1305,7 +1356,28 @@ func testValidateStatusUpdateForDeclarative(t *testing.T, apiVersion string) { ), ), expectedErrs: field.ErrorList{ - field.TooLong(field.NewPath("status", "devices").Index(0).Child("networkData", "hardwareAddress"), "", resource.NetworkDeviceDataHardwareAddressMaxLength).MarkCoveredByDeclarative().WithOrigin("maxLength").MarkAlpha(), + field.TooLong(field.NewPath("status", "devices").Index(0).Child("networkData", "hardwareAddress"), "", resource.NetworkDeviceDataHardwareAddressMaxLength).MarkCoveredByDeclarative().WithOrigin("maxBytes").MarkAlpha(), + }, + }, + "invalid status.devices.networkData.hardwareAddress too long with multi-byte characters repeating max bytes length times": { + old: mkValidResourceClaim(), + update: mkResourceClaimWithStatus( + tweakStatusDevices( + resource.AllocatedDeviceStatus{ + Driver: "dra.example.com", + Pool: "pool-0", + Device: "device-0", + NetworkData: &resource.NetworkDeviceData{ + // The G clef unicode character is exactly 4 bytes in length so repeating the character + // the same number of times as the maxBytes payload means that we are guaranteed to fail + // a byte-based length check, but not a character based check. + HardwareAddress: strings.Repeat("𝄞", resource.NetworkDeviceDataHardwareAddressMaxLength), + }, + }, + ), + ), + expectedErrs: field.ErrorList{ + field.TooLong(field.NewPath("status", "devices").Index(0).Child("networkData", "hardwareAddress"), "", resource.NetworkDeviceDataHardwareAddressMaxLength).MarkCoveredByDeclarative().WithOrigin("maxBytes").MarkAlpha(), }, }, "invalid status.devices.networkData.ips duplicate": {