From 07b84160bbbcf878f6d0348c1f837082f5be2e6b Mon Sep 17 00:00:00 2001 From: janeczku Date: Fri, 14 Oct 2016 21:33:22 +0200 Subject: [PATCH] Parameterize FQDN, state TXT record, sanitize labels --- Godeps/Godeps.json | 4 + .../github.com/valyala/fasttemplate/LICENSE | 22 +++ .../github.com/valyala/fasttemplate/README.md | 85 +++++++++ .../valyala/fasttemplate/template.go | 175 ++++++++++++++++++ config/config.go | 10 + external-dns.go | 73 +++++++- main.go | 3 + metadata/metadata.go | 23 ++- providers/rfc2136/rfc2136.go | 16 +- providers/route53/route53.go | 11 +- utils/utils.go | 64 ++++++- 11 files changed, 462 insertions(+), 24 deletions(-) create mode 100644 Godeps/_workspace/src/github.com/valyala/fasttemplate/LICENSE create mode 100644 Godeps/_workspace/src/github.com/valyala/fasttemplate/README.md create mode 100644 Godeps/_workspace/src/github.com/valyala/fasttemplate/template.go diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index 8e55236..7de741b 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -214,6 +214,10 @@ "Comment": "v0.1.0-82-g465def4", "Rev": "465def4b0436df65693acbb3e3c6ed1bdec79e24" }, + { + "ImportPath": "github.com/valyala/fasttemplate", + "Rev": "3b874956e03f1636d171bda64b130f9135f42cff" + }, { "ImportPath": "github.com/weppos/go-dnsimple/dnsimple", "Rev": "c82ccf106204c401644a24e8b45d16cf81e9cccb" diff --git a/Godeps/_workspace/src/github.com/valyala/fasttemplate/LICENSE b/Godeps/_workspace/src/github.com/valyala/fasttemplate/LICENSE new file mode 100644 index 0000000..7125a63 --- /dev/null +++ b/Godeps/_workspace/src/github.com/valyala/fasttemplate/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2015 Aliaksandr Valialkin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/Godeps/_workspace/src/github.com/valyala/fasttemplate/README.md b/Godeps/_workspace/src/github.com/valyala/fasttemplate/README.md new file mode 100644 index 0000000..3a4d56c --- /dev/null +++ b/Godeps/_workspace/src/github.com/valyala/fasttemplate/README.md @@ -0,0 +1,85 @@ +fasttemplate +============ + +Simple and fast template engine for Go. + +Fasttemplate peforms only a single task - it substitutes template placeholders +with user-defined values. At high speed :) + +Take a look at [quicktemplate](https://github.com/valyala/quicktemplate) if you need fast yet powerful html template engine. + +*Please note that fasttemplate doesn't do any escaping on template values +unlike [html/template](http://golang.org/pkg/html/template/) do. So values +must be properly escaped before passing them to fasttemplate.* + +Fasttemplate is faster than [text/template](http://golang.org/pkg/text/template/), +[strings.Replace](http://golang.org/pkg/strings/#Replace), +[strings.Replacer](http://golang.org/pkg/strings/#Replacer) +and [fmt.Fprintf](https://golang.org/pkg/fmt/#Fprintf) on placeholders' substitution. + +Below are benchmark results comparing fasttemplate performance to text/template, +strings.Replace, strings.Replacer and fmt.Fprintf: + +``` +$ go test -bench=. -benchmem +PASS +BenchmarkFmtFprintf-4 2000000 790 ns/op 0 B/op 0 allocs/op +BenchmarkStringsReplace-4 500000 3474 ns/op 2112 B/op 14 allocs/op +BenchmarkStringsReplacer-4 500000 2657 ns/op 2256 B/op 23 allocs/op +BenchmarkTextTemplate-4 500000 3333 ns/op 336 B/op 19 allocs/op +BenchmarkFastTemplateExecuteFunc-4 5000000 349 ns/op 0 B/op 0 allocs/op +BenchmarkFastTemplateExecute-4 3000000 383 ns/op 0 B/op 0 allocs/op +BenchmarkFastTemplateExecuteFuncString-4 3000000 549 ns/op 144 B/op 1 allocs/op +BenchmarkFastTemplateExecuteString-4 3000000 572 ns/op 144 B/op 1 allocs/op +BenchmarkFastTemplateExecuteTagFunc-4 2000000 743 ns/op 144 B/op 3 allocs/op +``` + + +Docs +==== + +See http://godoc.org/github.com/valyala/fasttemplate . + + +Usage +===== + +```go + template := "http://{{host}}/?q={{query}}&foo={{bar}}{{bar}}" + t := fasttemplate.New(template, "{{", "}}") + s := t.ExecuteString(map[string]interface{}{ + "host": "google.com", + "query": url.QueryEscape("hello=world"), + "bar": "foobar", + }) + fmt.Printf("%s", s) + + // Output: + // http://google.com/?q=hello%3Dworld&foo=foobarfoobar +``` + + +Advanced usage +============== + +```go + template := "Hello, [user]! You won [prize]!!! [foobar]" + t, err := fasttemplate.NewTemplate(template, "[", "]") + if err != nil { + log.Fatalf("unexpected error when parsing template: %s", err) + } + s := t.ExecuteFuncString(func(w io.Writer, tag string) (int, error) { + switch tag { + case "user": + return w.Write([]byte("John")) + case "prize": + return w.Write([]byte("$100500")) + default: + return w.Write([]byte(fmt.Sprintf("[unknown tag %q]", tag))) + } + }) + fmt.Printf("%s", s) + + // Output: + // Hello, John! You won $100500!!! [unknown tag "foobar"] +``` diff --git a/Godeps/_workspace/src/github.com/valyala/fasttemplate/template.go b/Godeps/_workspace/src/github.com/valyala/fasttemplate/template.go new file mode 100644 index 0000000..1385fab --- /dev/null +++ b/Godeps/_workspace/src/github.com/valyala/fasttemplate/template.go @@ -0,0 +1,175 @@ +// Package fasttemplate implements simple and fast template library. +// +// Fasttemplate is faster than text/template, strings.Replace +// and strings.Replacer. +// +// Fasttemplate ideally fits for fast and simple placeholders' substitutions. +package fasttemplate + +import ( + "bytes" + "fmt" + "io" + "sync" +) + +// Template implements simple template engine, which can be used for fast +// tags' (aka placeholders) substitution. +type Template struct { + texts [][]byte + tags []string + bytesBufferPool sync.Pool +} + +// New parses the given template using the given startTag and endTag +// as tag start and tag end. +// +// The returned template can be executed by concurrently running goroutines +// using Execute* methods. +// +// New panics if the given template cannot be parsed. Use NewTemplate instead +// if template may contain errors. +func New(template, startTag, endTag string) *Template { + t, err := NewTemplate(template, startTag, endTag) + if err != nil { + panic(err) + } + return t +} + +// NewTemplate parses the given template using the given startTag and endTag +// as tag start and tag end. +// +// The returned template can be executed by concurrently running goroutines +// using Execute* methods. +func NewTemplate(template, startTag, endTag string) (*Template, error) { + var t Template + + if len(startTag) == 0 { + panic("startTag cannot be empty") + } + if len(endTag) == 0 { + panic("endTag cannot be empty") + } + + s := []byte(template) + a := []byte(startTag) + b := []byte(endTag) + + for { + n := bytes.Index(s, a) + if n < 0 { + t.texts = append(t.texts, s) + break + } + t.texts = append(t.texts, s[:n]) + + s = s[n+len(a):] + n = bytes.Index(s, b) + if n < 0 { + return nil, fmt.Errorf("Cannot find end tag=%q in the template=%q starting from %q", endTag, template, s) + } + + t.tags = append(t.tags, string(s[:n])) + s = s[n+len(b):] + } + + t.bytesBufferPool.New = newBytesBuffer + return &t, nil +} + +func newBytesBuffer() interface{} { + return &bytes.Buffer{} +} + +// TagFunc can be used as a substitution value in the map passed to Execute*. +// Execute* functions pass tag (placeholder) name in 'tag' argument. +// +// TagFunc must be safe to call from concurrently running goroutines. +// +// TagFunc must write contents to w and return the number of bytes written. +type TagFunc func(w io.Writer, tag string) (int, error) + +// ExecuteFunc calls f on each template tag (placeholder) occurrence. +// +// Returns the number of bytes written to w. +func (t *Template) ExecuteFunc(w io.Writer, f TagFunc) (int64, error) { + var nn int64 + + n := len(t.texts) - 1 + for i := 0; i < n; i++ { + ni, err := w.Write(t.texts[i]) + if err != nil { + return nn, err + } + nn += int64(ni) + + if ni, err = f(w, t.tags[i]); err != nil { + return nn, err + } + nn += int64(ni) + } + ni, err := w.Write(t.texts[n]) + if err != nil { + return nn, err + } + nn += int64(ni) + return nn, nil +} + +// Execute substitutes template tags (placeholders) with the corresponding +// values from the map m and writes the result to the given writer w. +// +// Substitution map m may contain values with the following types: +// * []byte - the fastest value type +// * string - convenient value type +// * TagFunc - flexible value type +// +// Returns the number of bytes written to w. +func (t *Template) Execute(w io.Writer, m map[string]interface{}) (int64, error) { + return t.ExecuteFunc(w, func(w io.Writer, tag string) (int, error) { return stdTagFunc(w, tag, m) }) +} + +// ExecuteFuncString call f on each template tag (placeholder) occurrence +// and substitutes it with the data written to TagFunc's w. +// +// Returns the resulting string. +func (t *Template) ExecuteFuncString(f TagFunc) string { + w := t.bytesBufferPool.Get().(*bytes.Buffer) + if _, err := t.ExecuteFunc(w, f); err != nil { + panic(fmt.Sprintf("unexpected error: %s", err)) + } + s := string(w.Bytes()) + w.Reset() + t.bytesBufferPool.Put(w) + return s +} + +// ExecuteString substitutes template tags (placeholders) with the corresponding +// values from the map m and returns the result. +// +// Substitution map m may contain values with the following types: +// * []byte - the fastest value type +// * string - convenient value type +// * TagFunc - flexible value type +// +func (t *Template) ExecuteString(m map[string]interface{}) string { + return t.ExecuteFuncString(func(w io.Writer, tag string) (int, error) { return stdTagFunc(w, tag, m) }) +} + +func stdTagFunc(w io.Writer, tag string, m map[string]interface{}) (int, error) { + v := m[tag] + if v == nil { + return 0, nil + } + switch value := v.(type) { + case []byte: + return w.Write(value) + case string: + return w.Write([]byte(value)) + case TagFunc: + return value(w, tag) + default: + panic(fmt.Sprintf("tag=%q contains unexpected value type=%#v. Expected []byte, string or TagFunc", tag, v)) + } +} diff --git a/config/config.go b/config/config.go index 75970ed..0e3bbb9 100644 --- a/config/config.go +++ b/config/config.go @@ -8,12 +8,17 @@ import ( "github.com/rancher/external-dns/utils" ) +const ( + defaultNameTemplate = "%{{service_name}}.%{{stack_name}}.%{{environment_name}}" +) + var ( RootDomainName string TTL int CattleURL string CattleAccessKey string CattleSecretKey string + NameTemplate string ) func SetFromEnvironment() { @@ -21,6 +26,11 @@ func SetFromEnvironment() { CattleAccessKey = getEnv("CATTLE_ACCESS_KEY") CattleSecretKey = getEnv("CATTLE_SECRET_KEY") RootDomainName = utils.Fqdn(getEnv("ROOT_DOMAIN")) + NameTemplate = os.Getenv("NAME_TEMPLATE") + if len(NameTemplate) == 0 { + NameTemplate = defaultNameTemplate + } + TTLEnv := os.Getenv("TTL") i, err := strconv.Atoi(TTLEnv) if err != nil { diff --git a/external-dns.go b/external-dns.go index 737677b..04b3e98 100644 --- a/external-dns.go +++ b/external-dns.go @@ -2,10 +2,11 @@ package main import ( "fmt" + "strings" + "github.com/Sirupsen/logrus" "github.com/rancher/external-dns/config" "github.com/rancher/external-dns/utils" - "strings" ) func UpdateProviderDnsRecords(metadataRecs map[string]utils.DnsRecord) ([]utils.DnsRecord, error) { @@ -132,13 +133,75 @@ func getProviderDnsRecords() (map[string]utils.DnsRecord, error) { if err != nil { return nil, err } + ourRecords := make(map[string]utils.DnsRecord, len(allRecords)) + if len(allRecords) == 0 { + return ourRecords, nil + } + + stateFqdn := utils.StateFqdn(m.EnvironmentUUID, config.RootDomainName) + ourFqdns := make(map[string]struct{}) + + // Get the FQDNs that were created by us from the state RRSet + logrus.Debugf("Checking for state RRSet %s", stateFqdn) + for _, rec := range allRecords { + if rec.Fqdn == stateFqdn && rec.Type == "TXT" { + logrus.Debugf("FQDNs from state RRSet: %v", rec.Records) + for _, value := range rec.Records { + ourFqdns[value] = struct{}{} + } + ourRecords[stateFqdn] = rec + break + } + } + + for _, rec := range allRecords { + _, ok := ourFqdns[rec.Fqdn] + if ok && rec.Type == "A" { + ourRecords[rec.Fqdn] = rec + } + } + return ourRecords, nil +} + +// upgrade path from previous versions of external-dns. +// checks for any pre-existing A records with names matching the legacy +// suffix and TTLs matching the value of config.TTL. If any are found, +// a state RRSet is created in the zone using the FQDNs of the records +// as values. +func EnsureUpgradeToStateRRSet() error { + allRecords, err := provider.GetRecords() + if err != nil { + return err + } + + stateFqdn := utils.StateFqdn(m.EnvironmentUUID, config.RootDomainName) + logrus.Debugf("Checking for state RRSet %s", stateFqdn) + for _, rec := range allRecords { + if rec.Fqdn == stateFqdn && rec.Type == "TXT" { + logrus.Debugf("Found state RRSet with %d records", len(rec.Records)) + return nil + } + } + + logrus.Debug("State RRSet not found") + ourFqdns := make(map[string]struct{}) + // records created by previous versions will match this suffix joins := []string{m.EnvironmentName, config.RootDomainName} suffix := "." + strings.ToLower(strings.Join(joins, ".")) - for _, value := range allRecords { - if value.Type == "A" && strings.HasSuffix(value.Fqdn, suffix) && value.TTL == config.TTL { - ourRecords[value.Fqdn] = value + for _, rec := range allRecords { + if rec.Type == "A" && strings.HasSuffix(rec.Fqdn, suffix) && rec.TTL == config.TTL { + ourFqdns[rec.Fqdn] = struct{}{} } } - return ourRecords, nil + + if len(ourFqdns) > 0 { + logrus.Infof("Creating RRSet '%s TXT' for %d pre-existing records", stateFqdn, len(ourFqdns)) + stateRec := utils.StateRecord(stateFqdn, config.TTL, ourFqdns) + if err := provider.AddRecord(stateRec); err != nil { + return fmt.Errorf("Failed to add RRSet to provider %v: %v", stateRec, err) + } + } + + return nil } diff --git a/main.go b/main.go index d139641..bc2a04e 100644 --- a/main.go +++ b/main.go @@ -93,6 +93,9 @@ func main() { setEnv() go startHealthcheck() + if err := EnsureUpgradeToStateRRSet(); err != nil { + logrus.Fatalf("Failed to ensure upgrade: %v", err) + } version := "init" lastUpdated := time.Now() diff --git a/metadata/metadata.go b/metadata/metadata.go index 0a6a571..452c662 100644 --- a/metadata/metadata.go +++ b/metadata/metadata.go @@ -16,9 +16,10 @@ const ( type MetadataClient struct { MetadataClient *metadata.Client EnvironmentName string + EnvironmentUUID string } -func getEnvironmentName(m *metadata.Client) (string, error) { +func getEnvironment(m *metadata.Client) (string, string, error) { timeout := 30 * time.Second var err error var stack metadata.Stack @@ -28,10 +29,10 @@ func getEnvironmentName(m *metadata.Client) (string, error) { logrus.Errorf("Error reading stack info: %v...will retry", err) time.Sleep(i) } else { - return stack.EnvironmentName, nil + return stack.EnvironmentName, stack.EnvironmentUUID, nil } } - return "", fmt.Errorf("Error reading stack info: %v", err) + return "", "", fmt.Errorf("Error reading stack info: %v", err) } func NewMetadataClient() (*MetadataClient, error) { @@ -40,7 +41,7 @@ func NewMetadataClient() (*MetadataClient, error) { logrus.Fatalf("Failed to configure rancher-metadata: %v", err) } - envName, err := getEnvironmentName(m) + envName, envUUID, err := getEnvironment(m) if err != nil { logrus.Fatalf("Error reading stack info: %v", err) } @@ -48,6 +49,7 @@ func NewMetadataClient() (*MetadataClient, error) { return &MetadataClient{ MetadataClient: m, EnvironmentName: envName, + EnvironmentUUID: envUUID, }, nil } @@ -70,6 +72,7 @@ func (m *MetadataClient) getContainersDnsRecords(dnsEntries map[string]utils.Dns return err } + ourFqdns := make(map[string]struct{}) for _, container := range containers { if len(container.ServiceName) == 0 || len(container.Ports) == 0 || !containerStateOK(container) { continue @@ -101,11 +104,19 @@ func (m *MetadataClient) getContainersDnsRecords(dnsEntries map[string]utils.Dns ip = host.AgentIP } - fqdn := utils.ConvertToFqdn(container.ServiceName, container.StackName, m.EnvironmentName, config.RootDomainName) + fqdn := utils.FqdnFromTemplate(config.NameTemplate, container.ServiceName, container.StackName, + m.EnvironmentName, config.RootDomainName) records := []string{ip} dnsEntry := utils.DnsRecord{fqdn, records, "A", config.TTL} addToDnsEntries(dnsEntry, dnsEntries) + ourFqdns[fqdn] = struct{}{} + } + + if len(ourFqdns) > 0 { + fqdn := utils.StateFqdn(m.EnvironmentUUID, config.RootDomainName) + stateRec := utils.StateRecord(fqdn, config.TTL, ourFqdns) + addToDnsEntries(stateRec, dnsEntries) } return nil @@ -119,7 +130,7 @@ func addToDnsEntries(dnsEntry utils.DnsRecord, dnsEntries map[string]utils.DnsRe records = dnsEntries[dnsEntry.Fqdn].Records records = append(records, dnsEntry.Records...) } - dnsEntry = utils.DnsRecord{dnsEntry.Fqdn, records, "A", config.TTL} + dnsEntry = utils.DnsRecord{dnsEntry.Fqdn, records, dnsEntry.Type, dnsEntry.TTL} dnsEntries[dnsEntry.Fqdn] = dnsEntry } diff --git a/providers/rfc2136/rfc2136.go b/providers/rfc2136/rfc2136.go index 3672a8f..18c8cb6 100644 --- a/providers/rfc2136/rfc2136.go +++ b/providers/rfc2136/rfc2136.go @@ -128,24 +128,28 @@ OuterLoop: rrFqdn := rr.Header().Name rrTTL := int(rr.Header().Ttl) - var rrType, rrValue string + var rrType string + var rrValues []string switch rr.Header().Rrtype { case dns.TypeCNAME: - rrValue = rr.(*dns.CNAME).Target + rrValues = []string{rr.(*dns.CNAME).Target} rrType = "CNAME" case dns.TypeA: - rrValue = rr.(*dns.A).A.String() + rrValues = []string{rr.(*dns.A).A.String()} rrType = "A" case dns.TypeAAAA: - rrValue = rr.(*dns.AAAA).AAAA.String() + rrValues = []string{rr.(*dns.AAAA).AAAA.String()} rrType = "AAAA" + case dns.TypeTXT: + rrValues = rr.(*dns.TXT).Txt + rrType = "TXT" default: continue // Unhandled record type } for idx, existingRecord := range records { if existingRecord.Fqdn == rrFqdn && existingRecord.Type == rrType { - records[idx].Records = append(records[idx].Records, rrValue) + records[idx].Records = append(records[idx].Records, rrValues...) continue OuterLoop } } @@ -154,7 +158,7 @@ OuterLoop: Fqdn: rrFqdn, Type: rrType, TTL: rrTTL, - Records: []string{rrValue}, + Records: rrValues, } records = append(records, record) diff --git a/providers/route53/route53.go b/providers/route53/route53.go index 3bb2acb..9428bcf 100644 --- a/providers/route53/route53.go +++ b/providers/route53/route53.go @@ -57,7 +57,7 @@ func (r *Route53Provider) Init(rootDomainName string) error { return err } - logrus.Infof("Configured %s with hosted zone '%s' in region '%s' ", + logrus.Infof("Configured %s with hosted zone %s in region %s", r.GetName(), rootDomainName, region) return nil @@ -140,6 +140,9 @@ func (r *Route53Provider) changeRecord(record utils.DnsRecord, action string) er r.limiter.Wait(1) records := make([]*awsRoute53.ResourceRecord, len(record.Records)) for idx, value := range record.Records { + if record.Type == "TXT" { + value = `"` + value + `"` + } records[idx] = &awsRoute53.ResourceRecord{ Value: aws.String(value), } @@ -193,7 +196,11 @@ func (r *Route53Provider) GetRecords() ([]utils.DnsRecord, error) { } records := []string{} for _, rr := range rrSet.ResourceRecords { - records = append(records, *rr.Value) + value := *rr.Value + if *rrSet.Type == "TXT" { + value = strings.Trim(value, `"`) + } + records = append(records, value) } dnsRecord := utils.DnsRecord{ diff --git a/utils/utils.go b/utils/utils.go index 1954a1a..2f005d0 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -1,7 +1,17 @@ package utils import ( + "fmt" + "io" + "regexp" "strings" + + "github.com/Sirupsen/logrus" + "github.com/valyala/fasttemplate" +) + +const ( + stateRecordFqdnTemplate = "external-dns-%s.%s" ) type DnsRecord struct { @@ -23,11 +33,6 @@ func ConvertToServiceDnsRecord(dnsRecord DnsRecord) ServiceDnsRecord { return serviceRecord } -func ConvertToFqdn(serviceName, stackName, environmentName, rootDomainName string) string { - labels := []string{serviceName, stackName, environmentName, rootDomainName} - return strings.ToLower(strings.Join(labels, ".")) -} - // Fqdn ensures that the name is a fqdn adding a trailing dot if necessary. func Fqdn(name string) string { n := len(name) @@ -45,3 +50,52 @@ func UnFqdn(name string) string { } return name } + +func FqdnFromTemplate(template, serviceName, stackName, environmentName, rootDomainName string) string { + t, err := fasttemplate.NewTemplate(template, "%{{", "}}") + if err != nil { + logrus.Fatalf("error while parsing fqdn template: %s", err) + } + + fqdn := t.ExecuteFuncString(func(w io.Writer, tag string) (int, error) { + switch tag { + case "service_name": + return w.Write([]byte(sanitizeLabel(serviceName))) + case "stack_name": + return w.Write([]byte(sanitizeLabel(stackName))) + case "environment_name": + return w.Write([]byte(sanitizeLabel(environmentName))) + default: + return 0, fmt.Errorf("invalid placeholder '%q' in fqdn template", tag) + } + }) + + labels := []string{fqdn, rootDomainName} + return strings.ToLower(strings.Join(labels, ".")) +} + +func StateFqdn(environmentUUID, rootDomainName string) string { + fqdn := fmt.Sprintf(stateRecordFqdnTemplate, environmentUUID, rootDomainName) + return strings.ToLower(fqdn) +} + +func StateRecord(fqdn string, ttl int, entries map[string]struct{}) DnsRecord { + records := make([]string, len(entries)) + idx := 0 + for entry, _ := range entries { + records[idx] = entry + idx++ + } + return DnsRecord{fqdn, records, "TXT", ttl} +} + +// sanitizeLabel replaces characters that are not allowed in DNS labels with dashes. +// According to RFC 1123 the only characters allowed in DNS labels are A-Z, a-z, 0-9 +// and dashes ("-"). The latter must not appear at the start or end of a label. +func sanitizeLabel(label string) string { + re := regexp.MustCompile("[^a-zA-Z0-9-]") + dashes := regexp.MustCompile("[-]+") + label = re.ReplaceAllString(label, "-") + label = dashes.ReplaceAllString(label, "-") + return strings.Trim(label, "-") +}