Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions docs/tutorials/nodes.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
This tutorial describes how to configure ExternalDNS to use the cluster nodes as source.
Using nodes (`--source=node`) as source is possible to synchronize a DNS zone with the nodes of a cluster.

The node source adds an `A` record per each node `externalIP` (if not found, node's `internalIP` is used).
The TTL record can be set with the `external-dns.alpha.kubernetes.io/ttl` node annotation.
The node source adds an `A` record per each node `externalIP` (if not found, any IPv4 `internalIP` is used instead).
It also adds an `AAAA` record per each node IPv6 `internalIP`.
The TTL of the records can be set with the `external-dns.alpha.kubernetes.io/ttl` node annotation.

## Manifest (for cluster without RBAC enabled)

Expand Down
10 changes: 6 additions & 4 deletions source/compatibility.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,11 +157,13 @@ func legacyEndpointsFromDNSControllerNodePortService(svc *v1.Service, sc *servic
continue
}
for _, address := range node.Status.Addresses {
if address.Type == v1.NodeExternalIP && isExternal {
endpoints = append(endpoints, endpoint.NewEndpoint(hostname, endpoint.RecordTypeA, address.Address))
recordType := suitableType(address.Address)
// IPv6 addresses are labeled as NodeInternalIP despite being usable externally as well.
if isExternal && (address.Type == v1.NodeExternalIP || (address.Type == v1.NodeInternalIP && recordType == endpoint.RecordTypeAAAA)) {
endpoints = append(endpoints, endpoint.NewEndpoint(hostname, recordType, address.Address))
}
if address.Type == v1.NodeInternalIP && isInternal {
endpoints = append(endpoints, endpoint.NewEndpoint(hostname, endpoint.RecordTypeA, address.Address))
if isInternal && address.Type == v1.NodeInternalIP {
endpoints = append(endpoints, endpoint.NewEndpoint(hostname, recordType, address.Address))
}
}
}
Expand Down
36 changes: 25 additions & 11 deletions source/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,11 @@ func NewNodeSource(ctx context.Context, kubeClient kubernetes.Interface, annotat
}, nil
}

type endpointsKey struct {
dnsName string
recordType string
}

// Endpoints returns endpoint objects for each service that should be processed.
func (ns *nodeSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error) {
nodes, err := ns.nodeInformer.Lister().List(labels.Everything())
Expand All @@ -88,7 +93,7 @@ func (ns *nodeSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, erro
return nil, err
}

endpoints := map[string]*endpoint.Endpoint{}
endpoints := map[endpointsKey]*endpoint.Endpoint{}

// create endpoints for all nodes
for _, node := range nodes {
Expand All @@ -109,8 +114,7 @@ func (ns *nodeSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, erro

// create new endpoint with the information we already have
ep := &endpoint.Endpoint{
RecordType: "A", // hardcoded DNS record type
RecordTTL: ttl,
RecordTTL: ttl,
}

if ns.fqdnTemplate != nil {
Expand All @@ -134,14 +138,19 @@ func (ns *nodeSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, erro
return nil, fmt.Errorf("failed to get node address from %s: %s", node.Name, err.Error())
}

ep.Targets = endpoint.Targets(addrs)
ep.Labels = endpoint.NewLabels()

log.Debugf("adding endpoint %s", ep)
if _, ok := endpoints[ep.DNSName]; ok {
endpoints[ep.DNSName].Targets = append(endpoints[ep.DNSName].Targets, ep.Targets...)
} else {
endpoints[ep.DNSName] = ep
for _, addr := range addrs {
log.Debugf("adding endpoint %s target %s", ep, addr)
key := endpointsKey{
dnsName: ep.DNSName,
recordType: suitableType(addr),
}
if _, ok := endpoints[key]; !ok {
epCopy := *ep
epCopy.RecordType = key.recordType
endpoints[key] = &epCopy
}
endpoints[key].Targets = append(endpoints[key].Targets, addr)
}
}

Expand All @@ -163,13 +172,18 @@ func (ns *nodeSource) nodeAddresses(node *v1.Node) ([]string, error) {
v1.NodeExternalIP: {},
v1.NodeInternalIP: {},
}
var ipv6Addresses []string

for _, addr := range node.Status.Addresses {
addresses[addr.Type] = append(addresses[addr.Type], addr.Address)
// IPv6 addresses are labeled as NodeInternalIP despite being usable externally as well.
if addr.Type == v1.NodeInternalIP && suitableType(addr.Address) == endpoint.RecordTypeAAAA {
ipv6Addresses = append(ipv6Addresses, addr.Address)
}
}

if len(addresses[v1.NodeExternalIP]) > 0 {
return addresses[v1.NodeExternalIP], nil
return append(addresses[v1.NodeExternalIP], ipv6Addresses...), nil
}

if len(addresses[v1.NodeInternalIP]) > 0 {
Expand Down
57 changes: 56 additions & 1 deletion source/node_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,19 @@ func testNodeSourceEndpoints(t *testing.T) {
},
false,
},
{
"ipv6 node with fqdn returns one endpoint",
"",
"",
"node1.example.org",
[]v1.NodeAddress{{Type: v1.NodeInternalIP, Address: "2001:DB8::8"}},
map[string]string{},
map[string]string{},
[]*endpoint.Endpoint{
{RecordType: "AAAA", DNSName: "node1.example.org", Targets: endpoint.Targets{"2001:DB8::8"}},
},
false,
},
{
"node with fqdn template returns endpoint with expanded hostname",
"",
Expand Down Expand Up @@ -166,6 +179,20 @@ func testNodeSourceEndpoints(t *testing.T) {
},
false,
},
{
"node with fqdn template returns two endpoints with dual-stack IP addresses and expanded hostname",
"",
"{{.Name}}.example.org",
"node1",
[]v1.NodeAddress{{Type: v1.NodeExternalIP, Address: "1.2.3.4"}, {Type: v1.NodeInternalIP, Address: "2001:DB8::8"}},
map[string]string{},
map[string]string{},
[]*endpoint.Endpoint{
{RecordType: "A", DNSName: "node1.example.org", Targets: endpoint.Targets{"1.2.3.4"}},
{RecordType: "AAAA", DNSName: "node1.example.org", Targets: endpoint.Targets{"2001:DB8::8"}},
},
false,
},
{
"node with both external and internal IP returns an endpoint with external IP",
"",
Expand All @@ -179,6 +206,20 @@ func testNodeSourceEndpoints(t *testing.T) {
},
false,
},
{
"node with both external, internal, and IPv6 IP returns endpoints with external IPs",
"",
"",
"node1",
[]v1.NodeAddress{{Type: v1.NodeExternalIP, Address: "1.2.3.4"}, {Type: v1.NodeInternalIP, Address: "2.3.4.5"}, {Type: v1.NodeInternalIP, Address: "2001:DB8::8"}},
map[string]string{},
map[string]string{},
[]*endpoint.Endpoint{
{RecordType: "A", DNSName: "node1", Targets: endpoint.Targets{"1.2.3.4"}},
{RecordType: "AAAA", DNSName: "node1", Targets: endpoint.Targets{"2001:DB8::8"}},
},
false,
},
{
"node with only internal IP returns an endpoint with internal IP",
"",
Expand All @@ -192,6 +233,20 @@ func testNodeSourceEndpoints(t *testing.T) {
},
false,
},
{
"node with only internal IPs returns endpoints with internal IPs",
"",
"",
"node1",
[]v1.NodeAddress{{Type: v1.NodeInternalIP, Address: "2.3.4.5"}, {Type: v1.NodeInternalIP, Address: "2001:DB8::8"}},
map[string]string{},
map[string]string{},
[]*endpoint.Endpoint{
{RecordType: "A", DNSName: "node1", Targets: endpoint.Targets{"2.3.4.5"}},
{RecordType: "AAAA", DNSName: "node1", Targets: endpoint.Targets{"2001:DB8::8"}},
},
false,
},
{
"node with neither external nor internal IP returns no endpoints",
"",
Expand Down Expand Up @@ -318,7 +373,7 @@ func testNodeSourceEndpoints(t *testing.T) {
false,
},
{
"node with nil Lables returns valid endpoint",
"node with nil Labels returns valid endpoint",
"",
"",
"node1",
Expand Down
52 changes: 29 additions & 23 deletions source/pod.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,64 +76,70 @@ func NewPodSource(ctx context.Context, kubeClient kubernetes.Interface, namespac
func (*podSource) AddEventHandler(ctx context.Context, handler func()) {
}

type endpointKey struct {
domain string
recordType string
}

func (ps *podSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error) {
pods, err := ps.podInformer.Lister().Pods(ps.namespace).List(labels.Everything())
if err != nil {
return nil, err
}

domains := make(map[string][]string)
endpointMap := make(map[endpointKey][]string)
for _, pod := range pods {
if !pod.Spec.HostNetwork {
log.Debugf("skipping pod %s. hostNetwork=false", pod.Name)
continue
}

if domain, ok := pod.Annotations[internalHostnameAnnotationKey]; ok {
if _, ok := domains[domain]; !ok {
domains[domain] = []string{}
}
domains[domain] = append(domains[domain], pod.Status.PodIP)
addToEndpointMap(endpointMap, domain, suitableType(pod.Status.PodIP), pod.Status.PodIP)
}

if domain, ok := pod.Annotations[hostnameAnnotationKey]; ok {
if _, ok := domains[domain]; !ok {
domains[domain] = []string{}
}

node, _ := ps.nodeInformer.Lister().Get(pod.Spec.NodeName)
for _, address := range node.Status.Addresses {
if address.Type == corev1.NodeExternalIP {
domains[domain] = append(domains[domain], address.Address)
recordType := suitableType(address.Address)
// IPv6 addresses are labeled as NodeInternalIP despite being usable externally as well.
if address.Type == corev1.NodeExternalIP || (address.Type == corev1.NodeInternalIP && recordType == endpoint.RecordTypeAAAA) {
addToEndpointMap(endpointMap, domain, recordType, address.Address)
}
}
}

if ps.compatibility == "kops-dns-controller" {
if domain, ok := pod.Annotations[kopsDNSControllerInternalHostnameAnnotationKey]; ok {
if _, ok := domains[domain]; !ok {
domains[domain] = []string{}
}
domains[domain] = append(domains[domain], pod.Status.PodIP)
addToEndpointMap(endpointMap, domain, suitableType(pod.Status.PodIP), pod.Status.PodIP)
}

if domain, ok := pod.Annotations[kopsDNSControllerHostnameAnnotationKey]; ok {
if _, ok := domains[domain]; !ok {
domains[domain] = []string{}
}

node, _ := ps.nodeInformer.Lister().Get(pod.Spec.NodeName)
for _, address := range node.Status.Addresses {
if address.Type == corev1.NodeExternalIP {
domains[domain] = append(domains[domain], address.Address)
recordType := suitableType(address.Address)
// IPv6 addresses are labeled as NodeInternalIP despite being usable externally as well.
if address.Type == corev1.NodeExternalIP || (address.Type == corev1.NodeInternalIP && recordType == endpoint.RecordTypeAAAA) {
addToEndpointMap(endpointMap, domain, recordType, address.Address)
}
}
}
}
}
endpoints := []*endpoint.Endpoint{}
for domain, targets := range domains {
endpoints = append(endpoints, endpoint.NewEndpoint(domain, endpoint.RecordTypeA, targets...))
for key, targets := range endpointMap {
endpoints = append(endpoints, endpoint.NewEndpoint(key.domain, key.recordType, targets...))
}
return endpoints, nil
}

func addToEndpointMap(endpointMap map[endpointKey][]string, domain string, recordType string, address string) {
key := endpointKey{
domain: domain,
recordType: recordType,
}
if _, ok := endpointMap[key]; !ok {
endpointMap[key] = []string{}
}
endpointMap[key] = append(endpointMap[key], address)
}
Loading