Headless service: allow to specify target as NodeExternalIP or by annotation

If external-dns.alpha.kubernetes.io/target annotation is present on a
pod, it's value will be used as the target for the headless service.

If annotation external-dns.alpha.kubernetes.io/access=public is present,
NodeExternalIP of the node running the pod is used as the target for the
headless service.
This commit is contained in:
Alfred Krohmer 2021-04-27 11:00:18 +02:00 committed by Alfred Krohmer
parent 2d08e66611
commit ea45b03972
4 changed files with 210 additions and 14 deletions

View File

@ -198,3 +198,35 @@ kafka-1.ksvc.example.org
kafka-2.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.

View File

@ -259,11 +259,13 @@ func (sc *serviceSource) extractHeadlessEndpoints(svc *v1.Service, hostname stri
pods, err := sc.podInformer.Lister().Pods(svc.Namespace).List(selector) pods, err := sc.podInformer.Lister().Pods(svc.Namespace).List(selector)
if err != nil { 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 return endpoints
} }
targetsByHeadlessDomain := make(map[string][]string) endpointsType := getEndpointsTypeFromAnnotations(svc.Annotations)
targetsByHeadlessDomain := make(map[string]endpoint.Targets)
for _, subset := range endpointsObject.Subsets { for _, subset := range endpointsObject.Subsets {
addresses := subset.Addresses addresses := subset.Addresses
if svc.Spec.PublishNotReadyAddresses || sc.alwaysPublishNotReadyAddresses { if svc.Spec.PublishNotReadyAddresses || sc.alwaysPublishNotReadyAddresses {
@ -294,15 +296,29 @@ func (sc *serviceSource) extractHeadlessEndpoints(svc *v1.Service, hostname stri
} }
for _, headlessDomain := range headlessDomains { for _, headlessDomain := range headlessDomains {
var ep string targets := getTargetsFromTargetAnnotation(pod.Annotations)
if sc.publishHostIP { if len(targets) == 0 {
ep = pod.Status.HostIP if endpointsType == EndpointsTypeNodeExternalIP {
log.Debugf("Generating matching endpoint %s with HostIP %s", headlessDomain, ep) node, err := sc.nodeInformer.Lister().Get(pod.Spec.NodeName)
} else { if err != nil {
ep = address.IP log.Errorf("Get node[%s] of pod[%s] error: %v; not adding any NodeExternalIP endpoints", pod.Spec.NodeName, pod.GetName(), err)
log.Debugf("Generating matching endpoint %s with EndpointAddress IP %s", headlessDomain, ep) 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...)
} }
} }
} }

View File

@ -2008,15 +2008,18 @@ func TestHeadlessServices(t *testing.T) {
fqdnTemplate string fqdnTemplate string
ignoreHostnameAnnotation bool ignoreHostnameAnnotation bool
labels map[string]string labels map[string]string
annotations map[string]string svcAnnotations map[string]string
podAnnotations map[string]string
clusterIP string clusterIP string
podIPs []string podIPs []string
hostIPs []string
selector map[string]string selector map[string]string
lbs []string lbs []string
podnames []string podnames []string
hostnames []string hostnames []string
podsReady []bool podsReady []bool
publishNotReadyAddresses bool publishNotReadyAddresses bool
nodes []v1.Node
expected []*endpoint.Endpoint expected []*endpoint.Endpoint
expectError bool expectError bool
}{ }{
@ -2033,8 +2036,10 @@ func TestHeadlessServices(t *testing.T) {
map[string]string{ map[string]string{
hostnameAnnotationKey: "service.example.org", hostnameAnnotationKey: "service.example.org",
}, },
map[string]string{},
v1.ClusterIPNone, v1.ClusterIPNone,
[]string{"1.1.1.1", "1.1.1.2"}, []string{"1.1.1.1", "1.1.1.2"},
[]string{"", ""},
map[string]string{ map[string]string{
"component": "foo", "component": "foo",
}, },
@ -2043,6 +2048,7 @@ func TestHeadlessServices(t *testing.T) {
[]string{"foo-0", "foo-1"}, []string{"foo-0", "foo-1"},
[]bool{true, true}, []bool{true, true},
false, false,
[]v1.Node{},
[]*endpoint.Endpoint{ []*endpoint.Endpoint{
{DNSName: "foo-0.service.example.org", Targets: endpoint.Targets{"1.1.1.1"}}, {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"}}, {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{ map[string]string{
hostnameAnnotationKey: "service.example.org", hostnameAnnotationKey: "service.example.org",
}, },
map[string]string{},
v1.ClusterIPNone, v1.ClusterIPNone,
[]string{"1.1.1.1", "1.1.1.2"}, []string{"1.1.1.1", "1.1.1.2"},
[]string{"", ""},
map[string]string{ map[string]string{
"component": "foo", "component": "foo",
}, },
@ -2073,6 +2081,7 @@ func TestHeadlessServices(t *testing.T) {
[]string{"foo-0", "foo-1"}, []string{"foo-0", "foo-1"},
[]bool{true, true}, []bool{true, true},
false, false,
[]v1.Node{},
[]*endpoint.Endpoint{}, []*endpoint.Endpoint{},
false, false,
}, },
@ -2090,8 +2099,10 @@ func TestHeadlessServices(t *testing.T) {
hostnameAnnotationKey: "service.example.org", hostnameAnnotationKey: "service.example.org",
ttlAnnotationKey: "1", ttlAnnotationKey: "1",
}, },
map[string]string{},
v1.ClusterIPNone, v1.ClusterIPNone,
[]string{"1.1.1.1", "1.1.1.2"}, []string{"1.1.1.1", "1.1.1.2"},
[]string{"", ""},
map[string]string{ map[string]string{
"component": "foo", "component": "foo",
}, },
@ -2100,6 +2111,7 @@ func TestHeadlessServices(t *testing.T) {
[]string{"foo-0", "foo-1"}, []string{"foo-0", "foo-1"},
[]bool{true, true}, []bool{true, true},
false, false,
[]v1.Node{},
[]*endpoint.Endpoint{ []*endpoint.Endpoint{
{DNSName: "foo-0.service.example.org", Targets: endpoint.Targets{"1.1.1.1"}, RecordTTL: endpoint.TTL(1)}, {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)}, {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{ map[string]string{
hostnameAnnotationKey: "service.example.org", hostnameAnnotationKey: "service.example.org",
}, },
map[string]string{},
v1.ClusterIPNone, v1.ClusterIPNone,
[]string{"1.1.1.1", "1.1.1.2"}, []string{"1.1.1.1", "1.1.1.2"},
[]string{"", ""},
map[string]string{ map[string]string{
"component": "foo", "component": "foo",
}, },
@ -2130,6 +2144,7 @@ func TestHeadlessServices(t *testing.T) {
[]string{"foo-0", "foo-1"}, []string{"foo-0", "foo-1"},
[]bool{true, false}, []bool{true, false},
false, false,
[]v1.Node{},
[]*endpoint.Endpoint{ []*endpoint.Endpoint{
{DNSName: "foo-0.service.example.org", Targets: endpoint.Targets{"1.1.1.1"}}, {DNSName: "foo-0.service.example.org", Targets: endpoint.Targets{"1.1.1.1"}},
{DNSName: "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{ map[string]string{
hostnameAnnotationKey: "service.example.org", hostnameAnnotationKey: "service.example.org",
}, },
map[string]string{},
v1.ClusterIPNone, v1.ClusterIPNone,
[]string{"1.1.1.1", "1.1.1.2"}, []string{"1.1.1.1", "1.1.1.2"},
[]string{"", ""},
map[string]string{ map[string]string{
"component": "foo", "component": "foo",
}, },
@ -2159,6 +2176,7 @@ func TestHeadlessServices(t *testing.T) {
[]string{"foo-0", "foo-1"}, []string{"foo-0", "foo-1"},
[]bool{true, false}, []bool{true, false},
true, true,
[]v1.Node{},
[]*endpoint.Endpoint{ []*endpoint.Endpoint{
{DNSName: "foo-0.service.example.org", Targets: endpoint.Targets{"1.1.1.1"}}, {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"}}, {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{ map[string]string{
hostnameAnnotationKey: "service.example.org", hostnameAnnotationKey: "service.example.org",
}, },
map[string]string{},
v1.ClusterIPNone, v1.ClusterIPNone,
[]string{"1.1.1.1", "1.1.1.2"}, []string{"1.1.1.1", "1.1.1.2"},
[]string{"", ""},
map[string]string{ map[string]string{
"component": "foo", "component": "foo",
}, },
@ -2189,6 +2209,7 @@ func TestHeadlessServices(t *testing.T) {
[]string{"", ""}, []string{"", ""},
[]bool{true, true}, []bool{true, true},
false, false,
[]v1.Node{},
[]*endpoint.Endpoint{ []*endpoint.Endpoint{
{DNSName: "service.example.org", Targets: endpoint.Targets{"1.1.1.1", "1.1.1.2"}}, {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{ map[string]string{
hostnameAnnotationKey: "service.example.org", hostnameAnnotationKey: "service.example.org",
}, },
map[string]string{},
v1.ClusterIPNone, v1.ClusterIPNone,
[]string{"1.1.1.1", "1.1.1.1", "1.1.1.2"}, []string{"1.1.1.1", "1.1.1.1", "1.1.1.2"},
[]string{"", "", ""},
map[string]string{ map[string]string{
"component": "foo", "component": "foo",
}, },
@ -2217,11 +2240,120 @@ func TestHeadlessServices(t *testing.T) {
[]string{"", "", ""}, []string{"", "", ""},
[]bool{true, true, true}, []bool{true, true, true},
false, false,
[]v1.Node{},
[]*endpoint.Endpoint{ []*endpoint.Endpoint{
{DNSName: "service.example.org", Targets: endpoint.Targets{"1.1.1.1", "1.1.1.2"}}, {DNSName: "service.example.org", Targets: endpoint.Targets{"1.1.1.1", "1.1.1.2"}},
}, },
false, 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 tc := tc
t.Run(tc.title, func(t *testing.T) { t.Run(tc.title, func(t *testing.T) {
@ -2241,7 +2373,7 @@ func TestHeadlessServices(t *testing.T) {
Namespace: tc.svcNamespace, Namespace: tc.svcNamespace,
Name: tc.svcName, Name: tc.svcName,
Labels: tc.labels, Labels: tc.labels,
Annotations: tc.annotations, Annotations: tc.svcAnnotations,
}, },
Status: v1.ServiceStatus{}, Status: v1.ServiceStatus{},
} }
@ -2259,10 +2391,11 @@ func TestHeadlessServices(t *testing.T) {
Namespace: tc.svcNamespace, Namespace: tc.svcNamespace,
Name: podname, Name: podname,
Labels: tc.labels, Labels: tc.labels,
Annotations: tc.annotations, Annotations: tc.podAnnotations,
}, },
Status: v1.PodStatus{ 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{}) _, err = kubernetes.CoreV1().Endpoints(tc.svcNamespace).Create(context.Background(), endpointsObject, metav1.CreateOptions{})
require.NoError(t, err) 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. // Create our object under test and get the endpoints.
client, _ := NewServiceSource( client, _ := NewServiceSource(

View File

@ -44,6 +44,8 @@ const (
hostnameAnnotationKey = "external-dns.alpha.kubernetes.io/hostname" hostnameAnnotationKey = "external-dns.alpha.kubernetes.io/hostname"
// The annotation used for specifying whether the public or private interface address is used // The annotation used for specifying whether the public or private interface address is used
accessAnnotationKey = "external-dns.alpha.kubernetes.io/access" 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 // The annotation used for defining the desired ingress target
targetAnnotationKey = "external-dns.alpha.kubernetes.io/target" targetAnnotationKey = "external-dns.alpha.kubernetes.io/target"
// The annotation used for defining the desired DNS record TTL // The annotation used for defining the desired DNS record TTL
@ -59,6 +61,11 @@ const (
internalHostnameAnnotationKey = "external-dns.alpha.kubernetes.io/internal-hostname" internalHostnameAnnotationKey = "external-dns.alpha.kubernetes.io/internal-hostname"
) )
const (
EndpointsTypeNodeExternalIP = "NodeExternalIP"
EndpointsTypeHostIP = "HostIP"
)
// Provider-specific annotations // Provider-specific annotations
const ( const (
// The annotation used for determining if traffic will go through Cloudflare // 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] return annotations[accessAnnotationKey]
} }
func getEndpointsTypeFromAnnotations(annotations map[string]string) string {
return annotations[endpointsTypeAnnotationKey]
}
func getInternalHostnamesFromAnnotations(annotations map[string]string) []string { func getInternalHostnamesFromAnnotations(annotations map[string]string) []string {
internalHostnameAnnotation, exists := annotations[internalHostnameAnnotationKey] internalHostnameAnnotation, exists := annotations[internalHostnameAnnotationKey]
if !exists { if !exists {