Skip to content

Commit 8b52995

Browse files
author
Antonio Ojea
committed
kubeadm: validate podSubnet against node-cidr-mask
the controller manager should validate the podSubnet against the node-mask because if they are incorrect can cause the controller-manager to fail. We don't need to calculate the node-cidr-masks, because those should be provided by the user, if they are wrong we fail in validation.
1 parent ededd08 commit 8b52995

File tree

7 files changed

+246
-167
lines changed

7 files changed

+246
-167
lines changed

cmd/kubeadm/app/apis/kubeadm/validation/BUILD

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ go_test(
3131
deps = [
3232
"//cmd/kubeadm/app/apis/kubeadm:go_default_library",
3333
"//cmd/kubeadm/app/apis/kubeadm/v1beta2:go_default_library",
34+
"//cmd/kubeadm/app/features:go_default_library",
3435
"//staging/src/k8s.io/apimachinery/pkg/util/sets:go_default_library",
3536
"//staging/src/k8s.io/apimachinery/pkg/util/validation/field:go_default_library",
3637
"//vendor/github.com/spf13/pflag:go_default_library",

cmd/kubeadm/app/apis/kubeadm/validation/validation.go

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,59 @@ func ValidateIPNetFromString(subnetStr string, minAddrs int64, isDualStack bool,
403403
return allErrs
404404
}
405405

406+
// ValidatePodSubnetNodeMask validates that the relation between podSubnet and node-masks is correct
407+
func ValidatePodSubnetNodeMask(subnetStr string, c *kubeadm.ClusterConfiguration, fldPath *field.Path) field.ErrorList {
408+
allErrs := field.ErrorList{}
409+
// subnets were already validated
410+
subnets, _ := utilnet.ParseCIDRs(strings.Split(subnetStr, ","))
411+
for _, podSubnet := range subnets {
412+
// obtain podSubnet mask
413+
mask := podSubnet.Mask
414+
maskSize, _ := mask.Size()
415+
// obtain node-cidr-mask
416+
nodeMask := getClusterNodeMask(c, utilnet.IsIPv6(podSubnet.IP))
417+
// the pod subnet mask needs to allow one or multiple node-masks
418+
// i.e. if it has a /24 the node mask must be between 24 and 32 for ipv4
419+
if maskSize > nodeMask {
420+
allErrs = append(allErrs, field.Invalid(fldPath, podSubnet.String(), "pod subnet size is smaller than the node subnet size"))
421+
} else if (nodeMask - maskSize) > constants.PodSubnetNodeMaskMaxDiff {
422+
allErrs = append(allErrs, field.Invalid(fldPath, podSubnet.String(), fmt.Sprintf("pod subnet mask and node-mask difference can not be greater than %d", constants.PodSubnetNodeMaskMaxDiff)))
423+
}
424+
}
425+
return allErrs
426+
}
427+
428+
// getClusterNodeMask returns the corresponding node-cidr-mask
429+
// based on the Cluster configuration and the IP family
430+
// Default is 24 for IPv4 and 64 for IPv6
431+
func getClusterNodeMask(c *kubeadm.ClusterConfiguration, isIPv6 bool) int {
432+
// defaultNodeMaskCIDRIPv4 is default mask size for IPv4 node cidr for use by the controller manager
433+
const defaultNodeMaskCIDRIPv4 = 24
434+
// DefaultNodeMaskCIDRIPv6 is default mask size for IPv6 node cidr for use by the controller manager
435+
const defaultNodeMaskCIDRIPv6 = 64
436+
var maskSize int
437+
var maskArg string
438+
isDualStack := features.Enabled(c.FeatureGates, features.IPv6DualStack)
439+
440+
if isDualStack && isIPv6 {
441+
maskArg = "node-cidr-mask-size-ipv6"
442+
} else if isDualStack && !isIPv6 {
443+
maskArg = "node-cidr-mask-size-ipv4"
444+
} else {
445+
maskArg = "node-cidr-mask-size"
446+
}
447+
448+
if v, ok := c.ControllerManager.ExtraArgs[maskArg]; ok && v != "" {
449+
// assume it is an integer, if not it will fail later
450+
maskSize, _ = strconv.Atoi(v)
451+
} else if isIPv6 {
452+
maskSize = defaultNodeMaskCIDRIPv6
453+
} else {
454+
maskSize = defaultNodeMaskCIDRIPv4
455+
}
456+
return maskSize
457+
}
458+
406459
// ValidateNetworking validates networking configuration
407460
func ValidateNetworking(c *kubeadm.ClusterConfiguration, fldPath *field.Path) field.ErrorList {
408461
allErrs := field.ErrorList{}
@@ -417,7 +470,9 @@ func ValidateNetworking(c *kubeadm.ClusterConfiguration, fldPath *field.Path) fi
417470
allErrs = append(allErrs, ValidateIPNetFromString(c.Networking.ServiceSubnet, constants.MinimumAddressesInServiceSubnet, isDualStack, field.NewPath("serviceSubnet"))...)
418471
}
419472
if len(c.Networking.PodSubnet) != 0 {
420-
allErrs = append(allErrs, ValidateIPNetFromString(c.Networking.PodSubnet, constants.MinimumAddressesInServiceSubnet, isDualStack, field.NewPath("podSubnet"))...)
473+
allErrs = append(allErrs, ValidateIPNetFromString(c.Networking.PodSubnet, constants.MinimumAddressesInPodSubnet, isDualStack, field.NewPath("podSubnet"))...)
474+
// Pod subnet was already validated, we need to validate now against the node-mask
475+
allErrs = append(allErrs, ValidatePodSubnetNodeMask(c.Networking.PodSubnet, c, field.NewPath("podSubnet"))...)
421476
}
422477
return allErrs
423478
}

cmd/kubeadm/app/apis/kubeadm/validation/validation_test.go

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@ import (
2525
"k8s.io/apimachinery/pkg/util/sets"
2626
"k8s.io/apimachinery/pkg/util/validation/field"
2727
"k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm"
28+
kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm"
2829
kubeadmapiv1beta2 "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1beta2"
30+
"k8s.io/kubernetes/cmd/kubeadm/app/features"
2931
)
3032

3133
func TestValidateToken(t *testing.T) {
@@ -231,6 +233,60 @@ func TestValidateIPNetFromString(t *testing.T) {
231233
}
232234
}
233235

236+
func TestValidatePodSubnetNodeMask(t *testing.T) {
237+
var tests = []struct {
238+
name string
239+
subnet string
240+
cmExtraArgs map[string]string
241+
checkDualStack bool
242+
expected bool
243+
}{
244+
{"single IPv4, but mask too small. Default node-mask", "10.0.0.16/29", nil, false, false},
245+
{"single IPv4, but mask too small. Configured node-mask", "10.0.0.16/24", map[string]string{"node-cidr-mask-size": "23"}, false, false},
246+
{"single IPv6, but mask too small. Default node-mask", "2001:db8::1/112", nil, false, false},
247+
{"single IPv6, but mask too small. Configured node-mask", "2001:db8::1/64", map[string]string{"node-cidr-mask-size": "24"}, false, false},
248+
{"single IPv6, but mask difference greater than 16. Default node-mask", "2001:db8::1/12", nil, false, false},
249+
{"single IPv6, but mask difference greater than 16. Configured node-mask", "2001:db8::1/64", map[string]string{"node-cidr-mask-size": "120"}, false, false},
250+
{"single IPv4 CIDR", "10.0.0.16/12", nil, false, true},
251+
{"single IPv6 CIDR", "2001:db8::/48", nil, false, true},
252+
// dual-stack:
253+
{"dual IPv4 only, but mask too small. Default node-mask", "10.0.0.16/29", nil, true, false},
254+
{"dual IPv4 only, but mask too small. Configured node-mask", "10.0.0.16/24", map[string]string{"node-cidr-mask-size-ipv4": "23"}, true, false},
255+
{"dual IPv6 only, but mask too small. Default node-mask", "2001:db8::1/112", nil, true, false},
256+
{"dual IPv6 only, but mask too small. Configured node-mask", "2001:db8::1/64", map[string]string{"node-cidr-mask-size-ipv6": "24"}, true, false},
257+
{"dual IPv6 only, but mask difference greater than 16. Default node-mask", "2001:db8::1/12", nil, true, false},
258+
{"dual IPv6 only, but mask difference greater than 16. Configured node-mask", "2001:db8::1/64", map[string]string{"node-cidr-mask-size-ipv6": "120"}, true, false},
259+
{"dual IPv4 only CIDR", "10.0.0.16/12", nil, true, true},
260+
{"dual IPv6 only CIDR", "2001:db8::/48", nil, true, true},
261+
{"dual, but IPv4 mask too small. Default node-mask", "10.0.0.16/29,2001:db8::/48", nil, true, false},
262+
{"dual, but IPv4 mask too small. Configured node-mask", "10.0.0.16/24,2001:db8::/48", map[string]string{"node-cidr-mask-size-ipv4": "23"}, true, false},
263+
{"dual, but IPv6 mask too small. Default node-mask", "2001:db8::1/112,10.0.0.16/16", nil, true, false},
264+
{"dual, but IPv6 mask too small. Configured node-mask", "10.0.0.16/16,2001:db8::1/64", map[string]string{"node-cidr-mask-size-ipv6": "24"}, true, false},
265+
{"dual, but mask difference greater than 16. Default node-mask", "2001:db8::1/12,10.0.0.16/16", nil, true, false},
266+
{"dual, but mask difference greater than 16. Configured node-mask", "10.0.0.16/16,2001:db8::1/64", map[string]string{"node-cidr-mask-size-ipv6": "120"}, true, false},
267+
{"dual IPv4 IPv6", "2001:db8::/48,10.0.0.16/12", nil, true, true},
268+
{"dual IPv6 IPv4", "2001:db8::/48,10.0.0.16/12", nil, true, true},
269+
}
270+
for _, rt := range tests {
271+
cfg := &kubeadmapi.ClusterConfiguration{
272+
FeatureGates: map[string]bool{features.IPv6DualStack: rt.checkDualStack},
273+
ControllerManager: kubeadmapi.ControlPlaneComponent{
274+
ExtraArgs: rt.cmExtraArgs,
275+
},
276+
}
277+
actual := ValidatePodSubnetNodeMask(rt.subnet, cfg, nil)
278+
if (len(actual) == 0) != rt.expected {
279+
t.Errorf(
280+
"%s test case failed :\n\texpected: %t\n\t actual: %t\n\t err(s): %v\n\t",
281+
rt.name,
282+
rt.expected,
283+
(len(actual) == 0),
284+
actual,
285+
)
286+
}
287+
}
288+
}
289+
234290
func TestValidateHostPort(t *testing.T) {
235291
var tests = []struct {
236292
name string
@@ -1040,3 +1096,123 @@ func TestValidateEtcd(t *testing.T) {
10401096
}
10411097
}
10421098
}
1099+
1100+
func TestGetClusterNodeMask(t *testing.T) {
1101+
tests := []struct {
1102+
name string
1103+
cfg *kubeadmapi.ClusterConfiguration
1104+
isIPv6 bool
1105+
expectedMask int
1106+
}{
1107+
{
1108+
name: "ipv4 default mask",
1109+
cfg: &kubeadmapi.ClusterConfiguration{},
1110+
isIPv6: false,
1111+
expectedMask: 24,
1112+
},
1113+
{
1114+
name: "ipv4 custom mask",
1115+
cfg: &kubeadmapi.ClusterConfiguration{
1116+
ControllerManager: kubeadmapi.ControlPlaneComponent{
1117+
ExtraArgs: map[string]string{"node-cidr-mask-size": "23"},
1118+
},
1119+
},
1120+
isIPv6: false,
1121+
expectedMask: 23,
1122+
},
1123+
{
1124+
name: "ipv6 default mask",
1125+
cfg: &kubeadmapi.ClusterConfiguration{},
1126+
isIPv6: true,
1127+
expectedMask: 64,
1128+
},
1129+
{
1130+
name: "ipv6 custom mask",
1131+
cfg: &kubeadmapi.ClusterConfiguration{
1132+
ControllerManager: kubeadmapi.ControlPlaneComponent{
1133+
ExtraArgs: map[string]string{"node-cidr-mask-size": "83"},
1134+
},
1135+
},
1136+
isIPv6: true,
1137+
expectedMask: 83,
1138+
},
1139+
{
1140+
name: "dual ipv4 default mask",
1141+
cfg: &kubeadmapi.ClusterConfiguration{
1142+
FeatureGates: map[string]bool{features.IPv6DualStack: true},
1143+
},
1144+
isIPv6: false,
1145+
expectedMask: 24,
1146+
},
1147+
{
1148+
name: "dual ipv4 custom mask",
1149+
cfg: &kubeadmapi.ClusterConfiguration{
1150+
FeatureGates: map[string]bool{features.IPv6DualStack: true},
1151+
ControllerManager: kubeadmapi.ControlPlaneComponent{
1152+
ExtraArgs: map[string]string{"node-cidr-mask-size": "21", "node-cidr-mask-size-ipv4": "23"},
1153+
},
1154+
},
1155+
isIPv6: false,
1156+
expectedMask: 23,
1157+
},
1158+
{
1159+
name: "dual ipv6 default mask",
1160+
cfg: &kubeadmapi.ClusterConfiguration{
1161+
FeatureGates: map[string]bool{features.IPv6DualStack: true},
1162+
},
1163+
isIPv6: true,
1164+
expectedMask: 64,
1165+
},
1166+
{
1167+
name: "dual ipv6 custom mask",
1168+
cfg: &kubeadmapi.ClusterConfiguration{
1169+
FeatureGates: map[string]bool{features.IPv6DualStack: true},
1170+
ControllerManager: kubeadmapi.ControlPlaneComponent{
1171+
ExtraArgs: map[string]string{"node-cidr-mask-size-ipv6": "83"},
1172+
},
1173+
},
1174+
isIPv6: true,
1175+
expectedMask: 83,
1176+
},
1177+
{
1178+
name: "dual ipv4 custom mask",
1179+
cfg: &kubeadmapi.ClusterConfiguration{
1180+
FeatureGates: map[string]bool{features.IPv6DualStack: true},
1181+
ControllerManager: kubeadmapi.ControlPlaneComponent{
1182+
ExtraArgs: map[string]string{"node-cidr-mask-size-ipv4": "23"},
1183+
},
1184+
},
1185+
isIPv6: false,
1186+
expectedMask: 23,
1187+
},
1188+
{
1189+
name: "dual ipv6 default mask and legacy flag",
1190+
cfg: &kubeadmapi.ClusterConfiguration{
1191+
FeatureGates: map[string]bool{features.IPv6DualStack: true},
1192+
ControllerManager: kubeadmapi.ControlPlaneComponent{
1193+
ExtraArgs: map[string]string{"node-cidr-mask-size": "23"},
1194+
},
1195+
},
1196+
isIPv6: true,
1197+
expectedMask: 64,
1198+
},
1199+
{
1200+
name: "dual ipv6 custom mask and legacy flag",
1201+
cfg: &kubeadmapi.ClusterConfiguration{
1202+
FeatureGates: map[string]bool{features.IPv6DualStack: true},
1203+
ControllerManager: kubeadmapi.ControlPlaneComponent{
1204+
ExtraArgs: map[string]string{"node-cidr-mask-size": "23", "node-cidr-mask-size-ipv6": "83"},
1205+
},
1206+
},
1207+
isIPv6: true,
1208+
expectedMask: 83,
1209+
},
1210+
}
1211+
for _, test := range tests {
1212+
t.Run(test.name, func(t *testing.T) {
1213+
if mask := getClusterNodeMask(test.cfg, test.isIPv6); mask != test.expectedMask {
1214+
t.Errorf("expected mask: %d, got %d", test.expectedMask, mask)
1215+
}
1216+
})
1217+
}
1218+
}

cmd/kubeadm/app/constants/constants.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,15 @@ const (
199199
// We need at least ten, because the DNS service is always at the tenth cluster clusterIP
200200
MinimumAddressesInServiceSubnet = 10
201201

202+
// MinimumAddressesInPodSubnet defines minimum amount of pods in the cluster.
203+
// We need at least more than services, an IPv4 /28 or IPv6 /128 subnet means 14 util addresses
204+
MinimumAddressesInPodSubnet = 14
205+
206+
// PodSubnetNodeMaskMaxDiff is limited to 16 due to an issue with uncompressed IP bitmap in core:
207+
// xref: #44918
208+
// The node subnet mask size must be no more than the pod subnet mask size + 16
209+
PodSubnetNodeMaskMaxDiff = 16
210+
202211
// DefaultTokenDuration specifies the default amount of time that a bootstrap token will be valid
203212
// Default behaviour is 24 hours
204213
DefaultTokenDuration = 24 * time.Hour

cmd/kubeadm/app/phases/addons/proxy/proxy_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -194,12 +194,12 @@ func TestEnsureProxyAddon(t *testing.T) {
194194
controlPlaneConfig.LocalAPIEndpoint.AdvertiseAddress = "1.2.3"
195195
case IPv6SetBindAddress:
196196
controlPlaneConfig.LocalAPIEndpoint.AdvertiseAddress = "1:2::3:4"
197-
controlPlaneClusterConfig.Networking.PodSubnet = "2001:101::/96"
197+
controlPlaneClusterConfig.Networking.PodSubnet = "2001:101::/48"
198198
}
199199

200200
intControlPlane, err := configutil.DefaultedInitConfiguration(controlPlaneConfig, controlPlaneClusterConfig)
201201
if err != nil {
202-
t.Errorf("test failed to convert external to internal version")
202+
t.Errorf("test failed to convert external to internal version: %v", err)
203203
return
204204
}
205205
err = EnsureProxyAddon(&intControlPlane.ClusterConfiguration, &intControlPlane.LocalAPIEndpoint, client)

cmd/kubeadm/app/phases/controlplane/manifests.go

Lines changed: 0 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -270,52 +270,6 @@ func isValidAuthzMode(authzMode string) bool {
270270
return false
271271
}
272272

273-
// calcNodeCidrSize determines the size of the subnets used on each node, based
274-
// on the pod subnet provided. For IPv4, we assume that the pod subnet will
275-
// be /16 and use /24. If the pod subnet cannot be parsed, the IPv4 value will
276-
// be used (/24).
277-
//
278-
// For IPv6, the algorithm will do two three. First, the node CIDR will be set
279-
// to a multiple of 8, using the available bits for easier readability by user.
280-
// Second, the number of nodes will be 512 to 64K to attempt to maximize the
281-
// number of nodes (see NOTE below). Third, pod networks of /113 and larger will
282-
// be rejected, as the amount of bits available is too small.
283-
//
284-
// A special case is when the pod network size is /112, where /120 will be used,
285-
// only allowing 256 nodes and 256 pods.
286-
//
287-
// If the pod network size is /113 or larger, the node CIDR will be set to the same
288-
// size and this will be rejected later in validation.
289-
//
290-
// NOTE: Currently, the design allows a maximum of 64K nodes. This algorithm splits
291-
// the available bits to maximize the number used for nodes, but still have the node
292-
// CIDR be a multiple of eight.
293-
//
294-
func calcNodeCidrSize(podSubnet string) (string, bool) {
295-
maskSize := "24"
296-
isIPv6 := false
297-
if ip, podCidr, err := net.ParseCIDR(podSubnet); err == nil {
298-
if utilsnet.IsIPv6(ip) {
299-
var nodeCidrSize int
300-
isIPv6 = true
301-
podNetSize, totalBits := podCidr.Mask.Size()
302-
switch {
303-
case podNetSize == 112:
304-
// Special case, allows 256 nodes, 256 pods/node
305-
nodeCidrSize = 120
306-
case podNetSize < 112:
307-
// Use multiple of 8 for node CIDR, with 512 to 64K nodes
308-
nodeCidrSize = totalBits - ((totalBits-podNetSize-1)/8-1)*8
309-
default:
310-
// Not enough bits, will fail later, when validate
311-
nodeCidrSize = podNetSize
312-
}
313-
maskSize = strconv.Itoa(nodeCidrSize)
314-
}
315-
}
316-
return maskSize, isIPv6
317-
}
318-
319273
// getControllerManagerCommand builds the right controller manager command from the given config object and version
320274
func getControllerManagerCommand(cfg *kubeadmapi.ClusterConfiguration) []string {
321275

@@ -367,22 +321,6 @@ func getControllerManagerCommand(cfg *kubeadmapi.ClusterConfiguration) []string
367321
if present {
368322
defaultArguments["feature-gates"] = fmt.Sprintf("%s=%t", features.IPv6DualStack, enabled)
369323
}
370-
if cfg.Networking.PodSubnet != "" {
371-
if enabled {
372-
// any errors will be caught during validation
373-
subnets := strings.Split(cfg.Networking.PodSubnet, ",")
374-
for _, podSubnet := range subnets {
375-
if maskSize, isIPv6 := calcNodeCidrSize(podSubnet); isIPv6 {
376-
defaultArguments["node-cidr-mask-size-ipv6"] = maskSize
377-
} else {
378-
defaultArguments["node-cidr-mask-size-ipv4"] = maskSize
379-
}
380-
}
381-
} else {
382-
maskSize, _ := calcNodeCidrSize(cfg.Networking.PodSubnet)
383-
defaultArguments["node-cidr-mask-size"] = maskSize
384-
}
385-
}
386324

387325
command := []string{"kube-controller-manager"}
388326
command = append(command, kubeadmutil.BuildArgumentListFromMap(defaultArguments, cfg.ControllerManager.ExtraArgs)...)

0 commit comments

Comments
 (0)