diff options
Diffstat (limited to 'middleware/kubernetes/nametemplate')
-rw-r--r-- | middleware/kubernetes/nametemplate/nametemplate.go | 166 | ||||
-rw-r--r-- | middleware/kubernetes/nametemplate/nametemplate_test.go | 129 |
2 files changed, 295 insertions, 0 deletions
diff --git a/middleware/kubernetes/nametemplate/nametemplate.go b/middleware/kubernetes/nametemplate/nametemplate.go new file mode 100644 index 000000000..e61c036e8 --- /dev/null +++ b/middleware/kubernetes/nametemplate/nametemplate.go @@ -0,0 +1,166 @@ +package nametemplate + +import ( + "errors" + "fmt" + "strings" + + "github.com/miekg/coredns/middleware/kubernetes/util" +) + +// Likely symbols that require support: +// {id} +// {ip} +// {portname} +// {protocolname} +// {servicename} +// {namespace} +// {type} "svc" or "pod" +// {zone} + +// SkyDNS normal services have an A-record of the form "{servicename}.{namespace}.{type}.{zone}" +// This resolves to the cluster IP of the service. + +// SkyDNS headless services have an A-record of the form "{servicename}.{namespace}.{type}.{zone}" +// This resolves to the set of IPs of the pods selected by the Service. Clients are expected to +// consume the set or else use round-robin selection from the set. + +var symbols = map[string]string{ + "service": "{service}", + "namespace": "{namespace}", + "type": "{type}", + "zone": "{zone}", +} + +var types = []string{ + "svc", + "pod", +} + +// TODO: Validate that provided NameTemplate string only contains: +// * valid, known symbols, or +// * static strings + +// TODO: Support collapsing multiple segments into a symbol. Either: +// * all left-over segments are used as the "service" name, or +// * some scheme like "{namespace}.{namespace}" means use +// segments concatenated with a "." for the namespace, or +// * {namespace2:4} means use segements 2->4 for the namespace. + +// TODO: possibly need to store length of segmented format to handle cases +// where query string segments to a shorter or longer list than the template. +// When query string segments to shorter than template: +// * either wildcards are being used, or +// * we are not looking up an A, AAAA, or SRV record (eg NS), or +// * we can just short-circuit failure before hitting the k8s API. +// Where the query string is longer than the template, need to define which +// symbol consumes the other segments. Most likely this would be the servicename. +// Also consider how to handle static strings in the format template. +type NameTemplate struct { + formatString string + splitFormat []string + // Element is a map of element name :: index in the segmented record name for the named element + Element map[string]int +} + +func (t *NameTemplate) SetTemplate(s string) error { + var err error + fmt.Println() + + t.Element = map[string]int{} + + t.formatString = s + t.splitFormat = strings.Split(t.formatString, ".") + for templateIndex, v := range t.splitFormat { + elementPositionSet := false + for name, symbol := range symbols { + if v == symbol { + t.Element[name] = templateIndex + elementPositionSet = true + break + } + } + if !elementPositionSet { + if strings.Contains(v, "{") { + err = errors.New("Record name template contains the unknown symbol '" + v + "'") + fmt.Printf("[debug] %v\n", err) + return err + } else { + fmt.Printf("[debug] Template string has static element '%v'\n", v) + } + } + } + + return err +} + +// TODO: Find a better way to pull the data segments out of the +// query string based on the template. Perhaps it is better +// to treat the query string segments as a reverse stack and +// step down the stack to find the right element. + +func (t *NameTemplate) GetZoneFromSegmentArray(segments []string) string { + if index, ok := t.Element["zone"]; !ok { + return "" + } else { + return strings.Join(segments[index:len(segments)], ".") + } +} + +func (t *NameTemplate) GetNamespaceFromSegmentArray(segments []string) string { + return t.GetSymbolFromSegmentArray("namespace", segments) +} + +func (t *NameTemplate) GetServiceFromSegmentArray(segments []string) string { + return t.GetSymbolFromSegmentArray("service", segments) +} + +func (t *NameTemplate) GetTypeFromSegmentArray(segments []string) string { + typeSegment := t.GetSymbolFromSegmentArray("type", segments) + + // Limit type to known types symbols + if util.StringInSlice(typeSegment, types) { + return "" + } + + return typeSegment +} + +func (t *NameTemplate) GetSymbolFromSegmentArray(symbol string, segments []string) string { + if index, ok := t.Element[symbol]; !ok { + return "" + } else { + return segments[index] + } +} + +// GetRecordNameFromNameValues returns the string produced by applying the +// values to the NameTemplate format string. +func (t *NameTemplate) GetRecordNameFromNameValues(values NameValues) string { + recordName := make([]string, len(t.splitFormat)) + copy(recordName[:], t.splitFormat) + + for name, index := range t.Element { + if index == -1 { + continue + } + switch name { + case "type": + recordName[index] = values.TypeName + case "service": + recordName[index] = values.ServiceName + case "namespace": + recordName[index] = values.Namespace + case "zone": + recordName[index] = values.Zone + } + } + return strings.Join(recordName, ".") +} + +type NameValues struct { + ServiceName string + Namespace string + TypeName string + Zone string +} diff --git a/middleware/kubernetes/nametemplate/nametemplate_test.go b/middleware/kubernetes/nametemplate/nametemplate_test.go new file mode 100644 index 000000000..a866c4fb6 --- /dev/null +++ b/middleware/kubernetes/nametemplate/nametemplate_test.go @@ -0,0 +1,129 @@ +package nametemplate + +import ( + "fmt" + "strings" + "testing" +) + +const ( + zone = 0 + namespace = 1 + service = 2 +) + +// Map of format string :: expected locations of name symbols in the format. +// -1 value indicates that symbol does not exist in format. +var exampleTemplates = map[string][]int{ + "{service}.{namespace}.{zone}": []int{2, 1, 0}, // service symbol expected @ position 0, namespace @ 1, zone @ 2 + "{namespace}.{zone}": []int{1, 0, -1}, + "": []int{-1, -1, -1}, +} + +func TestSetTemplate(t *testing.T) { + fmt.Printf("\n") + for s, expectedValue := range exampleTemplates { + + n := new(NameTemplate) + n.SetTemplate(s) + + // check the indexes resulting from calling SetTemplate() against expectedValues + if expectedValue[zone] != -1 { + if n.Element["zone"] != expectedValue[zone] { + t.Errorf("Expected zone at index '%v', instead found at index '%v' for format string '%v'", expectedValue[zone], n.Element["zone"], s) + } + } + } +} + +func TestGetServiceFromSegmentArray(t *testing.T) { + var ( + n *NameTemplate + formatString string + queryString string + splitQuery []string + expectedService string + actualService string + ) + + // Case where template contains {service} + n = new(NameTemplate) + formatString = "{service}.{namespace}.{zone}" + n.SetTemplate(formatString) + + queryString = "myservice.mynamespace.coredns" + splitQuery = strings.Split(queryString, ".") + expectedService = "myservice" + actualService = n.GetServiceFromSegmentArray(splitQuery) + + if actualService != expectedService { + t.Errorf("Expected service name '%v', instead got service name '%v' for query string '%v' and format '%v'", expectedService, actualService, queryString, formatString) + } + + // Case where template does not contain {service} + n = new(NameTemplate) + formatString = "{namespace}.{zone}" + n.SetTemplate(formatString) + + queryString = "mynamespace.coredns" + splitQuery = strings.Split(queryString, ".") + expectedService = "" + actualService = n.GetServiceFromSegmentArray(splitQuery) + + if actualService != expectedService { + t.Errorf("Expected service name '%v', instead got service name '%v' for query string '%v' and format '%v'", expectedService, actualService, queryString, formatString) + } +} + +func TestGetZoneFromSegmentArray(t *testing.T) { + var ( + n *NameTemplate + formatString string + queryString string + splitQuery []string + expectedZone string + actualZone string + ) + + // Case where template contains {zone} + n = new(NameTemplate) + formatString = "{service}.{namespace}.{zone}" + n.SetTemplate(formatString) + + queryString = "myservice.mynamespace.coredns" + splitQuery = strings.Split(queryString, ".") + expectedZone = "coredns" + actualZone = n.GetZoneFromSegmentArray(splitQuery) + + if actualZone != expectedZone { + t.Errorf("Expected zone name '%v', instead got zone name '%v' for query string '%v' and format '%v'", expectedZone, actualZone, queryString, formatString) + } + + // Case where template does not contain {zone} + n = new(NameTemplate) + formatString = "{service}.{namespace}" + n.SetTemplate(formatString) + + queryString = "mynamespace.coredns" + splitQuery = strings.Split(queryString, ".") + expectedZone = "" + actualZone = n.GetZoneFromSegmentArray(splitQuery) + + if actualZone != expectedZone { + t.Errorf("Expected zone name '%v', instead got zone name '%v' for query string '%v' and format '%v'", expectedZone, actualZone, queryString, formatString) + } + + // Case where zone is multiple segments + n = new(NameTemplate) + formatString = "{service}.{namespace}.{zone}" + n.SetTemplate(formatString) + + queryString = "myservice.mynamespace.coredns.cluster.local" + splitQuery = strings.Split(queryString, ".") + expectedZone = "coredns.cluster.local" + actualZone = n.GetZoneFromSegmentArray(splitQuery) + + if actualZone != expectedZone { + t.Errorf("Expected zone name '%v', instead got zone name '%v' for query string '%v' and format '%v'", expectedZone, actualZone, queryString, formatString) + } +} |