diff --git a/docs/tutorials/hostport.md b/docs/tutorials/hostport.md index 75512fdf4d..774a756a6f 100644 --- a/docs/tutorials/hostport.md +++ b/docs/tutorials/hostport.md @@ -198,3 +198,35 @@ kafka-1.ksvc.example.org kafka-2.ksvc.example.org ``` +#### Using pods' HostIPs as targets + +Add the following annotation to your `Service`: + +```yaml +external-dns.alpha.kubernetes.io/endpoints-type: HostIP +``` + +external-dns will now publish the value of the `.status.hostIP` field of the pods backing your `Service`. +``` + +#### Using node external IPs as targets + +Add the following annotation to your `Service`: + +```yaml +external-dns.alpha.kubernetes.io/endpoints-type: NodeExternalIP +``` + +external-dns will now publish the node external IP (`.status.addresses` entries of with `type: NodeExternalIP`) of the nodes 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..9c47579dda 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) + endpointsType := getEndpointsTypeFromAnnotations(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 endpointsType == EndpointsTypeNodeExternalIP { + 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 endpointsType == EndpointsTypeHostIP || 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..3fffc256cd 100644 --- a/source/service_test.go +++ b/source/service_test.go @@ -2008,15 +2008,18 @@ 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 + hostIPs []string selector map[string]string lbs []string podnames []string hostnames []string podsReady []bool publishNotReadyAddresses bool + nodes []v1.Node expected []*endpoint.Endpoint expectError bool }{ @@ -2033,8 +2036,10 @@ 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"}, + []string{"", ""}, map[string]string{ "component": "foo", }, @@ -2043,6 +2048,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,8 +2069,10 @@ 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"}, + []string{"", ""}, map[string]string{ "component": "foo", }, @@ -2073,6 +2081,7 @@ func TestHeadlessServices(t *testing.T) { []string{"foo-0", "foo-1"}, []bool{true, true}, false, + []v1.Node{}, []*endpoint.Endpoint{}, false, }, @@ -2090,8 +2099,10 @@ 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"}, + []string{"", ""}, map[string]string{ "component": "foo", }, @@ -2100,6 +2111,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,8 +2132,10 @@ 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"}, + []string{"", ""}, map[string]string{ "component": "foo", }, @@ -2130,6 +2144,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,8 +2164,10 @@ 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"}, + []string{"", ""}, map[string]string{ "component": "foo", }, @@ -2159,6 +2176,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,8 +2197,10 @@ 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"}, + []string{"", ""}, map[string]string{ "component": "foo", }, @@ -2189,6 +2209,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,8 +2228,10 @@ 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"}, + []string{"", "", ""}, map[string]string{ "component": "foo", }, @@ -2217,11 +2240,120 @@ 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"}, + []string{""}, + 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 endpoints-type annotation is set", + "", + "testing", + "foo", + v1.ServiceTypeClusterIP, + "", + "", + false, + map[string]string{"component": "foo"}, + map[string]string{ + hostnameAnnotationKey: "service.example.org", + endpointsTypeAnnotationKey: EndpointsTypeNodeExternalIP, + }, + map[string]string{}, + v1.ClusterIPNone, + []string{"1.1.1.1"}, + []string{""}, + 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, + }, + { + "annotated Headless services return targets from hostIP if endpoints-type annotation is set", + "", + "testing", + "foo", + v1.ServiceTypeClusterIP, + "", + "", + false, + map[string]string{"component": "foo"}, + map[string]string{ + hostnameAnnotationKey: "service.example.org", + endpointsTypeAnnotationKey: EndpointsTypeHostIP, + }, + map[string]string{}, + v1.ClusterIPNone, + []string{"1.1.1.1"}, + []string{"1.2.3.4"}, + 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, + }, } { tc := tc t.Run(tc.title, func(t *testing.T) { @@ -2241,7 +2373,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,10 +2391,11 @@ 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], + PodIP: tc.podIPs[i], + HostIP: tc.hostIPs[i], }, } @@ -2298,6 +2431,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( diff --git a/source/source.go b/source/source.go index 91214b5f38..a59e368ef5 100644 --- a/source/source.go +++ b/source/source.go @@ -44,6 +44,8 @@ const ( hostnameAnnotationKey = "external-dns.alpha.kubernetes.io/hostname" // The annotation used for specifying whether the public or private interface address is used accessAnnotationKey = "external-dns.alpha.kubernetes.io/access" + // The annotation used for specifying the type of endpoints to use for headless services + endpointsTypeAnnotationKey = "external-dns.alpha.kubernetes.io/endpoints-type" // The annotation used for defining the desired ingress target targetAnnotationKey = "external-dns.alpha.kubernetes.io/target" // The annotation used for defining the desired DNS record TTL @@ -59,6 +61,11 @@ const ( internalHostnameAnnotationKey = "external-dns.alpha.kubernetes.io/internal-hostname" ) +const ( + EndpointsTypeNodeExternalIP = "NodeExternalIP" + EndpointsTypeHostIP = "HostIP" +) + // Provider-specific annotations const ( // The annotation used for determining if traffic will go through Cloudflare @@ -151,6 +158,10 @@ func getAccessFromAnnotations(annotations map[string]string) string { return annotations[accessAnnotationKey] } +func getEndpointsTypeFromAnnotations(annotations map[string]string) string { + return annotations[endpointsTypeAnnotationKey] +} + func getInternalHostnamesFromAnnotations(annotations map[string]string) []string { internalHostnameAnnotation, exists := annotations[internalHostnameAnnotationKey] if !exists {