From 294b24e6cdeba197d2c9a853203c3f8f727856ca Mon Sep 17 00:00:00 2001 From: Phil Brookes Date: Tue, 8 Oct 2024 19:35:47 +0200 Subject: [PATCH] add tooling to convert endpoints to a tree and back Signed-off-by: Maskym Vavilov --- internal/common/tree.go | 208 ++++++++ internal/common/tree_test.go | 902 +++++++++++++++++++++++++++++++++++ 2 files changed, 1110 insertions(+) create mode 100644 internal/common/tree.go create mode 100644 internal/common/tree_test.go diff --git a/internal/common/tree.go b/internal/common/tree.go new file mode 100644 index 00000000..8789840c --- /dev/null +++ b/internal/common/tree.go @@ -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 +} diff --git a/internal/common/tree_test.go b/internal/common/tree_test.go new file mode 100644 index 00000000..7aa5485f --- /dev/null +++ b/internal/common/tree_test.go @@ -0,0 +1,902 @@ +package common + +import ( + "testing" + + . "github.com/onsi/gomega" + . "github.com/onsi/gomega/gstruct" + + "k8s.io/utils/ptr" + "sigs.k8s.io/external-dns/endpoint" + + "github.com/kuadrant/dns-operator/api/v1alpha1" +) + +func Test_MakeTreeFromDNSRecord(t *testing.T) { + RegisterTestingT(t) + + scenarios := []struct { + Name string + DNSRecord *v1alpha1.DNSRecord + Verify func(tree *DNSTreeNode) + }{ + { + Name: "geo to tree", + DNSRecord: &v1alpha1.DNSRecord{ + Spec: v1alpha1.DNSRecordSpec{ + RootHost: "app.testdomain.com", + Endpoints: []*endpoint.Endpoint{ + { + DNSName: "app.testdomain.com", + RecordTTL: 300, + RecordType: "CNAME", + Targets: []string{ + "klb.testdomain.com", + }, + }, + { + DNSName: "ip1.testdomain.com", + RecordTTL: 60, + RecordType: "A", + Targets: []string{ + "172.32.200.1", + }, + }, + { + DNSName: "ip2.testdomain.com", + RecordTTL: 60, + RecordType: "A", + Targets: []string{ + "172.32.200.2", + }, + }, + { + DNSName: "eu.klb.testdomain.com", + RecordTTL: 60, + RecordType: "CNAME", + Targets: []string{ + "ip2.testdomain.com", + }, + }, + { + DNSName: "us.klb.testdomain.com", + RecordTTL: 60, + RecordType: "CNAME", + Targets: []string{ + "ip1.testdomain.com", + }, + }, + { + DNSName: "klb.testdomain.com", + ProviderSpecific: []endpoint.ProviderSpecificProperty{ + { + Name: "geo-code", + Value: "*", + }, + }, + RecordTTL: 300, + RecordType: "CNAME", + SetIdentifier: "", + Targets: []string{ + "eu.klb.testdomain.com", + }, + }, + { + DNSName: "klb.testdomain.com", + ProviderSpecific: []endpoint.ProviderSpecificProperty{ + { + Name: "geo-code", + Value: "GEO-NA", + }, + }, + RecordTTL: 300, + RecordType: "CNAME", + SetIdentifier: "", + Targets: []string{ + "us.klb.testdomain.com", + }, + }, + { + DNSName: "klb.testdomain.com", + ProviderSpecific: []endpoint.ProviderSpecificProperty{ + { + Name: "geo-code", + Value: "GEO-EU", + }, + }, + RecordTTL: 300, + RecordType: "CNAME", + SetIdentifier: "", + Targets: []string{ + "eu.klb.testdomain.com", + }, + }, + }, + }, + }, + Verify: func(tree *DNSTreeNode) { + Expect(tree.Name).To(Equal("app.testdomain.com")) + Expect(len(tree.Children)).To(Equal(1)) + + child := tree.Children[0] + Expect(child.Name).To(Equal("klb.testdomain.com")) + Expect(len(child.Children)).To(Equal(2)) + + Expect(len(child.DataSets)).To(Equal(3)) + Expect(child.DataSets[0]).To(Equal(DNSTreeNodeData{ + RecordType: endpoint.RecordTypeCNAME, + SetIdentifier: "", + RecordTTL: 300, + ProviderSpecific: endpoint.ProviderSpecific{ + { + Name: "geo-code", + Value: "*", + }, + }, + Targets: []string{ + "eu.klb.testdomain.com", + }, + })) + Expect(child.DataSets[1]).To(Equal(DNSTreeNodeData{ + RecordType: endpoint.RecordTypeCNAME, + SetIdentifier: "", + RecordTTL: 300, + ProviderSpecific: endpoint.ProviderSpecific{ + { + Name: "geo-code", + Value: "GEO-NA", + }, + }, + Targets: []string{ + "us.klb.testdomain.com", + }, + })) + Expect(child.DataSets[2]).To(Equal(DNSTreeNodeData{ + RecordType: endpoint.RecordTypeCNAME, + SetIdentifier: "", + RecordTTL: 300, + ProviderSpecific: endpoint.ProviderSpecific{ + { + Name: "geo-code", + Value: "GEO-EU", + }, + }, + Targets: []string{ + "eu.klb.testdomain.com", + }, + })) + + for _, c := range child.Children { + Expect([]string{"eu.klb.testdomain.com", "us.klb.testdomain.com"}).To(ContainElement(c.Name)) + Expect(len(c.Children)).To(Equal(1)) + + if c.Name == "eu.klb.testdomain.com" { + Expect(c.Children[0].Name).To(Equal("ip2.testdomain.com")) + Expect(len(c.Children[0].Children)).To(Equal(1)) + + Expect(c.Children[0].Children[0].Name).To(Equal("172.32.200.2")) + Expect(len(c.Children[0].Children[0].Children)).To(Equal(0)) + + } else if c.Name == "us.klb.testdomain.com" { + Expect(c.Children[0].Name).To(Equal("ip1.testdomain.com")) + Expect(len(c.Children[0].Children)).To(Equal(1)) + + Expect(c.Children[0].Children[0].Name).To(Equal("172.32.200.1")) + Expect(len(c.Children[0].Children[0].Children)).To(Equal(0)) + } + } + }, + }, + } + + for _, scenario := range scenarios { + t.Run(scenario.Name, func(t *testing.T) { + tree := MakeTreeFromDNSRecord(scenario.DNSRecord) + scenario.Verify(tree) + }) + } +} + +func Test_GetLeafsTargets(t *testing.T) { + RegisterTestingT(t) + + scenarios := []struct { + Name string + DNSRecord *v1alpha1.DNSRecord + Verify func(targets []string) + }{ + { + Name: "geo targets", + DNSRecord: &v1alpha1.DNSRecord{ + Spec: v1alpha1.DNSRecordSpec{ + RootHost: "app.testdomain.com", + Endpoints: []*endpoint.Endpoint{ + { + DNSName: "app.testdomain.com", + RecordTTL: 300, + RecordType: "CNAME", + Targets: []string{ + "klb.testdomain.com", + }, + }, + { + DNSName: "ip1.testdomain.com", + RecordTTL: 60, + RecordType: "A", + Targets: []string{ + "172.32.200.1", + }, + }, + { + DNSName: "ip2.testdomain.com", + RecordTTL: 60, + RecordType: "A", + Targets: []string{ + "172.32.200.2", + }, + }, + { + DNSName: "eu.klb.testdomain.com", + RecordTTL: 60, + RecordType: "CNAME", + Targets: []string{ + "ip2.testdomain.com", + }, + }, + { + DNSName: "us.klb.testdomain.com", + RecordTTL: 60, + RecordType: "CNAME", + Targets: []string{ + "ip1.testdomain.com", + }, + }, + { + DNSName: "klb.testdomain.com", + ProviderSpecific: []endpoint.ProviderSpecificProperty{ + { + Name: "geo-code", + Value: "*", + }, + }, + RecordTTL: 300, + RecordType: "CNAME", + SetIdentifier: "", + Targets: []string{ + "eu.klb.testdomain.com", + }, + }, + { + DNSName: "klb.testdomain.com", + ProviderSpecific: []endpoint.ProviderSpecificProperty{ + { + Name: "geo-code", + Value: "GEO-NA", + }, + }, + RecordTTL: 300, + RecordType: "CNAME", + SetIdentifier: "", + Targets: []string{ + "us.klb.testdomain.com", + }, + }, + { + DNSName: "klb.testdomain.com", + ProviderSpecific: []endpoint.ProviderSpecificProperty{ + { + Name: "geo-code", + Value: "GEO-EU", + }, + }, + RecordTTL: 300, + RecordType: "CNAME", + SetIdentifier: "", + Targets: []string{ + "eu.klb.testdomain.com", + }, + }, + }, + }, + }, + Verify: func(targets []string) { + Expect(targets).To(HaveLen(2)) + for _, target := range targets { + Expect(target).To(Or( + Equal("172.32.200.1"), + Equal("172.32.200.2"))) + } + }, + }, + } + + for _, scenario := range scenarios { + t.Run(scenario.Name, func(t *testing.T) { + targets := make([]string, 0) + GetLeafsTargets(MakeTreeFromDNSRecord(scenario.DNSRecord), &targets) + + // verify for passed in targets + scenario.Verify(targets) + + // verify for returned targets + scenario.Verify(*GetLeafsTargets(MakeTreeFromDNSRecord(scenario.DNSRecord), ptr.To([]string{}))) + }) + } +} + +func Test_RemoveNode(t *testing.T) { + RegisterTestingT(t) + + scenarios := []struct { + Name string + Tree *DNSTreeNode + RemoveNodes []*DNSTreeNode + VerifyTree func(tree *DNSTreeNode) + VerifyEndpoints func(endpoints *[]*endpoint.Endpoint) + }{ + { + Name: "remove eu / prune dead branch front", + Tree: getTestTree(), + RemoveNodes: []*DNSTreeNode{ + { + Name: "172.32.200.1", + }, + }, + VerifyTree: func(tree *DNSTreeNode) { + Expect(tree.Name).To(Equal("app.testdomain.com")) + Expect(len(tree.Children)).To(Equal(1)) + + child := tree.Children[0] + Expect(child.Name).To(Equal("klb.testdomain.com")) + Expect(len(child.Children)).To(Equal(1)) + + for _, c := range child.Children { + Expect([]string{"us.klb.testdomain.com"}).To(ContainElement(c.Name)) + Expect(len(c.Children)).To(Equal(1)) + + Expect(c.Children[0].Name).To(Equal("ip2.testdomain.com")) + Expect(len(c.Children[0].Children)).To(Equal(1)) + + Expect(c.Children[0].Children[0].Name).To(Equal("172.32.200.2")) + Expect(len(c.Children[0].Children[0].Children)).To(Equal(0)) + } + }, + VerifyEndpoints: func(endpoints *[]*endpoint.Endpoint) { + Expect(*endpoints).To(HaveLen(4)) + Expect(*endpoints).To(ConsistOf( + PointTo(MatchFields(IgnoreExtras, Fields{ + "DNSName": Equal("app.testdomain.com"), + "Targets": ConsistOf("klb.testdomain.com"), + })), + PointTo(MatchFields(IgnoreExtras, Fields{ + "DNSName": Equal("klb.testdomain.com"), + "Targets": ConsistOf("us.klb.testdomain.com"), + "ProviderSpecific": ConsistOf( + MatchFields(IgnoreExtras, Fields{ + "Name": Equal("geo-code"), + "Value": Equal("GEO-NA"), + })), + })), + PointTo(MatchFields(IgnoreExtras, Fields{ + "DNSName": Equal("us.klb.testdomain.com"), + "Targets": ConsistOf("ip2.testdomain.com"), + })), + PointTo(MatchFields(IgnoreExtras, Fields{ + "DNSName": Equal("ip2.testdomain.com"), + "Targets": ConsistOf("172.32.200.2"), + })), + )) + }, + }, + { + Name: "remove us / prune dead branch back", + Tree: getTestTree(), + RemoveNodes: []*DNSTreeNode{ + { + Name: "172.32.200.2", + }, + }, + VerifyTree: func(tree *DNSTreeNode) { + Expect(tree.Name).To(Equal("app.testdomain.com")) + Expect(len(tree.Children)).To(Equal(1)) + + child := tree.Children[0] + Expect(child.Name).To(Equal("klb.testdomain.com")) + Expect(len(child.Children)).To(Equal(1)) + + grandChild := child.Children[0] + Expect(grandChild.Name).To(Equal("eu.klb.testdomain.com")) + }, + VerifyEndpoints: func(endpoints *[]*endpoint.Endpoint) { + Expect(*endpoints).To(HaveLen(5)) + Expect(*endpoints).To(ConsistOf( + PointTo(MatchFields(IgnoreExtras, Fields{ + "DNSName": Equal("app.testdomain.com"), + "Targets": ConsistOf("klb.testdomain.com"), + })), + PointTo(MatchFields(IgnoreExtras, Fields{ + "DNSName": Equal("klb.testdomain.com"), + "Targets": ConsistOf("eu.klb.testdomain.com"), + "ProviderSpecific": ConsistOf( + MatchFields(IgnoreExtras, Fields{ + "Name": Equal("geo-code"), + "Value": Equal("GEO-EU"), + })), + })), + PointTo(MatchFields(IgnoreExtras, Fields{ + "DNSName": Equal("klb.testdomain.com"), + "Targets": ConsistOf("eu.klb.testdomain.com"), + "ProviderSpecific": ConsistOf( + MatchFields(IgnoreExtras, Fields{ + "Name": Equal("geo-code"), + "Value": Equal("*"), + })), + })), + PointTo(MatchFields(IgnoreExtras, Fields{ + "DNSName": Equal("eu.klb.testdomain.com"), + "Targets": ConsistOf("ip1.testdomain.com"), + })), + PointTo(MatchFields(IgnoreExtras, Fields{ + "DNSName": Equal("ip1.testdomain.com"), + "Targets": ConsistOf("172.32.200.1"), + })), + )) + }, + }, + { + Name: "remove eu and us / prune multiple dead branches", + Tree: getTestTree(), + RemoveNodes: []*DNSTreeNode{ + { + Name: "172.32.200.1", + }, + { + Name: "172.32.200.2", + }, + }, + VerifyTree: func(tree *DNSTreeNode) { + Expect(tree.Name).To(Equal("app.testdomain.com")) + Expect(len(tree.Children)).To(Equal(0)) + }, + VerifyEndpoints: func(endpoints *[]*endpoint.Endpoint) { + Expect(*endpoints).To(HaveLen(0)) + }, + }, + { + Name: "remove IP from a leaf", + Tree: &DNSTreeNode{ + Name: "app.testdomain.com", + Children: []*DNSTreeNode{ + { + Name: "klb.testdomain.com", + Children: []*DNSTreeNode{ + { + Name: "eu.klb.testdomain.com", + Children: []*DNSTreeNode{ + { + Name: "ip1.testdomain.com", + Children: []*DNSTreeNode{ + { + Name: "172.32.200.1", + }, + { + Name: "172.32.200.2", + }, + }, + DataSets: []DNSTreeNodeData{ + { + RecordType: "A", + Targets: []string{ + "172.32.200.1", + "172.32.200.2", + }, + }, + }, + }, + }, + DataSets: []DNSTreeNodeData{ + { + RecordType: "CNAME", + Targets: []string{ + "ip1.testdomain.com", + }, + }, + }, + }, + }, + DataSets: []DNSTreeNodeData{ + { + RecordType: "CNAME", + Targets: []string{ + "eu.klb.testdomain.com", + }, + }, + }, + }, + }, + DataSets: []DNSTreeNodeData{ + { + RecordType: "CNAME", + Targets: []string{ + "klb.testdomain.com", + }, + }, + }, + }, + RemoveNodes: []*DNSTreeNode{ + { + Name: "172.32.200.2", + }, + }, + VerifyTree: func(tree *DNSTreeNode) { + Expect(tree.Name).To(Equal("app.testdomain.com")) + Expect(len(tree.Children)).To(Equal(1)) + + Expect(tree.Children[0].Name).To(Equal("klb.testdomain.com")) + Expect(len(tree.Children[0].Children)).To(Equal(1)) + + Expect(tree.Children[0].Children[0].Name).To(Equal("eu.klb.testdomain.com")) + Expect(len(tree.Children[0].Children[0].Children)).To(Equal(1)) + + ipNode := tree.Children[0].Children[0].Children[0] + Expect(ipNode.Name).To(Equal("ip1.testdomain.com")) + Expect(len(ipNode.Children)).To(Equal(1)) + Expect(ipNode.Children[0].Name).To(Equal("172.32.200.1")) + Expect(len(ipNode.DataSets)).To(Equal(1)) + Expect(len(ipNode.DataSets[0].Targets)).To(Equal(1)) + Expect(ipNode.DataSets[0].Targets[0]).To(Equal("172.32.200.1")) + }, + VerifyEndpoints: func(endpoints *[]*endpoint.Endpoint) { + Expect(*endpoints).To(HaveLen(4)) + Expect(*endpoints).To(ConsistOf( + PointTo(MatchFields(IgnoreExtras, Fields{ + "DNSName": Equal("app.testdomain.com"), + "Targets": ConsistOf("klb.testdomain.com"), + })), + PointTo(MatchFields(IgnoreExtras, Fields{ + "DNSName": Equal("klb.testdomain.com"), + "Targets": ConsistOf("eu.klb.testdomain.com"), + })), + PointTo(MatchFields(IgnoreExtras, Fields{ + "DNSName": Equal("eu.klb.testdomain.com"), + "Targets": ConsistOf("ip1.testdomain.com"), + })), + PointTo(MatchFields(IgnoreExtras, Fields{ + "DNSName": Equal("ip1.testdomain.com"), + "Targets": ConsistOf("172.32.200.1"), + })), + )) + }, + }, + } + + for _, scenario := range scenarios { + t.Run(scenario.Name, func(t *testing.T) { + for _, rn := range scenario.RemoveNodes { + scenario.Tree.RemoveNode(rn) + } + scenario.VerifyTree(scenario.Tree) + scenario.VerifyEndpoints(ToEndpoints(scenario.Tree, ptr.To([]*endpoint.Endpoint{}))) + }) + } +} + +func Test_ToEndpoints(t *testing.T) { + RegisterTestingT(t) + + scenarios := []struct { + Name string + Record *v1alpha1.DNSRecord + Verify func(endpoints *[]*endpoint.Endpoint) + }{ + { + Name: "geo tree", + Record: &v1alpha1.DNSRecord{ + Spec: v1alpha1.DNSRecordSpec{ + RootHost: "app.testdomain.com", + Endpoints: []*endpoint.Endpoint{ + { + DNSName: "app.testdomain.com", + RecordTTL: 300, + RecordType: "CNAME", + Targets: []string{ + "klb.testdomain.com", + }, + }, + { + DNSName: "ip1.testdomain.com", + RecordTTL: 60, + RecordType: "A", + Targets: []string{ + "172.32.200.1", + }, + }, + { + DNSName: "ip2.testdomain.com", + RecordTTL: 60, + RecordType: "A", + Targets: []string{ + "172.32.200.2", + }, + }, + { + DNSName: "eu.klb.testdomain.com", + RecordTTL: 60, + RecordType: "CNAME", + Targets: []string{ + "ip2.testdomain.com", + }, + }, + { + DNSName: "us.klb.testdomain.com", + RecordTTL: 60, + RecordType: "CNAME", + Targets: []string{ + "ip1.testdomain.com", + }, + }, + { + DNSName: "klb.testdomain.com", + ProviderSpecific: []endpoint.ProviderSpecificProperty{ + { + Name: "geo-code", + Value: "*", + }, + }, + RecordTTL: 300, + RecordType: "CNAME", + SetIdentifier: "", + Targets: []string{ + "eu.klb.testdomain.com", + }, + }, + { + DNSName: "klb.testdomain.com", + ProviderSpecific: []endpoint.ProviderSpecificProperty{ + { + Name: "geo-code", + Value: "GEO-NA", + }, + }, + RecordTTL: 300, + RecordType: "CNAME", + SetIdentifier: "", + Targets: []string{ + "us.klb.testdomain.com", + }, + }, + { + DNSName: "klb.testdomain.com", + ProviderSpecific: []endpoint.ProviderSpecificProperty{ + { + Name: "geo-code", + Value: "GEO-EU", + }, + }, + RecordTTL: 300, + RecordType: "CNAME", + SetIdentifier: "", + Targets: []string{ + "eu.klb.testdomain.com", + }, + }, + }, + }, + }, + Verify: func(endpoints *[]*endpoint.Endpoint) { + Expect(len(*endpoints)).To(Equal(8)) + Expect(*endpoints).To(ContainElement(&endpoint.Endpoint{ + DNSName: "app.testdomain.com", + RecordTTL: 300, + RecordType: "CNAME", + Targets: []string{ + "klb.testdomain.com", + }, + })) + Expect(*endpoints).To(ContainElement(&endpoint.Endpoint{ + DNSName: "ip1.testdomain.com", + RecordTTL: 60, + RecordType: "A", + Targets: []string{ + "172.32.200.1", + }, + })) + Expect(*endpoints).To(ContainElement(&endpoint.Endpoint{ + DNSName: "ip2.testdomain.com", + RecordTTL: 60, + RecordType: "A", + Targets: []string{ + "172.32.200.2", + }, + })) + Expect(*endpoints).To(ContainElement(&endpoint.Endpoint{ + DNSName: "eu.klb.testdomain.com", + RecordTTL: 60, + RecordType: "CNAME", + Targets: []string{ + "ip2.testdomain.com", + }, + })) + Expect(*endpoints).To(ContainElement(&endpoint.Endpoint{ + DNSName: "us.klb.testdomain.com", + RecordTTL: 60, + RecordType: "CNAME", + Targets: []string{ + "ip1.testdomain.com", + }, + })) + Expect(*endpoints).To(ContainElement(&endpoint.Endpoint{ + DNSName: "klb.testdomain.com", + ProviderSpecific: []endpoint.ProviderSpecificProperty{ + { + Name: "geo-code", + Value: "*", + }, + }, + RecordTTL: 300, + RecordType: "CNAME", + SetIdentifier: "", + Targets: []string{ + "eu.klb.testdomain.com", + }, + })) + Expect(*endpoints).To(ContainElement(&endpoint.Endpoint{ + DNSName: "klb.testdomain.com", + ProviderSpecific: []endpoint.ProviderSpecificProperty{ + { + Name: "geo-code", + Value: "GEO-NA", + }, + }, + RecordTTL: 300, + RecordType: "CNAME", + SetIdentifier: "", + Targets: []string{ + "us.klb.testdomain.com", + }, + })) + Expect(*endpoints).To(ContainElement(&endpoint.Endpoint{ + DNSName: "klb.testdomain.com", + ProviderSpecific: []endpoint.ProviderSpecificProperty{ + { + Name: "geo-code", + Value: "GEO-EU", + }, + }, + RecordTTL: 300, + RecordType: "CNAME", + SetIdentifier: "", + Targets: []string{ + "eu.klb.testdomain.com", + }, + })) + }, + }, + } + + for _, scenario := range scenarios { + t.Run(scenario.Name, func(t *testing.T) { + endpoints := &[]*endpoint.Endpoint{} + tree := MakeTreeFromDNSRecord(scenario.Record) + ToEndpoints(tree, endpoints) + scenario.Verify(endpoints) + }) + } +} + +func getTestTree() *DNSTreeNode { + return &DNSTreeNode{ + Name: "app.testdomain.com", + Children: []*DNSTreeNode{ + { + Name: "klb.testdomain.com", + Children: []*DNSTreeNode{ + { + Name: "eu.klb.testdomain.com", + Children: []*DNSTreeNode{ + { + Name: "ip1.testdomain.com", + Children: []*DNSTreeNode{ + { + Name: "172.32.200.1", + }, + }, + DataSets: []DNSTreeNodeData{ + { + RecordType: "A", + Targets: []string{ + "172.32.200.1", + }, + }, + }, + }, + }, + DataSets: []DNSTreeNodeData{ + { + RecordType: "CNAME", + Targets: []string{ + "ip1.testdomain.com", + }, + }, + }, + }, + { + Name: "us.klb.testdomain.com", + Children: []*DNSTreeNode{ + { + Name: "ip2.testdomain.com", + Children: []*DNSTreeNode{ + { + Name: "172.32.200.2", + }, + }, + DataSets: []DNSTreeNodeData{ + { + RecordType: "A", + Targets: []string{ + "172.32.200.2", + }, + }, + }, + }, + }, + DataSets: []DNSTreeNodeData{ + { + RecordType: "CNAME", + Targets: []string{ + "ip2.testdomain.com", + }, + }, + }, + }, + }, + DataSets: []DNSTreeNodeData{ + { + RecordType: "CNAME", + ProviderSpecific: endpoint.ProviderSpecific{ + { + Name: "geo-code", + Value: "*", + }, + }, + Targets: []string{ + "eu.klb.testdomain.com", + }, + }, + { + RecordType: "CNAME", + ProviderSpecific: endpoint.ProviderSpecific{ + { + Name: "geo-code", + Value: "GEO-NA", + }, + }, + Targets: []string{ + "us.klb.testdomain.com", + }, + }, + { + RecordType: "CNAME", + ProviderSpecific: endpoint.ProviderSpecific{ + { + Name: "geo-code", + Value: "GEO-EU", + }, + }, + Targets: []string{ + "eu.klb.testdomain.com", + }, + }, + }, + }, + }, + DataSets: []DNSTreeNodeData{ + { + RecordType: "CNAME", + Targets: []string{ + "klb.testdomain.com", + }, + }, + }, + } +}