mirror of
https://github.com/StackExchange/dnscontrol.git
synced 2025-12-09 13:46:07 +08:00
# Issue Fixes https://github.com/StackExchange/dnscontrol/issues/3842 CC @das7pad # Resolution Convert domain.Name to IDN earlier in the pipeline. Hack the --domains processing to convert everything to IDN. * Domain names are now stored 3 ways: The original input from dnsconfig.js, canonical IDN format (`xn--...`), and Unicode format. All are downcased. Providers that haven't been updated will receive the IDN format instead of the original input format. This might break some providers but only for users with unicode in their D("domain.tld"). PLEASE TEST YOUR PROVIDER. * BIND filename formatting options have been added to access the new formats. # Breaking changes * BIND zonefiles may change. The default used the name input in the D() statement. It now defaults to the IDN name + "!tag" if there is a tag. * Providers that are not IDN-aware may break (hopefully only if they weren't processing IDN already) --------- Co-authored-by: Jakob Ackermann <das7pad@outlook.com>
305 lines
8 KiB
Go
305 lines
8 KiB
Go
package commands
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/StackExchange/dnscontrol/v4/models"
|
|
"github.com/StackExchange/dnscontrol/v4/pkg/diff2"
|
|
"github.com/StackExchange/dnscontrol/v4/pkg/js"
|
|
"github.com/StackExchange/dnscontrol/v4/pkg/printer"
|
|
"github.com/StackExchange/dnscontrol/v4/pkg/version"
|
|
"github.com/fatih/color"
|
|
"github.com/urfave/cli/v2"
|
|
)
|
|
|
|
// categories of commands
|
|
const (
|
|
catMain = "\b main" // screwed up to alphebatize first
|
|
catDebug = "debug"
|
|
catUtils = "utility"
|
|
)
|
|
|
|
var commands = []*cli.Command{}
|
|
|
|
func cmd(cat string, c *cli.Command) bool {
|
|
c.Category = cat
|
|
commands = append(commands, c)
|
|
return true
|
|
}
|
|
|
|
var _ = cmd(catDebug, &cli.Command{
|
|
Name: "version",
|
|
Usage: "Print version information",
|
|
Action: func(c *cli.Context) error {
|
|
_, err := fmt.Println(version.Version())
|
|
return err
|
|
},
|
|
})
|
|
|
|
// Run will execute the CLI
|
|
func Run(v string) int {
|
|
app := cli.NewApp()
|
|
app.Version = v
|
|
app.Name = "dnscontrol"
|
|
app.HideVersion = true
|
|
app.Usage = "DNSControl is a compiler and DSL for managing dns zones"
|
|
app.Flags = []cli.Flag{
|
|
&cli.BoolFlag{
|
|
Name: "debug",
|
|
Aliases: []string{"v"},
|
|
Usage: "Enable debug logging",
|
|
Destination: &printer.DefaultPrinter.Verbose,
|
|
},
|
|
&cli.BoolFlag{
|
|
Name: "allow-fetch",
|
|
Usage: "Enable JS fetch(), dangerous on untrusted code!",
|
|
Destination: &js.EnableFetch,
|
|
},
|
|
&cli.BoolFlag{
|
|
Name: "diff2",
|
|
Usage: "Obsolete flag. Will be removed in v5 or later",
|
|
Hidden: true,
|
|
Action: func(ctx *cli.Context, v bool) error {
|
|
pobsoleteDiff2FlagUsed = true
|
|
return nil
|
|
},
|
|
},
|
|
&cli.BoolFlag{
|
|
Name: "disableordering",
|
|
Usage: "Disables update reordering",
|
|
Destination: &diff2.DisableOrdering,
|
|
},
|
|
&cli.BoolFlag{
|
|
Name: "no-colors",
|
|
Usage: "Disable colors",
|
|
Destination: &color.NoColor,
|
|
Value: false,
|
|
},
|
|
}
|
|
sort.Sort(cli.CommandsByName(commands))
|
|
app.Commands = commands
|
|
app.EnableBashCompletion = true
|
|
app.BashComplete = func(cCtx *cli.Context) {
|
|
// ripped from cli.DefaultCompleteWithFlags
|
|
var lastArg string
|
|
|
|
if len(os.Args) > 2 {
|
|
lastArg = os.Args[len(os.Args)-2]
|
|
}
|
|
|
|
if lastArg != "" {
|
|
if strings.HasPrefix(lastArg, "-") {
|
|
if !islastFlagComplete(lastArg, app.Flags) {
|
|
dnscontrolPrintFlagSuggestions(lastArg, app.Flags, cCtx.App.Writer)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
dnscontrolPrintCommandSuggestions(app.Commands, cCtx.App.Writer)
|
|
}
|
|
if err := app.Run(os.Args); err != nil {
|
|
return 1
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// Shared config types
|
|
|
|
// GetDNSConfigArgs contains what we need to get a valid dns config.
|
|
// Could come from parsing js, or from stored json
|
|
type GetDNSConfigArgs struct {
|
|
ExecuteDSLArgs
|
|
JSONFile string
|
|
}
|
|
|
|
func (args *GetDNSConfigArgs) flags() []cli.Flag {
|
|
return append(args.ExecuteDSLArgs.flags(),
|
|
&cli.StringFlag{
|
|
Destination: &args.JSONFile,
|
|
Name: "ir",
|
|
Usage: "Read IR (json) directly from this file. Do not process DSL at all",
|
|
},
|
|
&cli.StringFlag{
|
|
Destination: &args.JSONFile,
|
|
Name: "json",
|
|
Hidden: true,
|
|
Usage: "same as -ir. only here for backwards compatibility, hence hidden",
|
|
},
|
|
)
|
|
}
|
|
|
|
// GetDNSConfig reads the json-formatted IR file. Or executes javascript. All depending on flags provided.
|
|
func GetDNSConfig(args GetDNSConfigArgs) (*models.DNSConfig, error) {
|
|
var err error
|
|
cfg := &models.DNSConfig{}
|
|
|
|
if args.JSONFile == "" {
|
|
// No IR file specified. Generate the IR by running dnsconfig.json
|
|
// as normal.
|
|
cfg, err = ExecuteDSL(args.ExecuteDSLArgs)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
} else {
|
|
// Read an IR file.
|
|
f, err := os.Open(args.JSONFile)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer f.Close()
|
|
dec := json.NewDecoder(f)
|
|
if err = dec.Decode(cfg); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return preloadProviders(cfg)
|
|
}
|
|
|
|
// the json only contains provider names inside domains. This denormalizes the data for more
|
|
// convenient access patterns. Does everything we need to prepare for the validation phase, but
|
|
// cannot do anything that requires the credentials file yet.
|
|
func preloadProviders(cfg *models.DNSConfig) (*models.DNSConfig, error) {
|
|
// build name to type maps
|
|
cfg.RegistrarsByName = map[string]*models.RegistrarConfig{}
|
|
cfg.DNSProvidersByName = map[string]*models.DNSProviderConfig{}
|
|
for _, reg := range cfg.Registrars {
|
|
cfg.RegistrarsByName[reg.Name] = reg
|
|
}
|
|
for _, p := range cfg.DNSProviders {
|
|
cfg.DNSProvidersByName[p.Name] = p
|
|
}
|
|
// make registrar and dns provider shims. Include name, type, and other metadata, but can't instantiate
|
|
// driver until we load creds in later
|
|
for _, d := range cfg.Domains {
|
|
reg, ok := cfg.RegistrarsByName[d.RegistrarName]
|
|
if !ok {
|
|
return nil, fmt.Errorf("registrar named %s expected for %s, but never registered", d.RegistrarName, d.Name)
|
|
}
|
|
d.RegistrarInstance = &models.RegistrarInstance{
|
|
ProviderBase: models.ProviderBase{
|
|
Name: reg.Name,
|
|
ProviderType: reg.Type,
|
|
},
|
|
}
|
|
for pName, n := range d.DNSProviderNames {
|
|
prov, ok := cfg.DNSProvidersByName[pName]
|
|
if !ok {
|
|
return nil, fmt.Errorf("DNS Provider named %s expected for %s, but never registered", pName, d.Name)
|
|
}
|
|
d.DNSProviderInstances = append(d.DNSProviderInstances, &models.DNSProviderInstance{
|
|
ProviderBase: models.ProviderBase{
|
|
Name: pName,
|
|
ProviderType: prov.Type,
|
|
},
|
|
NumberOfNameservers: n,
|
|
})
|
|
}
|
|
// sort so everything is deterministic
|
|
sort.Slice(d.DNSProviderInstances, func(i, j int) bool {
|
|
return d.DNSProviderInstances[i].Name < d.DNSProviderInstances[j].Name
|
|
})
|
|
}
|
|
return cfg, nil
|
|
}
|
|
|
|
// ExecuteDSLArgs are used anytime we need to read and execute dnscontrol DSL
|
|
type ExecuteDSLArgs struct {
|
|
JSFile string
|
|
JSONFile string
|
|
DevMode bool
|
|
Variable cli.StringSlice
|
|
}
|
|
|
|
func (args *ExecuteDSLArgs) flags() []cli.Flag {
|
|
return []cli.Flag{
|
|
&cli.StringFlag{
|
|
Name: "config",
|
|
Value: "dnsconfig.js",
|
|
Destination: &args.JSFile,
|
|
Usage: "File containing dns config in javascript DSL",
|
|
},
|
|
&cli.StringFlag{
|
|
Name: "js",
|
|
Value: "dnsconfig.js",
|
|
Hidden: true,
|
|
Destination: &args.JSFile,
|
|
Usage: "same as config. for back compatibility",
|
|
},
|
|
&cli.BoolFlag{
|
|
Name: "dev",
|
|
Destination: &args.DevMode,
|
|
Usage: "Use helpers.js from disk instead of embedded copy",
|
|
},
|
|
&cli.StringSliceFlag{
|
|
Name: "variable",
|
|
Aliases: []string{"v"},
|
|
Destination: &args.Variable,
|
|
Usage: "Add variable that is passed to JS",
|
|
},
|
|
}
|
|
}
|
|
|
|
// PrintJSONArgs are used anytime a command may print some json
|
|
type PrintJSONArgs struct {
|
|
Pretty bool
|
|
Output string
|
|
}
|
|
|
|
func (args *PrintJSONArgs) flags() []cli.Flag {
|
|
return []cli.Flag{
|
|
&cli.BoolFlag{
|
|
Name: "pretty",
|
|
Destination: &args.Pretty,
|
|
Usage: "Pretty print IR JSON",
|
|
},
|
|
&cli.StringFlag{
|
|
Name: "out",
|
|
Destination: &args.Output,
|
|
Usage: "File to write IR JSON to (default stdout)",
|
|
},
|
|
}
|
|
}
|
|
|
|
// GetCredentialsArgs encapsulates the flags/args for sub-commands that use the creds.json file.
|
|
type GetCredentialsArgs struct {
|
|
CredsFile string
|
|
}
|
|
|
|
func (args *GetCredentialsArgs) flags() []cli.Flag {
|
|
return []cli.Flag{
|
|
&cli.StringFlag{
|
|
Name: "creds",
|
|
Destination: &args.CredsFile,
|
|
Usage: "Provider credentials JSON file (or !program to execute program that outputs json)",
|
|
Value: "creds.json",
|
|
},
|
|
}
|
|
}
|
|
|
|
// FilterArgs encapsulates the flags/args for sub-commands that can filter by provider or domain.
|
|
type FilterArgs struct {
|
|
Providers string
|
|
Domains string
|
|
}
|
|
|
|
func (args *FilterArgs) flags() []cli.Flag {
|
|
return []cli.Flag{
|
|
&cli.StringFlag{
|
|
Name: "providers",
|
|
Destination: &args.Providers,
|
|
Usage: `Providers to enable (comma separated list); default is all. Can exclude individual providers from default by adding '"_exclude_from_defaults": "true"' to the credentials file for a provider`,
|
|
Value: "",
|
|
},
|
|
&cli.StringFlag{
|
|
Name: "domains",
|
|
Destination: &args.Domains,
|
|
Usage: `Comma separated list of domain names to include`,
|
|
Value: "",
|
|
},
|
|
}
|
|
}
|