Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

IPv6 internal node IPs are usable externally #3588

Merged
merged 2 commits into from
May 10, 2023
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 {
johngmyers marked this conversation as resolved.
Show resolved Hide resolved
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