Skip to content

Commit

Permalink
Merge pull request #266 from philbrookes/add-dnstree
Browse files Browse the repository at this point in the history
add tooling to convert endpoints to a tree and back
  • Loading branch information
maksymvavilov authored Oct 23, 2024
2 parents 3821308 + 294b24e commit 0ed8ff6
Show file tree
Hide file tree
Showing 2 changed files with 1,110 additions and 0 deletions.
208 changes: 208 additions & 0 deletions internal/common/tree.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
package common

import (
"slices"

"sigs.k8s.io/external-dns/endpoint"

"github.com/kuadrant/dns-operator/api/v1alpha1"
)

// DNSTreeNode stores a relation between endpoints that were parsed into a tree
type DNSTreeNode struct {
Name string
Children []*DNSTreeNode
DataSets []DNSTreeNodeData
}

// DNSTreeNodeData holds a data for the enpoint(s) that correspond to this node
type DNSTreeNodeData struct {
RecordType string
SetIdentifier string
RecordTTL endpoint.TTL
Labels endpoint.Labels
ProviderSpecific endpoint.ProviderSpecific
Targets []string
}

// RemoveNode removes a node from a tree.
// If the node was the only child of the parent node,
// the parent will be removed as well unless the parent is a root node
func (n *DNSTreeNode) RemoveNode(deleteNode *DNSTreeNode) {
// store indexes of dead branches
var deadBranches []int
var deadDataSets []string

if deleteNode == nil {
return
}

for i, node := range n.Children {
if node.Name == deleteNode.Name {
n.Children = append(n.Children[:i], n.Children[i+1:]...)

// we removed child, but need to clean up data sets
for j, dataset := range n.DataSets {
index := slices.Index(dataset.Targets, deleteNode.Name)
if index >= 0 {
n.DataSets[j].Targets = append(dataset.Targets[:index], dataset.Targets[index+1:]...)
}
}
return
}

// no children matched, try on each child
node.RemoveNode(deleteNode)

// the removed node was the only child - we have a dead branch (it is a leaf now)
// children are nil on leaf node
// not checking for it will nuke the whole tree as leafs will be considered dead branches
if node.Children != nil && isALeafNode(node) {
// store the index. indexes are in ascending order
deadBranches = append(deadBranches, i)

// we can't rely on indexes for data sets, so store node name
deadDataSets = append(deadDataSets, node.Name)
}
}

// prune dead branches separately from the main for loop.
// doing it inside will shift indexes the for loop is iterating through
for count, deadBranchIndex := range deadBranches {
// after the first removal, all subsequent deadBranchIndexes will de one to high,
// but since we have them ascending, we can use count as modifier
n.Children = append(n.Children[:deadBranchIndex-count], n.Children[deadBranchIndex-count+1:]...)
}

var healthyDataSets []DNSTreeNodeData
// clean up data nodes from dead branches
for _, dataSet := range n.DataSets {

// we are dealing with CNAMES only here.
// the A record is already removed from datasets
if !slices.Contains(deadDataSets, dataSet.Targets[0]) {
healthyDataSets = append(healthyDataSets, dataSet)
}
}
n.DataSets = healthyDataSets

}

// GetLeafsTargets returns IP or CNAME of the leafs of a tree.
// alternatively, it can populate the passed in array with pointers to targets
func GetLeafsTargets(node *DNSTreeNode, targets *[]string) *[]string {
if node == nil || targets == nil {
return &[]string{}
}

if isALeafNode(node) {
*targets = append(*targets, node.Name)
return nil
}
for _, child := range node.Children {
GetLeafsTargets(child, targets)
}
return targets
}

// ToEndpoints transforms a tree into an array of endpoints.
// The array could be returned or passed in to be populated
func ToEndpoints(node *DNSTreeNode, endpoints *[]*endpoint.Endpoint) *[]*endpoint.Endpoint {
if node == nil || endpoints == nil {
return &[]*endpoint.Endpoint{}
}

if isALeafNode(node) {
return endpoints
}
targets := []string{}
for _, child := range node.Children {
targets = append(targets, child.Name)
ToEndpoints(child, endpoints)
}

// this should not happen. the node is either leaf or has datasets (unless the cree was made manually)
if node.DataSets == nil {
*endpoints = append(*endpoints, &endpoint.Endpoint{
DNSName: node.Name,
Targets: targets,
})
return endpoints
}

for _, data := range node.DataSets {
*endpoints = append(*endpoints, &endpoint.Endpoint{
DNSName: node.Name,
Targets: data.Targets,
RecordType: data.RecordType,
RecordTTL: data.RecordTTL,
SetIdentifier: data.SetIdentifier,
Labels: data.Labels,
ProviderSpecific: data.ProviderSpecific,
})
}
return endpoints
}

func MakeTreeFromDNSRecord(record *v1alpha1.DNSRecord) *DNSTreeNode {
if record == nil {
return &DNSTreeNode{}
}
rootNode := &DNSTreeNode{Name: record.Spec.RootHost}
populateNode(rootNode, record)
return rootNode
}

func populateNode(node *DNSTreeNode, record *v1alpha1.DNSRecord) {
node.DataSets = findDataSets(node.Name, record)

children := findChildren(node.Name, record)
if len(children) == 0 {
return
}

for _, c := range children {
populateNode(c, record)
}
node.Children = children
}

func findChildren(name string, record *v1alpha1.DNSRecord) []*DNSTreeNode {
nodes := []*DNSTreeNode{}
targets := map[string]string{}
for _, ep := range record.Spec.Endpoints {
if ep.DNSName == name {
for _, t := range ep.Targets {
targets[t] = t
}
}
}
for _, t := range targets {
nodes = append(nodes, &DNSTreeNode{Name: t})
}

return nodes
}

func findDataSets(name string, record *v1alpha1.DNSRecord) []DNSTreeNodeData {
dataSets := []DNSTreeNodeData{}
for _, ep := range record.Spec.Endpoints {
if ep.DNSName == name {
dataSets = append(dataSets, DNSTreeNodeData{
RecordType: ep.RecordType,
RecordTTL: ep.RecordTTL,
SetIdentifier: ep.SetIdentifier,
Labels: ep.Labels,
ProviderSpecific: ep.ProviderSpecific,
Targets: ep.Targets,
})
}
}
return dataSets
}

// isALeafNode check if this is the last node in a tree
func isALeafNode(node *DNSTreeNode) bool {
// no children means this is pointing to an IP or a host outside of the DNS Record
return node.Children == nil || len(node.Children) == 0
}
Loading

0 comments on commit 0ed8ff6

Please sign in to comment.