From 7fc6b4157b7dff23772091250df91e4cc4b7ab57 Mon Sep 17 00:00:00 2001 From: Antonio Ojea Date: Wed, 21 Oct 2020 09:02:59 +0200 Subject: [PATCH] kubeadm validate maximum service subnet size Validate that the maximum service subnet size doesn't exceed the limits. Co-authored-by: Arvinderpal Wander --- .../app/apis/kubeadm/validation/validation.go | 21 ++++++++++++ .../kubeadm/validation/validation_test.go | 33 ++++++++++++++++++- cmd/kubeadm/app/constants/constants.go | 6 ++++ 3 files changed, 59 insertions(+), 1 deletion(-) diff --git a/cmd/kubeadm/app/apis/kubeadm/validation/validation.go b/cmd/kubeadm/app/apis/kubeadm/validation/validation.go index 316c1893f97..283102f367e 100644 --- a/cmd/kubeadm/app/apis/kubeadm/validation/validation.go +++ b/cmd/kubeadm/app/apis/kubeadm/validation/validation.go @@ -403,6 +403,25 @@ func ValidateIPNetFromString(subnetStr string, minAddrs int64, isDualStack bool, return allErrs } +// ValidateServiceSubnetSize validates that the maximum subnet size is not exceeded +// Should be a small cidr due to how it is stored in etcd. +// bigger cidr (specially those offered by IPv6) will add no value +// and significantly increase snapshotting time. +// NOTE: This is identical to validation performed in the apiserver. +func ValidateServiceSubnetSize(subnetStr string, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + // subnets were already validated + subnets, _ := utilnet.ParseCIDRs(strings.Split(subnetStr, ",")) + for _, serviceSubnet := range subnets { + ones, bits := serviceSubnet.Mask.Size() + if bits-ones > constants.MaximumBitsForServiceSubnet { + errMsg := fmt.Sprintf("specified service subnet is too large; for %d-bit addresses, the mask must be >= %d", bits, bits-constants.MaximumBitsForServiceSubnet) + allErrs = append(allErrs, field.Invalid(fldPath, serviceSubnet.String(), errMsg)) + } + } + return allErrs +} + // ValidatePodSubnetNodeMask validates that the relation between podSubnet and node-masks is correct func ValidatePodSubnetNodeMask(subnetStr string, c *kubeadm.ClusterConfiguration, fldPath *field.Path) field.ErrorList { allErrs := field.ErrorList{} @@ -468,6 +487,8 @@ func ValidateNetworking(c *kubeadm.ClusterConfiguration, fldPath *field.Path) fi if len(c.Networking.ServiceSubnet) != 0 { allErrs = append(allErrs, ValidateIPNetFromString(c.Networking.ServiceSubnet, constants.MinimumAddressesInServiceSubnet, isDualStack, field.NewPath("serviceSubnet"))...) + // Service subnet was already validated, we need to validate now the subnet size + allErrs = append(allErrs, ValidateServiceSubnetSize(c.Networking.ServiceSubnet, field.NewPath("serviceSubnet"))...) } if len(c.Networking.PodSubnet) != 0 { allErrs = append(allErrs, ValidateIPNetFromString(c.Networking.PodSubnet, constants.MinimumAddressesInPodSubnet, isDualStack, field.NewPath("podSubnet"))...) diff --git a/cmd/kubeadm/app/apis/kubeadm/validation/validation_test.go b/cmd/kubeadm/app/apis/kubeadm/validation/validation_test.go index 23a0707946f..c74c60cfbbf 100644 --- a/cmd/kubeadm/app/apis/kubeadm/validation/validation_test.go +++ b/cmd/kubeadm/app/apis/kubeadm/validation/validation_test.go @@ -287,6 +287,37 @@ func TestValidatePodSubnetNodeMask(t *testing.T) { } } +func TestValidateServiceSubnetSize(t *testing.T) { + var tests = []struct { + name string + subnet string + expected bool + }{ + {"single IPv4, but mask too large.", "10.0.0.16/2", false}, + {"single IPv6, but mask too large.", "2001:db8::1/64", false}, + {"single IPv4 CIDR", "10.0.0.16/12", true}, + {"single IPv6 CIDR", "2001:db8::/112", true}, + // dual-stack: + {"dual, but IPv4 mask too large.", "2001:db8::1/112,10.0.0.16/6", false}, + {"dual, but IPv6 mask too large.", "2001:db8::1/12,10.0.0.16/16", false}, + {"dual IPv4 IPv6", "10.0.0.16/12,2001:db8::/112", true}, + {"dual IPv6 IPv4", "2001:db8::/112,10.0.0.16/12", true}, + } + for _, rt := range tests { + + actual := ValidateServiceSubnetSize(rt.subnet, nil) + if (len(actual) == 0) != rt.expected { + t.Errorf( + "%s test case failed :\n\texpected: %t\n\t actual: %t\n\t err(s): %v\n\t", + rt.name, + rt.expected, + (len(actual) == 0), + actual, + ) + } + } +} + func TestValidateHostPort(t *testing.T) { var tests = []struct { name string @@ -521,7 +552,7 @@ func TestValidateInitConfiguration(t *testing.T) { }, }, Networking: kubeadm.Networking{ - ServiceSubnet: "2001:db8::1/98", + ServiceSubnet: "2001:db8::1/112", DNSDomain: "cluster.local", }, CertificatesDir: "/some/other/cert/dir", diff --git a/cmd/kubeadm/app/constants/constants.go b/cmd/kubeadm/app/constants/constants.go index 78218e0fef5..a9313574e17 100644 --- a/cmd/kubeadm/app/constants/constants.go +++ b/cmd/kubeadm/app/constants/constants.go @@ -199,6 +199,12 @@ const ( // We need at least ten, because the DNS service is always at the tenth cluster clusterIP MinimumAddressesInServiceSubnet = 10 + // MaximumBitsForServiceSubnet defines maximum possible size of the service subnet in terms of bits. + // For example, if the value is 20, then the largest supported service subnet is /12 for IPv4 and /108 for IPv6. + // Note however that anything in between /108 and /112 will be clamped to /112 due to the limitations of the underlying allocation logic. + // TODO: https://github.com/kubernetes/enhancements/pull/1881 + MaximumBitsForServiceSubnet = 20 + // MinimumAddressesInPodSubnet defines minimum amount of pods in the cluster. // We need at least more than services, an IPv4 /28 or IPv6 /128 subnet means 14 util addresses MinimumAddressesInPodSubnet = 14