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:
@ -112,10 +112,10 @@ func run(args PreviewArgs, push bool, interactive bool, out printer.CLI) error {
|
||||
totalCorrections := 0
|
||||
DomainLoop:
|
||||
for _, domain := range cfg.Domains {
|
||||
if !args.shouldRunDomain(domain.Name) {
|
||||
if !args.shouldRunDomain(domain.UniqueName) {
|
||||
continue
|
||||
}
|
||||
out.StartDomain(domain.Name)
|
||||
out.StartDomain(domain.UniqueName)
|
||||
nsList, err := nameservers.DetermineNameservers(domain)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -42,6 +42,56 @@ D("example.com", REGISTRAR, DnsProvider(r53),
|
||||
CNAME("test", "foo.example2.com."),
|
||||
GOOGLE_APPS_DOMAIN_MX
|
||||
);
|
||||
|
||||
{%endhighlight%}
|
||||
{% 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.
|
||||
|
@ -18,11 +18,13 @@ you can specify a `directory` where the provider will look for and create zone f
|
||||
{% highlight json %}
|
||||
{
|
||||
"bind": {
|
||||
"directory": "myzones"
|
||||
"directory": "myzones",
|
||||
"filenameformat": "%U.zone" << The default
|
||||
}
|
||||
}
|
||||
{% endhighlight %}
|
||||
|
||||
|
||||
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.
|
||||
@ -46,15 +48,55 @@ var BIND = NewDnsProvider('bind', 'BIND', {
|
||||
})
|
||||
{% 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
|
||||
|
||||
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.
|
||||
|
||||
```
|
||||
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
|
||||
|
||||
DNSControl assumes that SOA records are managed by the provider. Most
|
||||
|
@ -13,6 +13,7 @@ import (
|
||||
|
||||
"github.com/StackExchange/dnscontrol/v3/models"
|
||||
"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/_all"
|
||||
"github.com/StackExchange/dnscontrol/v3/providers/config"
|
||||
@ -93,6 +94,8 @@ func getDomainConfigWithNameservers(t *testing.T, prv providers.DNSServiceProvid
|
||||
dc := &models.DomainConfig{
|
||||
Name: domainName,
|
||||
}
|
||||
normalize.UpdateNameSplitHorizon(dc)
|
||||
|
||||
// fix up nameservers
|
||||
ns, err := prv.GetNameservers(domainName)
|
||||
if err != nil {
|
||||
|
@ -9,6 +9,8 @@ import (
|
||||
// DomainConfig describes a DNS domain (tecnically a DNS zone).
|
||||
type DomainConfig struct {
|
||||
Name string `json:"name"` // NO trailing "."
|
||||
Tag string `json:"-"` // split horizon tag
|
||||
UniqueName string `json:"-"` // .Name + "!" + .Tag
|
||||
RegistrarName string `json:"registrar"`
|
||||
DNSProviderNames map[string]int `json:"dnsProviders"`
|
||||
|
||||
|
24
pkg/js/parse_tests/037-splithor.js
Normal file
24
pkg/js/parse_tests/037-splithor.js
Normal 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")
|
||||
);
|
76
pkg/js/parse_tests/037-splithor.json
Normal file
76
pkg/js/parse_tests/037-splithor.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
@ -253,6 +253,11 @@ type Warning struct {
|
||||
|
||||
// ValidateAndNormalizeConfig performs and normalization and/or validation of the IR.
|
||||
func ValidateAndNormalizeConfig(config *models.DNSConfig) (errs []error) {
|
||||
err := processSplitHorizonDomains(config)
|
||||
if err != nil {
|
||||
return []error{err}
|
||||
}
|
||||
|
||||
for _, domain := range config.Domains {
|
||||
pTypes := []string{}
|
||||
for _, provider := range domain.DNSProviderInstances {
|
||||
@ -457,6 +462,48 @@ func ValidateAndNormalizeConfig(config *models.DNSConfig) (errs []error) {
|
||||
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) {
|
||||
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))
|
||||
|
@ -53,10 +53,14 @@ func initBind(config map[string]string, providermeta json.RawMessage) (providers
|
||||
// meta -- the json blob from NewReq('name', 'TYPE', meta)
|
||||
api := &bindProvider{
|
||||
directory: config["directory"],
|
||||
filenameformat: config["filenameformat"],
|
||||
}
|
||||
if api.directory == "" {
|
||||
api.directory = "zones"
|
||||
}
|
||||
if api.filenameformat == "" {
|
||||
api.filenameformat = "%U.zone"
|
||||
}
|
||||
if len(providermeta) != 0 {
|
||||
err := json.Unmarshal(providermeta, api)
|
||||
if err != nil {
|
||||
@ -98,6 +102,7 @@ type bindProvider struct {
|
||||
DefaultSoa SoaInfo `json:"default_soa"`
|
||||
nameservers []*models.Nameserver
|
||||
directory string
|
||||
filenameformat string
|
||||
zonefile string // Where the zone data is expected
|
||||
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)
|
||||
}
|
||||
|
||||
filenames, err := filepath.Glob(filepath.Join(c.directory, "*.zone"))
|
||||
var files []string
|
||||
f, err := os.Open(c.directory)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return files, fmt.Errorf("bind ListZones open dir %q: %w",
|
||||
c.directory, err)
|
||||
}
|
||||
var zones []string
|
||||
for _, n := range filenames {
|
||||
_, file := filepath.Split(n)
|
||||
zones = append(zones, strings.TrimSuffix(file, ".zone"))
|
||||
filenames, err := f.Readdirnames(-1)
|
||||
if err != nil {
|
||||
return files, fmt.Errorf("bind ListZones readdir %q: %w",
|
||||
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.
|
||||
@ -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)
|
||||
}
|
||||
|
||||
c.zonefile = filepath.Join(
|
||||
c.directory,
|
||||
strings.Replace(strings.ToLower(domain), "/", "_", -1)+".zone")
|
||||
|
||||
if c.zonefile == "" {
|
||||
// This layering violation is needed for tests only.
|
||||
// Otherwise, this is set already.
|
||||
c.zonefile = filepath.Join(c.directory,
|
||||
makeFileName(c.filenameformat, domain, domain, ""))
|
||||
}
|
||||
content, err := ioutil.ReadFile(c.zonefile)
|
||||
if os.IsNotExist(err) {
|
||||
// If the file doesn't exist, that's not an error. Just informational.
|
||||
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
|
||||
}
|
||||
if err != nil {
|
||||
@ -185,6 +195,9 @@ func (c *bindProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*models.
|
||||
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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
193
providers/bind/fnames.go
Normal file
193
providers/bind/fnames.go
Normal 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
|
||||
}
|
138
providers/bind/fnames_test.go
Normal file
138
providers/bind/fnames_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user