diff --git a/.goreleaser.yml b/.goreleaser.yml index b0d183176..abec1e093 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -159,9 +159,9 @@ release: ## Deprecation warnings > [!WARNING] - > - **32-bit binaries will no longer be distributed after September 10, 2023.** There is a proposal to stop shipping 32-bit binaries (packages and containers). If no objections are raised by Sept 10, 2023, new releases will not include them. See https://github.com/StackExchange/dnscontrol/issues/2461 for details. + > - **MSDNS maintainer needed!** Without a new volunteer, this DNS provider will lose support after April 2025. See https://github.com/StackExchange/dnscontrol/issues/2878 > - **Call for new volunteer maintainers for NAMEDOTCOM and SOFTLAYER.** These providers have no maintainer. Maintainers respond to PRs and fix bugs in a timely manner, and try to stay on top of protocol changes. - > - **ACME/Let's Encrypt support is frozen and will be removed after December 31, 2022.** The `get-certs` command (renews certs via Let's Encrypt) has no maintainer. There are other projects that do a better job. If you don't use this feature, please do not start. If you do use this feature, please plan on migrating to something else. See discussion in [issues/1400](https://github.com/StackExchange/dnscontrol/issues/1400) + > - **ACME/Let's Encrypt support is frozen and will be removed without notice between now and April 2025.** It has been unsupported since December 2022. If you don't use this feature, do not start. If you do use this feature, migrate ASAP. See discussion in [issues/1400](https://github.com/StackExchange/dnscontrol/issues/1400) ## Install diff --git a/README.md b/README.md index ca8c92c00..4992f9e9a 100644 --- a/README.md +++ b/README.md @@ -158,11 +158,11 @@ DNSControl can be installed via packages for macOS, Linux and Windows, or from s See [dnscontrol-action](https://github.com/koenrh/dnscontrol-action) or [gacts/install-dnscontrol](https://github.com/gacts/install-dnscontrol). -## Deprecation warnings (updated 2024-02-24) +## Deprecation warnings (updated 2024-03-25) -- **32-bit binaries will no longer be distributed after September 10, 2023.** There is a proposal to stop shipping 32-bit binaries (packages and containers). If no objections are raised by Sept 10, 2023, new releases will not include them. See https://github.com/StackExchange/dnscontrol/issues/2461 for details. +- **MSDNS maintainer needed!** Without a new volunteer, this DNS provider will lose support after April 2025. See https://github.com/StackExchange/dnscontrol/issues/2878 - **Call for new volunteer maintainers for NAMEDOTCOM and SOFTLAYER.** These providers have no maintainer. Maintainers respond to PRs and fix bugs in a timely manner, and try to stay on top of protocol changes. -- **ACME/Let's Encrypt support is frozen and will be removed after December 31, 2022.** The `get-certs` command (renews certs via Let's Encrypt) has no maintainer. There are other projects that do a better job. If you don't use this feature, please do not start. If you do use this feature, please plan on migrating to something else. See discussion in [issues/1400](https://github.com/StackExchange/dnscontrol/issues/1400) +- **ACME/Let's Encrypt support is frozen and will be removed without notice between now and April 2025.** It has been unsupported since December 2022. If you don't use this feature, do not start. If you do use this feature, migrate ASAP. See discussion in [issues/1400](https://github.com/StackExchange/dnscontrol/issues/1400) ## More info at our website diff --git a/commands/ppreviewPush.go b/commands/ppreviewPush.go new file mode 100644 index 000000000..f04bee6d1 --- /dev/null +++ b/commands/ppreviewPush.go @@ -0,0 +1,783 @@ +package commands + +import ( + "cmp" + "encoding/json" + "fmt" + "os" + "strings" + "sync" + + "github.com/StackExchange/dnscontrol/v4/models" + "github.com/StackExchange/dnscontrol/v4/pkg/bindserial" + "github.com/StackExchange/dnscontrol/v4/pkg/credsfile" + "github.com/StackExchange/dnscontrol/v4/pkg/nameservers" + "github.com/StackExchange/dnscontrol/v4/pkg/normalize" + "github.com/StackExchange/dnscontrol/v4/pkg/notifications" + "github.com/StackExchange/dnscontrol/v4/pkg/printer" + "github.com/StackExchange/dnscontrol/v4/pkg/zonerecs" + "github.com/StackExchange/dnscontrol/v4/providers" + "github.com/urfave/cli/v2" + "golang.org/x/exp/slices" + "golang.org/x/net/idna" +) + +type zoneCache struct { + cache map[string]*[]string + sync.Mutex +} + +var _ = cmd(catMain, func() *cli.Command { + var args PPreviewArgs + return &cli.Command{ + Name: "ppreview", + Usage: "read live configuration and identify changes to be made, without applying them", + Action: func(ctx *cli.Context) error { + return exit(PPreview(args)) + }, + Flags: args.flags(), + } +}()) + +// PPreviewArgs contains all data/flags needed to run preview, independently of CLI +type PPreviewArgs struct { + GetDNSConfigArgs + GetCredentialsArgs + FilterArgs + Notify bool + WarnChanges bool + ConcurMode string + NoPopulate bool + DePopulate bool + Full bool +} + +// ReportItem is a record of corrections for a particular domain/provider/registrar. +//type ReportItem struct { +// Domain string `json:"domain"` +// Corrections int `json:"corrections"` +// Provider string `json:"provider,omitempty"` +// Registrar string `json:"registrar,omitempty"` +//} + +func (args *PPreviewArgs) 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`, + }) + flags = append(flags, &cli.StringFlag{ + Name: "cmode", + Destination: &args.ConcurMode, + Value: "default", + Usage: `Which providers to run concurrently: all, default, none`, + Action: func(c *cli.Context, s string) error { + if !slices.Contains([]string{"all", "default", "none"}, s) { + fmt.Printf("%q is not a valid option for --cmode. Valie are: all, default, none", s) + } + return nil + }, + }) + flags = append(flags, &cli.BoolFlag{ + Name: "no-populate", + Destination: &args.NoPopulate, + Usage: `Do not auto-create zones at the provider`, + }) + flags = append(flags, &cli.BoolFlag{ + Name: "depopulate", + Destination: &args.NoPopulate, + Usage: `Delete unknown zones at provider (dangerous!)`, + }) + flags = append(flags, &cli.BoolFlag{ + Name: "full", + Destination: &args.Full, + Usage: `Add headings, providers names, notifications of no changes, etc`, + }) + flags = append(flags, &cli.IntFlag{ + Name: "reportmax", + Hidden: true, + Usage: `Limit the IGNORE/NO_PURGE report to this many lines (Expermental. Will change in the future.)`, + Action: func(ctx *cli.Context, max int) error { + printer.MaxReport = max + return nil + }, + }) + flags = append(flags, &cli.Int64Flag{ + Name: "bindserial", + Destination: &bindserial.ForcedValue, + Usage: `Force BIND serial numbers to this value (for reproducibility)`, + }) + return flags +} + +var _ = cmd(catMain, func() *cli.Command { + var args PPushArgs + return &cli.Command{ + Name: "ppush", + Usage: "identify changes to be made, and perform them", + Action: func(ctx *cli.Context) error { + return exit(PPush(args)) + }, + Flags: args.flags(), + } +}()) + +// PPushArgs contains all data/flags needed to run push, independently of CLI +type PPushArgs struct { + PPreviewArgs + Interactive bool + Report string +} + +func (args *PPushArgs) flags() []cli.Flag { + flags := args.PPreviewArgs.flags() + flags = append(flags, &cli.BoolFlag{ + Name: "i", + Destination: &args.Interactive, + Usage: "Interactive. Confirm or Exclude each correction before they run", + }) + flags = append(flags, &cli.StringFlag{ + Name: "report", + Destination: &args.Report, + Usage: `Generate a machine-parseable report of performed corrections.`, + }) + return flags +} + +// PPreview implements the preview subcommand. +func PPreview(args PPreviewArgs) error { + return prun(args, false, false, printer.DefaultPrinter, "") +} + +// PPush implements the push subcommand. +func PPush(args PPushArgs) error { + return prun(args.PPreviewArgs, true, args.Interactive, printer.DefaultPrinter, args.Report) +} + +var pobsoleteDiff2FlagUsed = false + +// run is the main routine common to preview/push +func prun(args PPreviewArgs, push bool, interactive bool, out printer.CLI, report string) error { + + // This is a hack until we have the new printer replacement. + printer.SkinnyReport = !args.Full + fullMode := args.Full + + if pobsoleteDiff2FlagUsed { + printer.Println("WARNING: Please remove obsolete --diff2 flag. This will be an error in v5 or later. See https://github.com/StackExchange/dnscontrol/issues/2262") + } + + out.PrintfIf(fullMode, "Reading dnsconfig.js or equiv.\n") + cfg, err := GetDNSConfig(args.GetDNSConfigArgs) + if err != nil { + return err + } + + out.PrintfIf(fullMode, "Reading creds.json or equiv.\n") + providerConfigs, err := credsfile.LoadProviderConfigs(args.CredsFile) + if err != nil { + return err + } + + out.PrintfIf(fullMode, "Creating an in-memory model of 'desired'...\n") + notifier, err := PInitializeProviders(cfg, providerConfigs, args.Notify) + if err != nil { + return err + } + + out.PrintfIf(fullMode, "Normalizing and validating 'desired'..\n") + errs := normalize.ValidateAndNormalizeConfig(cfg) + if PrintValidationErrors(errs) { + return fmt.Errorf("exiting due to validation errors") + } + + zcache := NewZoneCache() + + // Loop over all (or some) zones: + zonesToProcess := whichZonesToProcess(cfg.Domains, args.Domains) + zonesSerial, zonesConcurrent := splitConcurrent(zonesToProcess, args.ConcurMode) + out.PrintfIf(fullMode, "PHASE 1: GATHERING data\n") + var wg sync.WaitGroup + wg.Add(len(zonesConcurrent)) + out.Printf("CONCURRENTLY gathering %d zone(s)\n", len(zonesConcurrent)) + for _, zone := range optimizeOrder(zonesConcurrent) { + out.PrintfIf(fullMode, "Concurrently gathering: %q\n", zone.Name) + go func(zone *models.DomainConfig, args PPreviewArgs, zcache *zoneCache) { + defer wg.Done() + oneZone(zone, args, zcache) + }(zone, args, zcache) + } + out.Printf("SERIALLY gathering %d zone(s)\n", len(zonesSerial)) + for _, zone := range zonesSerial { + out.Printf("Serially Gathering: %q\n", zone.Name) + oneZone(zone, args, zcache) + } + out.PrintfIf(len(zonesConcurrent) > 0, "Waiting for concurrent gathering(s) to complete...") + wg.Wait() + out.PrintfIf(len(zonesConcurrent) > 0, "DONE\n") + + // Now we know what to do, print or do the tasks. + out.PrintfIf(fullMode, "PHASE 2: CORRECTIONS\n") + var totalCorrections int + var reportItems []*ReportItem + var anyErrors bool + for _, zone := range zonesToProcess { + out.StartDomain(zone.GetUniqueName()) + providersToProcess := whichProvidersToProcess(zone.DNSProviderInstances, args.Providers) + for _, provider := range zone.DNSProviderInstances { + skip := skipProvider(provider.Name, providersToProcess) + out.StartDNSProvider(provider.Name, skip) + if !skip { + corrections := zone.GetCorrections(provider.Name) + totalCorrections += len(corrections) + reportItems = append(reportItems, genReportItem(zone.Name, corrections, provider.Name)) + anyErrors = cmp.Or(anyErrors, pprintOrRunCorrections(zone.Name, provider.Name, corrections, out, push, interactive, notifier, report)) + out.EndProvider(provider.Name, len(corrections), nil) + } + } + skip := skipProvider(zone.RegistrarInstance.Name, providersToProcess) + out.StartRegistrar(zone.RegistrarName, !skip) + if skip { + corrections := zone.GetCorrections(zone.RegistrarInstance.Name) + totalCorrections += len(corrections) + reportItems = append(reportItems, genReportItem(zone.Name, corrections, zone.RegistrarName)) + anyErrors = cmp.Or(anyErrors, pprintOrRunCorrections(zone.Name, zone.RegistrarInstance.Name, corrections, out, push, interactive, notifier, report)) + out.EndProvider(zone.RegistrarName, len(corrections), nil) + } + } + + 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) + err = writeReport(report, reportItems) + if err != nil { + return fmt.Errorf("could not write report") + } + if anyErrors { + return fmt.Errorf("completed with errors") + } + if totalCorrections != 0 && args.WarnChanges { + return fmt.Errorf("there are pending changes") + } + return nil +} + +func whichZonesToProcess(domains []*models.DomainConfig, filter string) []*models.DomainConfig { + if filter == "" || filter == "all" { + return domains + } + + permitList := strings.Split(filter, ",") + var picked []*models.DomainConfig + for _, domain := range domains { + if domainInList(domain.Name, permitList) { + picked = append(picked, domain) + } + } + return picked +} + +// splitConcurrent takes a list of DomainConfigs and returns two lists. The +// first list is the items that do NOT support concurrency. The second is list +// the items that DO support concurrency. +func splitConcurrent(domains []*models.DomainConfig, filter string) (serial []*models.DomainConfig, concurrent []*models.DomainConfig) { + if filter == "none" { + return domains, nil + } else if filter == "all" { + return nil, domains + } + for _, dc := range domains { + if allConcur(dc) { + concurrent = append(concurrent, dc) + } else { + serial = append(serial, dc) + } + } + return +} + +// allConcur returns true if its registrar and all DNS providers support +// concurrency. Otherwise false is returned. +func allConcur(dc *models.DomainConfig) bool { + if !providers.ProviderHasCapability(dc.RegistrarInstance.ProviderType, providers.CanConcur) { + //fmt.Printf("WHY? %q: %+v\n", dc.Name, dc.RegistrarInstance) + return false + } + for _, p := range dc.DNSProviderInstances { + if !providers.ProviderHasCapability(p.ProviderType, providers.CanConcur) { + //fmt.Printf("WHY? %q: %+v\n", dc.Name, p) + return false + } + } + return true +} + +// optimizeOrder returns a list of DomainConfigs so that they gather fastest. +// +// The current algorithm is based on the heuistic that larger zones (zones with +// the most records) need the most time to be processed. Therefore, the largest +// zones are moved to the front of the list. +// This isn't perfect but it is good enough. +func optimizeOrder(zones []*models.DomainConfig) []*models.DomainConfig { + slices.SortFunc(zones, func(a, b *models.DomainConfig) int { + return len(b.Records) - len(a.Records) // Biggest to smallest. + }) + + // // For benchmarking. Randomize the list. If you aren't better + // // than random, you might as well not play. + // rand.Shuffle(len(zones), func(i, j int) { + // zones[i], zones[j] = zones[j], zones[i] + // }) + + return zones +} + +func oneZone(zone *models.DomainConfig, args PPreviewArgs, zc *zoneCache) { + // Fix the parent zone's delegation: (if able/needed) + //zone.NameserversMutex.Lock() + delegationCorrections := generateDelegationCorrections(zone, zone.DNSProviderInstances, zone.RegistrarInstance) + //zone.NameserversMutex.Unlock() + + // Loop over the (selected) providers configured for that zone: + providersToProcess := whichProvidersToProcess(zone.DNSProviderInstances, args.Providers) + for _, provider := range providersToProcess { + + // Populate the zones at the provider (if desired/needed/able): + if !args.NoPopulate { + populateCorrections := generatePopulateCorrections(provider, zone.Name, zc) + zone.StoreCorrections(provider.Name, populateCorrections) + } + + // Update the zone's records at the provider: + zoneCor, rep := generateZoneCorrections(zone, provider) + zone.StoreCorrections(provider.Name, rep) + zone.StoreCorrections(provider.Name, zoneCor) + } + + // Do the delegation corrections after the zones are updated. + zone.StoreCorrections(zone.RegistrarInstance.Name, delegationCorrections) +} + +func whichProvidersToProcess(providers []*models.DNSProviderInstance, filter string) []*models.DNSProviderInstance { + + if filter == "all" { // all + return providers + } + + permitList := strings.Split(filter, ",") + var picked []*models.DNSProviderInstance + + // Just the default providers: + if filter == "" { + for _, provider := range providers { + if provider.IsDefault { + picked = append(picked, provider) + } + } + return picked + } + + // Just the exact matches: + for _, provider := range providers { + for _, filterItem := range permitList { + if provider.Name == filterItem { + picked = append(picked, provider) + } + } + } + return picked +} + +func skipProvider(name string, providers []*models.DNSProviderInstance) bool { + return !slices.ContainsFunc(providers, func(p *models.DNSProviderInstance) bool { + return p.Name == name + }) +} + +func genReportItem(zname string, corrections []*models.Correction, pname string) *ReportItem { + + // Only count the actions, not the messages. + cnt := 0 + for _, cor := range corrections { + if cor.F != nil { + cnt++ + } + } + + r := ReportItem{ + Domain: zname, + Corrections: cnt, + Provider: pname, + } + return &r +} + +func pprintOrRunCorrections(zoneName string, providerName string, corrections []*models.Correction, out printer.CLI, push bool, interactive bool, notifier notifications.Notifier, report string) bool { + if len(corrections) == 0 { + return false + } + var anyErrors bool + for i, correction := range corrections { + out.PrintCorrection(i, correction) + var err error + if push { + if interactive && !out.PromptToRun() { + continue + } + if correction.F != nil { + err = correction.F() + if err != nil { + anyErrors = true + } + } + out.EndCorrection(err) + } + notifier.Notify(zoneName, providerName, correction.Msg, err, !push) + } + + _ = report // File name to write report to. + return anyErrors +} + +func writeReport(report string, reportItems []*ReportItem) error { + // No filename? No report. + if report == "" { + return nil + } + + f, err := os.OpenFile(report, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) + if err != nil { + return err + } + defer f.Close() + b, err := json.MarshalIndent(reportItems, "", " ") + if err != nil { + return err + } + if _, err := f.Write(b); err != nil { + return err + } + return nil +} + +func generatePopulateCorrections(provider *models.DNSProviderInstance, zoneName string, zcache *zoneCache) []*models.Correction { + + lister, ok := provider.Driver.(providers.ZoneLister) + if !ok { + return nil // We can't generate a list. No corrections are possible. + } + + z, err := zcache.zoneList(provider.Name, lister) + if err != nil { + return []*models.Correction{{Msg: fmt.Sprintf("zoneList failed for %q: %s", provider.Name, err)}} + } + zones := *z + + aceZoneName, _ := idna.ToASCII(zoneName) + if slices.Contains(zones, aceZoneName) { + return nil // zone exists. Nothing to do. + } + + creator, ok := provider.Driver.(providers.ZoneCreator) + if !ok { + return []*models.Correction{{Msg: fmt.Sprintf("Zone %q does not exist. Can not create because %q does not implement ZoneCreator", aceZoneName, provider.Name)}} + } + + return []*models.Correction{{ + Msg: fmt.Sprintf("Create zone '%s' in the '%s' profile", aceZoneName, provider.Name), + F: func() error { return creator.EnsureZoneExists(aceZoneName) }, + }} +} + +func generateZoneCorrections(zone *models.DomainConfig, provider *models.DNSProviderInstance) ([]*models.Correction, []*models.Correction) { + reports, zoneCorrections, err := zonerecs.CorrectZoneRecords(provider.Driver, zone) + if err != nil { + return []*models.Correction{{Msg: fmt.Sprintf("Domain %q provider %s Error: %s", zone.Name, provider.Name, err)}}, nil + } + return zoneCorrections, reports +} + +func generateDelegationCorrections(zone *models.DomainConfig, providers []*models.DNSProviderInstance, _ *models.RegistrarInstance) []*models.Correction { + //fmt.Printf("DEBUG: generateDelegationCorrections start zone=%q nsList = %v\n", zone.Name, zone.Nameservers) + nsList, err := nameservers.DetermineNameserversForProviders(zone, providers, true) + if err != nil { + return msg(fmt.Sprintf("DtermineNS: zone %q; Error: %s", zone.Name, err)) + } + zone.Nameservers = nsList + nameservers.AddNSRecords(zone) + + if len(zone.Nameservers) == 0 && zone.Metadata["no_ns"] != "true" { + return []*models.Correction{{Msg: fmt.Sprintf("No nameservers declared for domain %q; skipping registrar. Add {no_ns:'true'} to force", zone.Name)}} + } + + corrections, err := zone.RegistrarInstance.Driver.GetRegistrarCorrections(zone) + if err != nil { + return msg(fmt.Sprintf("zone %q; Rprovider %q; Error: %s", zone.Name, zone.RegistrarInstance.Name, err)) + } + return corrections +} + +func msg(s string) []*models.Correction { + return []*models.Correction{{Msg: s}} +} + +// PInitializeProviders takes (fully processed) configuration and instantiates all providers and returns them. +func PInitializeProviders(cfg *models.DNSConfig, providerConfigs map[string]map[string]string, notifyFlag bool) (notify notifications.Notifier, err error) { + var notificationCfg map[string]string + defer func() { + notify = notifications.Init(notificationCfg) + }() + 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 + } + } + + // Populate provider type ids based on values from creds.json: + msgs, err := ppopulateProviderTypes(cfg, providerConfigs) + if len(msgs) != 0 { + fmt.Fprintln(os.Stderr, strings.Join(msgs, "\n")) + } + if err != nil { + return + } + + 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 +} + +// pproviderTypeFieldName is the name of the field in creds.json that specifies the provider type id. +const pproviderTypeFieldName = "TYPE" + +// ppurl is the documentation URL to list in the warnings related to missing provider type ids. +const purl = "https://docs.dnscontrol.org/commands/creds-json" + +// ppopulateProviderTypes scans a DNSConfig for blank provider types and fills them in based on providerConfigs. +// That is, if the provider type is "-" or "", we take that as an flag +// that means this value should be replaced by the type found in creds.json. +func ppopulateProviderTypes(cfg *models.DNSConfig, providerConfigs map[string]map[string]string) ([]string, error) { + var msgs []string + + for i := range cfg.Registrars { + pType := cfg.Registrars[i].Type + pName := cfg.Registrars[i].Name + nt, warnMsg, err := prefineProviderType(pName, pType, providerConfigs[pName], "NewRegistrar") + cfg.Registrars[i].Type = nt + if warnMsg != "" { + msgs = append(msgs, warnMsg) + } + if err != nil { + return msgs, err + } + } + + for i := range cfg.DNSProviders { + pName := cfg.DNSProviders[i].Name + pType := cfg.DNSProviders[i].Type + nt, warnMsg, err := prefineProviderType(pName, pType, providerConfigs[pName], "NewDnsProvider") + cfg.DNSProviders[i].Type = nt + if warnMsg != "" { + msgs = append(msgs, warnMsg) + } + if err != nil { + return msgs, err + } + } + + // Update these fields set by + // commands/commands.go:preloadProviders(). + // This is probably a layering violation. That said, the + // fundamental problem here is that we're storing the provider + // instances by string name, not by a pointer to a struct. We + // should clean that up someday. + for _, domain := range cfg.Domains { // For each domain.. + for _, provider := range domain.DNSProviderInstances { // For each provider... + pName := provider.ProviderBase.Name + pType := provider.ProviderBase.ProviderType + nt, warnMsg, err := prefineProviderType(pName, pType, providerConfigs[pName], "NewDnsProvider") + provider.ProviderBase.ProviderType = nt + if warnMsg != "" { + msgs = append(msgs, warnMsg) + } + if err != nil { + return msgs, err + } + } + p := domain.RegistrarInstance + pName := p.Name + pType := p.ProviderType + nt, warnMsg, err := prefineProviderType(pName, pType, providerConfigs[pName], "NewRegistrar") + p.ProviderType = nt + if warnMsg != "" { + msgs = append(msgs, warnMsg) + } + if err != nil { + return msgs, err + } + } + + return puniqueStrings(msgs), nil +} + +// puniqueStrings takes an unsorted slice of strings and returns the +// unique strings, in the order they first appeared in the list. +func puniqueStrings(stringSlice []string) []string { + keys := make(map[string]bool) + list := []string{} + for _, entry := range stringSlice { + if _, ok := keys[entry]; !ok { + keys[entry] = true + list = append(list, entry) + } + } + return list +} + +func prefineProviderType(credEntryName string, t string, credFields map[string]string, source string) (replacementType string, warnMsg string, err error) { + + // t="" and t="-" are processed the same. Standardize on "-" to reduce the number of cases to check. + if t == "" { + t = "-" + } + + // Use cases: + // + // type credsType + // ---- --------- + // - or "" GANDI lookup worked. Nothing to say. + // - or "" - or "" ERROR "creds.json has invalid or missing data" + // GANDI "" WARNING "Working but.... Please fix as follows..." + // GANDI GANDI INFO "working but unneeded: clean up as follows..." + // GANDI NAMEDOT ERROR "error mismatched: please fix as follows..." + + // ERROR: Invalid. + // WARNING: Required change to remain compatible with 4.0 + // INFO: Post-4.0 cleanups or other non-required changes. + + if t != "-" { + // Old-style, dnsconfig.js specifies the type explicitly. + // This is supported but we suggest updates for future compatibility. + + // If credFields is nil, that means there was no entry in creds.json: + if credFields == nil { + // Warn the user to update creds.json in preparation for 4.0: + // In 4.0 this should be an error. We could default to a + // provider such as "NONE" but I suspect it would be confusing + // to users to see references to a provider name that they did + // not specify. + return t, fmt.Sprintf(`WARNING: For future compatibility, add this entry creds.json: %q: { %q: %q }, (See %s#missing)`, + credEntryName, pproviderTypeFieldName, t, + purl, + ), nil + } + + switch ct := credFields[pproviderTypeFieldName]; ct { + case "": + // Warn the user to update creds.json in preparation for 4.0: + // In 4.0 this should be an error. + return t, fmt.Sprintf(`WARNING: For future compatibility, update the %q entry in creds.json by adding: %q: %q, (See %s#missing)`, + credEntryName, + pproviderTypeFieldName, t, + purl, + ), nil + case "-": + // This should never happen. The user is specifying "-" in a place that it shouldn't be used. + return "-", "", fmt.Errorf(`ERROR: creds.json entry %q has invalid %q value %q (See %s#hyphen)`, + credEntryName, pproviderTypeFieldName, ct, + purl, + ) + case t: + // creds.json file is compatible with and dnsconfig.js can be updated. + return ct, fmt.Sprintf(`INFO: In dnsconfig.js %s(%q, %q) can be simplified to %s(%q) (See %s#cleanup)`, + source, credEntryName, t, + source, credEntryName, + purl, + ), nil + default: + // creds.json lists a TYPE but it doesn't match what's in dnsconfig.js! + return t, "", fmt.Errorf(`ERROR: Mismatch found! creds.json entry %q has %q set to %q but dnsconfig.js specifies %s(%q, %q) (See %s#mismatch)`, + credEntryName, + pproviderTypeFieldName, ct, + source, credEntryName, t, + purl, + ) + } + } + + // t == "-" + // New-style, dnsconfig.js does not specify the type (t == "") or a + // command line tool accepted "-" as a positional argument for + // backwards compatibility. + + // If credFields is nil, that means there was no entry in creds.json: + if credFields == nil { + return "", "", fmt.Errorf(`ERROR: creds.json is missing an entry called %q. Suggestion: %q: { %q: %q }, (See %s#missing)`, + credEntryName, + credEntryName, pproviderTypeFieldName, "FILL_IN_PROVIDER_TYPE", + purl, + ) + } + + // New-style, dnsconfig.js doesn't specifies the type. It will be + // looked up in creds.json. + switch ct := credFields[pproviderTypeFieldName]; ct { + case "": + return ct, "", fmt.Errorf(`ERROR: creds.json entry %q is missing: %q: %q, (See %s#fixcreds)`, + credEntryName, + pproviderTypeFieldName, "FILL_IN_PROVIDER_TYPE", + purl, + ) + case "-": + // This should never happen. The user is confused and specified "-" in the wrong place! + return "-", "", fmt.Errorf(`ERROR: creds.json entry %q has invalid %q value %q (See %s#hyphen)`, + credEntryName, + pproviderTypeFieldName, ct, + purl, + ) + default: + // use the value in creds.json (this should be the normal case) + return ct, "", nil + } + +} diff --git a/commands/previewPush.go b/commands/previewPush.go index 594383224..8872c1eb1 100644 --- a/commands/previewPush.go +++ b/commands/previewPush.go @@ -230,7 +230,7 @@ func run(args PreviewArgs, push bool, interactive bool, out printer.CLI, report // Correct the registrar... - nsList, err := nameservers.DetermineNameserversForProviders(domain, providersWithExistingZone) + nsList, err := nameservers.DetermineNameserversForProviders(domain, providersWithExistingZone, false) if err != nil { out.Errorf("ERROR: %s\n", err.Error()) return diff --git a/commands/zonecache.go b/commands/zonecache.go new file mode 100644 index 000000000..32aa4a8e1 --- /dev/null +++ b/commands/zonecache.go @@ -0,0 +1,27 @@ +package commands + +import "github.com/StackExchange/dnscontrol/v4/providers" + +func NewZoneCache() *zoneCache { + return &zoneCache{} +} + +func (zc *zoneCache) zoneList(name string, lister providers.ZoneLister) (*[]string, error) { + zc.Lock() + defer zc.Unlock() + + if zc.cache == nil { + zc.cache = map[string]*[]string{} + } + + if v, ok := zc.cache[name]; ok { + return v, nil + } + + zones, err := lister.ListZones() + if err != nil { + return nil, err + } + zc.cache[name] = &zones + return &zones, nil +} diff --git a/documentation/providers.md b/documentation/providers.md index 5626be138..f1adf61b0 100644 --- a/documentation/providers.md +++ b/documentation/providers.md @@ -17,13 +17,13 @@ If a feature is definitively not supported for whatever reason, we would also li | [`AKAMAIEDGEDNS`](providers/akamaiedgedns.md) | ❌ | ✅ | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ | ❌ | ❔ | ✅ | ✅ | ✅ | | [`AUTODNS`](providers/autodns.md) | ❌ | ✅ | ❌ | ❌ | ✅ | ✅ | ❔ | ❔ | ❔ | ❌ | ❔ | ✅ | ❌ | ❌ | ❌ | ❔ | ❌ | ❌ | ✅ | | [`AXFRDDNS`](providers/axfrddns.md) | ❌ | ✅ | ❌ | ❌ | ❔ | ✅ | ✅ | ❔ | ✅ | ✅ | ❔ | ✅ | ✅ | ✅ | ❔ | ✅ | ❌ | ❌ | ❌ | -| [`AZURE_DNS`](providers/azure_dns.md) | ✅ | ✅ | ❌ | ❌ | ❌ | ✅ | ❔ | ❌ | ❌ | ✅ | ❔ | ✅ | ❌ | ❌ | ❔ | ❔ | ✅ | ✅ | ✅ | +| [`AZURE_DNS`](providers/azure_dns.md) | ✅ | ✅ | ❌ | ✅ | ❌ | ✅ | ❔ | ❌ | ❌ | ✅ | ❔ | ✅ | ❌ | ❌ | ❔ | ❔ | ✅ | ✅ | ✅ | | [`AZURE_PRIVATE_DNS`](providers/azure_private_dns.md) | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❔ | ❌ | ❌ | ✅ | ❔ | ✅ | ❌ | ❌ | ❔ | ❔ | ✅ | ✅ | ✅ | | [`BIND`](providers/bind.md) | ✅ | ✅ | ❌ | ❌ | ❔ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | [`BUNNY_DNS`](providers/bunny_dns.md) | ❌ | ✅ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ | -| [`CLOUDFLAREAPI`](providers/cloudflareapi.md) | ✅ | ✅ | ❌ | ❌ | ✅ | ✅ | ❔ | ❌ | ✅ | ✅ | ❔ | ✅ | ✅ | ✅ | ❔ | ❔ | ❌ | ✅ | ✅ | +| [`CLOUDFLAREAPI`](providers/cloudflareapi.md) | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ | ❔ | ❌ | ✅ | ✅ | ❔ | ✅ | ✅ | ✅ | ❔ | ❔ | ❌ | ✅ | ✅ | | [`CLOUDNS`](providers/cloudns.md) | ❌ | ✅ | ❌ | ❌ | ✅ | ✅ | ❔ | ❌ | ❔ | ✅ | ❔ | ✅ | ✅ | ✅ | ❔ | ❔ | ❔ | ✅ | ✅ | -| [`CSCGLOBAL`](providers/cscglobal.md) | ✅ | ✅ | ✅ | ❌ | ❔ | ✅ | ❔ | ❔ | ❔ | ❔ | ❔ | ✅ | ❔ | ❔ | ❔ | ❔ | ❔ | ❌ | ✅ | +| [`CSCGLOBAL`](providers/cscglobal.md) | ✅ | ✅ | ✅ | ✅ | ❔ | ✅ | ❔ | ❔ | ❔ | ❔ | ❔ | ✅ | ❔ | ❔ | ❔ | ❔ | ❔ | ❌ | ✅ | | [`DESEC`](providers/desec.md) | ❌ | ✅ | ❌ | ❌ | ❔ | ✅ | ✅ | ❔ | ✅ | ✅ | ❔ | ✅ | ✅ | ✅ | ✅ | ❔ | ❔ | ✅ | ✅ | | [`DIGITALOCEAN`](providers/digitalocean.md) | ❌ | ✅ | ❌ | ❌ | ❔ | ✅ | ❔ | ❌ | ❔ | ❔ | ❔ | ✅ | ❔ | ❔ | ❔ | ❔ | ❔ | ✅ | ✅ | | [`DNSIMPLE`](providers/dnsimple.md) | ❌ | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ❔ | ✅ | ✅ | ❌ | ❌ | ❔ | ❌ | ❌ | ✅ | @@ -34,7 +34,7 @@ If a feature is definitively not supported for whatever reason, we would also li | [`EASYNAME`](providers/easyname.md) | ❌ | ❌ | ✅ | ❌ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ❌ | ❔ | | [`EXOSCALE`](providers/exoscale.md) | ❌ | ✅ | ❌ | ❌ | ✅ | ✅ | ❔ | ❌ | ❔ | ✅ | ❔ | ✅ | ❔ | ❌ | ❔ | ❔ | ❌ | ❌ | ❔ | | [`GANDI_V5`](providers/gandi_v5.md) | ❌ | ✅ | ✅ | ❌ | ✅ | ✅ | ❔ | ❌ | ❔ | ✅ | ❔ | ✅ | ✅ | ✅ | ❌ | ❔ | ❔ | ❌ | ✅ | -| [`GCLOUD`](providers/gcloud.md) | ✅ | ✅ | ❌ | ❌ | ✅ | ✅ | ❔ | ❌ | ❔ | ✅ | ❔ | ✅ | ✅ | ✅ | ❔ | ❔ | ✅ | ✅ | ✅ | +| [`GCLOUD`](providers/gcloud.md) | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ | ❔ | ❌ | ❔ | ✅ | ❔ | ✅ | ✅ | ✅ | ❔ | ❔ | ✅ | ✅ | ✅ | | [`GCORE`](providers/gcore.md) | ❌ | ✅ | ❌ | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❔ | ✅ | ❌ | ❌ | ❌ | ❔ | ✅ | ✅ | ✅ | | [`HEDNS`](providers/hedns.md) | ❌ | ✅ | ❌ | ❌ | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ❌ | ❌ | ❔ | ✅ | ✅ | ✅ | | [`HETZNER`](providers/hetzner.md) | ❌ | ✅ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ✅ | ✅ | ❔ | ✅ | ✅ | ✅ | @@ -59,7 +59,7 @@ If a feature is definitively not supported for whatever reason, we would also li | [`PORKBUN`](providers/porkbun.md) | ❌ | ✅ | ✅ | ❌ | ✅ | ❔ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ✅ | ❌ | ❔ | ❌ | ❌ | ✅ | | [`POWERDNS`](providers/powerdns.md) | ❌ | ✅ | ❌ | ❌ | ✅ | ✅ | ✅ | ❔ | ✅ | ✅ | ❔ | ✅ | ✅ | ✅ | ✅ | ❔ | ✅ | ✅ | ✅ | | [`REALTIMEREGISTER`](providers/realtimeregister.md) | ❌ | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ✅ | ✅ | -| [`ROUTE53`](providers/route53.md) | ✅ | ✅ | ✅ | ❌ | ❌ | ✅ | ❔ | ❌ | ❔ | ✅ | ❔ | ✅ | ❔ | ❔ | ❔ | ❔ | ✅ | ✅ | ✅ | +| [`ROUTE53`](providers/route53.md) | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ❔ | ❌ | ❔ | ✅ | ❔ | ✅ | ❔ | ❔ | ❔ | ❔ | ✅ | ✅ | ✅ | | [`RWTH`](providers/rwth.md) | ❌ | ✅ | ❌ | ❌ | ❌ | ✅ | ❔ | ❌ | ❌ | ✅ | ❔ | ✅ | ✅ | ❌ | ❔ | ❔ | ❌ | ❌ | ✅ | | [`SOFTLAYER`](providers/softlayer.md) | ❌ | ✅ | ❌ | ❌ | ❔ | ❔ | ❔ | ❌ | ❔ | ❔ | ❔ | ✅ | ❔ | ❔ | ❔ | ❔ | ❔ | ❌ | ❔ | | [`TRANSIP`](providers/transip.md) | ❌ | ✅ | ❌ | ❌ | ✅ | ✅ | ❌ | ❌ | ✅ | ❔ | ❔ | ✅ | ✅ | ✅ | ❌ | ❔ | ❔ | ❌ | ✅ | diff --git a/models/domain.go b/models/domain.go index 65118e00a..8666be924 100644 --- a/models/domain.go +++ b/models/domain.go @@ -3,6 +3,7 @@ package models import ( "fmt" "strings" + "sync" "github.com/qdm12/reprint" "golang.org/x/net/idna" @@ -23,9 +24,10 @@ type DomainConfig struct { // Metadata[DomainUniqueName] // .Name + "!" + .Tag // Metadata[DomainTag] // split horizon tag - Metadata map[string]string `json:"meta,omitempty"` - Records Records `json:"records"` - Nameservers []*Nameserver `json:"nameservers,omitempty"` + Metadata map[string]string `json:"meta,omitempty"` + Records Records `json:"records"` + Nameservers []*Nameserver `json:"nameservers,omitempty"` + NameserversMutex sync.Mutex `json:"-"` EnsureAbsent Records `json:"recordsabsent,omitempty"` // ENSURE_ABSENT KeepUnknown bool `json:"keepunknown,omitempty"` // NO_PURGE @@ -42,6 +44,12 @@ type DomainConfig struct { // 2. Final driver instances are loaded after we load credentials. Any actual provider interaction requires that. RegistrarInstance *RegistrarInstance `json:"-"` DNSProviderInstances []*DNSProviderInstance `json:"-"` + + // Pending work to do for each provider. Provider may be a registrar or DSP. + // pendingCorrectionsMutex sync.Mutex + pendingCorrections map[string]([]*Correction) // Work to be done for each provider + pendingCorrectionsOrder []string // Call the providers in this order + pendingCorrectionsMutex sync.Mutex // Protect pendingCorrections* } // GetSplitHorizonNames returns the domain's name, uniquename, and tag. @@ -141,3 +149,38 @@ func (dc *DomainConfig) Punycode() error { } return nil } + +func (dc *DomainConfig) StoreCorrections(providerName string, corrections []*Correction) { + dc.pendingCorrectionsMutex.Lock() + defer dc.pendingCorrectionsMutex.Unlock() + + if dc.pendingCorrections == nil { + // First time storing anything. + dc.pendingCorrections = make(map[string]([]*Correction)) + dc.pendingCorrections[providerName] = corrections + dc.pendingCorrectionsOrder = []string{providerName} + } else if c, ok := dc.pendingCorrections[providerName]; !ok { + // First time key used + dc.pendingCorrections[providerName] = corrections + dc.pendingCorrectionsOrder = []string{providerName} + } else { + // Add to existing. + dc.pendingCorrections[providerName] = append(c, corrections...) + dc.pendingCorrectionsOrder = append(dc.pendingCorrectionsOrder, providerName) + + } +} + +func (dc *DomainConfig) GetCorrections(providerName string) []*Correction { + dc.pendingCorrectionsMutex.Lock() + defer dc.pendingCorrectionsMutex.Unlock() + + if dc.pendingCorrections == nil { + // First time storing anything. + return nil + } + if c, ok := dc.pendingCorrections[providerName]; ok { + return c + } + return nil +} diff --git a/pkg/nameservers/nameservers.go b/pkg/nameservers/nameservers.go index 6df91f8e6..56faabd72 100644 --- a/pkg/nameservers/nameservers.go +++ b/pkg/nameservers/nameservers.go @@ -14,24 +14,26 @@ import ( // 1. All explicitly defined NAMESERVER records will be used. // 2. Each DSP declares how many nameservers to use. Default is all. 0 indicates to use none. func DetermineNameservers(dc *models.DomainConfig) ([]*models.Nameserver, error) { - return DetermineNameserversForProviders(dc, dc.DNSProviderInstances) + return DetermineNameserversForProviders(dc, dc.DNSProviderInstances, false) } // DetermineNameserversForProviders is like DetermineNameservers, for a subset of providers. -func DetermineNameserversForProviders(dc *models.DomainConfig, providers []*models.DNSProviderInstance) ([]*models.Nameserver, error) { - // always take explicit +func DetermineNameserversForProviders(dc *models.DomainConfig, providers []*models.DNSProviderInstance, silent bool) ([]*models.Nameserver, error) { + // start with the nameservers that have been explicitly added: ns := dc.Nameservers + for _, dnsProvider := range providers { n := dnsProvider.NumberOfNameservers if n == 0 { continue } - if !printer.SkinnyReport { + if !silent && !printer.SkinnyReport { fmt.Printf("----- Getting nameservers from: %s\n", dnsProvider.Name) } + nss, err := dnsProvider.Driver.GetNameservers(dc.Name) if err != nil { - return nil, err + return nil, fmt.Errorf("error while getting Nameservers for zone=%q with provider=%q: %w", dc.Name, dnsProvider.Name, err) } // Clean up the nameservers due to // https://github.com/StackExchange/dnscontrol/issues/491 diff --git a/pkg/printer/printer.go b/pkg/printer/printer.go index 9cc6d26ce..e5717145d 100644 --- a/pkg/printer/printer.go +++ b/pkg/printer/printer.go @@ -31,6 +31,7 @@ type Printer interface { Println(lines ...string) Warnf(fmt string, args ...interface{}) Errorf(fmt string, args ...interface{}) + PrintfIf(print bool, fmt string, args ...interface{}) } // Debugf is called to print/format debug information. @@ -58,6 +59,11 @@ func Warnf(fmt string, args ...interface{}) { // DefaultPrinter.Errorf(fmt, args...) // } +// PrintfIf is called to optionally print something. +func PrintfIf(print bool, fmt string, args ...interface{}) { + DefaultPrinter.PrintfIf(print, fmt, args...) +} + var ( // DefaultPrinter is the default Printer, used by Debugf, Printf, and Warnf. DefaultPrinter = &ConsolePrinter{ @@ -190,3 +196,10 @@ func (c ConsolePrinter) Warnf(format string, args ...interface{}) { func (c ConsolePrinter) Errorf(format string, args ...interface{}) { fmt.Fprintf(c.Writer, "ERROR: "+format, args...) } + +// Errorf is called to optionally print/format a message. +func (c ConsolePrinter) PrintfIf(print bool, format string, args ...interface{}) { + if print { + fmt.Fprintf(c.Writer, format, args...) + } +} diff --git a/providers/azuredns/azureDnsProvider.go b/providers/azuredns/azureDnsProvider.go index 849f8f5b0..916cc56b6 100644 --- a/providers/azuredns/azureDnsProvider.go +++ b/providers/azuredns/azureDnsProvider.go @@ -23,15 +23,13 @@ type azurednsProvider struct { zones map[string]*adns.Zone resourceGroup *string subscriptionID *string - rawRecords map[string][]*adns.RecordSet - zoneName map[string]string } func newAzureDNSDsp(conf map[string]string, metadata json.RawMessage) (providers.DNSServiceProvider, error) { return newAzureDNS(conf, metadata) } -func newAzureDNS(m map[string]string, metadata json.RawMessage) (*azurednsProvider, error) { +func newAzureDNS(m map[string]string, _ json.RawMessage) (*azurednsProvider, error) { subID, rg := m["SubscriptionID"], m["ResourceGroup"] clientID, clientSecret, tenantID := m["ClientID"], m["ClientSecret"], m["TenantID"] credential, authErr := aauth.NewClientSecretCredential(tenantID, clientID, clientSecret, nil) @@ -52,8 +50,6 @@ func newAzureDNS(m map[string]string, metadata json.RawMessage) (*azurednsProvid recordsClient: recordsClient, resourceGroup: to.StringPtr(rg), subscriptionID: to.StringPtr(subID), - rawRecords: map[string][]*adns.RecordSet{}, - zoneName: map[string]string{}, } err := api.getZones() if err != nil { @@ -66,7 +62,7 @@ var features = providers.DocumentationNotes{ // The default for unlisted capabilities is 'Cannot'. // See providers/capabilities.go for the entire list of capabilities. providers.CanGetZones: providers.Can(), - providers.CanConcur: providers.Cannot(), + providers.CanConcur: providers.Can(), providers.CanUseAlias: providers.Cannot("Azure DNS does not provide a generic ALIAS functionality. Use AZURE_ALIAS instead."), providers.CanUseAzureAlias: providers.Can(), providers.CanUseCAA: providers.Can(), @@ -187,9 +183,6 @@ func (a *azurednsProvider) getExistingRecords(domain string) (models.Records, [] existingRecords = append(existingRecords, nativeToRecords(set, zoneName)...) } - a.rawRecords[domain] = rawRecords - a.zoneName[domain] = zoneName - return existingRecords, rawRecords, zoneName, nil } diff --git a/providers/capabilities.go b/providers/capabilities.go index f0baa2d3e..6691c5699 100644 --- a/providers/capabilities.go +++ b/providers/capabilities.go @@ -2,7 +2,9 @@ package providers -import "log" +import ( + "log" +) // Capability is a bitmasked set of "features" that a provider supports. Only use constants from this package. type Capability uint32 diff --git a/providers/cloudflare/cloudflareProvider.go b/providers/cloudflare/cloudflareProvider.go index 0a4b75931..13cc08a05 100644 --- a/providers/cloudflare/cloudflareProvider.go +++ b/providers/cloudflare/cloudflareProvider.go @@ -8,6 +8,7 @@ import ( "os" "strconv" "strings" + "sync" "golang.org/x/net/idna" @@ -43,7 +44,7 @@ var features = providers.DocumentationNotes{ // The default for unlisted capabilities is 'Cannot'. // See providers/capabilities.go for the entire list of capabilities. providers.CanGetZones: providers.Can(), - providers.CanConcur: providers.Cannot(), + providers.CanConcur: providers.Can(), providers.CanUseAlias: providers.Can("CF automatically flattens CNAME records into A records dynamically"), providers.CanUseCAA: providers.Can(), providers.CanUseDSForChildren: providers.Can(), @@ -79,6 +80,7 @@ type cloudflareProvider struct { manageWorkers bool accountID string cfClient *cloudflare.API + sync.Mutex } // TODO(dlemenkov): remove this function after deleting all commented code referecing it @@ -94,27 +96,29 @@ type cloudflareProvider struct { // GetNameservers returns the nameservers for a domain. func (c *cloudflareProvider) GetNameservers(domain string) ([]*models.Nameserver, error) { - if c.domainIndex == nil { - if err := c.fetchDomainList(); err != nil { - return nil, err - } + if err := c.cacheDomainList(); err != nil { + return nil, err } + c.Lock() ns, ok := c.nameservers[domain] + c.Unlock() if !ok { - return nil, fmt.Errorf("nameservers for %s not found in cloudflare account", domain) + return nil, fmt.Errorf("nameservers for %s not found in cloudflare cache(%q)", domain, c.accountID) } return models.ToNameservers(ns) } // ListZones returns a list of the DNS zones. func (c *cloudflareProvider) ListZones() ([]string, error) { - if err := c.fetchDomainList(); err != nil { + if err := c.cacheDomainList(); err != nil { return nil, err } + c.Lock() zones := make([]string, 0, len(c.domainIndex)) for d := range c.domainIndex { zones = append(zones, d) } + c.Unlock() return zones, nil } @@ -178,12 +182,12 @@ func (c *cloudflareProvider) GetZoneRecords(domain string, meta map[string]strin } func (c *cloudflareProvider) getDomainID(name string) (string, error) { - if c.domainIndex == nil { - if err := c.fetchDomainList(); err != nil { - return "", err - } + if err := c.cacheDomainList(); err != nil { + return "", err } + c.Lock() id, ok := c.domainIndex[name] + c.Unlock() if !ok { return "", fmt.Errorf("'%s' not a zone in cloudflare account", name) } @@ -196,14 +200,6 @@ func (c *cloudflareProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, if err := c.preprocessConfig(dc); err != nil { return nil, err } - // for i := len(records) - 1; i >= 0; i-- { - // rec := records[i] - // // Delete ignore labels - // if labelMatches(dnsutil.TrimDomainName(rec.Original.(cloudflare.DNSRecord).Name, dc.Name), c.ignoredLabels) { - // printer.Debugf("ignored_label: %s\n", rec.Original.(cloudflare.DNSRecord).Name) - // records = append(records[:i], records[i+1:]...) - // } - // } checkNSModifications(dc) @@ -222,9 +218,6 @@ func (c *cloudflareProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, if rec.Metadata[metaProxy] != "off" { rec.TTL = 1 } - // if labelMatches(rec.GetLabel(), c.ignoredLabels) { - // log.Fatalf("FATAL: dnsconfig contains label that matches ignored_labels: %#v is in %v)\n", rec.GetLabel(), c.ignoredLabels) - // } } checkNSModifications(dc) @@ -815,11 +808,14 @@ func getProxyMetadata(r *models.RecordConfig) map[string]string { // EnsureZoneExists creates a zone if it does not exist func (c *cloudflareProvider) EnsureZoneExists(domain string) error { - if c.domainIndex == nil { - if err := c.fetchDomainList(); err != nil { - return err - } + if err := c.cacheDomainList(); err != nil { + return err } + // if c.domainIndex == nil { + // if err := c.fetchDomainList(); err != nil { + // return err + // } + // } if _, ok := c.domainIndex[domain]; ok { return nil } diff --git a/providers/cloudflare/rest.go b/providers/cloudflare/rest.go index 67876e6b3..a223f1e30 100644 --- a/providers/cloudflare/rest.go +++ b/providers/cloudflare/rest.go @@ -13,7 +13,10 @@ import ( ) // get list of domains for account. Cache so the ids can be looked up from domain name -func (c *cloudflareProvider) fetchDomainList() error { +func (c *cloudflareProvider) cacheDomainList() error { + c.Lock() + defer c.Unlock() + c.domainIndex = map[string]string{} c.nameservers = map[string][]string{} zones, err := c.cfClient.ListZones(context.Background()) diff --git a/providers/cscglobal/cscglobalProvider.go b/providers/cscglobal/cscglobalProvider.go index 2cfb86635..e52c2a526 100644 --- a/providers/cscglobal/cscglobalProvider.go +++ b/providers/cscglobal/cscglobalProvider.go @@ -28,7 +28,7 @@ var features = providers.DocumentationNotes{ // The default for unlisted capabilities is 'Cannot'. // See providers/capabilities.go for the entire list of capabilities. providers.CanGetZones: providers.Can(), - providers.CanConcur: providers.Cannot(), + providers.CanConcur: providers.Can(), providers.CanUseCAA: providers.Can(), providers.CanUseSRV: providers.Can(), providers.DocOfficiallySupported: providers.Can(), diff --git a/providers/gcloud/gcloudProvider.go b/providers/gcloud/gcloudProvider.go index c62ea9e4a..b9500a6af 100644 --- a/providers/gcloud/gcloudProvider.go +++ b/providers/gcloud/gcloudProvider.go @@ -26,7 +26,7 @@ var features = providers.DocumentationNotes{ // The default for unlisted capabilities is 'Cannot'. // See providers/capabilities.go for the entire list of capabilities. providers.CanGetZones: providers.Can(), - providers.CanConcur: providers.Cannot(), + providers.CanConcur: providers.Can(), providers.CanUseAlias: providers.Can(), providers.CanUseCAA: providers.Can(), providers.CanUseDSForChildren: providers.Can(), diff --git a/providers/providers.go b/providers/providers.go index e04ab35c4..79f86523b 100644 --- a/providers/providers.go +++ b/providers/providers.go @@ -171,10 +171,16 @@ func (n None) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correctio return nil, nil } +var featuresNone = DocumentationNotes{ + // The default for unlisted capabilities is 'Cannot'. + // See providers/capabilities.go for the entire list of capabilities. + CanConcur: Can(), +} + func init() { RegisterRegistrarType("NONE", func(map[string]string) (Registrar, error) { return None{}, nil - }) + }, featuresNone) } // CustomRType stores an rtype that is only valid for this DSP. diff --git a/providers/route53/route53Provider.go b/providers/route53/route53Provider.go index 03a7de4e1..d88658f17 100644 --- a/providers/route53/route53Provider.go +++ b/providers/route53/route53Provider.go @@ -27,12 +27,11 @@ import ( ) type route53Provider struct { - client *r53.Client - registrar *r53d.Client - delegationSet *string - zonesByID map[string]r53Types.HostedZone - zonesByDomain map[string]r53Types.HostedZone - originalRecords []r53Types.ResourceRecordSet + client *r53.Client + registrar *r53d.Client + delegationSet *string + zonesByID map[string]r53Types.HostedZone + zonesByDomain map[string]r53Types.HostedZone } func newRoute53Reg(conf map[string]string) (providers.Registrar, error) { @@ -79,7 +78,7 @@ var features = providers.DocumentationNotes{ // The default for unlisted capabilities is 'Cannot'. // See providers/capabilities.go for the entire list of capabilities. providers.CanGetZones: providers.Can(), - providers.CanConcur: providers.Cannot(), + providers.CanConcur: providers.Can(), providers.CanUseAlias: providers.Cannot("R53 does not provide a generic ALIAS functionality. Use R53_ALIAS instead."), providers.CanUseCAA: providers.Can(), providers.CanUseLOC: providers.Cannot(), @@ -267,7 +266,6 @@ func (r *route53Provider) getZoneRecords(zone r53Types.HostedZone) (models.Recor if err != nil { return nil, err } - r.originalRecords = records var existingRecords = []*models.RecordConfig{} for _, set := range records {