Skip to content

Commit

Permalink
Merge pull request #3588 from johngmyers/ipv6-external
Browse files Browse the repository at this point in the history
IPv6 internal node IPs are usable externally
  • Loading branch information
k8s-ci-robot authored May 10, 2023
2 parents 0313138 + 4745ddb commit 3a788d6
Show file tree
Hide file tree
Showing 10 changed files with 310 additions and 51 deletions.
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
31 changes: 20 additions & 11 deletions source/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ func (ns *nodeSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, erro
return nil, err
}

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

// create endpoints for all nodes
for _, node := range nodes {
Expand All @@ -109,8 +109,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 +133,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 := endpointKey{
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 +167,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
47 changes: 24 additions & 23 deletions source/pod.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,58 +82,59 @@ func (ps *podSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error
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.dnsName, key.recordType, targets...))
}
return endpoints, nil
}

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

0 comments on commit 3a788d6

Please sign in to comment.