diff --git a/cmd/kindnetd/cni.go b/cmd/kindnetd/cni.go index 8d2059b..aed7054 100644 --- a/cmd/kindnetd/cni.go +++ b/cmd/kindnetd/cni.go @@ -29,6 +29,7 @@ import ( "github.com/vishvananda/netlink" corev1 "k8s.io/api/core/v1" + utilsnet "k8s.io/utils/net" ) /* cni config management */ @@ -36,33 +37,41 @@ import ( // CNIConfigInputs is supplied to the CNI config template type CNIConfigInputs struct { PodCIDRs []string + RangeStart []string DefaultRoutes []string Mtu int } // ComputeCNIConfigInputs computes the template inputs for CNIConfigWriter func ComputeCNIConfigInputs(node *corev1.Node) CNIConfigInputs { - - defaultRoutes := []string{"0.0.0.0/0", "::/0"} - // check if is a dualstack cluster - if len(node.Spec.PodCIDRs) > 1 { - return CNIConfigInputs{ - PodCIDRs: node.Spec.PodCIDRs, - DefaultRoutes: defaultRoutes, + inputs := CNIConfigInputs{} + podCIDRs, _ := utilsnet.ParseCIDRs(node.Spec.PodCIDRs) // already validated + for _, podCIDR := range podCIDRs { + inputs.PodCIDRs = append(inputs.PodCIDRs, podCIDR.String()) + // define the default route + if utilsnet.IsIPv4CIDR(podCIDR) { + inputs.DefaultRoutes = append(inputs.DefaultRoutes, "0.0.0.0/0") + } else { + inputs.DefaultRoutes = append(inputs.DefaultRoutes, "::/0") } + // reserve the first IPs of the range + size := utilsnet.RangeSize(podCIDR) + podCapacity := node.Status.Capacity.Pods().Value() + if podCapacity == 0 { + podCapacity = 110 // default to 110 + } + rangeStart := "" + offset := size - podCapacity + if offset > 10 { // reserve the first 10 addresses of the Pod range if there is capacity + startAddress, err := utilsnet.GetIndexedIP(podCIDR, 10) + if err == nil { + rangeStart = startAddress.String() + } + } + inputs.RangeStart = append(inputs.RangeStart, rangeStart) + } - // the cluster is single stack - // we use the legacy node.Spec.PodCIDR for backwards compatibility - podCIDRs := []string{node.Spec.PodCIDR} - // This is a single stack cluster - defaultRoute := defaultRoutes[:1] - if isIPv6CIDRString(podCIDRs[0]) { - defaultRoute = defaultRoutes[1:] - } - return CNIConfigInputs{ - PodCIDRs: podCIDRs, - DefaultRoutes: defaultRoute, - } + return inputs } // GetMTU returns the MTU used for the IP family @@ -144,17 +153,15 @@ const cniConfigTemplate = ` "type": "host-local", "dataDir": "/run/cni-ipam-state", "routes": [ - {{$first := true}} - {{- range $route := .DefaultRoutes}} - {{if $first}}{{$first = false}}{{else}},{{end}} + {{- range $i, $route := .DefaultRoutes}} + {{- if gt $i 0 }},{{end}} { "dst": "{{ $route }}" } {{- end}} ], "ranges": [ - {{$first := true}} - {{- range $cidr := .PodCIDRs}} - {{if $first}}{{$first = false}}{{else}},{{end}} - [ { "subnet": "{{ $cidr }}" } ] + {{- range $i, $cidr := .PodCIDRs}} + {{- if gt $i 0 }},{{end}} + [ { "subnet": "{{ $cidr }}" {{ if index $.RangeStart $i }}, "rangeStart": "{{ index $.RangeStart $i }}" {{ end -}} } ] {{- end}} ] } @@ -188,16 +195,15 @@ const cniConfigTemplateBridge = ` "type": "host-local", "dataDir": "/run/cni-ipam-state", "ranges": [ - {{$first := true}} - {{- range $cidr := .PodCIDRs}} - {{if $first}}{{$first = false}}{{else}},{{end}} - [ { "subnet": "{{ $cidr }}" } ] + {{- range $i, $cidr := .PodCIDRs}} + {{- if gt $i 0 }},{{end}} + [ { "subnet": "{{ $cidr }}" {{ if index $.RangeStart $i }}, "rangeStart": "{{ index $.RangeStart $i }}" {{ end -}} } ] {{- end}} ] } - {{if .Mtu}}, + {{- if .Mtu}}, "mtu": {{ .Mtu }} - {{end}} + {{- end}} }, { "type": "portmap", diff --git a/cmd/kindnetd/cni_test.go b/cmd/kindnetd/cni_test.go new file mode 100644 index 0000000..46cec1a --- /dev/null +++ b/cmd/kindnetd/cni_test.go @@ -0,0 +1,127 @@ +/* +Copyright 2019 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 main + +import ( + "bytes" + "encoding/json" + "testing" + + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func Test_writeCNIConfig(t *testing.T) { + tests := []struct { + name string + node *v1.Node + mtu int + wantW string + wantErr bool + }{ + { + name: "ipv4 only and ptp plugin and start range", + mtu: 1500, + node: &v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "node0", + }, + Spec: v1.NodeSpec{ + PodCIDRs: []string{"192.168.0.0/24"}, + }, + Status: v1.NodeStatus{ + Capacity: v1.ResourceList{ + v1.ResourcePods: resource.MustParse("110"), + }, + }, + }, + }, + { + name: "dual stack only and ptp plugin and start range", + node: &v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "node0", + }, + Spec: v1.NodeSpec{ + PodCIDRs: []string{"192.168.0.0/24", "fd00:1:2:3::/96"}, + }, + Status: v1.NodeStatus{ + Capacity: v1.ResourceList{ + v1.ResourcePods: resource.MustParse("110"), + }, + }, + }, + }, + { + name: "ipv4 only and ptp plugin and no start range", + mtu: 1500, + node: &v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "node0", + }, + Spec: v1.NodeSpec{ + PodCIDRs: []string{"192.168.0.0/24"}, + }, + Status: v1.NodeStatus{ + Capacity: v1.ResourceList{ + v1.ResourcePods: resource.MustParse("255"), + }, + }, + }, + }, + { + name: "dual stack only and ptp plugin and no start range", + node: &v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "node0", + }, + Spec: v1.NodeSpec{ + PodCIDRs: []string{"192.168.0.0/24", "fd00:1:2:3::/96"}, + }, + Status: v1.NodeStatus{ + Capacity: v1.ResourceList{ + v1.ResourcePods: resource.MustParse("255"), + }, + }, + }, + }, + } + for _, tt := range tests { + for _, cniTemplate := range []string{cniConfigTemplate, cniConfigTemplateBridge} { + t.Run(tt.name, func(t *testing.T) { + w := &bytes.Buffer{} + data := ComputeCNIConfigInputs(tt.node) + data.Mtu = tt.mtu + if err := writeCNIConfig(w, cniTemplate, data); (err != nil) != tt.wantErr { + t.Errorf("writeCNIConfig() error = %v, wantErr %v", err, tt.wantErr) + return + } + t.Logf("CNI input:\n%#v", data) + t.Logf("CNI config:\n%s", w.String()) + // is valid json + if !json.Valid([]byte(w.String())) { + t.Errorf("Invalid Json: %s", w.String()) + } + if gotW := w.String(); gotW != tt.wantW { + // TODO validate the content + // t.Errorf("writeCNIConfig() = %v, want %v", gotW, tt.wantW) + } + }) + } + } +}