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
```
#### 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)
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...)
}
}
}

View File

@ -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(

View File

@ -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 {