diff --git a/pkg/apis/resource/validation/validation.go b/pkg/apis/resource/validation/validation.go index 3d411db46a3..2d5529c9532 100644 --- a/pkg/apis/resource/validation/validation.go +++ b/pkg/apis/resource/validation/validation.go @@ -33,6 +33,7 @@ import ( "k8s.io/apimachinery/pkg/api/operation" apiresource "k8s.io/apimachinery/pkg/api/resource" "k8s.io/apimachinery/pkg/api/validate" + "k8s.io/apimachinery/pkg/api/validate/content" apimachineryvalidation "k8s.io/apimachinery/pkg/api/validation" metav1validation "k8s.io/apimachinery/pkg/apis/meta/v1/validation" "k8s.io/apimachinery/pkg/runtime" @@ -1087,11 +1088,6 @@ func validateDeviceCounter(counter resource.Counter, fldPath *field.Path) field. func validateQualifiedName(name resource.QualifiedName, fldPath *field.Path) field.ErrorList { var allErrs field.ErrorList - if name == "" { - allErrs = append(allErrs, field.Required(fldPath, "name required")) - return allErrs - } - parts := strings.Split(string(name), "/") switch len(parts) { case 1: @@ -1125,15 +1121,11 @@ func validateQualifiedName(name resource.QualifiedName, fldPath *field.Path) fie func validateFullyQualifiedName(name resource.FullyQualifiedName, fldPath *field.Path) field.ErrorList { var allErrs field.ErrorList - if name == "" { - allErrs = append(allErrs, field.Required(fldPath, "name required")) - return allErrs - } allErrs = append(allErrs, validateQualifiedName(resource.QualifiedName(name), fldPath)...) - // validateQualifiedName checks that the name isn't empty and both parts are valid. + // validateQualifiedName checks that both parts are valid. // What we need to enforce here is that there really is a domain. - if name != "" && !strings.Contains(string(name), "/") { - allErrs = append(allErrs, field.Invalid(fldPath, name, "must include a domain")) + if !strings.Contains(string(name), "/") { + allErrs = append(allErrs, field.Invalid(fldPath, name, "a fully qualified name must be a domain and a name separated by a slash")) } return allErrs.WithOrigin("format=k8s-resource-fully-qualified-name") } @@ -1143,7 +1135,7 @@ func validateCIdentifier(id string, fldPath *field.Path) field.ErrorList { if len(id) > resource.DeviceMaxIDLength { allErrs = append(allErrs, field.TooLong(fldPath, "" /*unused*/, resource.DeviceMaxIDLength)) } - for _, msg := range validation.IsCIdentifier(id) { + for _, msg := range content.IsCIdentifier(id) { allErrs = append(allErrs, field.Invalid(fldPath, id, msg)) } return allErrs diff --git a/pkg/apis/resource/validation/validation_resourceclaim_test.go b/pkg/apis/resource/validation/validation_resourceclaim_test.go index 93c2d3d203c..598cfd6f2b0 100644 --- a/pkg/apis/resource/validation/validation_resourceclaim_test.go +++ b/pkg/apis/resource/validation/validation_resourceclaim_test.go @@ -263,8 +263,9 @@ func TestValidateClaim(t *testing.T) { field.Invalid(field.NewPath("spec", "devices", "constraints").Index(0).Child("requests").Index(1), badName, "a lowercase RFC 1123 label must consist of lower case alphanumeric characters or '-', and must start and end with an alphanumeric character (e.g. 'my-name', or '123-abc', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?')"), field.Invalid(field.NewPath("spec", "devices", "constraints").Index(0).Child("requests").Index(1), badName, "must be the name of a request in the claim or the name of a request and a subrequest separated by '/'"), field.Invalid(field.NewPath("spec", "devices", "constraints").Index(0).Child("matchAttribute"), "missing-domain", "a valid C identifier must start with alphabetic character or '_', followed by a string of alphanumeric characters or '_' (e.g. 'my_name', or 'MY_NAME', or 'MyName', regex used for validation is '[A-Za-z_][A-Za-z0-9_]*')").MarkCoveredByDeclarative(), - field.Invalid(field.NewPath("spec", "devices", "constraints").Index(0).Child("matchAttribute"), resource.FullyQualifiedName("missing-domain"), "must include a domain").MarkCoveredByDeclarative(), - field.Required(field.NewPath("spec", "devices", "constraints").Index(1).Child("matchAttribute"), "name required").MarkCoveredByDeclarative(), + field.Invalid(field.NewPath("spec", "devices", "constraints").Index(0).Child("matchAttribute"), resource.FullyQualifiedName("missing-domain"), "a fully qualified name must be a domain and a name separated by a slash").MarkCoveredByDeclarative(), + field.Invalid(field.NewPath("spec", "devices", "constraints").Index(1).Child("matchAttribute"), "", "a valid C identifier must start with alphabetic character or '_', followed by a string of alphanumeric characters or '_' (e.g. 'my_name', or 'MY_NAME', or 'MyName', regex used for validation is '[A-Za-z_][A-Za-z0-9_]*')").MarkCoveredByDeclarative(), + field.Invalid(field.NewPath("spec", "devices", "constraints").Index(1).Child("matchAttribute"), resource.FullyQualifiedName(""), "a fully qualified name must be a domain and a name separated by a slash").MarkCoveredByDeclarative(), field.Required(field.NewPath("spec", "devices", "constraints").Index(2).Child("matchAttribute"), ""), field.TooMany(field.NewPath("spec", "devices", "constraints"), resource.DeviceConstraintsMaxSize+1, resource.DeviceConstraintsMaxSize).MarkCoveredByDeclarative(), field.Invalid(field.NewPath("spec", "devices", "config").Index(0).Child("requests").Index(1), badName, "a lowercase RFC 1123 label must consist of lower case alphanumeric characters or '-', and must start and end with an alphanumeric character (e.g. 'my-name', or '123-abc', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?')"), @@ -322,8 +323,9 @@ func TestValidateClaim(t *testing.T) { "invalid-distinct-constraint": { wantFailures: field.ErrorList{ field.Invalid(field.NewPath("spec", "devices", "constraints").Index(0).Child("distinctAttribute"), "missing-domain", "a valid C identifier must start with alphabetic character or '_', followed by a string of alphanumeric characters or '_' (e.g. 'my_name', or 'MY_NAME', or 'MyName', regex used for validation is '[A-Za-z_][A-Za-z0-9_]*')"), - field.Invalid(field.NewPath("spec", "devices", "constraints").Index(0).Child("distinctAttribute"), resource.FullyQualifiedName("missing-domain"), "must include a domain"), - field.Required(field.NewPath("spec", "devices", "constraints").Index(1).Child("distinctAttribute"), "name required"), + field.Invalid(field.NewPath("spec", "devices", "constraints").Index(0).Child("distinctAttribute"), resource.FullyQualifiedName("missing-domain"), "a fully qualified name must be a domain and a name separated by a slash"), + field.Invalid(field.NewPath("spec", "devices", "constraints").Index(1).Child("distinctAttribute"), "", "a valid C identifier must start with alphabetic character or '_', followed by a string of alphanumeric characters or '_' (e.g. 'my_name', or 'MY_NAME', or 'MyName', regex used for validation is '[A-Za-z_][A-Za-z0-9_]*')"), + field.Invalid(field.NewPath("spec", "devices", "constraints").Index(1).Child("distinctAttribute"), resource.FullyQualifiedName(""), "a fully qualified name must be a domain and a name separated by a slash"), field.Required(field.NewPath("spec", "devices", "constraints").Index(2), `exactly one of "matchAttribute" or "distinctAttribute" is required, but multiple fields are set`)}, claim: func() *resource.ResourceClaim { claim := testClaim(goodName, goodNS, validClaimSpec) @@ -375,8 +377,9 @@ func TestValidateClaim(t *testing.T) { field.Invalid(field.NewPath("spec", "devices", "constraints").Index(0).Child("requests").Index(1), badName, "a lowercase RFC 1123 label must consist of lower case alphanumeric characters or '-', and must start and end with an alphanumeric character (e.g. 'my-name', or '123-abc', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?')"), field.Invalid(field.NewPath("spec", "devices", "constraints").Index(0).Child("requests").Index(1), badName, "must be the name of a request in the claim or the name of a request and a subrequest separated by '/'"), field.Invalid(field.NewPath("spec", "devices", "constraints").Index(0).Child("matchAttribute"), "missing-domain", "a valid C identifier must start with alphabetic character or '_', followed by a string of alphanumeric characters or '_' (e.g. 'my_name', or 'MY_NAME', or 'MyName', regex used for validation is '[A-Za-z_][A-Za-z0-9_]*')").MarkCoveredByDeclarative(), - field.Invalid(field.NewPath("spec", "devices", "constraints").Index(0).Child("matchAttribute"), resource.FullyQualifiedName("missing-domain"), "must include a domain").MarkCoveredByDeclarative(), - field.Required(field.NewPath("spec", "devices", "constraints").Index(1).Child("matchAttribute"), "name required").MarkCoveredByDeclarative(), + field.Invalid(field.NewPath("spec", "devices", "constraints").Index(0).Child("matchAttribute"), resource.FullyQualifiedName("missing-domain"), "a fully qualified name must be a domain and a name separated by a slash").MarkCoveredByDeclarative(), + field.Invalid(field.NewPath("spec", "devices", "constraints").Index(1).Child("matchAttribute"), "", "a valid C identifier must start with alphabetic character or '_', followed by a string of alphanumeric characters or '_' (e.g. 'my_name', or 'MY_NAME', or 'MyName', regex used for validation is '[A-Za-z_][A-Za-z0-9_]*')").MarkCoveredByDeclarative(), + field.Invalid(field.NewPath("spec", "devices", "constraints").Index(1).Child("matchAttribute"), resource.FullyQualifiedName(""), "a fully qualified name must be a domain and a name separated by a slash").MarkCoveredByDeclarative(), field.Required(field.NewPath("spec", "devices", "constraints").Index(2).Child("matchAttribute"), ""), field.Invalid(field.NewPath("spec", "devices", "config").Index(0).Child("requests").Index(1), badName, "a lowercase RFC 1123 label must consist of lower case alphanumeric characters or '-', and must start and end with an alphanumeric character (e.g. 'my-name', or '123-abc', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?')"), field.Invalid(field.NewPath("spec", "devices", "config").Index(0).Child("requests").Index(1), badName, "must be the name of a request in the claim or the name of a request and a subrequest separated by '/'"), diff --git a/pkg/registry/resource/resourceclaim/declarative_validation_test.go b/pkg/registry/resource/resourceclaim/declarative_validation_test.go index 6a53294e70c..ce39ea5d968 100644 --- a/pkg/registry/resource/resourceclaim/declarative_validation_test.go +++ b/pkg/registry/resource/resourceclaim/declarative_validation_test.go @@ -406,7 +406,7 @@ func testDeclarativeValidate(t *testing.T, apiVersion string) { tweakMatchAttribute("nodomain"), ), expectedErrs: field.ErrorList{ - field.Invalid(field.NewPath("spec", "devices", "constraints").Index(0).Child("matchAttribute"), "nodomain", "must include a domain").WithOrigin("format=k8s-resource-fully-qualified-name"), + field.Invalid(field.NewPath("spec", "devices", "constraints").Index(0).Child("matchAttribute"), "nodomain", "a fully qualified name must be a domain and a name separated by a slash").WithOrigin("format=k8s-resource-fully-qualified-name"), }, }, "match attribute empty": { @@ -414,7 +414,7 @@ func testDeclarativeValidate(t *testing.T, apiVersion string) { tweakMatchAttribute(""), ), expectedErrs: field.ErrorList{ - field.Required(field.NewPath("spec", "devices", "constraints").Index(0).Child("matchAttribute"), "name required"), + field.Invalid(field.NewPath("spec", "devices", "constraints").Index(0).Child("matchAttribute"), "", "").WithOrigin("format=k8s-resource-fully-qualified-name"), }, }, "match attribute with empty domain": { diff --git a/staging/src/k8s.io/api/resource/v1/types.go b/staging/src/k8s.io/api/resource/v1/types.go index 53d261c4344..f698be61969 100644 --- a/staging/src/k8s.io/api/resource/v1/types.go +++ b/staging/src/k8s.io/api/resource/v1/types.go @@ -568,6 +568,9 @@ const ResourceSliceMaxDeviceCountersPerSlice = 1024 // 64 * 16 type QualifiedName string // FullyQualifiedName is a QualifiedName where the domain is set. +// Format validation cannot be added to this type because one of its usages, +// DistinctAttribute, is validated conditionally. This conditional validation +// cannot be expressed declaratively. type FullyQualifiedName string // DeviceMaxDomainLength is the maximum length of the domain prefix in a fully-qualified name. diff --git a/staging/src/k8s.io/api/resource/v1beta1/types.go b/staging/src/k8s.io/api/resource/v1beta1/types.go index a1ea50f354d..e63de0d1bcc 100644 --- a/staging/src/k8s.io/api/resource/v1beta1/types.go +++ b/staging/src/k8s.io/api/resource/v1beta1/types.go @@ -572,6 +572,9 @@ const ResourceSliceMaxDeviceCountersPerSlice = 1024 // 64 * 16 type QualifiedName string // FullyQualifiedName is a QualifiedName where the domain is set. +// Format validation cannot be added to this type because one of its usages, +// DistinctAttribute, is validated conditionally. This conditional validation +// cannot be expressed declaratively. type FullyQualifiedName string // DeviceMaxDomainLength is the maximum length of the domain prefix in a fully-qualified name. diff --git a/staging/src/k8s.io/api/resource/v1beta2/types.go b/staging/src/k8s.io/api/resource/v1beta2/types.go index 2bb5a70d22d..c3e06f3edb5 100644 --- a/staging/src/k8s.io/api/resource/v1beta2/types.go +++ b/staging/src/k8s.io/api/resource/v1beta2/types.go @@ -568,6 +568,9 @@ const ResourceSliceMaxDeviceCountersPerSlice = 1024 // 64 * 16 type QualifiedName string // FullyQualifiedName is a QualifiedName where the domain is set. +// Format validation cannot be added to this type because one of its usages, +// DistinctAttribute, is validated conditionally. This conditional validation +// cannot be expressed declaratively. type FullyQualifiedName string // DeviceMaxDomainLength is the maximum length of the domain prefix in a fully-qualified name. diff --git a/staging/src/k8s.io/apimachinery/pkg/api/validate/content/identifier.go b/staging/src/k8s.io/apimachinery/pkg/api/validate/content/identifier.go index eb1b25d5ca9..3913ec99163 100644 --- a/staging/src/k8s.io/apimachinery/pkg/api/validate/content/identifier.go +++ b/staging/src/k8s.io/apimachinery/pkg/api/validate/content/identifier.go @@ -1,3 +1,19 @@ +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + package content import ( diff --git a/staging/src/k8s.io/apimachinery/pkg/api/validate/content/identifier_test.go b/staging/src/k8s.io/apimachinery/pkg/api/validate/content/identifier_test.go index 036137c232c..e5c1bf84ab2 100644 --- a/staging/src/k8s.io/apimachinery/pkg/api/validate/content/identifier_test.go +++ b/staging/src/k8s.io/apimachinery/pkg/api/validate/content/identifier_test.go @@ -1,3 +1,19 @@ +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + package content import ( diff --git a/staging/src/k8s.io/apimachinery/pkg/api/validate/strfmt.go b/staging/src/k8s.io/apimachinery/pkg/api/validate/strfmt.go index 9da8e3ad95a..9a21473055f 100644 --- a/staging/src/k8s.io/apimachinery/pkg/api/validate/strfmt.go +++ b/staging/src/k8s.io/apimachinery/pkg/api/validate/strfmt.go @@ -23,7 +23,6 @@ import ( "k8s.io/apimachinery/pkg/api/operation" "k8s.io/apimachinery/pkg/api/validate/content" - "k8s.io/apimachinery/pkg/util/validation" "k8s.io/apimachinery/pkg/util/validation/field" ) @@ -32,7 +31,7 @@ const ( defaultResourceRequestsPrefix = "requests." // Default namespace prefix. resourceDefaultNamespacePrefix = "kubernetes.io/" - cDeviceMaxLength = 32 + resourceDeviceMaxLength = 32 ) // ShortName verifies that the specified value is a valid "short name" @@ -234,15 +233,13 @@ func resourcesQualifiedName[T ~string](ctx context.Context, op operation.Operati } var allErrs field.ErrorList s := string(*value) - if len(s) == 0 { - allErrs = append(allErrs, field.Required(fldPath, "name required")) - return allErrs - } - parts := strings.Split(s, "/") + // TODO: This validation and the corresponding handwritten validation validateQualifiedName in + // pkg/apis/resource/validation/validation.go are not validating whether there are more than 1 + // slash. This should be fixed in both places. switch len(parts) { case 1: - allErrs = append(allErrs, validateCIdentifier(parts[0], fldPath)...) + allErrs = append(allErrs, validateCIdentifier(parts[0], resourceDeviceMaxLength, fldPath)...) case 2: if len(parts[0]) == 0 { allErrs = append(allErrs, field.Invalid(fldPath, "", "prefix must not be empty")) @@ -255,11 +252,8 @@ func resourcesQualifiedName[T ~string](ctx context.Context, op operation.Operati if len(parts[1]) == 0 { allErrs = append(allErrs, field.Invalid(fldPath, "", "name must not be empty")) } else { - allErrs = append(allErrs, validateCIdentifier(parts[1], fldPath)...) + allErrs = append(allErrs, validateCIdentifier(parts[1], resourceDeviceMaxLength, fldPath)...) } - // TODO: This validation and the corresponding handwritten validation validateQualifiedName in - // pkg/apis/resource/validation/validation.go are not validating whether there are more than 1 - // slash. This should be fixed in both places. } return allErrs } @@ -276,25 +270,20 @@ func ResourceFullyQualifiedName[T ~string](ctx context.Context, op operation.Ope return nil } var allErrs field.ErrorList - if len(*value) == 0 { - allErrs = append(allErrs, field.Required(fldPath, "name can't be empty")) - return allErrs - } - s := string(*value) - allErrs = append(allErrs, resourcesQualifiedName(ctx, op, fldPath, value, nil)...) + allErrs = append(allErrs, resourcesQualifiedName(ctx, op, fldPath, &s, nil)...) if !strings.Contains(s, "/") { - allErrs = append(allErrs, field.Invalid(fldPath, s, "a fully qualified name must be a prefix and a name separated by a slash")) + allErrs = append(allErrs, field.Invalid(fldPath, s, "a fully qualified name must be a domain and a name separated by a slash")) } return allErrs.WithOrigin("format=k8s-resource-fully-qualified-name") } -func validateCIdentifier(id string, fldPath *field.Path) field.ErrorList { +func validateCIdentifier(id string, length int, fldPath *field.Path) field.ErrorList { var allErrs field.ErrorList - if len(id) > cDeviceMaxLength { - allErrs = append(allErrs, field.TooLong(fldPath, id, cDeviceMaxLength)) + if len(id) > length { + allErrs = append(allErrs, field.TooLong(fldPath, id, length)) } - for _, msg := range validation.IsCIdentifier(id) { + for _, msg := range content.IsCIdentifier(id) { allErrs = append(allErrs, field.Invalid(fldPath, id, msg)) } return allErrs diff --git a/staging/src/k8s.io/apimachinery/pkg/api/validate/strfmt_test.go b/staging/src/k8s.io/apimachinery/pkg/api/validate/strfmt_test.go index a6d81514b73..a3c20926530 100644 --- a/staging/src/k8s.io/apimachinery/pkg/api/validate/strfmt_test.go +++ b/staging/src/k8s.io/apimachinery/pkg/api/validate/strfmt_test.go @@ -680,25 +680,26 @@ func TestResourceFullyQualifiedName(t *testing.T) { name: "invalid name with dots", input: "prefix.com/name.with.dots", wantErrs: field.ErrorList{ - field.Invalid(fldPath, "name.with.dots", "a valid C identifier must start with alphabetic character or '_', followed by a string of alphanumeric characters or '_' (e.g. 'my_name', or 'MY_NAME', or 'MyName', regex used for validation is '[A-Za-z_][A-Za-z0-9_]*')").WithOrigin("format=k8s-resource-fully-qualified-name"), + field.Invalid(fldPath, "name.with.dots", "a valid C identifier must start with alphabetic character").WithOrigin("format=k8s-resource-fully-qualified-name"), }, }, { name: "invalid name with dashes", input: "prefix.com/name-with-dashes", wantErrs: field.ErrorList{ - field.Invalid(fldPath, "name-with-dashes", "a valid C identifier must start with alphabetic character or '_', followed by a string of alphanumeric characters or '_' (e.g. 'my_name', or 'MY_NAME', or 'MyName', regex used for validation is '[A-Za-z_][A-Za-z0-9_]*')").WithOrigin("format=k8s-resource-fully-qualified-name"), + field.Invalid(fldPath, "name-with-dashes", "a valid C identifier must start with alphabetic character").WithOrigin("format=k8s-resource-fully-qualified-name"), }, }, { name: "invalid: no prefix", input: "name", wantErrs: field.ErrorList{ - field.Invalid(fldPath, "name", "a fully qualified name must be a prefix and a name separated by a slash").WithOrigin("format=k8s-resource-fully-qualified-name"), + field.Invalid(fldPath, "name", "a fully qualified name must be a domain and a name separated by a slash").WithOrigin("format=k8s-resource-fully-qualified-name"), }, }, { name: "invalid: empty", input: "", wantErrs: field.ErrorList{ - field.Required(fldPath, "name can't be empty"), + field.Invalid(fldPath, "", "a fully qualified name must be a domain and a name separated by a slash").WithOrigin("format=k8s-resource-fully-qualified-name"), + field.Invalid(fldPath, "", "a valid C identifier must start with alphabetic character").WithOrigin("format=k8s-resource-fully-qualified-name"), }, }, { name: "invalid: prefix too long", @@ -717,7 +718,7 @@ func TestResourceFullyQualifiedName(t *testing.T) { name: "invalid: prefix is not a valid DNS subdomain", input: "Prefix.com/name", wantErrs: field.ErrorList{ - field.Invalid(fldPath, "Prefix.com", "prefix: a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character").WithOrigin("format=k8s-resource-fully-qualified-name"), + field.Invalid(fldPath, "Prefix.com", "prefix: a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters").WithOrigin("format=k8s-resource-fully-qualified-name"), }, }, { name: "invalid: name is not a valid RFC 1123 label", diff --git a/staging/src/k8s.io/apimachinery/pkg/util/validation/validation.go b/staging/src/k8s.io/apimachinery/pkg/util/validation/validation.go index 8e76c313757..95f78e92fa7 100644 --- a/staging/src/k8s.io/apimachinery/pkg/util/validation/validation.go +++ b/staging/src/k8s.io/apimachinery/pkg/util/validation/validation.go @@ -237,19 +237,10 @@ func IsWildcardDNS1123Subdomain(value string) []string { return errs } -const cIdentifierFmt string = "[A-Za-z_][A-Za-z0-9_]*" -const identifierErrMsg string = "a valid C identifier must start with alphabetic character or '_', followed by a string of alphanumeric characters or '_'" - -var cIdentifierRegexp = regexp.MustCompile("^" + cIdentifierFmt + "$") - // IsCIdentifier tests for a string that conforms the definition of an identifier // in C. This checks the format, but not the length. -func IsCIdentifier(value string) []string { - if !cIdentifierRegexp.MatchString(value) { - return []string{RegexError(identifierErrMsg, cIdentifierFmt, "my_name", "MY_NAME", "MyName")} - } - return nil -} +// Deprecated: Use k8s.io/apimachinery/pkg/api/validate/content.IsCIdentifier instead. +var IsCIdentifier = content.IsCIdentifier // IsValidPortNum tests that the argument is a valid, non-zero port number. func IsValidPortNum(port int) []string { diff --git a/staging/src/k8s.io/apimachinery/pkg/util/validation/validation_test.go b/staging/src/k8s.io/apimachinery/pkg/util/validation/validation_test.go index acfe2cb7687..bd01941228d 100644 --- a/staging/src/k8s.io/apimachinery/pkg/util/validation/validation_test.go +++ b/staging/src/k8s.io/apimachinery/pkg/util/validation/validation_test.go @@ -116,31 +116,6 @@ func TestIsDNS1035Label(t *testing.T) { } } -func TestIsCIdentifier(t *testing.T) { - goodValues := []string{ - "a", "ab", "abc", "a1", "_a", "a_", "a_b", "a_1", "a__1__2__b", "__abc_123", - "A", "AB", "AbC", "A1", "_A", "A_", "A_B", "A_1", "A__1__2__B", "__123_ABC", - } - for _, val := range goodValues { - if msgs := IsCIdentifier(val); len(msgs) != 0 { - t.Errorf("expected true for '%s': %v", val, msgs) - } - } - - badValues := []string{ - "", "1", "123", "1a", - "-", "a-", "-a", "1-", "-1", "1_", "1_2", - ".", "a.", ".a", "a.b", "1.", ".1", "1.2", - " ", "a ", " a", "a b", "1 ", " 1", "1 2", - "#a#", - } - for _, val := range badValues { - if msgs := IsCIdentifier(val); len(msgs) == 0 { - t.Errorf("expected false for '%s'", val) - } - } -} - func TestIsValidPortNum(t *testing.T) { goodValues := []int{1, 2, 1000, 16384, 32768, 65535} for _, val := range goodValues { diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/format/k8s-resource-fully-qualified-name/doc_test.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/format/k8s-resource-fully-qualified-name/doc_test.go index 67d1fb66c17..6d4768b74fa 100644 --- a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/format/k8s-resource-fully-qualified-name/doc_test.go +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/format/k8s-resource-fully-qualified-name/doc_test.go @@ -38,9 +38,9 @@ func TestFullyQualifiedName(t *testing.T) { FullyQualifiedNameTypedefField: "my-prefix/", } st.Value(invalidStruct).ExpectMatches(field.ErrorMatcher{}.ByType().ByOrigin().ByField(), field.ErrorList{ - field.Invalid(field.NewPath("fullyQualifiedNameField"), "my_name", "").WithOrigin("format=k8s-resource-fully-qualified-name"), - field.Required(field.NewPath("fullyQualifiedNamePtrField"), ""), - field.Invalid(field.NewPath("fullyQualifiedNameTypedefField"), "my-prefix/", "").WithOrigin("format=k8s-resource-fully-qualified-name"), + field.Invalid(field.NewPath("fullyQualifiedNameField"), "my_name", "a fully qualified name must be a domain and a name separated by a slash").WithOrigin("format=k8s-resource-fully-qualified-name"), + field.Invalid(field.NewPath("fullyQualifiedNamePtrField"), "", "a valid C identifier must start with alphabetic character or '_', followed by a string of alphanumeric characters or '_'").WithOrigin("format=k8s-resource-fully-qualified-name"), + field.Invalid(field.NewPath("fullyQualifiedNameTypedefField"), "my-prefix/", "name must not be empty").WithOrigin("format=k8s-resource-fully-qualified-name"), }) // Test validation ratcheting st.Value(invalidStruct).OldValue(invalidStruct).ExpectValid() diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/format.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/format.go index d35294f0f2f..54322e05626 100644 --- a/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/format.go +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/format.go @@ -143,7 +143,7 @@ func (ftv formatTagValidator) Docs() TagDoc { Docs: "Deprecated: This field holds a case-insensitive Kubernetes \"long name\", aka a \"DNS subdomain\" value.", }, { Description: "k8s-resource-fully-qualified-name", - Docs: "This field holds a Kubernetes resource \"fully qualified name\" value.", + Docs: "This field holds a Kubernetes resource \"fully qualified name\" value. A fully qualified name must not be empty and must be composed of a prefix and a name, separated by a slash (e.g., \"prefix/name\"). The prefix must be a DNS subdomain, and the name part must be a C identifier with no more than 32 characters.", }, { Description: "k8s-resource-pool-name", Docs: "This field holds value with one or more Kubernetes \"long name\" parts separated by `/` and no longer than 253 characters.",