mirror of
https://github.com/StackExchange/dnscontrol.git
synced 2024-11-10 09:12:47 +08:00
293d5cb547
Co-authored-by: Jeffrey Cafferata <jeffrey@jcid.nl>
345 lines
8.8 KiB
Go
345 lines
8.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/urfave/cli/v2"
|
|
|
|
"github.com/fatih/color"
|
|
)
|
|
|
|
// 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)
|
|
return err
|
|
},
|
|
})
|
|
|
|
// Run will execute the CLI
|
|
func Run(v string) int {
|
|
version = v
|
|
app := cli.NewApp()
|
|
app.Version = version
|
|
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 {
|
|
obsoleteDiff2FlagUsed = 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: "",
|
|
},
|
|
}
|
|
}
|
|
|
|
func (args *FilterArgs) shouldRunProvider(name string, dc *models.DomainConfig) bool {
|
|
if args.Providers == "all" {
|
|
return true
|
|
}
|
|
if args.Providers == "" {
|
|
for _, pri := range dc.DNSProviderInstances {
|
|
if pri.Name == name {
|
|
return pri.IsDefault
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
for _, prov := range strings.Split(args.Providers, ",") {
|
|
if prov == name {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (args *FilterArgs) shouldRunDomain(d string) bool {
|
|
if args.Domains == "" {
|
|
return true
|
|
}
|
|
return domainInList(d, strings.Split(args.Domains, ","))
|
|
}
|
|
|
|
func domainInList(domain string, list []string) bool {
|
|
for _, item := range list {
|
|
if strings.HasPrefix(item, "*") && strings.HasSuffix(domain, item[1:]) {
|
|
return true
|
|
}
|
|
if item == domain {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|