diff --git a/pkg/apis/networking/validation/validation.go b/pkg/apis/networking/validation/validation.go index 5a3046cef15..cb1d1a6d331 100644 --- a/pkg/apis/networking/validation/validation.go +++ b/pkg/apis/networking/validation/validation.go @@ -18,6 +18,7 @@ package validation import ( "fmt" + "net/netip" "strings" v1 "k8s.io/api/core/v1" @@ -741,3 +742,77 @@ func validateClusterCIDRUpdateSpec(update, old *networking.ClusterCIDRSpec, fldP return allErrs } + +// ValidateIPAddressName validates that the name is the decimal representation of an IP address. +// IPAddress does not support generating names, prefix is not considered. +func ValidateIPAddressName(name string, prefix bool) []string { + var errs []string + ip, err := netip.ParseAddr(name) + if err != nil { + errs = append(errs, err.Error()) + } else if ip.String() != name { + errs = append(errs, "not a valid ip in canonical format") + + } + return errs +} + +func ValidateIPAddress(ipAddress *networking.IPAddress) field.ErrorList { + allErrs := apivalidation.ValidateObjectMeta(&ipAddress.ObjectMeta, false, ValidateIPAddressName, field.NewPath("metadata")) + errs := validateIPAddressParentReference(ipAddress.Spec.ParentRef, field.NewPath("spec")) + allErrs = append(allErrs, errs...) + return allErrs + +} + +// validateIPAddressParentReference ensures that the IPAddress ParenteReference exists and is valid. +func validateIPAddressParentReference(params *networking.ParentReference, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + + if params == nil { + allErrs = append(allErrs, field.Required(fldPath.Child("parentRef"), "")) + return allErrs + } + + fldPath = fldPath.Child("parentRef") + // group is required but the Core group used by Services is the empty value, so it can not be enforced + if params.Group != "" { + for _, msg := range validation.IsDNS1123Subdomain(params.Group) { + allErrs = append(allErrs, field.Invalid(fldPath.Child("group"), params.Group, msg)) + } + } + + // resource is required + if params.Resource == "" { + allErrs = append(allErrs, field.Required(fldPath.Child("resource"), "")) + } else { + for _, msg := range pathvalidation.IsValidPathSegmentName(params.Resource) { + allErrs = append(allErrs, field.Invalid(fldPath.Child("resource"), params.Resource, msg)) + } + } + + // name is required + if params.Name == "" { + allErrs = append(allErrs, field.Required(fldPath.Child("name"), "")) + } else { + for _, msg := range pathvalidation.IsValidPathSegmentName(params.Name) { + allErrs = append(allErrs, field.Invalid(fldPath.Child("name"), params.Name, msg)) + } + } + + // namespace is optional + if params.Namespace != "" { + for _, msg := range pathvalidation.IsValidPathSegmentName(params.Namespace) { + allErrs = append(allErrs, field.Invalid(fldPath.Child("namespace"), params.Namespace, msg)) + } + } + return allErrs +} + +// ValidateIPAddressUpdate tests if an update to an IPAddress is valid. +func ValidateIPAddressUpdate(update, old *networking.IPAddress) field.ErrorList { + var allErrs field.ErrorList + allErrs = append(allErrs, apivalidation.ValidateObjectMetaUpdate(&update.ObjectMeta, &old.ObjectMeta, field.NewPath("metadata"))...) + allErrs = append(allErrs, apivalidation.ValidateImmutableField(update.Spec.ParentRef, old.Spec.ParentRef, field.NewPath("spec").Child("parentRef"))...) + return allErrs +} diff --git a/pkg/apis/networking/validation/validation_test.go b/pkg/apis/networking/validation/validation_test.go index 7397f3a08c6..b575d486945 100644 --- a/pkg/apis/networking/validation/validation_test.go +++ b/pkg/apis/networking/validation/validation_test.go @@ -2195,3 +2195,215 @@ func TestValidateClusterConfigUpdate(t *testing.T) { }) } } + +func TestValidateIPAddress(t *testing.T) { + testCases := map[string]struct { + expectedErrors int + ipAddress *networking.IPAddress + }{ + "empty-ipaddress-bad-name": { + expectedErrors: 1, + ipAddress: &networking.IPAddress{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-name", + }, + Spec: networking.IPAddressSpec{ + ParentRef: &networking.ParentReference{ + Group: "", + Resource: "services", + Name: "foo", + Namespace: "bar", + }, + }, + }, + }, + "empty-ipaddress-bad-name-no-parent-reference": { + expectedErrors: 2, + ipAddress: &networking.IPAddress{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-name", + }, + }, + }, + + "good-ipaddress": { + expectedErrors: 0, + ipAddress: &networking.IPAddress{ + ObjectMeta: metav1.ObjectMeta{ + Name: "192.168.1.1", + }, + Spec: networking.IPAddressSpec{ + ParentRef: &networking.ParentReference{ + Group: "", + Resource: "services", + Name: "foo", + Namespace: "bar", + }, + }, + }, + }, + "good-ipaddress-gateway": { + expectedErrors: 0, + ipAddress: &networking.IPAddress{ + ObjectMeta: metav1.ObjectMeta{ + Name: "192.168.1.1", + }, + Spec: networking.IPAddressSpec{ + ParentRef: &networking.ParentReference{ + Group: "gateway.networking.k8s.io", + Resource: "gateway", + Name: "foo", + Namespace: "bar", + }, + }, + }, + }, + "good-ipv6address": { + expectedErrors: 0, + ipAddress: &networking.IPAddress{ + ObjectMeta: metav1.ObjectMeta{ + Name: "2001:4860:4860::8888", + }, + Spec: networking.IPAddressSpec{ + ParentRef: &networking.ParentReference{ + Group: "", + Resource: "services", + Name: "foo", + Namespace: "bar", + }, + }, + }, + }, + "non-canonica-ipv6address": { + expectedErrors: 1, + ipAddress: &networking.IPAddress{ + ObjectMeta: metav1.ObjectMeta{ + Name: "2001:4860:4860:0::8888", + }, + Spec: networking.IPAddressSpec{ + ParentRef: &networking.ParentReference{ + Group: "", + Resource: "services", + Name: "foo", + Namespace: "bar", + }, + }, + }, + }, + "missing-ipaddress-reference": { + expectedErrors: 1, + ipAddress: &networking.IPAddress{ + ObjectMeta: metav1.ObjectMeta{ + Name: "192.168.1.1", + }, + }, + }, + "wrong-ipaddress-reference": { + expectedErrors: 1, + ipAddress: &networking.IPAddress{ + ObjectMeta: metav1.ObjectMeta{ + Name: "192.168.1.1", + }, + Spec: networking.IPAddressSpec{ + ParentRef: &networking.ParentReference{ + Group: "custom.resource.com", + Resource: "services", + Name: "foo$%&", + Namespace: "", + }, + }, + }, + }, + "wrong-ipaddress-reference-multiple-errors": { + expectedErrors: 4, + ipAddress: &networking.IPAddress{ + ObjectMeta: metav1.ObjectMeta{ + Name: "192.168.1.1", + }, + Spec: networking.IPAddressSpec{ + ParentRef: &networking.ParentReference{ + Group: ".cust@m.resource.com", + Resource: "", + Name: "", + Namespace: "bar$$$$$%@", + }, + }, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + errs := ValidateIPAddress(testCase.ipAddress) + if len(errs) != testCase.expectedErrors { + t.Errorf("Expected %d errors, got %d errors: %v", testCase.expectedErrors, len(errs), errs) + } + }) + } +} + +func TestValidateIPAddressUpdate(t *testing.T) { + old := &networking.IPAddress{ + ObjectMeta: metav1.ObjectMeta{ + Name: "192.168.1.1", + ResourceVersion: "1", + }, + Spec: networking.IPAddressSpec{ + ParentRef: &networking.ParentReference{ + Group: "custom.resource.com", + Resource: "services", + Name: "foo", + Namespace: "bar", + }, + }, + } + + testCases := []struct { + name string + new func(svc *networking.IPAddress) *networking.IPAddress + expectErr bool + }{ + { + name: "Successful update, no changes", + new: func(old *networking.IPAddress) *networking.IPAddress { + out := old.DeepCopy() + return out + }, + expectErr: false, + }, + + { + name: "Failed update, update spec.ParentRef", + new: func(svc *networking.IPAddress) *networking.IPAddress { + out := svc.DeepCopy() + out.Spec.ParentRef = &networking.ParentReference{ + Group: "custom.resource.com", + Resource: "Gateway", + Name: "foo", + Namespace: "bar", + } + + return out + }, expectErr: true, + }, + { + name: "Failed update, delete spec.ParentRef", + new: func(svc *networking.IPAddress) *networking.IPAddress { + out := svc.DeepCopy() + out.Spec.ParentRef = nil + return out + }, expectErr: true, + }, + } + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + err := ValidateIPAddressUpdate(testCase.new(old), old) + if !testCase.expectErr && err != nil { + t.Errorf("ValidateIPAddressUpdate must be successful for test '%s', got %v", testCase.name, err) + } + if testCase.expectErr && err == nil { + t.Errorf("ValidateIPAddressUpdate must return error for test: %s, but got nil", testCase.name) + } + }) + } +}