package commands import ( "fmt" "log" "os" "github.com/StackExchange/dnscontrol/v2/models" "github.com/StackExchange/dnscontrol/v2/pkg/nameservers" "github.com/StackExchange/dnscontrol/v2/pkg/normalize" "github.com/StackExchange/dnscontrol/v2/pkg/notifications" "github.com/StackExchange/dnscontrol/v2/pkg/printer" "github.com/StackExchange/dnscontrol/v2/providers" "github.com/StackExchange/dnscontrol/v2/providers/config" "github.com/pkg/errors" "github.com/urfave/cli" ) var _ = cmd(catMain, func() *cli.Command { var args PreviewArgs return &cli.Command{ Name: "preview", Usage: "read live configuration and identify changes to be made, without applying them", Action: func(ctx *cli.Context) error { return exit(Preview(args)) }, Flags: args.flags(), } }()) // PreviewArgs contains all data/flags needed to run preview, independently of CLI type PreviewArgs struct { GetDNSConfigArgs GetCredentialsArgs FilterArgs Notify bool WarnChanges bool } func (args *PreviewArgs) flags() []cli.Flag { flags := args.GetDNSConfigArgs.flags() flags = append(flags, args.GetCredentialsArgs.flags()...) flags = append(flags, args.FilterArgs.flags()...) flags = append(flags, cli.BoolFlag{ Name: "notify", Destination: &args.Notify, Usage: `set to true to send notifications to configured destinations`, }) flags = append(flags, cli.BoolFlag{ Name: "expect-no-changes", Destination: &args.WarnChanges, Usage: `set to true for non-zero return code if there are changes`, }) return flags } var _ = cmd(catMain, func() *cli.Command { var args PushArgs return &cli.Command{ Name: "push", Usage: "identify changes to be made, and perform them", Action: func(ctx *cli.Context) error { return exit(Push(args)) }, Flags: args.flags(), } }()) // PushArgs contains all data/flags needed to run push, independently of CLI type PushArgs struct { PreviewArgs Interactive bool } func (args *PushArgs) flags() []cli.Flag { flags := args.PreviewArgs.flags() flags = append(flags, cli.BoolFlag{ Name: "i", Destination: &args.Interactive, Usage: "Interactive. Confirm or Exclude each correction before they run", }) return flags } // Preview implements the preview subcommand. func Preview(args PreviewArgs) error { return run(args, false, false, printer.DefaultPrinter) } // Push implements the push subcommand. func Push(args PushArgs) error { return run(args.PreviewArgs, true, args.Interactive, printer.DefaultPrinter) } // run is the main routine common to preview/push func run(args PreviewArgs, push bool, interactive bool, out printer.CLI) error { // TODO: make truly CLI independent. Perhaps return results on a channel as they occur cfg, err := GetDNSConfig(args.GetDNSConfigArgs) if err != nil { return err } errs := normalize.NormalizeAndValidateConfig(cfg) if PrintValidationErrors(errs) { return errors.Errorf("Exiting due to validation errors") } // TODO: notifier, err := InitializeProviders(args.CredsFile, cfg, args.Notify) if err != nil { return err } anyErrors := false totalCorrections := 0 DomainLoop: for _, domain := range cfg.Domains { if !args.shouldRunDomain(domain.Name) { continue } out.StartDomain(domain.Name) nsList, err := nameservers.DetermineNameservers(domain) if err != nil { return err } domain.Nameservers = nsList nameservers.AddNSRecords(domain) for _, provider := range domain.DNSProviderInstances { dc, err := domain.Copy() if err != nil { return err } shouldrun := args.shouldRunProvider(provider.Name, dc) out.StartDNSProvider(provider.Name, !shouldrun) if !shouldrun { continue } corrections, err := provider.Driver.GetDomainCorrections(dc) out.EndProvider(len(corrections), err) if err != nil { anyErrors = true continue DomainLoop } totalCorrections += len(corrections) anyErrors = printOrRunCorrections(domain.Name, provider.Name, corrections, out, push, interactive, notifier) || anyErrors } run := args.shouldRunProvider(domain.RegistrarName, domain) out.StartRegistrar(domain.RegistrarName, !run) if !run { continue } if len(domain.Nameservers) == 0 && domain.Metadata["no_ns"] != "true" { out.Warnf("No nameservers declared; skipping registrar. Add {no_ns:'true'} to force.\n") continue } dc, err := domain.Copy() if err != nil { log.Fatal(err) } corrections, err := domain.RegistrarInstance.Driver.GetRegistrarCorrections(dc) out.EndProvider(len(corrections), err) if err != nil { anyErrors = true continue } totalCorrections += len(corrections) anyErrors = printOrRunCorrections(domain.Name, domain.RegistrarName, corrections, out, push, interactive, notifier) || anyErrors } if os.Getenv("TEAMCITY_VERSION") != "" { fmt.Fprintf(os.Stderr, "##teamcity[buildStatus status='SUCCESS' text='%d corrections']", totalCorrections) } notifier.Done() out.Printf("Done. %d corrections.\n", totalCorrections) if anyErrors { return errors.Errorf("Completed with errors") } if totalCorrections != 0 && args.WarnChanges { return errors.Errorf("There are pending changes") } return nil } // InitializeProviders takes a creds file path and a DNSConfig object. Creates all providers with the proper types, and returns them. // nonDefaultProviders is a list of providers that should not be run unless explicitly asked for by flags. func InitializeProviders(credsFile string, cfg *models.DNSConfig, notifyFlag bool) (notify notifications.Notifier, err error) { var providerConfigs map[string]map[string]string var notificationCfg map[string]string defer func() { notify = notifications.Init(notificationCfg) }() providerConfigs, err = config.LoadProviderConfigs(credsFile) if err != nil { return } if notifyFlag { notificationCfg = providerConfigs["notifications"] } isNonDefault := map[string]bool{} for name, vals := range providerConfigs { // add "_exclude_from_defaults":"true" to a provider to exclude it from being run unless // -providers=all or -providers=name if vals["_exclude_from_defaults"] == "true" { isNonDefault[name] = true } } registrars := map[string]providers.Registrar{} dnsProviders := map[string]providers.DNSServiceProvider{} for _, d := range cfg.Domains { if registrars[d.RegistrarName] == nil { rCfg := cfg.RegistrarsByName[d.RegistrarName] r, err := providers.CreateRegistrar(rCfg.Type, providerConfigs[d.RegistrarName]) if err != nil { return nil, err } registrars[d.RegistrarName] = r } d.RegistrarInstance.Driver = registrars[d.RegistrarName] d.RegistrarInstance.IsDefault = !isNonDefault[d.RegistrarName] for _, pInst := range d.DNSProviderInstances { if dnsProviders[pInst.Name] == nil { dCfg := cfg.DNSProvidersByName[pInst.Name] prov, err := providers.CreateDNSProvider(dCfg.Type, providerConfigs[dCfg.Name], dCfg.Metadata) if err != nil { return nil, err } dnsProviders[pInst.Name] = prov } pInst.Driver = dnsProviders[pInst.Name] pInst.IsDefault = !isNonDefault[pInst.Name] } } return } func printOrRunCorrections(domain string, provider string, corrections []*models.Correction, out printer.CLI, push bool, interactive bool, notifier notifications.Notifier) (anyErrors bool) { anyErrors = false if len(corrections) == 0 { return false } for i, correction := range corrections { out.PrintCorrection(i, correction) var err error if push { if interactive && !out.PromptToRun() { continue } err = correction.F() out.EndCorrection(err) if err != nil { anyErrors = true } } notifier.Notify(domain, provider, correction.Msg, err, !push) } return anyErrors }