From 34717000dae84fb1c4df608370ad9bd572901f7b Mon Sep 17 00:00:00 2001 From: Dan Winship Date: Tue, 26 Dec 2023 19:26:24 -0500 Subject: [PATCH] Split out IP validation functions into their own file (No code changes.) --- .../apimachinery/pkg/util/validation/ip.go | 61 ++++ .../pkg/util/validation/ip_test.go | 320 ++++++++++++++++++ .../pkg/util/validation/validation.go | 40 --- .../pkg/util/validation/validation_test.go | 296 ---------------- 4 files changed, 381 insertions(+), 336 deletions(-) create mode 100644 staging/src/k8s.io/apimachinery/pkg/util/validation/ip.go create mode 100644 staging/src/k8s.io/apimachinery/pkg/util/validation/ip_test.go diff --git a/staging/src/k8s.io/apimachinery/pkg/util/validation/ip.go b/staging/src/k8s.io/apimachinery/pkg/util/validation/ip.go new file mode 100644 index 00000000000..566480f7098 --- /dev/null +++ b/staging/src/k8s.io/apimachinery/pkg/util/validation/ip.go @@ -0,0 +1,61 @@ +/* +Copyright 2023 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 validation + +import ( + "k8s.io/apimachinery/pkg/util/validation/field" + netutils "k8s.io/utils/net" +) + +// IsValidIP tests that the argument is a valid IP address. +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)").WithOrigin("format=ip-sloppy")) + } + return allErrors +} + +// IsValidIPv4Address tests that the argument is a valid IPv4 address. +func IsValidIPv4Address(fldPath *field.Path, value string) field.ErrorList { + var allErrors field.ErrorList + ip := netutils.ParseIPSloppy(value) + if ip == nil || ip.To4() == nil { + allErrors = append(allErrors, field.Invalid(fldPath, value, "must be a valid IPv4 address")) + } + return allErrors +} + +// IsValidIPv6Address tests that the argument is a valid IPv6 address. +func IsValidIPv6Address(fldPath *field.Path, value string) field.ErrorList { + var allErrors field.ErrorList + ip := netutils.ParseIPSloppy(value) + if ip == nil || ip.To4() != nil { + allErrors = append(allErrors, field.Invalid(fldPath, value, "must be a valid IPv6 address")) + } + return allErrors +} + +// IsValidCIDR tests that the argument is a valid CIDR value. +func IsValidCIDR(fldPath *field.Path, value string) field.ErrorList { + var allErrors field.ErrorList + _, _, err := netutils.ParseCIDRSloppy(value) + if err != nil { + allErrors = append(allErrors, field.Invalid(fldPath, value, "must be a valid CIDR value, (e.g. 10.9.8.0/24 or 2001:db8::/64)")) + } + return allErrors +} diff --git a/staging/src/k8s.io/apimachinery/pkg/util/validation/ip_test.go b/staging/src/k8s.io/apimachinery/pkg/util/validation/ip_test.go new file mode 100644 index 00000000000..f7968ca1f48 --- /dev/null +++ b/staging/src/k8s.io/apimachinery/pkg/util/validation/ip_test.go @@ -0,0 +1,320 @@ +/* +Copyright 2023 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 validation + +import ( + "strings" + "testing" + + "k8s.io/apimachinery/pkg/util/validation/field" +) + +func TestIsValidIP(t *testing.T) { + for _, tc := range []struct { + name string + in string + family int + err string + }{ + // GOOD VALUES + { + name: "ipv4", + in: "1.2.3.4", + family: 4, + }, + { + name: "ipv4, all zeros", + in: "0.0.0.0", + family: 4, + }, + { + name: "ipv4, max", + in: "255.255.255.255", + family: 4, + }, + { + name: "ipv6", + in: "1234::abcd", + family: 6, + }, + { + name: "ipv6, all zeros, collapsed", + in: "::", + family: 6, + }, + { + name: "ipv6, max", + in: "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", + family: 6, + }, + + // GOOD, THOUGH NON-CANONICAL, VALUES + { + name: "ipv6, all zeros, expanded (non-canonical)", + in: "0:0:0:0:0:0:0:0", + family: 6, + }, + { + name: "ipv6, leading 0s (non-canonical)", + in: "0001:002:03:4::", + family: 6, + }, + { + name: "ipv6, capital letters (non-canonical)", + in: "1234::ABCD", + family: 6, + }, + + // BAD VALUES WE CURRENTLY CONSIDER GOOD + { + name: "ipv4 with leading 0s", + in: "1.1.1.01", + family: 4, + }, + { + name: "ipv4-in-ipv6 value", + in: "::ffff:1.1.1.1", + family: 4, + }, + + // BAD VALUES + { + name: "empty string", + in: "", + err: "must be a valid IP address", + }, + { + name: "junk", + in: "aaaaaaa", + err: "must be a valid IP address", + }, + { + name: "domain name", + in: "myhost.mydomain", + err: "must be a valid IP address", + }, + { + name: "cidr", + in: "1.2.3.0/24", + err: "must be a valid IP address", + }, + { + name: "ipv4 with out-of-range octets", + in: "1.2.3.400", + err: "must be a valid IP address", + }, + { + name: "ipv4 with negative octets", + in: "-1.0.0.0", + err: "must be a valid IP address", + }, + { + name: "ipv6 with out-of-range segment", + in: "2001:db8::10005", + err: "must be a valid IP address", + }, + { + name: "ipv4:port", + in: "1.2.3.4:80", + err: "must be a valid IP address", + }, + { + name: "ipv6 with brackets", + in: "[2001:db8::1]", + err: "must be a valid IP address", + }, + { + name: "[ipv6]:port", + in: "[2001:db8::1]:80", + err: "must be a valid IP address", + }, + { + name: "host:port", + in: "example.com:80", + err: "must be a valid IP address", + }, + { + name: "ipv6 with zone", + in: "1234::abcd%eth0", + err: "must be a valid IP address", + }, + { + name: "ipv4 with zone", + in: "169.254.0.0%eth0", + err: "must be a valid IP address", + }, + } { + t.Run(tc.name, func(t *testing.T) { + errs := IsValidIP(field.NewPath(""), tc.in) + if tc.err == "" { + if len(errs) != 0 { + t.Errorf("expected %q to be valid but got: %v", tc.in, errs) + } + } else { + if len(errs) != 1 { + t.Errorf("expected %q to have 1 error but got: %v", tc.in, errs) + } else if !strings.Contains(errs[0].Detail, tc.err) { + t.Errorf("expected error for %q to contain %q but got: %q", tc.in, tc.err, errs[0].Detail) + } + } + + errs = IsValidIPv4Address(field.NewPath(""), tc.in) + if tc.family == 4 { + if len(errs) != 0 { + t.Errorf("expected %q to pass IsValidIPv4Address but got: %v", tc.in, errs) + } + } else { + if len(errs) == 0 { + t.Errorf("expected %q to fail IsValidIPv4Address", tc.in) + } + } + + errs = IsValidIPv6Address(field.NewPath(""), tc.in) + if tc.family == 6 { + if len(errs) != 0 { + t.Errorf("expected %q to pass IsValidIPv6Address but got: %v", tc.in, errs) + } + } else { + if len(errs) == 0 { + t.Errorf("expected %q to fail IsValidIPv6Address", tc.in) + } + } + }) + } +} + +func TestIsValidCIDR(t *testing.T) { + for _, tc := range []struct { + name string + in string + err string + }{ + // GOOD VALUES + { + name: "ipv4", + in: "1.0.0.0/8", + }, + { + name: "ipv4, all IPs", + in: "0.0.0.0/0", + }, + { + name: "ipv4, single IP", + in: "1.1.1.1/32", + }, + { + name: "ipv6", + in: "2001:4860:4860::/48", + }, + { + name: "ipv6, all IPs", + in: "::/0", + }, + { + name: "ipv6, single IP", + in: "::1/128", + }, + + // GOOD, THOUGH NON-CANONICAL, VALUES + { + name: "ipv6, extra 0s (non-canonical)", + in: "2a00:79e0:2:0::/64", + }, + { + name: "ipv6, capital letters (non-canonical)", + in: "2001:DB8::/64", + }, + + // BAD VALUES WE CURRENTLY CONSIDER GOOD + { + name: "ipv4 with leading 0s", + in: "1.1.01.0/24", + }, + { + name: "ipv4-in-ipv6 with ipv4-sized prefix", + in: "::ffff:1.1.1.0/24", + }, + { + name: "ipv4-in-ipv6 with ipv6-sized prefix", + in: "::ffff:1.1.1.0/120", + }, + { + name: "ipv4 with bits past prefix", + in: "1.2.3.4/24", + }, + { + name: "ipv6 with bits past prefix", + in: "2001:db8::1/64", + }, + { + name: "prefix length with leading 0s", + in: "192.168.0.0/016", + }, + + // BAD VALUES + { + name: "empty string", + in: "", + err: "must be a valid CIDR value", + }, + { + name: "junk", + in: "aaaaaaa", + err: "must be a valid CIDR value", + }, + { + name: "IP address", + in: "1.2.3.4", + err: "must be a valid CIDR value", + }, + { + name: "partial URL", + in: "192.168.0.1/healthz", + err: "must be a valid CIDR value", + }, + { + name: "partial URL 2", + in: "192.168.0.1/0/99", + err: "must be a valid CIDR value", + }, + { + name: "negative prefix length", + in: "192.168.0.0/-16", + err: "must be a valid CIDR value", + }, + { + name: "prefix length with sign", + in: "192.168.0.0/+16", + err: "must be a valid CIDR value", + }, + } { + t.Run(tc.name, func(t *testing.T) { + errs := IsValidCIDR(field.NewPath(""), tc.in) + if tc.err == "" { + if len(errs) != 0 { + t.Errorf("expected %q to be valid but got: %v", tc.in, errs) + } + } else { + if len(errs) != 1 { + t.Errorf("expected %q to have 1 error but got: %v", tc.in, errs) + } else if !strings.Contains(errs[0].Detail, tc.err) { + t.Errorf("expected error for %q to contain %q but got: %q", tc.in, tc.err, errs[0].Detail) + } + } + }) + } +} 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 feb3d259b8a..b6be7af1b16 100644 --- a/staging/src/k8s.io/apimachinery/pkg/util/validation/validation.go +++ b/staging/src/k8s.io/apimachinery/pkg/util/validation/validation.go @@ -24,7 +24,6 @@ import ( "unicode" "k8s.io/apimachinery/pkg/util/validation/field" - netutils "k8s.io/utils/net" ) const qnameCharFmt string = "[A-Za-z0-9]" @@ -369,45 +368,6 @@ func IsValidPortName(port string) []string { return errs } -// IsValidIP tests that the argument is a valid IP address. -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)").WithOrigin("format=ip-sloppy")) - } - return allErrors -} - -// IsValidIPv4Address tests that the argument is a valid IPv4 address. -func IsValidIPv4Address(fldPath *field.Path, value string) field.ErrorList { - var allErrors field.ErrorList - ip := netutils.ParseIPSloppy(value) - if ip == nil || ip.To4() == nil { - allErrors = append(allErrors, field.Invalid(fldPath, value, "must be a valid IPv4 address")) - } - return allErrors -} - -// IsValidIPv6Address tests that the argument is a valid IPv6 address. -func IsValidIPv6Address(fldPath *field.Path, value string) field.ErrorList { - var allErrors field.ErrorList - ip := netutils.ParseIPSloppy(value) - if ip == nil || ip.To4() != nil { - allErrors = append(allErrors, field.Invalid(fldPath, value, "must be a valid IPv6 address")) - } - return allErrors -} - -// IsValidCIDR tests that the argument is a valid CIDR value. -func IsValidCIDR(fldPath *field.Path, value string) field.ErrorList { - var allErrors field.ErrorList - _, _, err := netutils.ParseCIDRSloppy(value) - if err != nil { - allErrors = append(allErrors, field.Invalid(fldPath, value, "must be a valid CIDR value, (e.g. 10.9.8.0/24 or 2001:db8::/64)")) - } - return allErrors -} - const percentFmt string = "[0-9]+%" const percentErrMsg string = "a valid percent string must be a numeric string followed by an ending '%'" 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 8bcf36b6b3d..848fb60edb9 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 @@ -322,302 +322,6 @@ func TestIsValidLabelValue(t *testing.T) { } } -func TestIsValidIP(t *testing.T) { - for _, tc := range []struct { - name string - in string - family int - err string - }{ - // GOOD VALUES - { - name: "ipv4", - in: "1.2.3.4", - family: 4, - }, - { - name: "ipv4, all zeros", - in: "0.0.0.0", - family: 4, - }, - { - name: "ipv4, max", - in: "255.255.255.255", - family: 4, - }, - { - name: "ipv6", - in: "1234::abcd", - family: 6, - }, - { - name: "ipv6, all zeros, collapsed", - in: "::", - family: 6, - }, - { - name: "ipv6, max", - in: "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", - family: 6, - }, - - // GOOD, THOUGH NON-CANONICAL, VALUES - { - name: "ipv6, all zeros, expanded (non-canonical)", - in: "0:0:0:0:0:0:0:0", - family: 6, - }, - { - name: "ipv6, leading 0s (non-canonical)", - in: "0001:002:03:4::", - family: 6, - }, - { - name: "ipv6, capital letters (non-canonical)", - in: "1234::ABCD", - family: 6, - }, - - // BAD VALUES WE CURRENTLY CONSIDER GOOD - { - name: "ipv4 with leading 0s", - in: "1.1.1.01", - family: 4, - }, - { - name: "ipv4-in-ipv6 value", - in: "::ffff:1.1.1.1", - family: 4, - }, - - // BAD VALUES - { - name: "empty string", - in: "", - err: "must be a valid IP address", - }, - { - name: "junk", - in: "aaaaaaa", - err: "must be a valid IP address", - }, - { - name: "domain name", - in: "myhost.mydomain", - err: "must be a valid IP address", - }, - { - name: "cidr", - in: "1.2.3.0/24", - err: "must be a valid IP address", - }, - { - name: "ipv4 with out-of-range octets", - in: "1.2.3.400", - err: "must be a valid IP address", - }, - { - name: "ipv4 with negative octets", - in: "-1.0.0.0", - err: "must be a valid IP address", - }, - { - name: "ipv6 with out-of-range segment", - in: "2001:db8::10005", - err: "must be a valid IP address", - }, - { - name: "ipv4:port", - in: "1.2.3.4:80", - err: "must be a valid IP address", - }, - { - name: "ipv6 with brackets", - in: "[2001:db8::1]", - err: "must be a valid IP address", - }, - { - name: "[ipv6]:port", - in: "[2001:db8::1]:80", - err: "must be a valid IP address", - }, - { - name: "host:port", - in: "example.com:80", - err: "must be a valid IP address", - }, - { - name: "ipv6 with zone", - in: "1234::abcd%eth0", - err: "must be a valid IP address", - }, - { - name: "ipv4 with zone", - in: "169.254.0.0%eth0", - err: "must be a valid IP address", - }, - } { - t.Run(tc.name, func(t *testing.T) { - errs := IsValidIP(field.NewPath(""), tc.in) - if tc.err == "" { - if len(errs) != 0 { - t.Errorf("expected %q to be valid but got: %v", tc.in, errs) - } - } else { - if len(errs) != 1 { - t.Errorf("expected %q to have 1 error but got: %v", tc.in, errs) - } else if !strings.Contains(errs[0].Detail, tc.err) { - t.Errorf("expected error for %q to contain %q but got: %q", tc.in, tc.err, errs[0].Detail) - } - } - - errs = IsValidIPv4Address(field.NewPath(""), tc.in) - if tc.family == 4 { - if len(errs) != 0 { - t.Errorf("expected %q to pass IsValidIPv4Address but got: %v", tc.in, errs) - } - } else { - if len(errs) == 0 { - t.Errorf("expected %q to fail IsValidIPv4Address", tc.in) - } - } - - errs = IsValidIPv6Address(field.NewPath(""), tc.in) - if tc.family == 6 { - if len(errs) != 0 { - t.Errorf("expected %q to pass IsValidIPv6Address but got: %v", tc.in, errs) - } - } else { - if len(errs) == 0 { - t.Errorf("expected %q to fail IsValidIPv6Address", tc.in) - } - } - }) - } -} - -func TestIsValidCIDR(t *testing.T) { - for _, tc := range []struct { - name string - in string - err string - }{ - // GOOD VALUES - { - name: "ipv4", - in: "1.0.0.0/8", - }, - { - name: "ipv4, all IPs", - in: "0.0.0.0/0", - }, - { - name: "ipv4, single IP", - in: "1.1.1.1/32", - }, - { - name: "ipv6", - in: "2001:4860:4860::/48", - }, - { - name: "ipv6, all IPs", - in: "::/0", - }, - { - name: "ipv6, single IP", - in: "::1/128", - }, - - // GOOD, THOUGH NON-CANONICAL, VALUES - { - name: "ipv6, extra 0s (non-canonical)", - in: "2a00:79e0:2:0::/64", - }, - { - name: "ipv6, capital letters (non-canonical)", - in: "2001:DB8::/64", - }, - - // BAD VALUES WE CURRENTLY CONSIDER GOOD - { - name: "ipv4 with leading 0s", - in: "1.1.01.0/24", - }, - { - name: "ipv4-in-ipv6 with ipv4-sized prefix", - in: "::ffff:1.1.1.0/24", - }, - { - name: "ipv4-in-ipv6 with ipv6-sized prefix", - in: "::ffff:1.1.1.0/120", - }, - { - name: "ipv4 with bits past prefix", - in: "1.2.3.4/24", - }, - { - name: "ipv6 with bits past prefix", - in: "2001:db8::1/64", - }, - { - name: "prefix length with leading 0s", - in: "192.168.0.0/016", - }, - - // BAD VALUES - { - name: "empty string", - in: "", - err: "must be a valid CIDR value", - }, - { - name: "junk", - in: "aaaaaaa", - err: "must be a valid CIDR value", - }, - { - name: "IP address", - in: "1.2.3.4", - err: "must be a valid CIDR value", - }, - { - name: "partial URL", - in: "192.168.0.1/healthz", - err: "must be a valid CIDR value", - }, - { - name: "partial URL 2", - in: "192.168.0.1/0/99", - err: "must be a valid CIDR value", - }, - { - name: "negative prefix length", - in: "192.168.0.0/-16", - err: "must be a valid CIDR value", - }, - { - name: "prefix length with sign", - in: "192.168.0.0/+16", - err: "must be a valid CIDR value", - }, - } { - t.Run(tc.name, func(t *testing.T) { - errs := IsValidCIDR(field.NewPath(""), tc.in) - if tc.err == "" { - if len(errs) != 0 { - t.Errorf("expected %q to be valid but got: %v", tc.in, errs) - } - } else { - if len(errs) != 1 { - t.Errorf("expected %q to have 1 error but got: %v", tc.in, errs) - } else if !strings.Contains(errs[0].Detail, tc.err) { - t.Errorf("expected error for %q to contain %q but got: %q", tc.in, tc.err, errs[0].Detail) - } - } - }) - } -} - func TestIsHTTPHeaderName(t *testing.T) { goodValues := []string{ // Common ones