cli

package module
v0.0.0-...-e5c0ba0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 25, 2026 License: BSD-2-Clause Imports: 42 Imported by: 1

Documentation

Overview

* Copyright (c) 2024 Johan Stenstam, [email protected]

* Copyright (c) 2024 Johan Stenstam, [email protected]

* Copyright (c) 2026 Johan Stenstam, [email protected] * * CLI commands for inspecting agent SDE (SynchedDataEngine) status. * Provides "agent zone edits list [--zone {zone}]" with per-RR * tracking state and outbound queue status.

* Copyright (c) 2024 Johan Stenstam, [email protected]

* Copyright (c) 2024 Johan Stenstam, [email protected]

* Copyright (c) 2025 Johan Stenstam, [email protected]

* Copyright (c) 2024 Johan Stenstam, [email protected]

* Copyright (c) 2024 Johan Stenstam, [email protected]

* Copyright (c) 2024 Johan Stenstam, [email protected]

* Copyright (c) 2024 Johan Stenstam, [email protected]

* Copyright (c) 2024 Johan Stenstam, [email protected]

* Copyright (c) 2026 Johan Stenstam, [email protected] * * CLI commands for managing combiner edit approval workflow. * Provides "combiner zone edits {list|approve|reject|clear}".

* Copyright (c) Johan Stenstam, [email protected]

* Copyright (c) 2024 Johan Stenstam, [email protected]

* Copyright (c) Johan Stenstam, [email protected]

* Copyright (c) Johan Stenstam, [email protected]

* Copyright (c) 2024 Johan Stenstam, [email protected]

* Copyright (c) Johan Stenstam, [email protected]

* Copyright (c) Johan Stenstam, [email protected]

* Copyright (c) 2026 Johan Stenstam, [email protected] * * Distribution management CLI commands for agents and combiners

* Copyright (c) 2024 Johan Stenstam, [email protected]

* Johan Stenstam

* Copyright (c) 2026 Johan Stenstam, [email protected]

* Copyright (c) Johan Stenstam, [email protected]

* Copyright (c) Johan Stenstam, [email protected]

* Copyright (c) 2024 Johan Stenstam, [email protected]

* Copyright (c) Johan Stenstam, [email protected]

* Copyright (c) 2024 Johan Stenstam, [email protected]

* Copyright (c) 2024 Johan Stenstam, [email protected]

* Copyright (c) 2026 Johan Stenstam, [email protected] * * Transaction diagnostic CLI commands for agents and combiners. * Provides visibility into open outgoing/incoming transactions and error history.

* Copyright (c) Johan Stenstam, [email protected]

* Johan Stenstam

* Copyright (c) Johan Stenstam, [email protected]

* Copyright (c) Johan Stenstam, [email protected]

Index

Constants

View Source
const DefaultDomainSuffix = "example.com."

Default domain suffix for CLI

Variables

View Source
var AgentCmd = &cobra.Command{
	Use:   "agent",
	Short: "TDNS Agent commands",
}
View Source
var AgentDistribCmd = &cobra.Command{
	Use:   "distrib",
	Short: "Manage distributions",
	Long:  `Commands for managing distributions created by this agent, including listing distributions and purging completed distributions.`,
}
View Source
var AgentTransactionCmd = &cobra.Command{
	Use:   "transaction",
	Short: "Transaction diagnostics",
	Long:  `Commands for diagnosing open transactions, pending confirmations, and errors.`,
}
View Source
var AgentZoneCmd = &cobra.Command{
	Use:   "zone",
	Short: "Agent zone management commands",
}

AgentZoneCmd is the agent-specific "zone" command group. It contains only the zone subcommands relevant to the agent, plus the new addrr/delrr commands for managing synced RRs.

View Source
var AuthCmd = &cobra.Command{
	Use:   "auth",
	Short: "Interact with tdns-auth (authoritative) via API",
}

AuthCmd is the parent command for all auth-related commands

View Source
var Base32Cmd = &cobra.Command{
	Use:   "base32",
	Short: "Convert data to/from base32 encoding and domain format",
	Long: `This command converts data to or from base32 encoding and domain format.
It can read from standard input or from a file.

Examples:
  echo '{"name":"example","value":123}' | tdns base32 encode --suffix=example.com.
  cat domains.txt | tdns base32 decode`,
}

base32Cmd represents the base32 command

View Source
var Base32decodeCmd = &cobra.Command{
	Use:   "decode",
	Short: "Decode base32 domain data to JSON",
	Long:  `Decode base32 domain data from stdin to JSON.`,
	Run: func(cmd *cobra.Command, args []string) {

		stat, _ := os.Stdin.Stat()
		if (stat.Mode() & os.ModeCharDevice) != 0 {
			fmt.Println("No stdin data provided. Please pipe domain data to this command.")
			fmt.Println("Example: cat domains.txt | tdns base32 decode")
			return
		}

		cookie, _ := cmd.Flags().GetString("cookie")

		reader := bufio.NewReader(os.Stdin)
		processBase32DomainsToJson(reader, cmd, cookie)
	},
}

decodeCmd represents the decode subcommand

View Source
var Base32encodeCmd = &cobra.Command{
	Use:   "encode",
	Short: "Encode JSON data to base32 domain format",
	Long:  `Encode JSON data from stdin to base32 domain format.`,
	Run: func(cmd *cobra.Command, args []string) {

		stat, _ := os.Stdin.Stat()
		if (stat.Mode() & os.ModeCharDevice) != 0 {
			fmt.Println("No stdin data provided. Please pipe JSON data to this command.")
			fmt.Println("Example: echo '{\"name\":\"example\"}' | tdns base32 encode --suffix=example.com.")
			return
		}

		suffix, _ := cmd.Flags().GetString("suffix")
		cookie, _ := cmd.Flags().GetString("cookie")

		if suffix == "" {
			fmt.Fprintf(os.Stderr, "Error: domain suffix is required. Use --suffix flag.\n")
			return
		}

		if !strings.HasSuffix(suffix, ".") {
			suffix += "."
			fmt.Fprintf(os.Stderr, "Note: Added trailing dot to domain suffix: %s\n", suffix)
		}

		reader := bufio.NewReader(os.Stdin)
		processJsonToBase32Domains(reader, suffix, cookie)
	},
}

encodeCmd represents the encode subcommand

View Source
var CatalogCmd = &cobra.Command{
	Use:   "catalog",
	Short: "Manage catalog zones (RFC 9432)",
	Long:  `Create and manage catalog zones, add/remove member zones and groups.`,
}

CatalogCmd is the root command for catalog zone management

View Source
var CatalogGroupCmd = &cobra.Command{
	Use:   "group",
	Short: "Manage groups in catalog",
}

CatalogGroupCmd is the subcommand group for group operations

View Source
var CatalogNotifyCmd = &cobra.Command{
	Use:   "notify",
	Short: "Manage notify addresses for catalog zones",
}

CatalogNotifyCmd is the subcommand group for notify address operations

View Source
var CatalogZoneCmd = &cobra.Command{
	Use:   "zone",
	Short: "Manage member zones in catalog",
}

CatalogZoneCmd is the subcommand group for zone operations

View Source
var CatalogZoneGroupCmd = &cobra.Command{
	Use:   "group",
	Short: "Manage group associations for zones",
}

CatalogZoneGroupCmd is the subcommand group for zone-group associations

View Source
var ChildCmd = &cobra.Command{
	Use:   "child",
	Short: "Prefix, only useable via the 'child update create' subcommand",
}
View Source
var CombinerCmd = &cobra.Command{
	Use:   "combiner",
	Short: "TDNS Combiner commands",
}
View Source
var CombinerDistribCmd = &cobra.Command{
	Use:   "distrib",
	Short: "Manage distributions",
	Long:  `Commands for managing distributions created by this combiner, including listing distributions and purging completed distributions.`,
}
View Source
var CombinerShowDataCmd = &cobra.Command{
	Use:   "show-combiner-data",
	Short: "Show the combiner's local data store (merged + per-agent)",
	Long: `Display the combiner's CombinerData (merged view) and AgentContributions
(per-agent breakdown) for all zones or a specific zone.

Example:
  tdns-cliv2 combiner show-combiner-data
  tdns-cliv2 combiner show-combiner-data --zone whisky.dnslab.`,
	Run: func(cmd *cobra.Command, args []string) {
		zone, _ := cmd.Flags().GetString("zone")

		resp, err := SendCombinerDebugCmd(tdns.CombinerDebugPost{
			Command: "show-combiner-data",
			Zone:    zone,
		})
		if err != nil {
			log.Fatalf("Error: %v", err)
		}

		if resp.Error {
			log.Fatalf("Error: %s", resp.ErrorMsg)
		}

		hasMerged := len(resp.CombinerData) > 0
		hasContribs := len(resp.AgentContributions) > 0

		if !hasMerged && !hasContribs {
			fmt.Printf("No combiner data stored\n")
			return
		}

		if hasContribs {
			fmt.Printf("Per-Agent Contributions\n")
			fmt.Printf("=======================\n\n")

			zones := sortedKeys(resp.AgentContributions)
			for _, zoneName := range zones {
				agentMap := resp.AgentContributions[zoneName]
				fmt.Printf("Zone: %s\n", zoneName)
				fmt.Printf("────────────────────────────────────────\n")

				agents := sortedKeys(agentMap)
				for _, agentID := range agents {
					ownerMap := agentMap[agentID]
					fmt.Printf("  Agent: %s\n", agentID)

					owners := sortedKeys(ownerMap)
					for _, owner := range owners {
						rrTypeMap := ownerMap[owner]

						rrTypes := sortedKeys(rrTypeMap)
						for _, rrTypeName := range rrTypes {
							rrs := rrTypeMap[rrTypeName]
							fmt.Printf("    %s %s (%d records):\n", owner, rrTypeName, len(rrs))
							for _, rr := range rrs {
								fmt.Printf("      %s\n", rr)
							}
						}
					}
				}
				fmt.Printf("\n")
			}
		}

		if hasMerged {
			fmt.Printf("Merged CombinerData\n")
			fmt.Printf("===================\n\n")

			zones := sortedKeys(resp.CombinerData)
			for _, zoneName := range zones {
				ownerMap := resp.CombinerData[zoneName]
				fmt.Printf("Zone: %s\n", zoneName)
				fmt.Printf("────────────────────────────────────────\n")

				owners := sortedKeys(ownerMap)
				for _, ownerName := range owners {
					rrTypeMap := ownerMap[ownerName]
					rrTypes := sortedKeys(rrTypeMap)
					for _, rrTypeName := range rrTypes {
						rrs := rrTypeMap[rrTypeName]
						fmt.Printf("  %s %s (%d records):\n", ownerName, rrTypeName, len(rrs))
						for _, rr := range rrs {
							fmt.Printf("    %s\n", rr)
						}
					}
				}
				fmt.Printf("\n")
			}
		}
	},
}
View Source
var CombinerTransactionCmd = &cobra.Command{
	Use:   "transaction",
	Short: "Transaction diagnostics",
	Long:  `Commands for diagnosing transaction errors on the combiner.`,
}
View Source
var ConfigCmd = &cobra.Command{
	Use:   "config",
	Short: "Commands to reload config, reload zones, etc",
}
View Source
var DaemonApiCmd = &cobra.Command{
	Use:   "api",
	Short: "request a statusd api summary",
	Long: `The daemon api queries the statusd for the provided API
and prints that out in a (hopefully) comprehensible fashion.`,
	Run: func(cmd *cobra.Command, args []string) {
		prefixcmd, _ := getCommandContext("daemon")
		api, _ := getApiClient(prefixcmd, true)

		if len(args) != 0 {
			log.Fatal("api must have no arguments")
		}
		api.ShowApi()
	},
}
View Source
var DaemonCmd = &cobra.Command{
	Use:   "daemon",
	Short: "Only useful via sub-commands",
}
View Source
var DaemonReloadCmd = &cobra.Command{
	Use: "reload",

	Short: "Reload config from file",
	Long: `Reload config from file (the assumption is that something in the config has changed).
Right now this doesn't do much, but later on various services will be able to restart.`,
	Run: func(cmd *cobra.Command, args []string) {
		prefixcmd, _ := getCommandContext("daemon")
		api, _ := getApiClient(prefixcmd, true)

		_, resp, _ := api.UpdateDaemon(tdns.CommandPost{Command: "reload"}, true)
		fmt.Printf("Reload: %s Message: %s\n", resp.Status, resp.Msg)
	},
}
View Source
var DaemonRestartCmd = &cobra.Command{
	Use:   "restart",
	Short: "Stop and then start the management daemon",
	Run: func(cmd *cobra.Command, args []string) {
		prefixcmd, _ := getCommandContext("daemon")
		api, _ := getApiClient(prefixcmd, true)

		api.StopDaemon()
		time.Sleep(4 * time.Second)

		if clearLogFile != "" {
			if f, err := os.OpenFile(clearLogFile, os.O_WRONLY|os.O_TRUNC, 0); err != nil {
				fmt.Printf("Warning: could not truncate log file %q: %v\n", clearLogFile, err)
			} else {
				f.Close()
				fmt.Printf("Log file truncated: %s\n", clearLogFile)
			}
		}

		maxwait := viper.GetInt("cli.maxwait")
		if maxwait < MaxWait {
			maxwait = MaxWait
		}

		clientKey := getClientKeyFromParent(prefixcmd)
		if clientKey == "" {
			clientKey = "tdns-auth"
		}
		daemonCommand := ""
		if apiDetails := getApiDetailsByClientKey(clientKey); apiDetails != nil {
			if cmdStr, ok := apiDetails["command"].(string); ok && cmdStr != "" {
				daemonCommand = cmdStr
			}
		}

		if daemonCommand == "" {
			daemonCommand = viper.GetString("common.command")
		}

		if updateBinary {
			dstbin := daemonCommand
			if dstbin == "" {
				fmt.Printf("Update binary: destination unspecified (key: apiservers[].command or common.command)\n")
				os.Exit(1)
			}
			srcbin := filepath.Join(os.TempDir(), filepath.Base(dstbin))
			dstat, err := os.Stat(dstbin)
			if err != nil {
				fmt.Printf("Error from stat(dst: %q): %v\n", dstbin, err)
				os.Exit(1)
			}
			if tdns.Globals.Debug {
				fmt.Printf("ModTime(%s): %v\n", dstbin, dstat.ModTime())
			}

			sstat, err := os.Stat(srcbin)
			if err != nil {
				fmt.Printf("Error from stat(src: %q): %v\n", srcbin, err)
				os.Exit(1)
			}
			if tdns.Globals.Debug {
				fmt.Printf("ModTime(%s): %v\n", srcbin, sstat.ModTime())
			}

			if sstat.ModTime().After(dstat.ModTime()) {
				fmt.Printf("%s is newer than %s. Will update installed binary.\n",
					srcbin, dstbin)
				n, err := tdns.CopyFile(srcbin, dstbin)
				if err != nil {
					fmt.Printf("Error copying %s to %s: %v\n", srcbin, dstbin, err)
					os.Exit(1)
				}
				fmt.Printf("Successfully copied %s to %s (%d bytes)\n", srcbin, dstbin, n)
			} else {
				fmt.Printf("%s is not newer than %s. No update.\n", srcbin, dstbin)
			}
		}

		daemonFlags := extractDaemonFlags(cmd)

		api.StartDaemon(maxwait, false, daemonCommand, daemonFlags)
	},
}
View Source
var DaemonStartCmd = &cobra.Command{
	Use:   "start",
	Short: "Start the axfr-statusd daemon",
	Long:  `Start the axfr-statusd daemon. If it was already running, then this is a no-op.`,
	Run: func(cmd *cobra.Command, args []string) {
		prefixcmd, _ := getCommandContext("daemon")
		api, _ := getApiClient(prefixcmd, true)

		maxwait := viper.GetInt("cli.maxwait")
		if maxwait < MaxWait {
			maxwait = MaxWait
		}

		clientKey := getClientKeyFromParent(prefixcmd)
		if clientKey == "" {
			clientKey = "tdns-auth"
		}
		daemonCommand := ""
		if apiDetails := getApiDetailsByClientKey(clientKey); apiDetails != nil {
			if cmdStr, ok := apiDetails["command"].(string); ok && cmdStr != "" {
				daemonCommand = cmdStr
			}
		}

		daemonFlags := extractDaemonFlags(cmd)

		api.StartDaemon(maxwait, tdns.Globals.Slurp, daemonCommand, daemonFlags)
	},
}
View Source
var DaemonStatusCmd = &cobra.Command{
	Use: "status",

	Short: "Query for the status of the management daemon",
	Long:  `Query for the status of the management daemon`,
	Run: func(cmd *cobra.Command, args []string) {
		prefixcmd, _ := getCommandContext("daemon")
		api, _ := getApiClient(prefixcmd, true)
		_, resp, _ := api.UpdateDaemon(tdns.CommandPost{Command: "status"},
			true)
		fmt.Printf("Status: %s Message: %s\n", resp.Status, resp.Msg)
	},
}
View Source
var DaemonStopCmd = &cobra.Command{
	Use:   "stop",
	Short: "Stop the management daemon",
	Long:  `Stop the management daemon. If it was not running, then this is a no-op.`,
	Run: func(cmd *cobra.Command, args []string) {
		prefixcmd, _ := getCommandContext("daemon")
		api, _ := getApiClient(prefixcmd, true)

		api.StopDaemon()
	},
}
View Source
var DbCmd = &cobra.Command{
	Use:   "db",
	Short: "TDNS DB commands",
}
View Source
var DdnsCmd = &cobra.Command{
	Use:   "ddns",
	Short: "Send a DDNS update. Only usable via sub-commands.",
}
View Source
var DebugAgentCmd = &cobra.Command{
	Use:   "debug",
	Short: "TDNS-AGENT debugging commands",
}
View Source
var DebugAgentDumpAgentRegistryCmd = &cobra.Command{
	Use:   "dump-agentregistry",
	Short: "Dump the agent registry",
	Run: func(cmd *cobra.Command, args []string) {
		req := tdns.AgentMgmtPost{
			Command: "dump-agentregistry",
		}

		amr, err := SendAgentDebugCmd(req, false)
		if err != nil {
			log.Fatalf("Error: %v", err)
		}

		if amr.Error {
			log.Fatalf("Error: %s", amr.ErrorMsg)
		}

		if len(amr.AgentRegistry.RegularS) == 0 {
			fmt.Printf("No agent registry data in response from agent %q", amr.Identity)
			os.Exit(1)
		}

		if len(amr.AgentRegistry.RegularS) > 0 {
			var agentNames []tdns.AgentId
			for _, agent := range amr.AgentRegistry.RegularS {
				agentNames = append(agentNames, agent.Identity)
			}
			fmt.Printf("Agent registry contains %d agents: %v\n", len(agentNames), agentNames)
			for _, agent := range amr.AgentRegistry.RegularS {
				err := PrintAgent(agent, false)
				if err != nil {
					log.Printf("Error printing agent: %v", err)
				}
				fmt.Println()
			}
		} else {
			fmt.Printf("No remote agents found in the agent registry data from agent %q", amr.Identity)
		}
	},
}
View Source
var DebugAgentDumpZoneDataRepoCmd = &cobra.Command{
	Use:    "dump-zonedatarepo",
	Short:  "Dump the zone data repo (deprecated: use show-synced-data)",
	Hidden: true,
	Run:    DebugAgentShowSyncedDataCmd.Run,
}

Keep the old command name as alias for compatibility

View Source
var DebugAgentHsyncCmd = &cobra.Command{
	Use:   "hsync",
	Short: "HSYNC debugging commands",
}

Debug agent subcommand for HSYNC

View Source
var DebugAgentQueueStatusCmd = &cobra.Command{
	Use:   "queue-status",
	Short: "Show reliable message queue status and pending messages",
	Run: func(cmd *cobra.Command, args []string) {
		req := tdns.AgentMgmtPost{
			Command: "queue-status",
		}

		_, buf, err := func() (*tdns.AgentMgmtResponse, []byte, error) {
			prefixcmd, _ := getCommandContext("debug")
			api, err := getApiClient(prefixcmd, true)
			if err != nil {
				log.Fatalf("Error getting API client: %v", err)
			}
			_, buf, err := api.RequestNG("POST", "/agent/debug", req, true)
			return nil, buf, err
		}()
		if err != nil {
			log.Fatalf("API request failed: %v", err)
		}

		var resp map[string]interface{}
		if err := json.Unmarshal(buf, &resp); err != nil {
			log.Fatalf("Failed to parse response: %v", err)
		}

		if errVal, ok := resp["error"].(bool); ok && errVal {
			if errMsg, ok := resp["error_msg"].(string); ok {
				log.Fatalf("Error: %s", errMsg)
			}
			log.Fatalf("Error in response")
		}

		if msg, ok := resp["msg"].(string); ok && msg != "" {
			fmt.Println(msg)
		}

		data, ok := resp["data"].(map[string]interface{})
		if !ok {
			fmt.Println("No queue data available")
			return
		}

		if stats, ok := data["stats"].(map[string]interface{}); ok {
			fmt.Println("\nQueue Statistics:")
			if v, ok := stats["total_pending"].(float64); ok {
				fmt.Printf("  Pending:   %d\n", int(v))
			}
			if v, ok := stats["total_delivered"].(float64); ok {
				fmt.Printf("  Delivered: %d\n", int(v))
			}
			if v, ok := stats["total_failed"].(float64); ok {
				fmt.Printf("  Failed:    %d\n", int(v))
			}
			if v, ok := stats["total_expired"].(float64); ok {
				fmt.Printf("  Expired:   %d\n", int(v))
			}
			if byState, ok := stats["by_state"].(map[string]interface{}); ok && len(byState) > 0 {
				fmt.Printf("  By state:  ")
				first := true
				for state, count := range byState {
					if !first {
						fmt.Printf(", ")
					}
					fmt.Printf("%s=%d", state, int(count.(float64)))
					first = false
				}
				fmt.Println()
			}
		}

		messages, ok := data["messages"].([]interface{})
		if !ok || len(messages) == 0 {
			fmt.Println("\nNo pending messages")
			return
		}

		fmt.Printf("\nPending Messages (%d):\n", len(messages))

		verbose := false
		if v, err := cmd.Flags().GetBool("verbose"); err == nil {
			verbose = v
		}

		if verbose {
			for _, mRaw := range messages {
				m, ok := mRaw.(map[string]interface{})
				if !ok {
					continue
				}
				fmt.Println()
				fmt.Printf("  Distribution ID: %s\n", getStringValue(m, "distribution_id"))
				fmt.Printf("  Recipient:       %s (%s)\n", getStringValue(m, "recipient_id"), getStringValue(m, "recipient_type"))
				fmt.Printf("  Zone:            %s\n", getStringValue(m, "zone"))
				fmt.Printf("  State:           %s\n", getStringValue(m, "state"))
				fmt.Printf("  Priority:        %s\n", getStringValue(m, "priority"))
				fmt.Printf("  Attempts:        %s\n", getStringValue(m, "attempt_count"))
				fmt.Printf("  Age:             %s\n", getStringValue(m, "age"))
				fmt.Printf("  Created:         %s\n", getStringValue(m, "created_at"))
				fmt.Printf("  Expires:         %s\n", getStringValue(m, "expires_at"))
				fmt.Printf("  Next attempt:    %s\n", getStringValue(m, "next_attempt"))
				if lastAttempt := getStringValue(m, "last_attempt"); lastAttempt != "" {
					fmt.Printf("  Last attempt:    %s\n", lastAttempt)
				}
				if lastErr := getStringValue(m, "last_error"); lastErr != "" {
					fmt.Printf("  Last error:      %s\n", lastErr)
				}
			}
		} else {
			var rows []string
			rows = append(rows, "DistID | Recipient | Type | Zone | State | Attempts | Age | Error")
			for _, mRaw := range messages {
				m, ok := mRaw.(map[string]interface{})
				if !ok {
					continue
				}
				distID := getStringValue(m, "distribution_id")
				if len(distID) > 16 {
					distID = distID[:16] + "..."
				}
				lastErr := getStringValue(m, "last_error")
				if len(lastErr) > 30 {
					lastErr = lastErr[:30] + "..."
				}
				attempts := "0"
				if v, ok := m["attempt_count"].(float64); ok {
					attempts = fmt.Sprintf("%d", int(v))
				}
				rows = append(rows, fmt.Sprintf("%s | %s | %s | %s | %s | %s | %s | %s",
					distID,
					getStringValue(m, "recipient_id"),
					getStringValue(m, "recipient_type"),
					getStringValue(m, "zone"),
					getStringValue(m, "state"),
					attempts,
					getStringValue(m, "age"),
					lastErr,
				))
			}
			if len(rows) > 1 {
				output := columnize.SimpleFormat(rows)
				fmt.Println(output)
			}
		}
	},
}
View Source
var DebugAgentRegistryCmd = &cobra.Command{
	Use:   "agentregistry",
	Short: "Test the agent registry",
	Run: func(cmd *cobra.Command, args []string) {
		conf := tdns.Config{
			MultiProvider: &tdns.MultiProviderConf{
				Identity: "local",
			},
		}
		ar := conf.NewAgentRegistry()
		ar.LocateInterval = 10
		ar.S.Set("local", &tdns.Agent{
			Identity: "local",
		})

		ar.AddRemoteAgent("agent.example.com", &tdns.Agent{
			Identity: "agent.example.com",
		})

		ar.AddRemoteAgent("agent.example.org", &tdns.Agent{
			Identity: "agent.example.org",
		})

		fmt.Printf("Agent registry:\ntype=%T\n", ar.S)
		fmt.Printf("Agent registry:\n%d shards\n", ar.S.NumShards())
		for item := range ar.S.IterBuffered() {

			fmt.Printf("Agent registry:\n%s\n", item.Key)
			fmt.Printf("Agent registry:\n%+v\n", item.Val)
		}
	},
}
View Source
var DebugAgentResyncCmd = &cobra.Command{
	Use:   "resync",
	Short: "Re-send all local changes to combiner and remote agents",
	Long: `Re-send all locally stored synced data for a zone to the combiner and
all remote agents. Use this when the combiner or remote agents have lost
state and need to be brought back in sync.

Example:
  tdns-cliv2 agent debug resync --zone whisky.dnslab.`,
	Run: func(cmd *cobra.Command, args []string) {
		PrepArgs("zonename")

		req := tdns.AgentMgmtPost{
			Command: "resync",
			Zone:    tdns.ZoneName(tdns.Globals.Zonename),
		}

		amr, err := SendAgentDebugCmd(req, false)
		if err != nil {
			log.Fatalf("Error: %v", err)
		}

		if amr.Error {
			log.Fatalf("Error: %s", amr.ErrorMsg)
		}

		fmt.Printf("Resync for zone %s:\n", tdns.Globals.Zonename)
		if amr.Msg != "" {
			fmt.Printf("  %s\n", amr.Msg)
		}
	},
}
View Source
var DebugAgentSendNotifyCmd = &cobra.Command{
	Use:   "send-notify",
	Short: "Tell agent to send a NOTIFY message to the other agents",
	Run: func(cmd *cobra.Command, args []string) {
		PrepArgs("zonename", "identity")

		notifyRRtype = strings.ToUpper(notifyRRtype)
		if notifyRRtype != "NS" && notifyRRtype != "DNSKEY" {
			log.Fatalf("Error: RR type must be either NS or DNSKEY (is %q)", notifyRRtype)
		}

		if dnsRecord == "" {
			log.Fatalf("Error: DNS record is required")
		}

		var rr dns.RR
		var err error

		if rr, err = dns.NewRR(dnsRecord); err != nil {
			log.Fatalf("Error: Invalid DNS record (did not parse): %v", err)
		}

		rrs := []string{rr.String()}

		rrtype := dns.StringToType[notifyRRtype]
		if rrtype == 0 {
			log.Fatalf("Error: Invalid RR type: %s", notifyRRtype)
		}

		req := tdns.AgentMgmtPost{
			Command:     "send-notify",
			MessageType: tdns.AgentMsgNotify,
			RRType:      rrtype,
			Zone:        tdns.ZoneName(tdns.Globals.Zonename),
			AgentId:     tdns.Globals.AgentId,
			RRs:         rrs,
		}

		_, err = SendAgentDebugCmd(req, true)
		if err != nil {
			log.Fatalf("Error: %v", err)
		}
	},
}
View Source
var DebugAgentSendRfiCmd = &cobra.Command{
	Use:   "send-rfi",
	Short: "Tell agent to send an RFI message to another agent",
	Run: func(cmd *cobra.Command, args []string) {
		PrepArgs("zonename", "identity")

		rfitype = strings.ToUpper(rfitype)
		validRfiTypes := map[string]bool{"CONFIG": true, "SYNC": true, "AUDIT": true, "EDITS": true}
		if !validRfiTypes[rfitype] {
			log.Fatalf("Error: RFI type must be one of CONFIG, SYNC, AUDIT, or EDITS (is %q)", rfitype)
		}

		if rfitype == "CONFIG" && rfisubtype == "" {
			log.Fatalf("Error: CONFIG RFI requires --subtype (upstream, downstream, sig0key)")
		}
		rfisubtype = strings.ToLower(rfisubtype)

		req := tdns.AgentMgmtPost{
			Command:     "send-rfi",
			MessageType: tdns.AgentMsgRfi,
			RfiType:     rfitype,
			RfiSubtype:  rfisubtype,
			Zone:        tdns.ZoneName(tdns.Globals.Zonename),
			AgentId:     tdns.Globals.AgentId,
		}

		amr, err := SendAgentDebugCmd(req, false)
		if err != nil {
			log.Fatalf("Error: %v", err)
		}

		if amr.Error {
			log.Fatalf("Error: %s", amr.ErrorMsg)
		}

		fmt.Printf("Result from %s RFI message sent to agent %q:\n", amr.RfiType, amr.Identity)
		if amr.Msg != "" {
			fmt.Printf("%s\n", amr.Msg)
		}
		if len(amr.RfiResponse) > 0 {
			switch {
			case rfitype == "CONFIG" && (rfisubtype == "upstream" || rfisubtype == "downstream"):
				var out []string
				if tdns.Globals.ShowHeaders {
					out = append(out, "Zone|Provider|Where|XFR src|XFR dst|XFR auth")
				}
				for aid, rfidata := range amr.RfiResponse {
					if rfidata.Error {
						fmt.Printf("  %s: error: %s\n", aid, rfidata.ErrorMsg)
						continue
					}
					if len(rfidata.ZoneXfrSrcs) > 0 {
						out = append(out, fmt.Sprintf("%s|%s|upstream|%v|%v|%v", tdns.Globals.Zonename, aid, rfidata.ZoneXfrSrcs, "", rfidata.ZoneXfrAuth))
					}
					if len(rfidata.ZoneXfrDsts) > 0 {
						out = append(out, fmt.Sprintf("%s|%s|downstream|%v|%v|%v", tdns.Globals.Zonename, aid, "", rfidata.ZoneXfrDsts, rfidata.ZoneXfrAuth))
					}
				}
				if len(out) > 0 {
					fmt.Printf("%s\n", columnize.SimpleFormat(out))
				}

			case rfitype == "CONFIG":
				for aid, rfidata := range amr.RfiResponse {
					if rfidata.Error {
						fmt.Printf("  %s: error: %s\n", aid, rfidata.ErrorMsg)
					} else {
						fmt.Printf("  %s: %s\n", aid, rfidata.Msg)
						for k, v := range rfidata.ConfigData {
							fmt.Printf("    %s: %s\n", k, v)
						}
					}
				}

			case rfitype == "SYNC":
				for aid, rfidata := range amr.RfiResponse {
					if rfidata.Error {
						fmt.Printf("  %s: error: %s\n", aid, rfidata.ErrorMsg)
					} else {
						fmt.Printf("  %s: %s\n", aid, rfidata.Msg)
					}
				}

			case rfitype == "AUDIT":
				for aid, rfidata := range amr.RfiResponse {
					if rfidata.Error {
						fmt.Printf("  %s: error: %s\n", aid, rfidata.ErrorMsg)
					} else {
						fmt.Printf("  %s: %s\n", aid, rfidata.Msg)
						if rfidata.AuditData != nil {
							auditJSON, err := json.MarshalIndent(rfidata.AuditData, "    ", "  ")
							if err != nil {
								fmt.Printf("    Error formatting audit data: %v\n", err)
							} else {
								fmt.Printf("    Audit data:\n    %s\n", string(auditJSON))
							}
						}
					}
				}

			case rfitype == "EDITS":
				for aid, rfidata := range amr.RfiResponse {
					if rfidata.Error {
						fmt.Printf("  %s: error: %s\n", aid, rfidata.ErrorMsg)
					} else {
						fmt.Printf("  %s: %s\n", aid, rfidata.Msg)
					}
				}
			}
		} else {
			fmt.Printf("No RFI data in response from agent %q\n", amr.Identity)
		}
	},
}
View Source
var DebugAgentSendSyncToCmd = &cobra.Command{
	Use:   "send-sync-to <RR> [<RR>...]",
	Short: "Send a SYNC message to a remote agent (real transport)",
	Long: `Create and send a real SYNC message to a specified remote agent.
Uses the actual transport (CHUNK NOTIFY + fallback).
RRs are validated before sending.

Example:
  tdns-cliv2 debug agent send-sync-to \
    --to agent.provider-b.example.com. \
    --zone example.com. \
    "example.com. 3600 IN NS ns1.provider-a.example.com." \
    "example.com. 3600 IN NS ns2.provider-a.example.com."`,
	Args: cobra.MinimumNArgs(1),
	Run: func(cmd *cobra.Command, args []string) {
		toAgent, _ := cmd.Flags().GetString("to")
		if toAgent == "" {
			log.Fatalf("Error: --to agent ID is required")
		}

		zone, _ := cmd.Flags().GetString("zone")
		if zone == "" {
			log.Fatalf("Error: --zone is required")
		}

		// Validate all RRs by parsing them
		var validRRs []string
		for _, rrStr := range args {
			rr, err := dns.NewRR(rrStr)
			if err != nil {
				log.Fatalf("Error: Invalid DNS record %q: %v", rrStr, err)
			}
			validRRs = append(validRRs, rr.String())
		}

		req := tdns.AgentMgmtPost{
			Command: "send-sync-to",
			Zone:    tdns.ZoneName(zone),
			AgentId: tdns.AgentId(toAgent),
			RRs:     validRRs,
		}

		amr, err := SendAgentDebugCmd(req, false)
		if err != nil {
			log.Fatalf("Error: %v", err)
		}

		if amr.Error {
			log.Fatalf("Error: %s", amr.ErrorMsg)
		}

		fmt.Printf("SYNC sent successfully:\n")
		fmt.Printf("  To: %s\n", toAgent)
		fmt.Printf("  Zone: %s\n", zone)
		fmt.Printf("  Records: %d\n", len(validRRs))
		fmt.Printf("\n%s\n", amr.Msg)

		if amr.Data != nil {
			if dataMap, ok := amr.Data.(map[string]interface{}); ok {
				if corrID, ok := dataMap["distribution_id"]; ok {
					fmt.Printf("  Distribution ID: %v\n", corrID)
				}
				if status, ok := dataMap["status"]; ok {
					fmt.Printf("  Status: %v\n", status)
				}
			}
		}
	},
}
View Source
var DebugAgentShowCombinerDataCmd = &cobra.Command{
	Use:   "show-combiner-data",
	Short: "Show combiner's local modifications store",
	Long: `Display the combiner's stored local modifications that are applied to zones.
Data is sorted by: zone → RRtype → RRs

Example:
  tdns-cliv2 debug agent show-combiner-data
  tdns-cliv2 debug agent show-combiner-data --zone example.com`,
	Run: func(cmd *cobra.Command, args []string) {
		zone, _ := cmd.Flags().GetString("zone")

		req := tdns.AgentMgmtPost{
			Command: "show-combiner-data",
		}
		if zone != "" {
			req.Zone = tdns.ZoneName(zone)
		}

		amr, err := SendAgentDebugCmd(req, false)
		if err != nil {
			log.Fatalf("Error: %v", err)
		}

		if amr.Error {
			log.Fatalf("Error: %s", amr.ErrorMsg)
		}

		if amr.Data == nil {
			fmt.Printf("No combiner data available\n")
			return
		}

		dataMap, ok := amr.Data.(map[string]interface{})
		if !ok {
			fmt.Printf("Invalid combiner data format\n")
			return
		}

		combinerData, ok := dataMap["combiner_data"].(map[string]interface{})
		if !ok || len(combinerData) == 0 {
			fmt.Printf("No local modifications stored in combiner\n")
			return
		}

		fmt.Printf("Combiner Local Modifications\n")
		fmt.Printf("=============================\n\n")

		for zoneName, ownerMapInterface := range combinerData {
			fmt.Printf("Zone: %s\n", zoneName)
			fmt.Printf("────────────────────────────────────────\n")

			ownerMap, ok := ownerMapInterface.(map[string]interface{})
			if !ok || len(ownerMap) == 0 {
				fmt.Printf("  (no modifications)\n\n")
				continue
			}

			for ownerName, rrTypeMapInterface := range ownerMap {
				fmt.Printf("  Owner: %s\n", ownerName)

				rrTypeMap, ok := rrTypeMapInterface.(map[string]interface{})
				if !ok || len(rrTypeMap) == 0 {
					fmt.Printf("    (no RRsets)\n")
					continue
				}

				for rrTypeStr, rrStringsInterface := range rrTypeMap {
					rrStrings, ok := rrStringsInterface.([]interface{})
					if !ok {
						continue
					}

					fmt.Printf("    %s (%d records):\n", rrTypeStr, len(rrStrings))
					for _, rrInterface := range rrStrings {
						if rrStr, ok := rrInterface.(string); ok {
							fmt.Printf("      %s\n", rrStr)
						}
					}
				}
				fmt.Printf("\n")
			}
		}
	},
}
View Source
var DebugAgentShowKeyInventoryCmd = &cobra.Command{
	Use:   "show-key-inventory",
	Short: "Show DNSKEY inventory received from signer (KEYSTATE)",
	Long: `Display the last KEYSTATE inventory received from the signer for a zone.
Shows all keys reported by the signer's KeyDB with their state
(created, published, standby, active, retired, foreign).

Keys marked "foreign" are from other providers' signers.

Example:
  tdns-cliv2 agent debug show-key-inventory --zone whisky.dnslab.`,
	Run: func(cmd *cobra.Command, args []string) {
		PrepArgs("zonename")

		req := tdns.AgentMgmtPost{
			Command: "show-key-inventory",
			Zone:    tdns.ZoneName(tdns.Globals.Zonename),
		}

		amr, err := SendAgentDebugCmd(req, false)
		if err != nil {
			log.Fatalf("Error: %v", err)
		}

		if amr.Error {
			log.Fatalf("Error: %s", amr.ErrorMsg)
		}

		if amr.Data == nil {
			fmt.Println(amr.Msg)
			return
		}

		dataBytes, err := json.Marshal(amr.Data)
		if err != nil {
			log.Fatalf("Failed to marshal inventory data: %v", err)
		}
		var snapshot tdns.KeyInventorySnapshot
		if err := json.Unmarshal(dataBytes, &snapshot); err != nil {
			log.Fatalf("Failed to parse inventory data: %v", err)
		}

		fmt.Printf("DNSKEY Inventory for zone %s\n", snapshot.Zone)
		fmt.Printf("Received: %s from %s\n", snapshot.Received.Format("2006-01-02 15:04:05"), snapshot.SenderID)
		fmt.Printf("────────────────────────────────────────\n")

		if len(snapshot.Inventory) == 0 {
			fmt.Printf("  (no keys)\n")
			return
		}

		out := []string{"KeyTag|Algorithm|Flags|State|Role|Key data"}
		for _, entry := range snapshot.Inventory {
			role := "local"
			if entry.State == "foreign" {
				role = "REMOTE"
			}
			algStr := dns.AlgorithmToString[entry.Algorithm]
			if algStr == "" {
				algStr = fmt.Sprintf("ALG%d", entry.Algorithm)
			}
			flagDesc := "ZSK"
			if entry.Flags&0x0001 != 0 {
				flagDesc = "KSK"
			}
			keyData := truncatePubKey(entry.KeyRR)
			out = append(out, fmt.Sprintf("%d|%s|%d (%s)|%s|%s|%s",
				entry.KeyTag, algStr, entry.Flags, flagDesc, entry.State, role, keyData))
		}
		fmt.Println(columnize.SimpleFormat(out))
	},
}
View Source
var DebugAgentShowSyncedDataCmd = &cobra.Command{
	Use:   "show-synced-data",
	Short: "Show synchronized data from peer agents (moved to: agent zone edits list)",
	Run: func(cmd *cobra.Command, args []string) {
		fmt.Println("This command has moved to: agent zone edits list")
		fmt.Println("Usage: tdns-cliv2 agent zone edits list [--zone <zone>]")
	},
}
View Source
var DebugAgentSyncStateCmd = &cobra.Command{
	Use:   "sync-state",
	Short: "Show sync state for a zone",
	Long: `Display the current synchronization state for a zone.

Example:
  tdns-cliv2 debug agent sync-state --zone example.com`,
	Run: func(cmd *cobra.Command, args []string) {
		PrepArgs("zonename")

		req := tdns.AgentMgmtPost{
			Command: "hsync-sync-state",
			Zone:    tdns.ZoneName(tdns.Globals.Zonename),
		}

		amr, err := SendAgentDebugCmd(req, false)
		if err != nil {
			log.Fatalf("Error: %v", err)
		}

		if amr.Error {
			log.Fatalf("Error: %s", amr.ErrorMsg)
		}

		fmt.Printf("Sync State for zone %s:\n", tdns.Globals.Zonename)
		fmt.Printf("%s\n", amr.Msg)

		if amr.Data != nil {
			if dataMap, ok := amr.Data.(map[string]interface{}); ok {
				if zdr, ok := dataMap["zone_data_repo"]; ok {
					dump.P(zdr)
				}
			}
		}
	},
}
View Source
var DebugCmd = &cobra.Command{
	Use:   "debug",
	Short: "A brief description of your command",
	Run: func(cmd *cobra.Command, args []string) {
		fmt.Println("debug called")
	},
}
View Source
var DebugHsyncChunkRecvCmd = &cobra.Command{
	Use:   "chunk-recv",
	Short: "Show recently received CHUNKs",
	Long:  `Display CHUNK messages received via DNS transport.`,
	Run: func(cmd *cobra.Command, args []string) {
		parent, _ := getCommandContext("debug")
		api, err := getApiClient(parent, true)
		if err != nil {
			log.Fatalf("Error getting API client: %v", err)
		}

		req := tdns.AgentMgmtPost{
			Command: "hsync-chunk-recv",
		}

		_, buf, err := api.RequestNG("POST", "/agent/debug", req, true)
		if err != nil {
			log.Fatalf("API request failed: %v", err)
		}

		var resp tdns.AgentMgmtResponse
		if err := json.Unmarshal(buf, &resp); err != nil {
			log.Fatalf("Failed to parse response: %v", err)
		}

		if resp.Error {
			log.Fatalf("API error: %s", resp.ErrorMsg)
		}

		fmt.Println("Recently received CHUNKs:")
		fmt.Println(resp.Msg)
	},
}

DebugHsyncChunkRecvCmd shows received CHUNKs

View Source
var DebugHsyncChunkSendCmd = &cobra.Command{
	Use:   "chunk-send",
	Short: "Send a test CHUNK to a peer",
	Long: `Manually send a CHUNK message to a peer for testing.
This is useful for testing DNS transport without full sync operations.`,
	Run: func(cmd *cobra.Command, args []string) {
		PrepArgs("zonename")

		if hsyncPeerID == "" {
			log.Fatalf("Error: --peer is required")
		}

		parent, _ := getCommandContext("debug")
		api, err := getApiClient(parent, true)
		if err != nil {
			log.Fatalf("Error getting API client: %v", err)
		}

		req := tdns.AgentMgmtPost{
			Command: "hsync-chunk-send",
			Zone:    tdns.ZoneName(tdns.Globals.Zonename),
			AgentId: tdns.AgentId(hsyncPeerID),
		}

		_, buf, err := api.RequestNG("POST", "/agent/debug", req, true)
		if err != nil {
			log.Fatalf("API request failed: %v", err)
		}

		var resp tdns.AgentMgmtResponse
		if err := json.Unmarshal(buf, &resp); err != nil {
			log.Fatalf("Failed to parse response: %v", err)
		}

		if resp.Error {
			log.Fatalf("API error: %s", resp.ErrorMsg)
		}

		fmt.Printf("CHUNK sent successfully to peer %s\n", hsyncPeerID)
		fmt.Printf("Distribution ID: %s\n", resp.Msg)
	},
}

DebugHsyncChunkSendCmd sends a test CHUNK

View Source
var DebugHsyncInitDbCmd = &cobra.Command{
	Use:   "init-db",
	Short: "Initialize HSYNC database tables",
	Long:  `Create or verify the HSYNC database tables exist.`,
	Run: func(cmd *cobra.Command, args []string) {
		parent, _ := getCommandContext("debug")
		api, err := getApiClient(parent, true)
		if err != nil {
			log.Fatalf("Error getting API client: %v", err)
		}

		req := tdns.AgentMgmtPost{
			Command: "hsync-init-db",
		}

		_, buf, err := api.RequestNG("POST", "/agent/debug", req, true)
		if err != nil {
			log.Fatalf("API request failed: %v", err)
		}

		var resp tdns.AgentMgmtResponse
		if err := json.Unmarshal(buf, &resp); err != nil {
			log.Fatalf("Failed to parse response: %v", err)
		}

		if resp.Error {
			log.Fatalf("API error: %s", resp.ErrorMsg)
		}

		fmt.Println("HSYNC database tables initialized successfully")
	},
}

DebugHsyncInitDbCmd initializes HSYNC database tables

View Source
var DelCmd = &cobra.Command{
	Use:   "del",
	Short: "Delegation prefix command. Only usable via sub-commands.",
}
View Source
var DeleteCmd = &cobra.Command{
	Use:   "delete [job-id]",
	Short: "Delete scan job(s)",
	Long:  `Delete a specific scan job by job ID, or all jobs if --all is used`,
	Args:  cobra.MaximumNArgs(1),
	Run: func(cmd *cobra.Command, args []string) {

		api, err := getApiClient("scanner", true)
		if err != nil {
			log.Fatalf("Error getting API client for scanner: %v", err)
		}

		deleteAll, _ := cmd.Flags().GetBool("all")

		var endpoint string
		if deleteAll {
			endpoint = "/scanner/delete?all=true"
		} else if len(args) > 0 {
			endpoint = fmt.Sprintf("/scanner/delete?job_id=%s", args[0])
		} else {
			log.Fatal("Error: either specify a job ID or use --all flag")
		}

		status, buf, err := api.RequestNG("DELETE", endpoint, nil, true)
		if err != nil {
			log.Fatalf("Error from scanner API: %v", err)
		}

		if status == http.StatusNotFound {
			log.Fatalf("Job not found")
		}
		if status == http.StatusBadRequest {
			log.Fatalf("Bad request: %s", string(buf))
		}
		if status != http.StatusOK {
			log.Fatalf("Unexpected status code: %d", status)
		}

		var resp tdns.ScannerResponse
		err = json.Unmarshal(buf, &resp)
		if err != nil {
			log.Fatalf("Error unmarshaling response: %v", err)
		}

		if resp.Error {
			log.Fatalf("Error: %s", resp.ErrorMsg)
		}

		fmt.Printf("%s\n", resp.Msg)
	},
}
View Source
var DsyncDiscoveryCmd = &cobra.Command{
	Use:   "dsync-query",
	Short: "Send a DNS query for 'zone. DSYNC' and present the result.",
	Run: func(cmd *cobra.Command, args []string) {

		PrepArgs("zonename")
		tdns.Globals.Zonename = dns.Fqdn(tdns.Globals.Zonename)

		ctx, cancel, imr, err := StartImrForCli("")
		if err != nil {
			log.Fatalf("Error: %v", err)
		}
		defer cancel()

		dsync_res, err := imr.DsyncDiscovery(ctx, tdns.Globals.Zonename, tdns.Globals.Verbose)
		if err != nil {
			log.Fatalf("Error: %v", err)
		}

		fmt.Printf("Parent: %s\n", dsync_res.Parent)
		if len(dsync_res.Rdata) == 0 {
			fmt.Printf("No DSYNC record associated with '%s'\n", tdns.Globals.Zonename)
		} else {
			for _, nr := range dsync_res.Rdata {
				fmt.Printf("%s\tIN\tDSYNC\t%s\n", dsync_res.Qname, nr.String())
			}
		}
	},
}
View Source
var ExitCmd = &cobra.Command{
	Use:   "exit",
	Short: "Exit the interactive shell",
	Run: func(cmd *cobra.Command, args []string) {
		Terminate()
	},
}

exitCmd represents the exit command

View Source
var (
	GenerateCmd = &cobra.Command{
		Use:   "generate",
		Short: "Generate DNS records or encodings",
	}
)
View Source
var ImrCmd = &cobra.Command{
	Use:   "imr",
	Short: "Interact with tdns-imr via API",
}

ImrCmd is the parent command for all IMR-related commands

View Source
var ImrDumpCmd = &cobra.Command{
	Use:   "dump",
	Short: "List records in the RRsetCache",

	Run: func(cmd *cobra.Command, args []string) {
		fmt.Printf("Listing records in the RRsetCache\n")
		if Conf.Internal.RRsetCache == nil {
			fmt.Println("RRsetCache is nil")
			return
		}

		items := []core.Tuple[string, cache.CachedRRset]{}
		for item := range Conf.Internal.RRsetCache.RRsets.IterBuffered() {
			items = append(items, item)
		}
		sort.Slice(items, func(i, j int) bool {
			return lessByReverseLabels(items[i].Val.Name, items[j].Val.Name)
		})
		for _, it := range items {
			PrintCacheItem(it, ".")
		}
	},
}
View Source
var ImrFlushCmd = &cobra.Command{
	Use:   "flush",
	Short: "Flush cached data",
}
View Source
var ImrQueryCmd = &cobra.Command{
	Use:   "query [name] [type]",
	Short: "Query DNS records",
	Long:  `Query DNS records for a given name and type`,
	Args:  cobra.ExactArgs(2),
	Run: func(cmd *cobra.Command, args []string) {
		if len(args) < 2 {
			fmt.Println("Error: both name and type are required.")
			_ = cmd.Usage()
			return
		}
		fmt.Printf("Querying %s for %s records (verbose mode: %t)\n", args[0], args[1], tdns.Globals.Verbose)

		qname := dns.Fqdn(args[0])
		if _, ok := dns.IsDomainName(qname); !ok {
			fmt.Printf("Not a valid domain name: '%s'\n", qname)
			return
		}

		qtype, exist := dns.StringToType[strings.ToUpper(args[1])]
		if !exist {
			fmt.Printf("Not a valid DNS RR type: '%s'\n", args[1])
			return
		}

		if Conf.Internal.RecursorCh == nil {
			fmt.Printf("No active channel to RecursorEngine. Terminating.\n")
			return
		}

		resp := make(chan tdns.ImrResponse, 1)
		Conf.Internal.RecursorCh <- tdns.ImrRequest{
			Qname:      qname,
			Qclass:     dns.ClassINET,
			Qtype:      qtype,
			ResponseCh: resp,
		}

		select {
		case r := <-resp:
			// Check cache entry to determine if this is a negative response
			var cached *cache.CachedRRset
			if Conf.Internal.RRsetCache != nil {
				cached = Conf.Internal.RRsetCache.Get(qname, qtype)
			}

			if cached != nil && (cached.Context == cache.ContextNXDOMAIN || cached.Context == cache.ContextNoErrNoAns) {

				vstate := cached.State
				stateStr := cache.ValidationStateToString[vstate]
				ctxStr := cache.CacheContextToString[cached.Context]

				fmt.Printf("%s %s (state: %s)\n", qname, ctxStr, stateStr)

				if tdns.Globals.Verbose {

					if vstate == cache.ValidationStateIndeterminate {
						fmt.Printf("Proof: not possible for zone in state=indeterminate\n")
					} else if len(cached.NegAuthority) > 0 {
						fmt.Printf("Proof:\n")
						for _, negRRset := range cached.NegAuthority {
							if negRRset != nil {
								for _, rr := range negRRset.RRs {
									fmt.Printf("  %s\n", rr.String())
								}
								for _, rr := range negRRset.RRSIGs {
									fmt.Printf("  %s\n", rr.String())
								}
							}
						}
					} else if cached.RRset != nil {

						for _, rr := range cached.RRset.RRs {
							if rr.Header().Rrtype == dns.TypeSOA {
								fmt.Printf("  %s\n", rr.String())
							}
						}
					}
				} else {
					fmt.Printf("Proof only presented in verbose mode\n")
				}
			} else if r.RRset != nil {

				vstate := cache.ValidationStateNone
				if cached != nil {
					vstate = cached.State
				}
				suffix := fmt.Sprintf(" (state: %s)", cache.ValidationStateToString[vstate])

				for _, rr := range r.RRset.RRs {
					switch rr.Header().Rrtype {
					case qtype, dns.TypeCNAME:
						fmt.Printf("%s%s\n", rr.String(), suffix)
					default:
						fmt.Printf("Not printing: %q\n", rr.String())
					}
				}
				for _, rr := range r.RRset.RRSIGs {
					fmt.Printf("%s\n", rr.String())
				}
			} else if r.Error {
				fmt.Printf("Error: %s\n", r.ErrorMsg)
			} else {
				fmt.Printf("No records found: %s\n", r.Msg)
			}
		case <-time.After(3 * time.Second):
			fmt.Println("Timeout waiting for response")
			return
		}
	},
}

Query command - takes name and type

View Source
var ImrSetCmd = &cobra.Command{
	Use:   "set",
	Short: "Set IMR runtime parameters",
}
View Source
var ImrShowCmd = &cobra.Command{
	Use:   "show",
	Short: "Show IMR state",
}
View Source
var ImrStatsCmd = &cobra.Command{
	Use:   "stats",
	Short: "Show statistics",
	Long:  `Show DNS query statistics`,
	Args:  cobra.NoArgs,
	Run: func(cmd *cobra.Command, args []string) {
		fmt.Println("Showing statistics")
	},
}

Stats command - no arguments

View Source
var ImrZoneCmd = &cobra.Command{
	Use:   "zone",
	Short: "prefix command for zone operations",
	Long:  `prefix command for zone operations`,
}

Zone command - takes zone name

View Source
var JwtCmd = &cobra.Command{
	Use:   "jwt",
	Short: "JWT inspection and manipulation commands",
}
View Source
var KeysCmd = &cobra.Command{
	Use:   "keys",
	Short: "JOSE keypair for secure CHUNK (generate, show)",
	Long:  `Generate a JOSE keypair or display the public key. Uses the server config file (agent or combiner) to get long_term_jose_priv_key path, or --server-config.`,
}
View Source
var KeystoreCmd = &cobra.Command{
	Use:   "keystore",
	Short: "Prefix command to access different features of tdns-auth truststore",
	Long: `The TDNS-AUTH keystore is where SIG(0) key pairs for zones are kept.
The CLI contains functions for listing SIG(0) key pairs, adding and
deleting keys.`,
	Run: func(cmd *cobra.Command, args []string) {
		fmt.Println("keystore called. This is likely a mistake, sub command needed")
	},
}
View Source
var MaxWait int
View Source
var NewState string
View Source
var NotifyCmd = &cobra.Command{
	Use:   "notify",
	Short: "The 'notify' command is only usable via defined sub-commands",
}
View Source
var PingCmd = &cobra.Command{
	Use:   "ping",
	Short: "Send an API ping request and present the response",
	Run: func(cmd *cobra.Command, args []string) {
		prefixcmd, _ := getCommandContext("ping")

		if len(args) != 0 {
			log.Fatal("ping must have no arguments")
		}

		api, err := getApiClient(prefixcmd, true)
		if err != nil {
			log.Fatalf("Error getting API client for %s: %v", prefixcmd, err)
		}

		pr, err := api.SendPing(tdns.Globals.PingCount, false)
		if err != nil {
			if strings.Contains(err.Error(), "connection refused") {
				fmt.Printf("Error: connection refused. Most likely the daemon is not running\n")
				os.Exit(1)
			} else {
				log.Fatalf("Error from SendPing: %v", err)
			}
		}

		uptime := time.Since(pr.BootTime).Truncate(time.Second)
		weeks := uptime / (7 * 24 * time.Hour)
		uptime %= 7 * 24 * time.Hour
		days := uptime / (24 * time.Hour)
		uptime %= 24 * time.Hour
		hours := uptime / time.Hour
		uptime %= time.Hour
		minutes := uptime / time.Minute
		uptime %= time.Minute
		seconds := uptime / time.Second

		var uptimeStr string
		if weeks > 0 {
			uptimeStr = fmt.Sprintf("%dw%dd", weeks, days)
		} else if days > 0 {
			uptimeStr = fmt.Sprintf("%dd%dh", days, hours)
		} else if hours > 0 {
			uptimeStr = fmt.Sprintf("%dh%dm", hours, minutes)
		} else {
			uptimeStr = fmt.Sprintf("%dm%ds", minutes, seconds)
		}

		if tdns.Globals.Verbose {
			fmt.Printf("%s (version %s): pings: %d, pongs: %d, uptime: %s, time: %s, client: %s\n",
				pr.Msg, pr.Version, pr.Pings, pr.Pongs, uptimeStr, pr.Time.Format(timelayout), pr.Client)
		} else {
			fmt.Printf("%s: pings: %d, pongs: %d, uptime: %s, time: %s\n",
				pr.Msg, pr.Pings, pr.Pongs, uptimeStr, pr.Time.Format(timelayout))
		}
	},
}
View Source
var QuitCmd = &cobra.Command{
	Use:    "quit",
	Short:  "Exit the interactive shell",
	Long:   `Exit the interactive shell`,
	Hidden: true,
	Run: func(cmd *cobra.Command, args []string) {
		Terminate()
	},
}

quitCmd represents the quit command

View Source
var ReportCmd = &cobra.Command{
	Use:   "report <qname>",
	Short: "Send a report and (optionally) discover DSYNC via the internal resolver (imr)",
	Run: func(cmd *cobra.Command, args []string) {

		PrepArgs("zonename")
		dsyncLookup := true
		if dsyncTarget != "" && dsyncPort != 0 {
			dsyncLookup = false
		}

		ctx, cancel := context.WithCancel(context.Background())
		defer cancel()

		tdns.Globals.App.Type = tdns.AppTypeCli
		if tdns.Globals.Debug {
			fmt.Printf("ReportCmd: Calling Conf.MainInit(%q)\n", tdns.DefaultCliCfgFile)
		}
		if err := Conf.MainInit(ctx, tdns.DefaultCliCfgFile); err != nil {
			tdns.Shutdowner(&Conf, fmt.Sprintf("Error initializing tdns-cli: %v", err))
		}

		if reportSender == "" {
			fmt.Printf("Error: sender not specified\n")
			return
		}

		if dsyncLookup {

			_, cancel, imr, err := StartImrForCli("")
			if err != nil {
				log.Fatalf("Error initializing IMR: %v", err)
			}
			defer cancel()

			log.Printf("ReportCmd: Discovering DSYNC via IMR for %s", tdns.Globals.Zonename)

			dsyncRes, derr := imr.DsyncDiscovery(ctx, tdns.Globals.Zonename, tdns.Globals.Verbose)
			if derr != nil {
				log.Printf("ReportCmd: DSYNC discovery error: %v", derr)
				return
			}

			// Find DSYNC record with REPORT scheme
			var reportDSYNC *core.DSYNC
			for _, ds := range dsyncRes.Rdata {
				if ds.Scheme == core.SchemeReport {
					reportDSYNC = ds
					break
				}
			}

			if reportDSYNC == nil {
				log.Printf("ReportCmd: no DSYNC REPORT found for %s, aborting report", tdns.Globals.Zonename)
				return
			}

			targetIP = reportDSYNC.Target
			port = strconv.Itoa(int(reportDSYNC.Port))
		} else {
			targetIP = dsyncTarget
			port = strconv.Itoa(dsyncPort)
		}

		if edeCode == 0 {
			edeCode = int(edns0.EDEMPZoneXfrFailure)
		}

		m := new(dns.Msg)
		m.SetNotify(tdns.Globals.Zonename)
		err := edns0.AddReportOptionToMessage(m, &edns0.ReportOption{
			ZoneName: tdns.Globals.Zonename,
			EDECode:  uint16(edeCode),
			Severity: 17,
			Sender:   reportSender,
			Details:  reportDetails,
		})
		if err != nil {
			log.Printf("ReportCmd: failed to build report EDNS0 option: %v", err)
			return
		}

		log.Printf("ReportCmd: sending report to %s:%s (from DSYNC REPORT)", targetIP, port)

		c := core.NewDNSClient(core.TransportDo53, port, nil)

		if reportTsig {

			tsig := tdns.Globals.TsigKeys[reportSender+".key."]
			if tsig == nil {
				fmt.Printf("Error: tsig key not found for sender: %s\n", reportSender)
				return
			}

			// There is no built-in map or function in miekg/dns for this, so we use a switch.
			var alg string
			switch strings.ToLower(tsig.Algorithm) {
			case "hmac-sha1":
				alg = dns.HmacSHA1
			case "hmac-sha256":
				alg = dns.HmacSHA256
			case "hmac-sha384":
				alg = dns.HmacSHA384
			case "hmac-sha512":
				alg = dns.HmacSHA512
			default:
				alg = tsig.Algorithm
			}
			if tdns.Globals.Debug {
				fmt.Printf("TSIG signing the report with %s\n", tsig.Name)
			}

			tsigMap := map[string]string{tsig.Name: tsig.Secret}
			if c.DNSClientUDP != nil {
				c.DNSClientUDP.TsigSecret = tsigMap
			}
			if c.DNSClientTCP != nil {
				c.DNSClientTCP.TsigSecret = tsigMap
			}
			if c.DNSClientTLS != nil {
				c.DNSClientTLS.TsigSecret = tsigMap
			}
			m.SetTsig(tsig.Name, alg, 300, time.Now().Unix())
		}

		if tdns.Globals.Debug {
			fmt.Printf("%s\n", m.String())
		}

		resp, _, err := c.Exchange(m, targetIP, false)
		if err != nil {
			fmt.Printf("ReportCmd: error sending report: %v\n", err)
			os.Exit(1)
		}
		rcode := resp.Rcode
		if rcode == dns.RcodeSuccess {
			fmt.Printf("Report accepted (rcode: %s)\n", dns.RcodeToString[rcode])
		} else {
			hasede, edecode, edemsg := edns0.ExtractEDEFromMsg(resp)
			if hasede {
				fmt.Printf("Error: rcode: %s, EDE Message: %s (EDE code: %d)\n", dns.RcodeToString[rcode], edemsg, edecode)
			} else {
				fmt.Printf("Error: rcode: %s\n", dns.RcodeToString[rcode])
				fmt.Printf("Response msg:\n%s\n", resp.String())
			}
			os.Exit(1)
		}
	},
}
View Source
var ResultsCmd = &cobra.Command{
	Use:   "results [job-id]",
	Short: "Get results of a completed scan job",
	Long:  `Get detailed results of a completed scan job by job ID. Use --delete to delete the job after retrieving results.`,
	Args:  cobra.ExactArgs(1),
	Run: func(cmd *cobra.Command, args []string) {

		api, err := getApiClient("scanner", true)
		if err != nil {
			log.Fatalf("Error getting API client for scanner: %v", err)
		}

		jobID := args[0]
		endpoint := fmt.Sprintf("/scanner/status?job_id=%s", jobID)

		status, buf, err := api.RequestNG("GET", endpoint, nil, true)
		if err != nil {
			log.Fatalf("Error from scanner API: %v", err)
		}

		if status == http.StatusNotFound {
			log.Fatalf("Job not found: %s", jobID)
		}
		if status != http.StatusOK {
			log.Fatalf("Unexpected status code: %d", status)
		}

		var job tdns.ScanJobStatus
		err = json.Unmarshal(buf, &job)
		if err != nil {
			log.Fatalf("Error unmarshaling job status: %v", err)
		}

		if job.Status != "completed" {
			fmt.Printf("Job %s is not completed yet. Status: %s\n", jobID, job.Status)
			fmt.Printf("Progress: %d/%d tuples processed\n", job.ProcessedTuples, job.TotalTuples)
			return
		}

		if job.Error {
			fmt.Printf("Job %s completed with error: %s\n", jobID, job.ErrorMsg)
			return
		}

		deleteFlag, _ := cmd.Flags().GetBool("delete")
		if deleteFlag {

			deleteEndpoint := fmt.Sprintf("/scanner/delete?job_id=%s", jobID)
			delStatus, delBuf, err := api.RequestNG("DELETE", deleteEndpoint, nil, true)
			if err != nil {
				log.Printf("Warning: Error deleting job %s: %v", jobID, err)
			} else if delStatus == http.StatusOK {
				fmt.Printf("Job %s deleted successfully\n", jobID)
			} else {
				log.Printf("Warning: Failed to delete job %s: status %d, response: %s", jobID, delStatus, string(delBuf))
			}
		}

		if tdns.Globals.Verbose {
			// Pretty print JSON
			var prettyJSON bytes.Buffer
			json.Indent(&prettyJSON, buf, "", "  ")
			fmt.Println(prettyJSON.String())
		} else {

			fmt.Printf("Job ID: %s\n", job.JobID)
			fmt.Printf("Status: %s\n", job.Status)
			fmt.Printf("Total Responses: %d\n\n", len(job.Responses))

			for i, resp := range job.Responses {
				fmt.Printf("Response %d: %s (%s)\n", i+1, resp.Qname, tdns.ScanTypeToString[resp.ScanType])
				if resp.DataChanged {
					fmt.Printf("  Data changed: Yes\n")
				} else {
					fmt.Printf("  Data changed: No\n")
				}
				if resp.AllNSInSync {
					fmt.Printf("  All NS in sync: Yes\n")
				} else if len(resp.Options) > 0 {
					for _, opt := range resp.Options {
						if opt == "all-ns" {
							fmt.Printf("  All NS in sync: No\n")
							break
						}
					}
				}
				if resp.Error {
					fmt.Printf("  Error: %s\n", resp.ErrorMsg)
				}
				fmt.Println()
			}
		}
	},
}
View Source
var RootKeysCmd = &cobra.Command{
	Use:   "keys",
	Short: "Generate long-term keypairs for agent/combiner (JOSE)",
	Long: `Generate JOSE keypairs used by tdns-agentv2 and tdns-combinerv2 for
authenticated NOTIFY(CHUNK) and API traffic. Use one keypair per party:
  - Agent:  agent.jose.private (+ optional agent.jose.pub for combiner config)
  - Combiner: combiner.jose.private (+ optional combiner.jose.pub for agent config)
`,
}

RootKeysCmd is the root-level "keys" command (e.g. tdns-cli keys generate). It does not require tdns-cli config or API; use for generating JOSE keypairs on disk.

View Source
var ScanCdsCmd = &cobra.Command{
	Use:   "cds [zone...]",
	Short: "Send CDS scan request with ScanTuple data to tdns-scanner",
	Long:  `Send CDS scan request for one or more zones. Zones can be specified as arguments or via --zone flag.`,
	Args:  cobra.MinimumNArgs(0),
	Run: func(cmd *cobra.Command, args []string) {

		api, err := getApiClient("scanner", true)
		if err != nil {
			log.Fatalf("Error getting API client for scanner: %v", err)
		}

		// Get zones from command arguments
		var zones []string

		if len(args) > 0 {
			for _, arg := range args {
				zone := strings.TrimSpace(arg)
				if zone != "" {
					zones = append(zones, zone)
				}
			}
		}

		if len(zones) == 0 {
			if tdns.Globals.Zonename == "" {
				log.Fatal("Error: specify zones as arguments or use --zone flag")
			}
			zones = []string{tdns.Globals.Zonename}
		}

		if tdns.Globals.Verbose {
			fmt.Printf("Scanning %d zones: %v\n", len(zones), zones)
		}

		scanTuples := make([]tdns.ScanTuple, 0, len(zones))
		for _, zone := range zones {
			zone = dns.Fqdn(zone)

			scanTuple := tdns.ScanTuple{
				Zone: zone,

				CurrentData: tdns.CurrentScanData{
					CDS: nil,
				},
			}
			scanTuples = append(scanTuples, scanTuple)
		}

		post := tdns.ScannerPost{
			Command:    "SCAN",
			ScanType:   tdns.ScanCDS,
			ScanTuples: scanTuples,
		}

		status, buf, err := api.RequestNG("POST", "/scanner", post, true)
		if err != nil {
			log.Fatalf("Error from scanner API: %v", err)
		}

		if tdns.Globals.Verbose {
			fmt.Printf("Status: %d\n", status)
		}

		var resp tdns.ScannerResponse
		err = json.Unmarshal(buf, &resp)
		if err != nil {
			log.Fatalf("Error unmarshaling response: %v", err)
		}

		if resp.Error {
			log.Fatalf("Error from scanner: %s", resp.ErrorMsg)
		}

		fmt.Printf("Scanner response: %s\n", resp.Msg)
		if resp.Status != "" {
			fmt.Printf("Status: %s\n", resp.Status)
		}
		if resp.JobID != "" {
			fmt.Printf("Job ID: %s\n", resp.JobID)
			fmt.Printf("Use 'tdns-cli scanner status %s' to check job status\n", resp.JobID)
		}
	},
}
View Source
var ScanCmd = &cobra.Command{
	Use:   "scan",
	Short: "Send scan requests to tdns-scanner",
}
View Source
var ScannerCmd = &cobra.Command{
	Use:   "scanner",
	Short: "Interact with tdns-scanner via API",
}
View Source
var ServerName string = "PLACEHOLDER"
View Source
var StatusCmd = &cobra.Command{
	Use:   "status [job-id]",
	Short: "Get status of scan job(s)",
	Long:  `Get status of a specific scan job by job ID, or list all jobs if no job ID is provided`,
	Args:  cobra.MaximumNArgs(1),
	Run: func(cmd *cobra.Command, args []string) {

		api, err := getApiClient("scanner", true)
		if err != nil {
			log.Fatalf("Error getting API client for scanner: %v", err)
		}

		var endpoint string
		if len(args) > 0 {

			endpoint = fmt.Sprintf("/scanner/status?job_id=%s", args[0])
		} else {

			endpoint = "/scanner/status"
		}

		status, buf, err := api.RequestNG("GET", endpoint, nil, true)
		if err != nil {
			log.Fatalf("Error from scanner API: %v", err)
		}

		if status == http.StatusNotFound {
			log.Fatalf("Job not found")
		}
		if status == http.StatusBadRequest {
			log.Fatalf("Bad request: %s", string(buf))
		}
		if status != http.StatusOK {
			log.Fatalf("Unexpected status code: %d", status)
		}

		if len(args) > 0 {
			// Single job - show detailed status
			var job tdns.ScanJobStatus
			err = json.Unmarshal(buf, &job)
			if err != nil {
				log.Fatalf("Error unmarshaling job status: %v", err)
			}

			fmt.Printf("Job ID: %s\n", job.JobID)
			fmt.Printf("Status: %s\n", job.Status)
			fmt.Printf("Created: %s\n", job.CreatedAt.Format("2006-01-02 15:04:05"))
			if job.StartedAt != nil {
				fmt.Printf("Started: %s\n", job.StartedAt.Format("2006-01-02 15:04:05"))
			}
			if job.CompletedAt != nil {
				fmt.Printf("Completed: %s\n", job.CompletedAt.Format("2006-01-02 15:04:05"))
			}
			fmt.Printf("Progress: %d/%d tuples processed\n", job.ProcessedTuples, job.TotalTuples)

			if job.Error {
				fmt.Printf("Error: %s\n", job.ErrorMsg)
			}

			if len(job.Responses) > 0 {
				fmt.Printf("\nResults (%d responses):\n", len(job.Responses))
				for i, resp := range job.Responses {
					fmt.Printf("\n  Response %d:\n", i+1)
					fmt.Printf("    Qname: %s\n", resp.Qname)
					fmt.Printf("    Scan Type: %s\n", tdns.ScanTypeToString[resp.ScanType])
					fmt.Printf("    Data Changed: %t\n", resp.DataChanged)
					if resp.AllNSInSync {
						fmt.Printf("    All NS In Sync: true\n")
					} else if len(resp.Options) > 0 {
						for _, opt := range resp.Options {
							if opt == "all-ns" {
								fmt.Printf("    All NS In Sync: false\n")
								break
							}
						}
					}
					if resp.Error {
						fmt.Printf("    Error: %s\n", resp.ErrorMsg)
					}
				}
			}
		} else {
			// All jobs - show summary table
			var jobs []*tdns.ScanJobStatus
			err = json.Unmarshal(buf, &jobs)
			if err != nil {
				log.Fatalf("Error unmarshaling job list: %v", err)
			}

			if len(jobs) == 0 {
				fmt.Println("No jobs found")
				return
			}

			sort.Slice(jobs, func(i, j int) bool {
				return jobs[i].CreatedAt.After(jobs[j].CreatedAt)
			})

			t := acidtab.New("JOB ID", "STATUS", "CREATED", "PROGRESS", "ERROR")
			for _, job := range jobs {
				progress := fmt.Sprintf("%d/%d", job.ProcessedTuples, job.TotalTuples)
				errorStr := ""
				if job.Error {
					errorStr = job.ErrorMsg
					if len(errorStr) > 30 {
						errorStr = errorStr[:27] + "..."
					}
				}
				created := job.CreatedAt.Format("2006-01-02 15:04:05")
				t.Row(job.JobID, job.Status, created, progress, errorStr)
			}
			fmt.Println(t.String())
		}
	},
}
View Source
var StopCmd = &cobra.Command{
	Use:   "stop",
	Short: "Send stop command to tdns-auth / tdns-agent",
	Run: func(cmd *cobra.Command, args []string) {
		SendCommand("stop", ".")
	},
}
View Source
var TheAgentId string
View Source
var ToRFC3597Cmd = &cobra.Command{
	Use:   "rfc3597",
	Short: "Generate the RFC 3597 representation of a DNS record",
	Run: func(cmd *cobra.Command, args []string) {
		if rrstr == "" {
			log.Fatalf("Record to generate RFC 3597 representation for not specified.")
		}

		rr, err := dns.NewRR(rrstr)
		if err != nil {
			log.Fatalf("Could not parse record \"%s\": %v", rrstr, err)
		}

		fmt.Printf("Normal   (len=%d): \"%s\"\n", dns.Len(rr), rr.String())
		u := new(dns.RFC3597)
		u.ToRFC3597(rr)
		fmt.Printf("RFC 3597 (len=%d): \"%s\"\n", dns.Len(u), u.String())
	},
}
View Source
var TruststoreCmd = &cobra.Command{
	Use:   "truststore",
	Short: "Prefix command to access different features of tdns-auth truststore",
	Long: `The TDNS-AUTH truststore is where SIG(0) public keys for child zones are kept.
The CLI contains functions for listing trusted SIG(0) keys, adding and
deleting child keys as well as changing the trust state of individual keys.`,
}
View Source
var UpdateCmd = &cobra.Command{
	Use:   "update",
	Short: "[OBE] Create and ultimately send a DNS UPDATE msg for zone auth data",
}
View Source
var VersionCmd = &cobra.Command{
	Use:   "version",
	Short: "Print the version of the app, more or less verbosely",
	Run: func(cmd *cobra.Command, args []string) {
		fmt.Printf("This is %s, version %s, compiled on %v\n", tdns.Globals.App.Name, tdns.Globals.App.Version, tdns.Globals.App.Date)
	},
}
View Source
var ZoneCmd = &cobra.Command{
	Use:   "zone",
	Short: "Prefix command, not usable by itself",
}

Functions

func CreateUpdate

func CreateUpdate(updateType string)

CreateUpdate starts an interactive CLI for composing, signing, and sending DNS UPDATEs.

It initializes the keystore and signing state, then enters a prompt loop that lets the user add or delete RRs, view the pending update, sign it with SIG(0) keys (from a keyfile or the keystore), select the target server, and send the update. The function updates package-level globals such as `zone`, `signer`, and `server` as needed. It may call os.Exit on fatal initialization or signing errors.

The updateType parameter selects the operational context used to initialize the interactive session (for example "child" or "zone") and does not affect the format of the DNS UPDATE messages produced.

func DnssecGenDS

func DnssecGenDS() error

func DnssecKeyMgmt

func DnssecKeyMgmt(cmd string) error

func ListMPZones

func ListMPZones(cr tdns.ZoneResponse)

ListMPZones displays multi-provider zone details in tabular format.

func ListZones

func ListZones(cr tdns.ZoneResponse)

func PrepArgs

func PrepArgs(args ...interface{})

PrepArgs validates and normalizes CLI parameters. It can work in two modes: 1. Legacy mode: reads from global variables (tdns.Globals.*) 2. Flag mode: reads from cobra.Command flags (pass cmd as first argument)

Usage:

PrepArgs("zonename")                    // reads from tdns.Globals.Zonename
PrepArgs(cmd, "zonename")               // reads from --zone flag
PrepArgs(cmd, "zonename", "service")    // reads from --zone and --service flags

func PrintAgent

func PrintAgent(agent *tdns.Agent, showZones bool) error

func PrintCacheItem

func PrintCacheItem(item core.Tuple[string, cache.CachedRRset], suffix string)

func PrintHsyncRRs

func PrintHsyncRRs(agentid tdns.AgentId, rrs []string)

func PrintUpdateResult

func PrintUpdateResult(ur tdns.UpdateResult)

func ReadZoneData

func ReadZoneData(zonename string) error

func SendAgentDebugCmd

func SendAgentDebugCmd(req tdns.AgentMgmtPost, printJson bool) (*tdns.AgentMgmtResponse, error)

func SendAgentMgmtCmd

func SendAgentMgmtCmd(req *tdns.AgentMgmtPost, prefix string) (*tdns.AgentMgmtResponse, error)

func SendAuthPeerCmd

func SendAuthPeerCmd(req tdns.AuthPeerPost) (*tdns.AuthPeerResponse, error)

SendAuthPeerCmd sends a peer command to the auth server's /auth/peer endpoint.

func SendCatalogCommand

func SendCatalogCommand(api *tdns.ApiClient, data tdns.CatalogPost) (*tdns.CatalogResponse, error)

SendCatalogCommand sends a catalog command to the API

func SendCombinerEditCmd

func SendCombinerEditCmd(req tdns.CombinerEditPost) (*tdns.CombinerEditResponse, error)

SendCombinerEditCmd sends a combiner edit management request to the combiner API.

func SendCommand

func SendCommand(cmd, zone string) (string, error)

func SendCommandNG

func SendCommandNG(api *tdns.ApiClient, data tdns.CommandPost) (tdns.CommandResponse, error)

func SendConfigCommand

func SendConfigCommand(api *tdns.ApiClient, data tdns.ConfigPost) (tdns.ConfigResponse, error)

func SendDebug

func SendDebug(api *tdns.ApiClient, data tdns.DebugPost) tdns.DebugResponse

func SendDelegationCmd

func SendDelegationCmd(api *tdns.ApiClient, data tdns.DelegationPost) (tdns.DelegationResponse, error)

func SendDsyncCommand

func SendDsyncCommand(api *tdns.ApiClient, data tdns.ZoneDsyncPost) (tdns.ZoneDsyncResponse, error)

func SendKeystoreCmd

func SendKeystoreCmd(api *tdns.ApiClient, data tdns.KeystorePost) (tdns.KeystoreResponse, error)

func SendNotify

func SendNotify(zonename string, ntype string)

func SendTruststore

func SendTruststore(api *tdns.ApiClient, data tdns.TruststorePost) (tdns.TruststoreResponse, error)

func SendZoneCommand

func SendZoneCommand(api *tdns.ApiClient, data tdns.ZonePost) (tdns.ZoneResponse, error)

func SetRootCommand

func SetRootCommand(cmd *cobra.Command)

SetRootCommand allows the root package to provide the root command reference

func Sig0KeyMgmt

func Sig0KeyMgmt(cmd string) error

func Sig0TrustMgmt

func Sig0TrustMgmt(subcommand string) error

func StartImrForCli

func StartImrForCli(rootHints string) (context.Context, context.CancelFunc, *tdns.Imr, error)

StartImrForCli initializes and starts the internal IMR for CLI commands. It sets up the minimal config, starts RecursorEngine, and waits for initialization. Returns the context, cancel function, and the Imr instance, or an error if initialization fails.

func StartInteractiveMode

func StartInteractiveMode()

func Terminate

func Terminate()

func ValidateBySection

func ValidateBySection(config *Config, configsections map[string]interface{}, cfgfile string) error

func ValidateConfig

func ValidateConfig(v *viper.Viper, cfgfile string) error

func ValidateZoneConfig

func ValidateZoneConfig(v *viper.Viper, cfgfile string) error

func VerboseListZone

func VerboseListZone(cr tdns.ZoneResponse)

func VerifyAndSendLocalDNSRecord

func VerifyAndSendLocalDNSRecord(zonename, dnsRecord, cmd string) error

Types

type CommandNode

type CommandNode struct {
	Name        string                  // Command name
	Command     *cobra.Command          // Reference to original Cobra command
	SubCommands map[string]*CommandNode // Child commands
	Parent      *CommandNode            // Parent command (nil for root)
	Args        []string                // Expected arguments from Use field
	Guide       string                  // Guide text for this command
}

CommandNode represents a node in our command tree

func BuildCommandTree

func BuildCommandTree(cmd *cobra.Command, parent *CommandNode) *CommandNode

BuildCommandTree creates a tree structure from Cobra commands

func (*CommandNode) DebugPrint

func (n *CommandNode) DebugPrint(indent string)

Debug function to print the tree structure

type Config

type Config struct {
	Verbose *bool    `validate:"required"`
	Zones   []string `validate:"required"`
}

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL