mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-30 15:05:27 +00:00
Merge pull request #130355 from yongruilin/validation_origin
validation: Add Origin field to field.Error for more precise error tracking
This commit is contained in:
commit
803e9d6495
@ -133,7 +133,7 @@ func ValidateAnnotations(annotations map[string]string, fldPath *field.Path) fie
|
||||
func ValidateDNS1123Label(value string, fldPath *field.Path) field.ErrorList {
|
||||
allErrs := field.ErrorList{}
|
||||
for _, msg := range validation.IsDNS1123Label(value) {
|
||||
allErrs = append(allErrs, field.Invalid(fldPath, value, msg))
|
||||
allErrs = append(allErrs, field.Invalid(fldPath, value, msg).WithOrigin("format=dns-label"))
|
||||
}
|
||||
return allErrs
|
||||
}
|
||||
@ -142,7 +142,7 @@ func ValidateDNS1123Label(value string, fldPath *field.Path) field.ErrorList {
|
||||
func ValidateQualifiedName(value string, fldPath *field.Path) field.ErrorList {
|
||||
allErrs := field.ErrorList{}
|
||||
for _, msg := range validation.IsQualifiedName(value) {
|
||||
allErrs = append(allErrs, field.Invalid(fldPath, value, msg))
|
||||
allErrs = append(allErrs, field.Invalid(fldPath, value, msg).WithOrigin("format=qualified-name"))
|
||||
}
|
||||
return allErrs
|
||||
}
|
||||
@ -6266,7 +6266,7 @@ func ValidateReplicationControllerSpec(spec, oldSpec *core.ReplicationController
|
||||
allErrs := field.ErrorList{}
|
||||
allErrs = append(allErrs, ValidateNonnegativeField(int64(spec.MinReadySeconds), fldPath.Child("minReadySeconds"))...)
|
||||
allErrs = append(allErrs, ValidateNonEmptySelector(spec.Selector, fldPath.Child("selector"))...)
|
||||
allErrs = append(allErrs, ValidateNonnegativeField(int64(spec.Replicas), fldPath.Child("replicas"))...)
|
||||
allErrs = append(allErrs, ValidateNonnegativeField(int64(spec.Replicas), fldPath.Child("replicas")).WithOrigin("minimum")...)
|
||||
allErrs = append(allErrs, ValidatePodTemplateSpecForRC(spec.Template, spec.Selector, spec.Replicas, fldPath.Child("template"), opts)...)
|
||||
return allErrs
|
||||
}
|
||||
@ -7466,7 +7466,7 @@ func validateEndpointAddress(address *core.EndpointAddress, fldPath *field.Path)
|
||||
// During endpoint update, verify that NodeName is a DNS subdomain and transition rules allow the update
|
||||
if address.NodeName != nil {
|
||||
for _, msg := range ValidateNodeName(*address.NodeName, false) {
|
||||
allErrs = append(allErrs, field.Invalid(fldPath.Child("nodeName"), *address.NodeName, msg))
|
||||
allErrs = append(allErrs, field.Invalid(fldPath.Child("nodeName"), *address.NodeName, msg).WithOrigin("format=dns-label"))
|
||||
}
|
||||
}
|
||||
allErrs = append(allErrs, ValidateNonSpecialIP(address.IP, fldPath.Child("ip"))...)
|
||||
@ -7485,20 +7485,20 @@ func ValidateNonSpecialIP(ipAddress string, fldPath *field.Path) field.ErrorList
|
||||
allErrs := field.ErrorList{}
|
||||
ip := netutils.ParseIPSloppy(ipAddress)
|
||||
if ip == nil {
|
||||
allErrs = append(allErrs, field.Invalid(fldPath, ipAddress, "must be a valid IP address"))
|
||||
allErrs = append(allErrs, field.Invalid(fldPath, ipAddress, "must be a valid IP address").WithOrigin("format=ip-sloppy"))
|
||||
return allErrs
|
||||
}
|
||||
if ip.IsUnspecified() {
|
||||
allErrs = append(allErrs, field.Invalid(fldPath, ipAddress, fmt.Sprintf("may not be unspecified (%v)", ipAddress)))
|
||||
allErrs = append(allErrs, field.Invalid(fldPath, ipAddress, fmt.Sprintf("may not be unspecified (%v)", ipAddress)).WithOrigin("format=non-special-ip"))
|
||||
}
|
||||
if ip.IsLoopback() {
|
||||
allErrs = append(allErrs, field.Invalid(fldPath, ipAddress, "may not be in the loopback range (127.0.0.0/8, ::1/128)"))
|
||||
allErrs = append(allErrs, field.Invalid(fldPath, ipAddress, "may not be in the loopback range (127.0.0.0/8, ::1/128)").WithOrigin("format=non-special-ip"))
|
||||
}
|
||||
if ip.IsLinkLocalUnicast() {
|
||||
allErrs = append(allErrs, field.Invalid(fldPath, ipAddress, "may not be in the link-local range (169.254.0.0/16, fe80::/10)"))
|
||||
allErrs = append(allErrs, field.Invalid(fldPath, ipAddress, "may not be in the link-local range (169.254.0.0/16, fe80::/10)").WithOrigin("format=non-special-ip"))
|
||||
}
|
||||
if ip.IsLinkLocalMulticast() {
|
||||
allErrs = append(allErrs, field.Invalid(fldPath, ipAddress, "may not be in the link-local multicast range (224.0.0.0/24, ff02::/10)"))
|
||||
allErrs = append(allErrs, field.Invalid(fldPath, ipAddress, "may not be in the link-local multicast range (224.0.0.0/24, ff02::/10)").WithOrigin("format=non-special-ip"))
|
||||
}
|
||||
return allErrs
|
||||
}
|
||||
@ -7511,7 +7511,7 @@ func validateEndpointPort(port *core.EndpointPort, requireName bool, fldPath *fi
|
||||
allErrs = append(allErrs, ValidateDNS1123Label(port.Name, fldPath.Child("name"))...)
|
||||
}
|
||||
for _, msg := range validation.IsValidPortNum(int(port.Port)) {
|
||||
allErrs = append(allErrs, field.Invalid(fldPath.Child("port"), port.Port, msg))
|
||||
allErrs = append(allErrs, field.Invalid(fldPath.Child("port"), port.Port, msg).WithOrigin("portNum"))
|
||||
}
|
||||
if len(port.Protocol) == 0 {
|
||||
allErrs = append(allErrs, field.Required(fldPath.Child("protocol"), ""))
|
||||
|
@ -9183,7 +9183,7 @@ func TestValidateContainers(t *testing.T) {
|
||||
t.Fatal("expected error but received none")
|
||||
}
|
||||
|
||||
if diff := cmp.Diff(tc.expectedErrors, errs, cmpopts.IgnoreFields(field.Error{}, "BadValue", "Detail")); diff != "" {
|
||||
if diff := cmp.Diff(tc.expectedErrors, errs, cmpopts.IgnoreFields(field.Error{}, "BadValue", "Detail", "Origin")); diff != "" {
|
||||
t.Errorf("unexpected diff in errors (-want, +got):\n%s", diff)
|
||||
t.Errorf("INFO: all errors:\n%s", prettyErrorList(errs))
|
||||
}
|
||||
@ -16791,144 +16791,179 @@ func TestValidateReplicationController(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
errorCases := map[string]core.ReplicationController{
|
||||
errorCases := map[string]struct {
|
||||
rc core.ReplicationController
|
||||
expectedOrigin []string
|
||||
}{
|
||||
"zero-length ID": {
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "", Namespace: metav1.NamespaceDefault},
|
||||
Spec: core.ReplicationControllerSpec{
|
||||
Selector: validSelector,
|
||||
Template: &validPodTemplate.Template,
|
||||
rc: core.ReplicationController{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "", Namespace: metav1.NamespaceDefault},
|
||||
Spec: core.ReplicationControllerSpec{
|
||||
Selector: validSelector,
|
||||
Template: &validPodTemplate.Template,
|
||||
},
|
||||
},
|
||||
},
|
||||
"missing-namespace": {
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "abc-123"},
|
||||
Spec: core.ReplicationControllerSpec{
|
||||
Selector: validSelector,
|
||||
Template: &validPodTemplate.Template,
|
||||
rc: core.ReplicationController{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "abc-123"},
|
||||
Spec: core.ReplicationControllerSpec{
|
||||
Selector: validSelector,
|
||||
Template: &validPodTemplate.Template,
|
||||
},
|
||||
},
|
||||
},
|
||||
"empty selector": {
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault},
|
||||
Spec: core.ReplicationControllerSpec{
|
||||
Template: &validPodTemplate.Template,
|
||||
rc: core.ReplicationController{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault},
|
||||
Spec: core.ReplicationControllerSpec{
|
||||
Template: &validPodTemplate.Template,
|
||||
},
|
||||
},
|
||||
},
|
||||
"selector_doesnt_match": {
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault},
|
||||
Spec: core.ReplicationControllerSpec{
|
||||
Selector: map[string]string{"foo": "bar"},
|
||||
Template: &validPodTemplate.Template,
|
||||
rc: core.ReplicationController{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault},
|
||||
Spec: core.ReplicationControllerSpec{
|
||||
Selector: map[string]string{"foo": "bar"},
|
||||
Template: &validPodTemplate.Template,
|
||||
},
|
||||
},
|
||||
},
|
||||
"invalid manifest": {
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault},
|
||||
Spec: core.ReplicationControllerSpec{
|
||||
Selector: validSelector,
|
||||
rc: core.ReplicationController{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault},
|
||||
Spec: core.ReplicationControllerSpec{
|
||||
Selector: validSelector,
|
||||
},
|
||||
},
|
||||
},
|
||||
"read-write persistent disk with > 1 pod": {
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "abc"},
|
||||
Spec: core.ReplicationControllerSpec{
|
||||
Replicas: 2,
|
||||
Selector: validSelector,
|
||||
Template: &readWriteVolumePodTemplate.Template,
|
||||
rc: core.ReplicationController{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "abc"},
|
||||
Spec: core.ReplicationControllerSpec{
|
||||
Replicas: 2,
|
||||
Selector: validSelector,
|
||||
Template: &readWriteVolumePodTemplate.Template,
|
||||
},
|
||||
},
|
||||
},
|
||||
"negative_replicas": {
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault},
|
||||
Spec: core.ReplicationControllerSpec{
|
||||
Replicas: -1,
|
||||
Selector: validSelector,
|
||||
rc: core.ReplicationController{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault},
|
||||
Spec: core.ReplicationControllerSpec{
|
||||
Replicas: -1,
|
||||
Selector: validSelector,
|
||||
},
|
||||
},
|
||||
expectedOrigin: []string{
|
||||
"minimum",
|
||||
},
|
||||
},
|
||||
"invalid_label": {
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "abc-123",
|
||||
Namespace: metav1.NamespaceDefault,
|
||||
Labels: map[string]string{
|
||||
"NoUppercaseOrSpecialCharsLike=Equals": "bar",
|
||||
rc: core.ReplicationController{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "abc-123",
|
||||
Namespace: metav1.NamespaceDefault,
|
||||
Labels: map[string]string{
|
||||
"NoUppercaseOrSpecialCharsLike=Equals": "bar",
|
||||
},
|
||||
},
|
||||
Spec: core.ReplicationControllerSpec{
|
||||
Selector: validSelector,
|
||||
Template: &validPodTemplate.Template,
|
||||
},
|
||||
},
|
||||
Spec: core.ReplicationControllerSpec{
|
||||
Selector: validSelector,
|
||||
Template: &validPodTemplate.Template,
|
||||
},
|
||||
},
|
||||
"invalid_label 2": {
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "abc-123",
|
||||
Namespace: metav1.NamespaceDefault,
|
||||
Labels: map[string]string{
|
||||
"NoUppercaseOrSpecialCharsLike=Equals": "bar",
|
||||
rc: core.ReplicationController{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "abc-123",
|
||||
Namespace: metav1.NamespaceDefault,
|
||||
Labels: map[string]string{
|
||||
"NoUppercaseOrSpecialCharsLike=Equals": "bar",
|
||||
},
|
||||
},
|
||||
Spec: core.ReplicationControllerSpec{
|
||||
Template: &invalidPodTemplate.Template,
|
||||
},
|
||||
},
|
||||
Spec: core.ReplicationControllerSpec{
|
||||
Template: &invalidPodTemplate.Template,
|
||||
},
|
||||
},
|
||||
"invalid_annotation": {
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "abc-123",
|
||||
Namespace: metav1.NamespaceDefault,
|
||||
Annotations: map[string]string{
|
||||
"NoUppercaseOrSpecialCharsLike=Equals": "bar",
|
||||
rc: core.ReplicationController{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "abc-123",
|
||||
Namespace: metav1.NamespaceDefault,
|
||||
Annotations: map[string]string{
|
||||
"NoUppercaseOrSpecialCharsLike=Equals": "bar",
|
||||
},
|
||||
},
|
||||
Spec: core.ReplicationControllerSpec{
|
||||
Selector: validSelector,
|
||||
Template: &validPodTemplate.Template,
|
||||
},
|
||||
},
|
||||
Spec: core.ReplicationControllerSpec{
|
||||
Selector: validSelector,
|
||||
Template: &validPodTemplate.Template,
|
||||
},
|
||||
},
|
||||
"invalid restart policy 1": {
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "abc-123",
|
||||
Namespace: metav1.NamespaceDefault,
|
||||
},
|
||||
Spec: core.ReplicationControllerSpec{
|
||||
Selector: validSelector,
|
||||
Template: &core.PodTemplateSpec{
|
||||
Spec: podtest.MakePodSpec(podtest.SetRestartPolicy(core.RestartPolicyOnFailure)),
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: validSelector,
|
||||
rc: core.ReplicationController{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "abc-123",
|
||||
Namespace: metav1.NamespaceDefault,
|
||||
},
|
||||
Spec: core.ReplicationControllerSpec{
|
||||
Selector: validSelector,
|
||||
Template: &core.PodTemplateSpec{
|
||||
Spec: podtest.MakePodSpec(podtest.SetRestartPolicy(core.RestartPolicyOnFailure)),
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: validSelector,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"invalid restart policy 2": {
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "abc-123",
|
||||
Namespace: metav1.NamespaceDefault,
|
||||
},
|
||||
Spec: core.ReplicationControllerSpec{
|
||||
Selector: validSelector,
|
||||
Template: &core.PodTemplateSpec{
|
||||
Spec: podtest.MakePodSpec(podtest.SetRestartPolicy(core.RestartPolicyNever)),
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: validSelector,
|
||||
rc: core.ReplicationController{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "abc-123",
|
||||
Namespace: metav1.NamespaceDefault,
|
||||
},
|
||||
Spec: core.ReplicationControllerSpec{
|
||||
Selector: validSelector,
|
||||
Template: &core.PodTemplateSpec{
|
||||
Spec: podtest.MakePodSpec(podtest.SetRestartPolicy(core.RestartPolicyNever)),
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: validSelector,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"template may not contain ephemeral containers": {
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "abc-123", Namespace: metav1.NamespaceDefault},
|
||||
Spec: core.ReplicationControllerSpec{
|
||||
Replicas: 1,
|
||||
Selector: validSelector,
|
||||
Template: &core.PodTemplateSpec{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: validSelector,
|
||||
rc: core.ReplicationController{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "abc-123", Namespace: metav1.NamespaceDefault},
|
||||
Spec: core.ReplicationControllerSpec{
|
||||
Replicas: 1,
|
||||
Selector: validSelector,
|
||||
Template: &core.PodTemplateSpec{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: validSelector,
|
||||
},
|
||||
Spec: podtest.MakePodSpec(
|
||||
podtest.SetEphemeralContainers(core.EphemeralContainer{EphemeralContainerCommon: core.EphemeralContainerCommon{Name: "debug", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}),
|
||||
),
|
||||
},
|
||||
Spec: podtest.MakePodSpec(
|
||||
podtest.SetEphemeralContainers(core.EphemeralContainer{EphemeralContainerCommon: core.EphemeralContainerCommon{Name: "debug", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}),
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for k, v := range errorCases {
|
||||
errs := ValidateReplicationController(&v, PodValidationOptions{})
|
||||
errs := ValidateReplicationController(&v.rc, PodValidationOptions{})
|
||||
if len(errs) == 0 {
|
||||
t.Errorf("expected failure for %s", k)
|
||||
}
|
||||
|
||||
expectedOrigins := sets.NewString(v.expectedOrigin...)
|
||||
|
||||
for i := range errs {
|
||||
field := errs[i].Field
|
||||
if !strings.HasPrefix(field, "spec.template.") &&
|
||||
@ -16944,6 +16979,16 @@ func TestValidateReplicationController(t *testing.T) {
|
||||
field != "status.replicas" {
|
||||
t.Errorf("%s: missing prefix for: %v", k, errs[i])
|
||||
}
|
||||
|
||||
if len(v.expectedOrigin) > 0 && errs[i].Origin != "" {
|
||||
if !expectedOrigins.Has(errs[i].Origin) {
|
||||
t.Errorf("%s: unexpected origin for: %v, expected one of %v", k, errs[i].Origin, v.expectedOrigin)
|
||||
}
|
||||
expectedOrigins.Delete(errs[i].Origin)
|
||||
}
|
||||
}
|
||||
if len(expectedOrigins) > 0 {
|
||||
t.Errorf("%s: missing errors with origin: %v", k, expectedOrigins.List())
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -20674,7 +20719,7 @@ func TestValidateEndpointsCreate(t *testing.T) {
|
||||
errorCases := map[string]struct {
|
||||
endpoints core.Endpoints
|
||||
errorType field.ErrorType
|
||||
errorDetail string
|
||||
errorOrigin string
|
||||
}{
|
||||
"missing namespace": {
|
||||
endpoints: core.Endpoints{ObjectMeta: metav1.ObjectMeta{Name: "mysvc"}},
|
||||
@ -20685,14 +20730,12 @@ func TestValidateEndpointsCreate(t *testing.T) {
|
||||
errorType: "FieldValueRequired",
|
||||
},
|
||||
"invalid namespace": {
|
||||
endpoints: core.Endpoints{ObjectMeta: metav1.ObjectMeta{Name: "mysvc", Namespace: "no@#invalid.;chars\"allowed"}},
|
||||
errorType: "FieldValueInvalid",
|
||||
errorDetail: dnsLabelErrMsg,
|
||||
endpoints: core.Endpoints{ObjectMeta: metav1.ObjectMeta{Name: "mysvc", Namespace: "no@#invalid.;chars\"allowed"}},
|
||||
errorType: "FieldValueInvalid",
|
||||
},
|
||||
"invalid name": {
|
||||
endpoints: core.Endpoints{ObjectMeta: metav1.ObjectMeta{Name: "-_Invliad^&Characters", Namespace: "namespace"}},
|
||||
errorType: "FieldValueInvalid",
|
||||
errorDetail: dnsSubdomainLabelErrMsg,
|
||||
endpoints: core.Endpoints{ObjectMeta: metav1.ObjectMeta{Name: "-_Invliad^&Characters", Namespace: "namespace"}},
|
||||
errorType: "FieldValueInvalid",
|
||||
},
|
||||
"empty addresses": {
|
||||
endpoints: core.Endpoints{
|
||||
@ -20712,7 +20755,7 @@ func TestValidateEndpointsCreate(t *testing.T) {
|
||||
}},
|
||||
},
|
||||
errorType: "FieldValueInvalid",
|
||||
errorDetail: "must be a valid IP address",
|
||||
errorOrigin: "format=ip-sloppy",
|
||||
},
|
||||
"Multiple ports, one without name": {
|
||||
endpoints: core.Endpoints{
|
||||
@ -20733,7 +20776,7 @@ func TestValidateEndpointsCreate(t *testing.T) {
|
||||
}},
|
||||
},
|
||||
errorType: "FieldValueInvalid",
|
||||
errorDetail: "between",
|
||||
errorOrigin: "portNum",
|
||||
},
|
||||
"Invalid protocol": {
|
||||
endpoints: core.Endpoints{
|
||||
@ -20754,7 +20797,7 @@ func TestValidateEndpointsCreate(t *testing.T) {
|
||||
}},
|
||||
},
|
||||
errorType: "FieldValueInvalid",
|
||||
errorDetail: "must be a valid IP address",
|
||||
errorOrigin: "format=ip-sloppy",
|
||||
},
|
||||
"Port missing number": {
|
||||
endpoints: core.Endpoints{
|
||||
@ -20765,7 +20808,7 @@ func TestValidateEndpointsCreate(t *testing.T) {
|
||||
}},
|
||||
},
|
||||
errorType: "FieldValueInvalid",
|
||||
errorDetail: "between",
|
||||
errorOrigin: "portNum",
|
||||
},
|
||||
"Port missing protocol": {
|
||||
endpoints: core.Endpoints{
|
||||
@ -20786,7 +20829,7 @@ func TestValidateEndpointsCreate(t *testing.T) {
|
||||
}},
|
||||
},
|
||||
errorType: "FieldValueInvalid",
|
||||
errorDetail: "loopback",
|
||||
errorOrigin: "format=non-special-ip",
|
||||
},
|
||||
"Address is link-local": {
|
||||
endpoints: core.Endpoints{
|
||||
@ -20797,7 +20840,7 @@ func TestValidateEndpointsCreate(t *testing.T) {
|
||||
}},
|
||||
},
|
||||
errorType: "FieldValueInvalid",
|
||||
errorDetail: "link-local",
|
||||
errorOrigin: "format=non-special-ip",
|
||||
},
|
||||
"Address is link-local multicast": {
|
||||
endpoints: core.Endpoints{
|
||||
@ -20808,7 +20851,7 @@ func TestValidateEndpointsCreate(t *testing.T) {
|
||||
}},
|
||||
},
|
||||
errorType: "FieldValueInvalid",
|
||||
errorDetail: "link-local multicast",
|
||||
errorOrigin: "format=non-special-ip",
|
||||
},
|
||||
"Invalid AppProtocol": {
|
||||
endpoints: core.Endpoints{
|
||||
@ -20819,14 +20862,14 @@ func TestValidateEndpointsCreate(t *testing.T) {
|
||||
}},
|
||||
},
|
||||
errorType: "FieldValueInvalid",
|
||||
errorDetail: "name part must consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character",
|
||||
errorOrigin: "format=qualified-name",
|
||||
},
|
||||
}
|
||||
|
||||
for k, v := range errorCases {
|
||||
t.Run(k, func(t *testing.T) {
|
||||
if errs := ValidateEndpointsCreate(&v.endpoints); len(errs) == 0 || errs[0].Type != v.errorType || !strings.Contains(errs[0].Detail, v.errorDetail) {
|
||||
t.Errorf("Expected error type %s with detail %q, got %v", v.errorType, v.errorDetail, errs)
|
||||
if errs := ValidateEndpointsCreate(&v.endpoints); len(errs) == 0 || errs[0].Type != v.errorType || errs[0].Origin != v.errorOrigin {
|
||||
t.Errorf("Expected error type %s with origin %q, got %#v", v.errorType, v.errorOrigin, errs[0])
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -21190,7 +21233,7 @@ func TestValidateSchedulingGates(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
errs := validateSchedulingGates(tt.schedulingGates, fieldPath)
|
||||
if diff := cmp.Diff(tt.wantFieldErrors, errs); diff != "" {
|
||||
if diff := cmp.Diff(tt.wantFieldErrors, errs, cmpopts.IgnoreFields(field.Error{}, "Detail", "Origin")); diff != "" {
|
||||
t.Errorf("unexpected field errors (-want, +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
|
@ -19,9 +19,12 @@ package validation
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
"k8s.io/dynamic-resource-allocation/api"
|
||||
)
|
||||
|
||||
// assertFailures compares the expected against the actual errors.
|
||||
@ -31,22 +34,13 @@ import (
|
||||
// is informative.
|
||||
func assertFailures(tb testing.TB, want, got field.ErrorList) bool {
|
||||
tb.Helper()
|
||||
if !assert.Equal(tb, want, got) {
|
||||
logFailures(tb, "Wanted failures", want)
|
||||
logFailures(tb, "Got failures", got)
|
||||
if diff := cmp.Diff(want, got, cmpopts.IgnoreFields(field.Error{}, "Origin"), cmp.AllowUnexported(api.UniqueString{})); diff != "" {
|
||||
tb.Errorf("unexpected field errors (-want, +got):\n%s", diff)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func logFailures(tb testing.TB, header string, errs field.ErrorList) {
|
||||
tb.Helper()
|
||||
tb.Logf("%s:\n", header)
|
||||
for _, err := range errs {
|
||||
tb.Logf("- %s\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTruncateIfTooLong(t *testing.T) {
|
||||
for name, tc := range map[string]struct {
|
||||
str string
|
||||
|
@ -33,6 +33,20 @@ type Error struct {
|
||||
Field string
|
||||
BadValue interface{}
|
||||
Detail string
|
||||
|
||||
// Origin uniquely identifies where this error was generated from. It is used in testing to
|
||||
// compare expected errors against actual errors without relying on exact detail string matching.
|
||||
// This allows tests to verify the correct validation logic triggered the error
|
||||
// regardless of how the error message might be formatted or localized.
|
||||
//
|
||||
// The value should be either:
|
||||
// - A simple camelCase identifier (e.g., "maximum", "maxItems")
|
||||
// - A structured format using "format=<dash-style-identifier>" for validation errors related to specific formats
|
||||
// (e.g., "format=dns-label", "format=qualified-name")
|
||||
//
|
||||
// Origin should be set in the most deeply nested validation function that
|
||||
// can still identify the unique source of the error.
|
||||
Origin string
|
||||
}
|
||||
|
||||
var _ error = &Error{}
|
||||
@ -96,6 +110,12 @@ func (v *Error) ErrorBody() string {
|
||||
return s
|
||||
}
|
||||
|
||||
// WithOrigin adds origin information to the FieldError
|
||||
func (v *Error) WithOrigin(o string) *Error {
|
||||
v.Origin = o
|
||||
return v
|
||||
}
|
||||
|
||||
// ErrorType is a machine readable value providing more detail about why
|
||||
// a field is invalid. These values are expected to match 1-1 with
|
||||
// CauseType in api/types.go.
|
||||
@ -169,32 +189,32 @@ func (t ErrorType) String() string {
|
||||
|
||||
// TypeInvalid returns a *Error indicating "type is invalid"
|
||||
func TypeInvalid(field *Path, value interface{}, detail string) *Error {
|
||||
return &Error{ErrorTypeTypeInvalid, field.String(), value, detail}
|
||||
return &Error{ErrorTypeTypeInvalid, field.String(), value, detail, ""}
|
||||
}
|
||||
|
||||
// NotFound returns a *Error indicating "value not found". This is
|
||||
// used to report failure to find a requested value (e.g. looking up an ID).
|
||||
func NotFound(field *Path, value interface{}) *Error {
|
||||
return &Error{ErrorTypeNotFound, field.String(), value, ""}
|
||||
return &Error{ErrorTypeNotFound, field.String(), value, "", ""}
|
||||
}
|
||||
|
||||
// Required returns a *Error indicating "value required". This is used
|
||||
// to report required values that are not provided (e.g. empty strings, null
|
||||
// values, or empty arrays).
|
||||
func Required(field *Path, detail string) *Error {
|
||||
return &Error{ErrorTypeRequired, field.String(), "", detail}
|
||||
return &Error{ErrorTypeRequired, field.String(), "", detail, ""}
|
||||
}
|
||||
|
||||
// Duplicate returns a *Error indicating "duplicate value". This is
|
||||
// used to report collisions of values that must be unique (e.g. names or IDs).
|
||||
func Duplicate(field *Path, value interface{}) *Error {
|
||||
return &Error{ErrorTypeDuplicate, field.String(), value, ""}
|
||||
return &Error{ErrorTypeDuplicate, field.String(), value, "", ""}
|
||||
}
|
||||
|
||||
// Invalid returns a *Error indicating "invalid value". This is used
|
||||
// to report malformed values (e.g. failed regex match, too long, out of bounds).
|
||||
func Invalid(field *Path, value interface{}, detail string) *Error {
|
||||
return &Error{ErrorTypeInvalid, field.String(), value, detail}
|
||||
return &Error{ErrorTypeInvalid, field.String(), value, detail, ""}
|
||||
}
|
||||
|
||||
// NotSupported returns a *Error indicating "unsupported value".
|
||||
@ -209,7 +229,7 @@ func NotSupported[T ~string](field *Path, value interface{}, validValues []T) *E
|
||||
}
|
||||
detail = "supported values: " + strings.Join(quotedValues, ", ")
|
||||
}
|
||||
return &Error{ErrorTypeNotSupported, field.String(), value, detail}
|
||||
return &Error{ErrorTypeNotSupported, field.String(), value, detail, ""}
|
||||
}
|
||||
|
||||
// Forbidden returns a *Error indicating "forbidden". This is used to
|
||||
@ -217,7 +237,7 @@ func NotSupported[T ~string](field *Path, value interface{}, validValues []T) *E
|
||||
// some conditions, but which are not permitted by current conditions (e.g.
|
||||
// security policy).
|
||||
func Forbidden(field *Path, detail string) *Error {
|
||||
return &Error{ErrorTypeForbidden, field.String(), "", detail}
|
||||
return &Error{ErrorTypeForbidden, field.String(), "", detail, ""}
|
||||
}
|
||||
|
||||
// TooLong returns a *Error indicating "too long". This is used to report that
|
||||
@ -231,7 +251,7 @@ func TooLong(field *Path, value interface{}, maxLength int) *Error {
|
||||
} else {
|
||||
msg = "value is too long"
|
||||
}
|
||||
return &Error{ErrorTypeTooLong, field.String(), "<value omitted>", msg}
|
||||
return &Error{ErrorTypeTooLong, field.String(), "<value omitted>", msg, ""}
|
||||
}
|
||||
|
||||
// TooLongMaxLength returns a *Error indicating "too long".
|
||||
@ -259,14 +279,14 @@ func TooMany(field *Path, actualQuantity, maxQuantity int) *Error {
|
||||
actual = omitValue
|
||||
}
|
||||
|
||||
return &Error{ErrorTypeTooMany, field.String(), actual, msg}
|
||||
return &Error{ErrorTypeTooMany, field.String(), actual, msg, ""}
|
||||
}
|
||||
|
||||
// InternalError returns a *Error indicating "internal error". This is used
|
||||
// to signal that an error was found that was not directly related to user
|
||||
// input. The err argument must be non-nil.
|
||||
func InternalError(field *Path, err error) *Error {
|
||||
return &Error{ErrorTypeInternal, field.String(), nil, err.Error()}
|
||||
return &Error{ErrorTypeInternal, field.String(), nil, err.Error(), ""}
|
||||
}
|
||||
|
||||
// ErrorList holds a set of Errors. It is plausible that we might one day have
|
||||
@ -285,6 +305,14 @@ func NewErrorTypeMatcher(t ErrorType) utilerrors.Matcher {
|
||||
}
|
||||
}
|
||||
|
||||
// WithOrigin sets the origin for all errors in the list and returns the updated list.
|
||||
func (list ErrorList) WithOrigin(origin string) ErrorList {
|
||||
for _, err := range list {
|
||||
err.Origin = origin
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
// ToAggregate converts the ErrorList into an errors.Aggregate.
|
||||
func (list ErrorList) ToAggregate() utilerrors.Aggregate {
|
||||
if len(list) == 0 {
|
||||
|
@ -173,3 +173,41 @@ func TestNotSupported(t *testing.T) {
|
||||
t.Errorf("Expected: %s\n, but got: %s\n", expected, notSupported.ErrorBody())
|
||||
}
|
||||
}
|
||||
|
||||
func TestErrorOrigin(t *testing.T) {
|
||||
err := Invalid(NewPath("field"), "value", "detail")
|
||||
|
||||
// Test WithOrigin
|
||||
newErr := err.WithOrigin("origin1")
|
||||
if newErr.Origin != "origin1" {
|
||||
t.Errorf("Expected Origin to be 'origin1', got '%s'", newErr.Origin)
|
||||
}
|
||||
if err.Origin != "origin1" {
|
||||
t.Errorf("Expected Origin to be 'origin1', got '%s'", err.Origin)
|
||||
}
|
||||
}
|
||||
|
||||
func TestErrorListOrigin(t *testing.T) {
|
||||
// Create an ErrorList with multiple errors
|
||||
list := ErrorList{
|
||||
Invalid(NewPath("field1"), "value1", "detail1"),
|
||||
Invalid(NewPath("field2"), "value2", "detail2"),
|
||||
Required(NewPath("field3"), "detail3"),
|
||||
}
|
||||
|
||||
// Test WithOrigin
|
||||
newList := list.WithOrigin("origin1")
|
||||
// Check that WithOrigin returns the modified list
|
||||
for i, err := range newList {
|
||||
if err.Origin != "origin1" {
|
||||
t.Errorf("Error %d: Expected Origin to be 'origin2', got '%s'", i, err.Origin)
|
||||
}
|
||||
}
|
||||
|
||||
// Check that the original list was also modified (WithOrigin modifies and returns the same list)
|
||||
for i, err := range list {
|
||||
if err.Origin != "origin1" {
|
||||
t.Errorf("Error %d: Expected original list Origin to be 'origin2', got '%s'", i, err.Origin)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -373,7 +373,7 @@ func IsValidPortName(port string) []string {
|
||||
func IsValidIP(fldPath *field.Path, value string) field.ErrorList {
|
||||
var allErrors field.ErrorList
|
||||
if netutils.ParseIPSloppy(value) == nil {
|
||||
allErrors = append(allErrors, field.Invalid(fldPath, value, "must be a valid IP address, (e.g. 10.9.8.7 or 2001:db8::ffff)"))
|
||||
allErrors = append(allErrors, field.Invalid(fldPath, value, "must be a valid IP address, (e.g. 10.9.8.7 or 2001:db8::ffff)").WithOrigin("format=ip-sloppy"))
|
||||
}
|
||||
return allErrors
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user