Skip to content

Commit

Permalink
Headless service: allow to specify target as NodeExternalIP or by ann…
Browse files Browse the repository at this point in the history
…otation

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.
  • Loading branch information
alfredkrohmer committed May 5, 2022
1 parent 2d08e66 commit ea45b03
Show file tree
Hide file tree
Showing 4 changed files with 210 additions and 14 deletions.
32 changes: 32 additions & 0 deletions docs/tutorials/hostport.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
36 changes: 26 additions & 10 deletions source/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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...)
}
}
}
Expand Down
145 changes: 141 additions & 4 deletions source/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}{
Expand All @@ -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",
},
Expand All @@ -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"}},
Expand All @@ -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",
},
Expand All @@ -2073,6 +2081,7 @@ func TestHeadlessServices(t *testing.T) {
[]string{"foo-0", "foo-1"},
[]bool{true, true},
false,
[]v1.Node{},
[]*endpoint.Endpoint{},
false,
},
Expand All @@ -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",
},
Expand All @@ -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)},
Expand All @@ -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",
},
Expand All @@ -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"}},
Expand All @@ -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",
},
Expand All @@ -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"}},
Expand All @@ -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",
},
Expand All @@ -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"}},
},
Expand All @@ -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",
},
Expand All @@ -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) {
Expand All @@ -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{},
}
Expand All @@ -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],
},
}

Expand Down Expand Up @@ -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(
Expand Down
Loading

0 comments on commit ea45b03

Please sign in to comment.