1
0
mirror of https://github.com/StackExchange/dnscontrol.git synced 2024-05-11 05:55:12 +00:00

NEW FEATURE: Support Split Horizon DNS (#1034)

* Implement main feature
* BIND: Permit printf-like file name formats
* BIND: Make filenameformat work forwards and backwards.
* Fix extrator test cases
This commit is contained in:
Tom Limoncelli
2021-02-05 12:12:45 -05:00
committed by GitHub
parent 36289f7157
commit c547beacc0
11 changed files with 612 additions and 24 deletions

View File

@ -112,10 +112,10 @@ func run(args PreviewArgs, push bool, interactive bool, out printer.CLI) error {
totalCorrections := 0 totalCorrections := 0
DomainLoop: DomainLoop:
for _, domain := range cfg.Domains { for _, domain := range cfg.Domains {
if !args.shouldRunDomain(domain.Name) { if !args.shouldRunDomain(domain.UniqueName) {
continue continue
} }
out.StartDomain(domain.Name) out.StartDomain(domain.UniqueName)
nsList, err := nameservers.DetermineNameservers(domain) nsList, err := nameservers.DetermineNameservers(domain)
if err != nil { if err != nil {
return err return err

View File

@ -42,6 +42,56 @@ D("example.com", REGISTRAR, DnsProvider(r53),
CNAME("test", "foo.example2.com."), CNAME("test", "foo.example2.com."),
GOOGLE_APPS_DOMAIN_MX GOOGLE_APPS_DOMAIN_MX
); );
{%endhighlight%} {%endhighlight%}
{% include endExample.html %} {% include endExample.html %}
# Split Horizon DNS
DNSControl supports Split Horizon DNS. Simply
define the domain two or more times, each with
their own unique parameters.
To differentiate the different domains, specify the domains as
`domain.tld!tag`, such as `example.com!inside` and
`example.com!outside`.
{% include startExample.html %}
{% highlight js %}
var REG = NewRegistrar("Third-Party", "NONE");
var DNS_INSIDE = NewDnsProvider("Cloudflare", "CLOUDFLAREAPI");
var DNS_OUTSIDE = NewDnsProvider("bind", "BIND");
D("example.com!inside", REG, DnsProvider(DNS_INSIDE),
A("www", "10.10.10.10")
);
D("example.com!outside", REG, DnsProvider(DNS_OUTSIDE),
A("www", "20.20.20.20")
);
D_EXTEND("example.com!inside",
A("internal", "10.99.99.99")
);
{%endhighlight%}
{% include endExample.html %}
A domain name without a `!` is assigned a tag that is the empty
string. For example, `example.com` and `example.com!` are equivalent.
However, we strongly recommend against using the empty tag, as it
risks creating confusion. In other words, if you have `domain.tld`
and `domain.tld!external` you now require humans to remember that
`domain.tld` is the external one. I mean... the internal one. You
may have noticed this mistake, but will your coworkers? Will you in
six months? You get the idea.
DNSControl command line flag `--domains` is an exact match. If you
define domains `example.com!george` and `example.com!john` then:
* `--domains=example.com` will not match either domain.
* `--domains='example.com!george'` will match only match the first.
* `--domains='example.com!george',example.com!john` will match both.
NOTE: The quotes are required if your shell treats `!` as a special
character, which is probably does. If you see an error that mentions
`event not found` you probably forgot the quotes.

View File

@ -18,11 +18,13 @@ you can specify a `directory` where the provider will look for and create zone f
{% highlight json %} {% highlight json %}
{ {
"bind": { "bind": {
"directory": "myzones" "directory": "myzones",
"filenameformat": "%U.zone" << The default
} }
} }
{% endhighlight %} {% endhighlight %}
The BIND accepts some optional metadata via your DNS config when you create the provider: The BIND accepts some optional metadata via your DNS config when you create the provider:
In this example we set the default SOA settings and NS records. In this example we set the default SOA settings and NS records.
@ -46,15 +48,55 @@ var BIND = NewDnsProvider('bind', 'BIND', {
}) })
{% endhighlight %} {% endhighlight %}
# filenameformat
The `filenameformat` parameter specifies the file name to be used when
writing the zone file. The default is acceptable in most cases: the
name as specified in the `D()` function, plus ".zone".
The filenameformat is a string with a few printf-like `%` verbs:
* `%U` the domain name as specified in `D()`
* `%D` the domain name without any split horizon tag
* `%T` the split horizon tag, or "", see `D()`
* `%?x` this returns `x` if the split horizon tag is non-null, otherwise nothing. `x` can be any printable.
* `%%` `%`
* ordinary characters (not `%`) are copied unchanged to the output stream
* FYI: format strings must not end with an incomplete `%` or `%?`
* FYI: `/` or other filesystem separators result in undefined behavior
Typical values:
* `%U.zone` (The default)
* `example.com.zone` or `example.com!tag.zone`
* `%T%*U%D.zone` (optional tag and `_` + domain + `.zone`)
* `tag_example.com.zone` or `example.com.zone`
* `db_%T%?_%D`
* `db_inside_example.com` or `db_example.com`
* `db_%D`
* `db_example.com`
The last example will generate the same name for both
`D("example.tld!inside")` and `D("example.tld!outside")`. This
assumes two BIND providers are configured in `creds.json`, eacch with
a different `directory` setting. Otherwise `dnscontrol` will write
both domains to the same file, flapping between the two back and
forth.
# FYI: get-zones # FYI: get-zones
When used with "get-zones", specifying "all" scans the directory for The dnscontrol `get-zones all` subcommand scans the directory for
any files named `*.zone` and assumes they are zone files. any files named `*.zone` and assumes they are zone files.
``` ```
dnscontrol get-zones --format=nameonly - BIND all dnscontrol get-zones --format=nameonly - BIND all
``` ```
If `filenameformat` is defined, `dnscontrol` makes an guess at which
filenames are zones but doesn't try to hard to get it right, which is
mathematically impossible in all cases. Feel free to file an issue if
your format string doesn't work. I love a challenge!
# FYI: SOA Records # FYI: SOA Records
DNSControl assumes that SOA records are managed by the provider. Most DNSControl assumes that SOA records are managed by the provider. Most

View File

@ -13,6 +13,7 @@ import (
"github.com/StackExchange/dnscontrol/v3/models" "github.com/StackExchange/dnscontrol/v3/models"
"github.com/StackExchange/dnscontrol/v3/pkg/nameservers" "github.com/StackExchange/dnscontrol/v3/pkg/nameservers"
"github.com/StackExchange/dnscontrol/v3/pkg/normalize"
"github.com/StackExchange/dnscontrol/v3/providers" "github.com/StackExchange/dnscontrol/v3/providers"
_ "github.com/StackExchange/dnscontrol/v3/providers/_all" _ "github.com/StackExchange/dnscontrol/v3/providers/_all"
"github.com/StackExchange/dnscontrol/v3/providers/config" "github.com/StackExchange/dnscontrol/v3/providers/config"
@ -93,6 +94,8 @@ func getDomainConfigWithNameservers(t *testing.T, prv providers.DNSServiceProvid
dc := &models.DomainConfig{ dc := &models.DomainConfig{
Name: domainName, Name: domainName,
} }
normalize.UpdateNameSplitHorizon(dc)
// fix up nameservers // fix up nameservers
ns, err := prv.GetNameservers(domainName) ns, err := prv.GetNameservers(domainName)
if err != nil { if err != nil {

View File

@ -9,6 +9,8 @@ import (
// DomainConfig describes a DNS domain (tecnically a DNS zone). // DomainConfig describes a DNS domain (tecnically a DNS zone).
type DomainConfig struct { type DomainConfig struct {
Name string `json:"name"` // NO trailing "." Name string `json:"name"` // NO trailing "."
Tag string `json:"-"` // split horizon tag
UniqueName string `json:"-"` // .Name + "!" + .Tag
RegistrarName string `json:"registrar"` RegistrarName string `json:"registrar"`
DNSProviderNames map[string]int `json:"dnsProviders"` DNSProviderNames map[string]int `json:"dnsProviders"`

View File

@ -0,0 +1,24 @@
var REG = NewRegistrar("Third-Party", "NONE");
var DNS_MAIN = NewDnsProvider("otherconfig", "CLOUDFLAREAPI");
var DNS_INSIDE = NewDnsProvider("Cloudflare", "CLOUDFLAREAPI");
var DNS_OUTSIDE = NewDnsProvider("bind", "BIND");
D("example.com", REG, DnsProvider(DNS_MAIN),
A("main", "3.3.3.3")
);
D("example.com!inside", REG, DnsProvider(DNS_INSIDE),
A("main", "1.1.1.1")
);
D("example.com!outside", REG, DnsProvider(DNS_OUTSIDE),
A("main", "8.8.8.8")
);
D_EXTEND("example.com",
A("www", "33.33.33.33")
);
D_EXTEND("example.com!inside",
A("main", "11.11.11.11")
);

View File

@ -0,0 +1,76 @@
{
"dns_providers": [
{
"name": "otherconfig",
"type": "CLOUDFLAREAPI"
},
{
"name": "Cloudflare",
"type": "CLOUDFLAREAPI"
},
{
"name": "bind",
"type": "BIND"
}
],
"domains": [
{
"dnsProviders": {
"otherconfig": -1
},
"name": "example.com",
"records": [
{
"name": "main",
"target": "3.3.3.3",
"type": "A"
},
{
"name": "www",
"target": "33.33.33.33",
"type": "A"
}
],
"registrar": "Third-Party"
},
{
"dnsProviders": {
"Cloudflare": -1
},
"name": "example.com!inside",
"records": [
{
"name": "main",
"target": "1.1.1.1",
"type": "A"
},
{
"name": "main",
"target": "11.11.11.11",
"type": "A"
}
],
"registrar": "Third-Party"
},
{
"dnsProviders": {
"bind": -1
},
"name": "example.com!outside",
"records": [
{
"name": "main",
"target": "8.8.8.8",
"type": "A"
}
],
"registrar": "Third-Party"
}
],
"registrars": [
{
"name": "Third-Party",
"type": "NONE"
}
]
}

View File

@ -253,6 +253,11 @@ type Warning struct {
// ValidateAndNormalizeConfig performs and normalization and/or validation of the IR. // ValidateAndNormalizeConfig performs and normalization and/or validation of the IR.
func ValidateAndNormalizeConfig(config *models.DNSConfig) (errs []error) { func ValidateAndNormalizeConfig(config *models.DNSConfig) (errs []error) {
err := processSplitHorizonDomains(config)
if err != nil {
return []error{err}
}
for _, domain := range config.Domains { for _, domain := range config.Domains {
pTypes := []string{} pTypes := []string{}
for _, provider := range domain.DNSProviderInstances { for _, provider := range domain.DNSProviderInstances {
@ -457,6 +462,48 @@ func ValidateAndNormalizeConfig(config *models.DNSConfig) (errs []error) {
return errs return errs
} }
// UpdateNameSplitHorizon fills in the split horizon fields.
func UpdateNameSplitHorizon(dc *models.DomainConfig) {
if dc.UniqueName == "" {
dc.UniqueName = dc.Name
}
if dc.Tag == "" {
l := strings.SplitN(dc.Name, "!", 2)
if len(l) == 2 {
dc.Name = l[0]
dc.Tag = l[1]
}
}
}
// processSplitHorizonDomains finds "domain.tld!tag" domains and pre-processes them.
func processSplitHorizonDomains(config *models.DNSConfig) error {
// Parse out names and tags.
for _, d := range config.Domains {
UpdateNameSplitHorizon(d)
}
// Verify uniquenames are unique
seen := map[string]bool{}
for _, d := range config.Domains {
if seen[d.UniqueName] {
return fmt.Errorf("duplicate domain name: %q", d.UniqueName)
}
seen[d.UniqueName] = true
}
return nil
}
// parseDomainSpec parses "domain.tld!tag" into its component parts.
func parseDomainSpec(s string) (domain, tag string) {
l := strings.SplitN(s, "!", 2)
if len(l) == 2 {
return l[0], l[1]
}
return l[0], ""
}
func checkAutoDNSSEC(dc *models.DomainConfig) (errs []error) { func checkAutoDNSSEC(dc *models.DomainConfig) (errs []error) {
if dc.AutoDNSSEC != "" && dc.AutoDNSSEC != "on" && dc.AutoDNSSEC != "off" { if dc.AutoDNSSEC != "" && dc.AutoDNSSEC != "on" && dc.AutoDNSSEC != "off" {
errs = append(errs, fmt.Errorf("Domain %q AutoDNSSEC=%q is invalid (expecting \"\", \"off\", or \"on\")", dc.Name, dc.AutoDNSSEC)) errs = append(errs, fmt.Errorf("Domain %q AutoDNSSEC=%q is invalid (expecting \"\", \"off\", or \"on\")", dc.Name, dc.AutoDNSSEC))

View File

@ -53,10 +53,14 @@ func initBind(config map[string]string, providermeta json.RawMessage) (providers
// meta -- the json blob from NewReq('name', 'TYPE', meta) // meta -- the json blob from NewReq('name', 'TYPE', meta)
api := &bindProvider{ api := &bindProvider{
directory: config["directory"], directory: config["directory"],
filenameformat: config["filenameformat"],
} }
if api.directory == "" { if api.directory == "" {
api.directory = "zones" api.directory = "zones"
} }
if api.filenameformat == "" {
api.filenameformat = "%U.zone"
}
if len(providermeta) != 0 { if len(providermeta) != 0 {
err := json.Unmarshal(providermeta, api) err := json.Unmarshal(providermeta, api)
if err != nil { if err != nil {
@ -98,6 +102,7 @@ type bindProvider struct {
DefaultSoa SoaInfo `json:"default_soa"` DefaultSoa SoaInfo `json:"default_soa"`
nameservers []*models.Nameserver nameservers []*models.Nameserver
directory string directory string
filenameformat string
zonefile string // Where the zone data is expected zonefile string // Where the zone data is expected
zoneFileFound bool // Did the zonefile exist? zoneFileFound bool // Did the zonefile exist?
} }
@ -117,16 +122,19 @@ func (c *bindProvider) ListZones() ([]string, error) {
return nil, fmt.Errorf("directory %q does not exist", c.directory) return nil, fmt.Errorf("directory %q does not exist", c.directory)
} }
filenames, err := filepath.Glob(filepath.Join(c.directory, "*.zone")) var files []string
f, err := os.Open(c.directory)
if err != nil { if err != nil {
return nil, err return files, fmt.Errorf("bind ListZones open dir %q: %w",
c.directory, err)
} }
var zones []string filenames, err := f.Readdirnames(-1)
for _, n := range filenames { if err != nil {
_, file := filepath.Split(n) return files, fmt.Errorf("bind ListZones readdir %q: %w",
zones = append(zones, strings.TrimSuffix(file, ".zone")) c.directory, err)
} }
return zones, nil
return extractZonesFromFilenames(c.filenameformat, filenames), nil
} }
// GetZoneRecords gets the records of a zone and returns them in RecordConfig format. // GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
@ -137,15 +145,17 @@ func (c *bindProvider) GetZoneRecords(domain string) (models.Records, error) {
fmt.Printf("\nWARNING: BIND directory %q does not exist!\n", c.directory) fmt.Printf("\nWARNING: BIND directory %q does not exist!\n", c.directory)
} }
c.zonefile = filepath.Join( if c.zonefile == "" {
c.directory, // This layering violation is needed for tests only.
strings.Replace(strings.ToLower(domain), "/", "_", -1)+".zone") // Otherwise, this is set already.
c.zonefile = filepath.Join(c.directory,
makeFileName(c.filenameformat, domain, domain, ""))
}
content, err := ioutil.ReadFile(c.zonefile) content, err := ioutil.ReadFile(c.zonefile)
if os.IsNotExist(err) { if os.IsNotExist(err) {
// If the file doesn't exist, that's not an error. Just informational. // If the file doesn't exist, that's not an error. Just informational.
c.zoneFileFound = false c.zoneFileFound = false
fmt.Fprintf(os.Stderr, "File not found: '%v'\n", c.zonefile) fmt.Fprintf(os.Stderr, "File does not yet exist: %q\n", c.zonefile)
return nil, nil return nil, nil
} }
if err != nil { if err != nil {
@ -185,6 +195,9 @@ func (c *bindProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*models.
comments = append(comments, "Automatic DNSSEC signing requested") comments = append(comments, "Automatic DNSSEC signing requested")
} }
c.zonefile = filepath.Join(c.directory,
makeFileName(c.filenameformat, dc.UniqueName, dc.Name, dc.Tag))
foundRecords, err := c.GetZoneRecords(dc.Name) foundRecords, err := c.GetZoneRecords(dc.Name)
if err != nil { if err != nil {
return nil, err return nil, err

193
providers/bind/fnames.go Normal file
View File

@ -0,0 +1,193 @@
package bind
import (
"bytes"
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
)
// makeFileName uses format to generate a zone's filename. See the
func makeFileName(format, uniquename, domain, tag string) string {
if format == "" {
fmt.Fprintf(os.Stderr, "BUG: makeFileName called with null format\n")
return uniquename
}
var b bytes.Buffer
tokens := strings.Split(format, "")
lastpos := len(tokens) - 1
for pos := 0; pos < len(tokens); pos++ {
tok := tokens[pos]
if tok != "%" {
b.WriteString(tok)
continue
}
if pos == lastpos {
b.WriteString("%(format may not end in %)")
continue
}
pos++
tok = tokens[pos]
switch tok {
case "D":
b.WriteString(domain)
case "T":
b.WriteString(tag)
case "U":
b.WriteString(uniquename)
case "?":
if pos == lastpos {
b.WriteString("%(format may not end in %?)")
continue
}
pos++
tok = tokens[pos]
if tag != "" {
b.WriteString(tok)
}
default:
fmt.Fprintf(&b, "%%(unknown %%verb %%%s)", tok)
}
}
return b.String()
}
// extractZonesFromFilenames extracts the zone names from a list of filenames
// based on the format string used to create the files. It is mathematically
// impossible to do this correctly for all format strings, but typical format
// strings are supported.
func extractZonesFromFilenames(format string, names []string) []string {
var zones []string
// Generate a regex that will extract the zonename from a filename.
extractor, err := makeExtractor(format)
if err != nil {
// Give up. Return the list of filenames.
return names
}
re := regexp.MustCompile(extractor)
//
for _, n := range names {
_, file := filepath.Split(n)
l := re.FindStringSubmatch(file)
// l[1:] is a list of matches and null strings. Pick the first non-null string.
if len(l) > 1 {
for _, s := range l[1:] {
if s != "" {
zones = append(zones, s)
break
}
}
}
}
return zones
}
// makeExtractor generates a regex that extracts domain names from filenames.
// format specifies the format string used by makeFileName to generate such
// filenames. It is mathematically impossible to do this correctly for all
// format strings, but typical format strings are supported.
func makeExtractor(format string) (string, error) {
// The algorithm works as follows.
// We generate a regex that is A or A|B.
// A is the regex that works if tag is non-null.
// B is the regex that assumes tags are "".
// If no tag-related verbs are used, A is sufficient.
// If a tag-related verb is used, we append | and generate B, which does
// Each % verb is turned into an appropriate subexpression based on pass.
// NB: This is some rather fancy CS stuff just to make the
// "get-zones all" command work for BIND. That's a lot of work for
// a feature that isn't going to be used very often, if at all.
// Therefore if this ever becomes a maintenance bother, we can just
// replace this with something more simple. For example, the
// creds.json file could specify the regex and humans can specify
// the Extractor themselves. Or, just remove this feature from the
// BIND driver.
var b bytes.Buffer
tokens := strings.Split(format, "")
lastpos := len(tokens) - 1
generateB := false
for pass := range []int{0, 1} {
for pos := 0; pos < len(tokens); pos++ {
tok := tokens[pos]
if tok == "." {
// dots are escaped
b.WriteString(`\.`)
continue
}
if tok != "%" {
// ordinary runes are passed unmodified.
b.WriteString(tok)
continue
}
if pos == lastpos {
return ``, fmt.Errorf("format may not end in %%: %q", format)
}
// Process % verbs
// Move to the next token, which is the verb name: D, U, etc.
pos++
tok = tokens[pos]
switch tok {
case "D":
b.WriteString(`(.*)`)
case "T":
if pass == 0 {
// On the second pass, nothing is generated.
b.WriteString(`.*`)
}
case "U":
if pass == 0 {
b.WriteString(`(.*)!.+`)
} else {
b.WriteString(`(.*)`)
}
generateB = true
case "?":
if pos == lastpos {
return ``, fmt.Errorf("format may not end in %%?: %q", format)
}
// Move to the next token, the tag-only char.
pos++
tok = tokens[pos]
if pass == 0 {
// On the second pass, nothing is generated.
b.WriteString(tok)
}
generateB = true
default:
return ``, fmt.Errorf("unknown %%verb %%%s: %q", tok, format)
}
}
// At the end of the first pass determine if we need the second pass.
if pass == 0 {
if generateB {
// We had a %? token. Now repeat the process
// but generate an "or" that assumes no tags.
b.WriteString(`|`)
} else {
break
}
}
}
return b.String(), nil
}

View File

@ -0,0 +1,138 @@
package bind
import (
"reflect"
"testing"
)
func Test_makeFileName(t *testing.T) {
uu := "uni"
dd := "domy"
tt := "tagy"
fmtDefault := "%U.zone"
fmtBasic := "%U - %T - %D"
fmtBk1 := "db_%U" // Something I've seen in books on DNS
fmtBk2 := "db_%T_%D" // Something I've seen in books on DNS
fmtFancy := "%T%?_%D.zone" // Include the tag_ only if there is a tag
fmtErrorPct := "literal%"
fmtErrorOpt := "literal%?"
fmtErrorUnk := "literal%o" // Unknown % verb
type args struct {
format string
uniquename string
domain string
tag string
}
tests := []struct {
name string
args args
want string
}{
{"literal", args{"literal", uu, dd, tt}, "literal"},
{"basic", args{fmtBasic, uu, dd, tt}, "uni - tagy - domy"},
{"solo", args{"%D", uu, dd, tt}, "domy"},
{"front", args{"%Daaa", uu, dd, tt}, "domyaaa"},
{"tail", args{"bbb%D", uu, dd, tt}, "bbbdomy"},
{"def", args{fmtDefault, uu, dd, tt}, "uni.zone"},
{"bk1", args{fmtBk1, uu, dd, tt}, "db_uni"},
{"bk2", args{fmtBk2, uu, dd, tt}, "db_tagy_domy"},
{"fanWI", args{fmtFancy, uu, dd, tt}, "tagy_domy.zone"},
{"fanWO", args{fmtFancy, uu, dd, ""}, "domy.zone"},
{"errP", args{fmtErrorPct, uu, dd, tt}, "literal%(format may not end in %)"},
{"errQ", args{fmtErrorOpt, uu, dd, tt}, "literal%(format may not end in %?)"},
{"errU", args{fmtErrorUnk, uu, dd, tt}, "literal%(unknown %verb %o)"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := makeFileName(tt.args.format, tt.args.uniquename, tt.args.domain, tt.args.tag); got != tt.want {
t.Errorf("makeFileName() = %v, want %v", got, tt.want)
}
})
}
}
func Test_makeExtractor(t *testing.T) {
type args struct {
format string
}
tests := []struct {
name string
args args
want string
wantErr bool
}{
// TODO: Add test cases.
{"u", args{"%U.zone"}, `(.*)!.+\.zone|(.*)\.zone`, false},
{"d", args{"%D.zone"}, `(.*)\.zone`, false},
{"basic", args{"%U - %T - %D"}, `(.*)!.+ - .* - (.*)|(.*) - - (.*)`, false},
{"bk1", args{"db_%U"}, `db_(.*)!.+|db_(.*)`, false},
{"bk2", args{"db_%T_%D"}, `db_.*_(.*)`, false},
{"fan", args{"%T%?_%D.zone"}, `.*_(.*)\.zone|(.*)\.zone`, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := makeExtractor(tt.args.format)
if (err != nil) != tt.wantErr {
t.Errorf("makeExtractor() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("makeExtractor() = %v, want %v", got, tt.want)
}
})
}
}
func Test_extractZonesFromFilenames(t *testing.T) {
type args struct {
format string
names []string
}
// A list of filenames one might find in a directory.
filelist := []string{
"foo!one.zone", // u
"dom.tld!two.zone", // u
"foo.zone", // d
"dom.tld.zone", // d
"foo!one - one - foo", // basic
"dom.tld!two - two - dom.tld", // basic
"db_foo", // bk1
"db_dom.tld", // bk1
"db_dom.tld!tag", // bk1
"db_inside_foo", // bk2
"db_outside_dom.tld", // bk2
"db__example.com", // bk2
"dom.zone", // fan
"example.com.zone", // fan (no tag)
"mytag_example.com.zone", // fan (w/ tag)
"dom.zone", // fan (no tag)
"mytag_dom.zone", // fan (w/ tag)
}
tests := []struct {
name string
args args
want []string
}{
{"0", args{"%D.zone", []string{"foo.zone", "dom.tld.zone"}}, []string{"foo", "dom.tld"}},
{"1", args{"%U.zone", []string{"foo.zone", "dom.tld.zone"}}, []string{"foo", "dom.tld"}},
{"2", args{"%T%?_%D.zone", []string{"inside_ex.tld.zone", "foo.zone", "dom.tld.zone"}}, []string{"ex.tld", "foo", "dom.tld"}},
{"d", args{"%D.zone", filelist}, []string{"foo!one", "dom.tld!two", "foo", "dom.tld", "dom", "example.com", "mytag_example.com", "dom", "mytag_dom"}},
{"u", args{"%U.zone", filelist}, []string{"foo", "dom.tld", "foo", "dom.tld", "dom", "example.com", "mytag_example.com", "dom", "mytag_dom"}},
{"bk1", args{"db_%U", filelist}, []string{"foo", "dom.tld", "dom.tld", "inside_foo", "outside_dom.tld", "_example.com"}},
{"bk2", args{"db_%T_%D", filelist}, []string{"foo", "dom.tld", "example.com"}},
{"fan", args{"%T%?_%D.zone", filelist}, []string{"foo!one", "dom.tld!two", "foo", "dom.tld", "dom", "example.com", "example.com", "dom", "dom"}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := extractZonesFromFilenames(tt.args.format, tt.args.names); !reflect.DeepEqual(got, tt.want) {
ext, _ := makeExtractor(tt.args.format)
t.Errorf("extractZonesFromFilenames() = %v, want %v Fm=%s Ex=%s", got, tt.want, tt.args.format, ext)
}
})
}
}