diff --git a/docs/tutorials/hostport.md b/docs/tutorials/hostport.md index 75512fdf4d..9ae1314bb3 100644 --- a/docs/tutorials/hostport.md +++ b/docs/tutorials/hostport.md @@ -198,3 +198,24 @@ kafka-1.ksvc.example.org kafka-2.ksvc.example.org ``` +#### Using node external IPs as targets + +Add the following annotation to your `Service`: + +```yaml +external-dns.alpha.kubernetes.io/access: public +``` + +external-dns will now publish the node external IP of the node on which the pods backing your `Service` are running. + +#### Using pod annotations to specify target IPs + +Add the following annotation to the **pods** backing your `Service`: + +```yaml +external-dns.alpha.kubernetes.io/target: "1.2.3.4" +``` + +external-dns will publish the IP specified in the annotation of each pod instead of using the podIP advertised by Kubernetes. + +This can be useful e.g. if you are NATing public IPs onto your pod IPs and want to publish these in DNS. diff --git a/source/service.go b/source/service.go index ef3acd97df..11f6c4c0de 100644 --- a/source/service.go +++ b/source/service.go @@ -259,11 +259,13 @@ func (sc *serviceSource) extractHeadlessEndpoints(svc *v1.Service, hostname stri pods, err := sc.podInformer.Lister().Pods(svc.Namespace).List(selector) if err != nil { - log.Errorf("List Pods of service[%s] error:%v", svc.GetName(), err) + log.Errorf("List pods of service[%s] error: %v", svc.GetName(), err) return endpoints } - targetsByHeadlessDomain := make(map[string][]string) + access := getAccessFromAnnotations(svc.Annotations) + + targetsByHeadlessDomain := make(map[string]endpoint.Targets) for _, subset := range endpointsObject.Subsets { addresses := subset.Addresses if svc.Spec.PublishNotReadyAddresses || sc.alwaysPublishNotReadyAddresses { @@ -294,15 +296,29 @@ func (sc *serviceSource) extractHeadlessEndpoints(svc *v1.Service, hostname stri } for _, headlessDomain := range headlessDomains { - var ep string - if sc.publishHostIP { - ep = pod.Status.HostIP - log.Debugf("Generating matching endpoint %s with HostIP %s", headlessDomain, ep) - } else { - ep = address.IP - log.Debugf("Generating matching endpoint %s with EndpointAddress IP %s", headlessDomain, ep) + targets := getTargetsFromTargetAnnotation(pod.Annotations) + if len(targets) == 0 { + if access == "public" { + node, err := sc.nodeInformer.Lister().Get(pod.Spec.NodeName) + if err != nil { + log.Errorf("Get node[%s] of pod[%s] error: %v; not adding any NodeExternalIP endpoints", pod.Spec.NodeName, pod.GetName(), err) + return endpoints + } + for _, address := range node.Status.Addresses { + if address.Type == v1.NodeExternalIP { + targets = endpoint.Targets{address.Address} + log.Debugf("Generating matching endpoint %s with NodeExternalIP %s", headlessDomain, address.Address) + } + } + } else if sc.publishHostIP { + targets = endpoint.Targets{pod.Status.HostIP} + log.Debugf("Generating matching endpoint %s with HostIP %s", headlessDomain, pod.Status.HostIP) + } else { + targets = endpoint.Targets{address.IP} + log.Debugf("Generating matching endpoint %s with EndpointAddress IP %s", headlessDomain, address.IP) + } } - targetsByHeadlessDomain[headlessDomain] = append(targetsByHeadlessDomain[headlessDomain], ep) + targetsByHeadlessDomain[headlessDomain] = append(targetsByHeadlessDomain[headlessDomain], targets...) } } } diff --git a/source/service_test.go b/source/service_test.go index a13e151698..9956088d84 100644 --- a/source/service_test.go +++ b/source/service_test.go @@ -2008,7 +2008,8 @@ func TestHeadlessServices(t *testing.T) { fqdnTemplate string ignoreHostnameAnnotation bool labels map[string]string - annotations map[string]string + svcAnnotations map[string]string + podAnnotations map[string]string clusterIP string podIPs []string selector map[string]string @@ -2017,6 +2018,7 @@ func TestHeadlessServices(t *testing.T) { hostnames []string podsReady []bool publishNotReadyAddresses bool + nodes []v1.Node expected []*endpoint.Endpoint expectError bool }{ @@ -2033,6 +2035,7 @@ func TestHeadlessServices(t *testing.T) { map[string]string{ hostnameAnnotationKey: "service.example.org", }, + map[string]string{}, v1.ClusterIPNone, []string{"1.1.1.1", "1.1.1.2"}, map[string]string{ @@ -2043,6 +2046,7 @@ func TestHeadlessServices(t *testing.T) { []string{"foo-0", "foo-1"}, []bool{true, true}, false, + []v1.Node{}, []*endpoint.Endpoint{ {DNSName: "foo-0.service.example.org", Targets: endpoint.Targets{"1.1.1.1"}}, {DNSName: "foo-1.service.example.org", Targets: endpoint.Targets{"1.1.1.2"}}, @@ -2063,6 +2067,7 @@ func TestHeadlessServices(t *testing.T) { map[string]string{ hostnameAnnotationKey: "service.example.org", }, + map[string]string{}, v1.ClusterIPNone, []string{"1.1.1.1", "1.1.1.2"}, map[string]string{ @@ -2073,6 +2078,7 @@ func TestHeadlessServices(t *testing.T) { []string{"foo-0", "foo-1"}, []bool{true, true}, false, + []v1.Node{}, []*endpoint.Endpoint{}, false, }, @@ -2090,6 +2096,7 @@ func TestHeadlessServices(t *testing.T) { hostnameAnnotationKey: "service.example.org", ttlAnnotationKey: "1", }, + map[string]string{}, v1.ClusterIPNone, []string{"1.1.1.1", "1.1.1.2"}, map[string]string{ @@ -2100,6 +2107,7 @@ func TestHeadlessServices(t *testing.T) { []string{"foo-0", "foo-1"}, []bool{true, true}, false, + []v1.Node{}, []*endpoint.Endpoint{ {DNSName: "foo-0.service.example.org", Targets: endpoint.Targets{"1.1.1.1"}, RecordTTL: endpoint.TTL(1)}, {DNSName: "foo-1.service.example.org", Targets: endpoint.Targets{"1.1.1.2"}, RecordTTL: endpoint.TTL(1)}, @@ -2120,6 +2128,7 @@ func TestHeadlessServices(t *testing.T) { map[string]string{ hostnameAnnotationKey: "service.example.org", }, + map[string]string{}, v1.ClusterIPNone, []string{"1.1.1.1", "1.1.1.2"}, map[string]string{ @@ -2130,6 +2139,7 @@ func TestHeadlessServices(t *testing.T) { []string{"foo-0", "foo-1"}, []bool{true, false}, false, + []v1.Node{}, []*endpoint.Endpoint{ {DNSName: "foo-0.service.example.org", Targets: endpoint.Targets{"1.1.1.1"}}, {DNSName: "service.example.org", Targets: endpoint.Targets{"1.1.1.1"}}, @@ -2149,6 +2159,7 @@ func TestHeadlessServices(t *testing.T) { map[string]string{ hostnameAnnotationKey: "service.example.org", }, + map[string]string{}, v1.ClusterIPNone, []string{"1.1.1.1", "1.1.1.2"}, map[string]string{ @@ -2159,6 +2170,7 @@ func TestHeadlessServices(t *testing.T) { []string{"foo-0", "foo-1"}, []bool{true, false}, true, + []v1.Node{}, []*endpoint.Endpoint{ {DNSName: "foo-0.service.example.org", Targets: endpoint.Targets{"1.1.1.1"}}, {DNSName: "foo-1.service.example.org", Targets: endpoint.Targets{"1.1.1.2"}}, @@ -2179,6 +2191,7 @@ func TestHeadlessServices(t *testing.T) { map[string]string{ hostnameAnnotationKey: "service.example.org", }, + map[string]string{}, v1.ClusterIPNone, []string{"1.1.1.1", "1.1.1.2"}, map[string]string{ @@ -2189,6 +2202,7 @@ func TestHeadlessServices(t *testing.T) { []string{"", ""}, []bool{true, true}, false, + []v1.Node{}, []*endpoint.Endpoint{ {DNSName: "service.example.org", Targets: endpoint.Targets{"1.1.1.1", "1.1.1.2"}}, }, @@ -2207,6 +2221,7 @@ func TestHeadlessServices(t *testing.T) { map[string]string{ hostnameAnnotationKey: "service.example.org", }, + map[string]string{}, v1.ClusterIPNone, []string{"1.1.1.1", "1.1.1.1", "1.1.1.2"}, map[string]string{ @@ -2217,11 +2232,86 @@ func TestHeadlessServices(t *testing.T) { []string{"", "", ""}, []bool{true, true, true}, false, + []v1.Node{}, []*endpoint.Endpoint{ {DNSName: "service.example.org", Targets: endpoint.Targets{"1.1.1.1", "1.1.1.2"}}, }, false, }, + { + "annotated Headless services return targets from pod annotation", + "", + "testing", + "foo", + v1.ServiceTypeClusterIP, + "", + "", + false, + map[string]string{"component": "foo"}, + map[string]string{ + hostnameAnnotationKey: "service.example.org", + }, + map[string]string{ + targetAnnotationKey: "1.2.3.4", + }, + v1.ClusterIPNone, + []string{"1.1.1.1"}, + map[string]string{ + "component": "foo", + }, + []string{}, + []string{"foo"}, + []string{"", "", ""}, + []bool{true, true, true}, + false, + []v1.Node{}, + []*endpoint.Endpoint{ + {DNSName: "service.example.org", Targets: endpoint.Targets{"1.2.3.4"}}, + }, + false, + }, + { + "annotated Headless services return targets from node external IP if public access annotation is set", + "", + "testing", + "foo", + v1.ServiceTypeClusterIP, + "", + "", + false, + map[string]string{"component": "foo"}, + map[string]string{ + hostnameAnnotationKey: "service.example.org", + accessAnnotationKey: "public", + }, + map[string]string{}, + v1.ClusterIPNone, + []string{"1.1.1.1"}, + map[string]string{ + "component": "foo", + }, + []string{}, + []string{"foo"}, + []string{"", "", ""}, + []bool{true, true, true}, + false, + []v1.Node{ + { + Status: v1.NodeStatus{ + Addresses: []v1.NodeAddress{ + { + Type: v1.NodeExternalIP, + Address: "1.2.3.4", + }, + }, + }, + }, + }, + []*endpoint.Endpoint{ + {DNSName: "service.example.org", Targets: endpoint.Targets{"1.2.3.4"}}, + }, + false, + }, } { tc := tc t.Run(tc.title, func(t *testing.T) { @@ -2241,7 +2331,7 @@ func TestHeadlessServices(t *testing.T) { Namespace: tc.svcNamespace, Name: tc.svcName, Labels: tc.labels, - Annotations: tc.annotations, + Annotations: tc.svcAnnotations, }, Status: v1.ServiceStatus{}, } @@ -2259,7 +2349,7 @@ func TestHeadlessServices(t *testing.T) { Namespace: tc.svcNamespace, Name: podname, Labels: tc.labels, - Annotations: tc.annotations, + Annotations: tc.podAnnotations, }, Status: v1.PodStatus{ PodIP: tc.podIPs[i], @@ -2298,6 +2388,10 @@ func TestHeadlessServices(t *testing.T) { } _, err = kubernetes.CoreV1().Endpoints(tc.svcNamespace).Create(context.Background(), endpointsObject, metav1.CreateOptions{}) require.NoError(t, err) + for _, node := range tc.nodes { + _, err = kubernetes.CoreV1().Nodes().Create(context.Background(), &node, metav1.CreateOptions{}) + require.NoError(t, err) + } // Create our object under test and get the endpoints. client, _ := NewServiceSource(