diff --git a/dns-controller/pkg/watchers/pod.go b/dns-controller/pkg/watchers/pod.go index 6bda5ca75c160..d6d914d3c3232 100644 --- a/dns-controller/pkg/watchers/pod.go +++ b/dns-controller/pkg/watchers/pod.go @@ -29,6 +29,7 @@ import ( "k8s.io/klog/v2" "k8s.io/kops/dns-controller/pkg/dns" "k8s.io/kops/dns-controller/pkg/util" + "k8s.io/kops/upup/pkg/fi/utils" ) // PodController watches for Pods with dns annotations @@ -142,6 +143,25 @@ func (c *PodController) runWatcher(stopCh <-chan struct{}) { } } +// podIPsMatchingNodeInternalIP checks which pod IPs match a NodeInternalIP address on the specified node +func (c *PodController) podIPsMatchingNodeInternalIP(pod *v1.Pod) []string { + ctx := context.TODO() + var internalIPs []string + node, err := c.client.CoreV1().Nodes().Get(ctx, pod.Spec.NodeName, metav1.GetOptions{}) + if err != nil { + klog.Warningf("Failed to get node %q: %v", pod.Spec.NodeName, err) + return []string{} + } + for _, podIP := range pod.Status.PodIPs { + for _, addr := range node.Status.Addresses { + if addr.Type == v1.NodeInternalIP && addr.Address == podIP.IP { + internalIPs = append(internalIPs, addr.Address) + } + } + } + return internalIPs +} + // updatePodRecords will apply the records for the specified pod. It returns the key that was set. func (c *PodController) updatePodRecords(pod *v1.Pod) string { var records []dns.Record @@ -175,28 +195,55 @@ func (c *PodController) updatePodRecords(pod *v1.Pod) string { } specInternal := pod.Annotations[AnnotationNameDNSInternal] + var recordType dns.RecordType + if specInternal != "" { - var aliases []string - if pod.Spec.HostNetwork { - if pod.Spec.NodeName != "" { - aliases = append(aliases, "node/"+pod.Spec.NodeName+"/internal") - } - } else { - klog.V(4).Infof("Pod %q had %s=%s, but was not HostNetwork", pod.Name, AnnotationNameDNSInternal, specInternal) - } tokens := strings.Split(specInternal, ",") - for _, token := range tokens { - token = strings.TrimSpace(token) - fqdn := dns.EnsureDotSuffix(token) - for _, alias := range aliases { - records = append(records, dns.Record{ - RecordType: dns.RecordTypeAlias, - FQDN: fqdn, - Value: alias, - }) + if pod.Spec.NodeName != "" && pod.Spec.HostNetwork { + if len(pod.Status.PodIPs) == 1 { + // The pod has a single IP, we will use it without checking against NodeInternalIP + if utils.IsIPv6IP(pod.Status.PodIP) { + recordType = dns.RecordTypeAAAA + } else { + recordType = dns.RecordTypeA + } + for _, token := range tokens { + token = strings.TrimSpace(token) + fqdn := dns.EnsureDotSuffix(token) + records = append(records, dns.Record{ + RecordType: recordType, + FQDN: fqdn, + Value: pod.Status.PodIP, + }) + } + } else { + // The pod has multiple IPs, we need to check which match NodeInternalIP + podInternalIPs := c.podIPsMatchingNodeInternalIP(pod) + if len(podInternalIPs) == 0 { + klog.V(4).Infof("Pod %q IPs do not match any NodeInternalIP on node %q", pod.Name, pod.Spec.NodeName) + } else { + for _, token := range tokens { + token = strings.TrimSpace(token) + fqdn := dns.EnsureDotSuffix(token) + for _, podIP := range podInternalIPs { + if utils.IsIPv6IP(podIP) { + recordType = dns.RecordTypeAAAA + } else { + recordType = dns.RecordTypeA + } + records = append(records, dns.Record{ + RecordType: recordType, + FQDN: fqdn, + Value: podIP, + }) + } + } + } } + } else { + klog.V(4).Infof("Pod %q had %s=%s, but was not HostNetwork or NodeName not set", pod.Name, AnnotationNameDNSInternal, specInternal) } } else { klog.V(4).Infof("Pod %q did not have %s label", pod.Name, AnnotationNameDNSInternal) diff --git a/dns-controller/pkg/watchers/pod_test.go b/dns-controller/pkg/watchers/pod_test.go index bf376e4025eac..8b174345ca7d4 100644 --- a/dns-controller/pkg/watchers/pod_test.go +++ b/dns-controller/pkg/watchers/pod_test.go @@ -28,14 +28,34 @@ import ( "k8s.io/kops/dns-controller/pkg/dns" ) -func TestPodController(t *testing.T) { +func TestPodControllerMultipleIPs(t *testing.T) { ctx := context.Background() + + nspec := &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-node", + }, + Status: corev1.NodeStatus{ + Addresses: []corev1.NodeAddress{ + // Internal IPs allocated to the Pod + {Type: corev1.NodeInternalIP, Address: "10.0.0.1"}, + {Type: corev1.NodeInternalIP, Address: "fd00:1234:5678:abcd::1"}, + // The following 2 addresses should be ignored, as they are not associated allocated to the Pod + {Type: corev1.NodeInternalIP, Address: "10.0.0.3"}, + {Type: corev1.NodeInternalIP, Address: "10.0.0.4"}, + // External IPs + {Type: corev1.NodeExternalIP, Address: "2001:db8:0:0:0:ff00:42:8329"}, + {Type: corev1.NodeExternalIP, Address: "54.100.0.1"}, + }, + }, + } + pspec := &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "somepod", Namespace: "kube-system", Annotations: map[string]string{ - "dns.alpha.kubernetes.io/internal": "internal.a.foo.com", + "dns.alpha.kubernetes.io/internal": "internal.a.foo.com,internal.b.foo.com", "dns.alpha.kubernetes.io/external": "a.foo.com", }, }, @@ -47,14 +67,93 @@ func TestPodController(t *testing.T) { PodIP: "10.0.0.1", PodIPs: []corev1.PodIP{ {IP: "10.0.0.1"}, + {IP: "fd00:1234:5678:abcd::1"}, {IP: "2001:db8:0:0:0:ff00:42:8329"}, + {IP: "54.100.0.1"}, }, }, } client := fake.NewClientset() + + nodes := client.CoreV1().Nodes() + _, err := nodes.Create(ctx, nspec, metav1.CreateOptions{}) + if err != nil { + t.Fatalf("unexpected error creating node: %v", err) + } + pods := client.CoreV1().Pods("kube-system") + _, err = pods.Create(ctx, pspec, metav1.CreateOptions{}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + ch := make(chan struct{}) + scope := &fakeScope{ + readyCh: ch, + records: make(map[string][]dns.Record), + } + + dnsctx := &fakeDNSContext{ + scope: scope, + } + + c, err := NewPodController(client, dnsctx, "kube-system") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + go c.Run() + + select { + case <-ch: + case <-time.After(time.Second): + t.Fatalf("update was not marked as complete") + } + + c.Stop() + + want := map[string][]dns.Record{ + "kube-system/somepod": { + {RecordType: "_alias", FQDN: "a.foo.com.", Value: "node/my-node/external"}, + {RecordType: "A", FQDN: "internal.a.foo.com.", Value: "10.0.0.1"}, + {RecordType: "AAAA", FQDN: "internal.a.foo.com.", Value: "fd00:1234:5678:abcd::1"}, + {RecordType: "A", FQDN: "internal.b.foo.com.", Value: "10.0.0.1"}, + {RecordType: "AAAA", FQDN: "internal.b.foo.com.", Value: "fd00:1234:5678:abcd::1"}, + }, + } + if diff := cmp.Diff(scope.records, want); diff != "" { + t.Fatalf("generated records did not match expected; diff=%s", diff) + } +} + +func TestPodControllerSingleIP(t *testing.T) { + // Validates that if a pod has a single IP address, no call to the API server will be made to fetch Node information + ctx := context.Background() + + pspec := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "somepod", + Namespace: "kube-system", + Annotations: map[string]string{ + "dns.alpha.kubernetes.io/internal": "internal.a.foo.com,internal.b.foo.com", + }, + }, + Spec: corev1.PodSpec{ + HostNetwork: true, + NodeName: "my-node", + }, + Status: corev1.PodStatus{ + PodIP: "fd00:1234:5678:abcd::1", + PodIPs: []corev1.PodIP{ + {IP: "fd00:1234:5678:abcd::1"}, + }, + }, + } + client := fake.NewClientset() + + pods := client.CoreV1().Pods("kube-system") _, err := pods.Create(ctx, pspec, metav1.CreateOptions{}) if err != nil { t.Fatalf("unexpected error: %v", err) @@ -87,8 +186,8 @@ func TestPodController(t *testing.T) { want := map[string][]dns.Record{ "kube-system/somepod": { - {RecordType: "_alias", FQDN: "a.foo.com.", Value: "node/my-node/external"}, - {RecordType: "_alias", FQDN: "internal.a.foo.com.", Value: "node/my-node/internal"}, + {RecordType: "AAAA", FQDN: "internal.a.foo.com.", Value: "fd00:1234:5678:abcd::1"}, + {RecordType: "AAAA", FQDN: "internal.b.foo.com.", Value: "fd00:1234:5678:abcd::1"}, }, } if diff := cmp.Diff(scope.records, want); diff != "" {