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 31, 2021
1 parent 5806e34 commit 30608f8
Show file tree
Hide file tree
Showing 3 changed files with 139 additions and 12 deletions.
21 changes: 21 additions & 0 deletions docs/tutorials/hostport.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
30 changes: 21 additions & 9 deletions source/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,8 @@ func (sc *serviceSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, e
func (sc *serviceSource) extractHeadlessEndpoints(svc *v1.Service, hostname string, ttl endpoint.TTL) []*endpoint.Endpoint {
var endpoints []*endpoint.Endpoint

access := getAccessFromAnnotations(svc.Annotations)

labelSelector, err := metav1.ParseToLabelSelector(labels.Set(svc.Spec.Selector).AsSelectorPreValidated().String())
if err != nil {
return nil
Expand All @@ -275,7 +277,7 @@ func (sc *serviceSource) extractHeadlessEndpoints(svc *v1.Service, hostname stri
return endpoints
}

targetsByHeadlessDomain := make(map[string][]string)
targetsByHeadlessDomain := make(map[string]endpoint.Targets)
for _, subset := range endpointsObject.Subsets {
addresses := subset.Addresses
if svc.Spec.PublishNotReadyAddresses || sc.alwaysPublishNotReadyAddresses {
Expand Down Expand Up @@ -306,15 +308,25 @@ 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, _ := sc.nodeInformer.Lister().Get(pod.Spec.NodeName)
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...)
}
}
}
Expand Down
100 changes: 97 additions & 3 deletions source/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2086,7 +2086,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
Expand All @@ -2095,6 +2096,7 @@ func TestHeadlessServices(t *testing.T) {
hostnames []string
podsReady []bool
publishNotReadyAddresses bool
nodes []v1.Node
expected []*endpoint.Endpoint
expectError bool
}{
Expand All @@ -2111,6 +2113,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{
Expand All @@ -2121,6 +2124,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 @@ -2141,6 +2145,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{
Expand All @@ -2151,6 +2156,7 @@ func TestHeadlessServices(t *testing.T) {
[]string{"foo-0", "foo-1"},
[]bool{true, true},
false,
[]v1.Node{},
[]*endpoint.Endpoint{},
false,
},
Expand All @@ -2168,6 +2174,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{
Expand All @@ -2178,6 +2185,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 @@ -2198,6 +2206,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{
Expand All @@ -2208,6 +2217,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 @@ -2227,6 +2237,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{
Expand All @@ -2237,6 +2248,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 @@ -2257,6 +2269,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{
Expand All @@ -2267,6 +2280,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 @@ -2285,6 +2299,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{
Expand All @@ -2295,11 +2310,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,
},
} {
t.Run(tc.title, func(t *testing.T) {
// Create a Kubernetes testing client
Expand All @@ -2316,7 +2406,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 @@ -2334,7 +2424,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],
Expand Down Expand Up @@ -2373,6 +2463,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

0 comments on commit 30608f8

Please sign in to comment.