Files
jarl/cmd/jarl/config_cmds.go
T
adamandClaude Opus 5.5 384f6b3060 Give every view a TSIG key that selects it
Each view's match-clients starts with "key jarl-view-NAME" (after
"!key" for the views listed after it), so a query signed with a view's
key lands in that view from any address. The secrets are derived from a
random seed in Jarl's database. The delegation check signs its queries
to this server's own addresses with the key of the zone's view: before,
they got whichever view matched the server's address, usually internal.
jarl view key / GET /views/{id}/key show a key for dig -k (tsig:write,
audited). jarl-view- key names are reserved; the importer drops view
keys from a configuration Jarl wrote.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 18:19:10 +01:00

810 lines
23 KiB
Go

package main
import (
"fmt"
"io"
"net/url"
"slices"
"strings"
"github.com/spf13/cobra"
"jarl/internal/api"
"jarl/internal/model"
)
func viewCmd() *cobra.Command {
cmd := &cobra.Command{Use: "view", Aliases: []string{"views"}, Short: "Manage views"}
list := &cobra.Command{
Use: "list",
Short: "List views in match order",
RunE: func(cmd *cobra.Command, args []string) error {
c, err := newClient()
if err != nil {
return err
}
var views []api.View
if err := c.Get(ctx(), "/views", nil, &views); err != nil {
return err
}
return output(views, func(w io.Writer) {
fmt.Fprintln(w, "#\tID\tVIEW\tMATCH-CLIENTS\tMATCH-DESTINATIONS\tZONES\tCOLOR")
for i, v := range views {
fmt.Fprintf(w, "%d\t%d\t%s\t%s\t%s\t%d\t%s\n", i+1, v.ID, v.Name, orAny(v.MatchClients), orAny(v.MatchDestinations), v.Zones, orDefault(v.Color))
}
})
},
}
var clients, dests []string
var recursiveOnly, comment string
var adopt, cascade, yes bool
viewBody := func(cmd *cobra.Command) map[string]any {
body := map[string]any{}
f := cmd.Flags()
if f.Changed("match-clients") {
body["match_clients"] = clients
}
if f.Changed("match-destinations") {
body["match_destinations"] = dests
}
if f.Changed("recursive-only") {
switch recursiveOnly {
case "yes", "true":
body["match_recursive_only"] = true
case "no", "false":
body["match_recursive_only"] = false
default:
body["match_recursive_only"] = nil
}
}
if f.Changed("comment") {
body["comment"] = comment
}
return body
}
addViewFlags := func(c *cobra.Command) {
c.Flags().StringSliceVar(&clients, "match-clients", nil, "match-clients elements (comma separated or repeated)")
c.Flags().StringSliceVar(&dests, "match-destinations", nil, "match-destinations elements")
c.Flags().StringVar(&recursiveOnly, "recursive-only", "", "match-recursive-only: yes, no or default")
c.Flags().StringVar(&comment, "comment", "", "comment")
}
create := &cobra.Command{
Use: "create NAME",
Short: "Create a view (added after the existing views)",
Example: ` jarl view create internal --match-clients 192.168.0.0/16,localhost --adopt-zones
jarl view create external --match-clients any`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
c, err := newClient()
if err != nil {
return err
}
body := viewBody(cmd)
body["name"] = args[0]
if adopt {
body["adopt_zones"] = true
}
resp, err := c.Mutate(ctx(), "POST", "/views", nil, body)
if err != nil {
return err
}
return report(resp)
},
}
addViewFlags(create)
create.Flags().BoolVar(&adopt, "adopt-zones", false, "move zones that are not in a view into this one (required for the first view if zones exist)")
var rename string
update := &cobra.Command{
Use: "update VIEW",
Short: "Change a view",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
c, err := newClient()
if err != nil {
return err
}
body := viewBody(cmd)
if rename != "" {
body["name"] = rename
}
resp, err := c.Mutate(ctx(), "PATCH", "/views/"+url.PathEscape(args[0]), nil, body)
if err != nil {
return err
}
return report(resp)
},
}
addViewFlags(update)
update.Flags().StringVar(&rename, "rename", "", "new name")
del := &cobra.Command{
Use: "delete VIEW",
Short: "Delete a view",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
if cascade && !confirm("Delete view "+args[0]+" with all its zones?", yes) {
return fmt.Errorf("canceled")
}
c, err := newClient()
if err != nil {
return err
}
qv := q()
if cascade {
qv.Set("cascade", "1")
}
resp, err := c.Mutate(ctx(), "DELETE", "/views/"+url.PathEscape(args[0]), qv, nil)
if err != nil {
return err
}
return report(resp)
},
}
del.Flags().BoolVar(&cascade, "cascade", false, "also delete the view's zones")
del.Flags().BoolVarP(&yes, "yes", "y", false, "don't ask for confirmation")
order := &cobra.Command{
Use: "order VIEW...",
Short: "Set the order in which views are matched (list every view)",
Args: cobra.MinimumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
c, err := newClient()
if err != nil {
return err
}
var views []api.View
if err := c.Get(ctx(), "/views", nil, &views); err != nil {
return err
}
byName := map[string]int64{}
for _, v := range views {
byName[strings.ToLower(v.Name)] = v.ID
}
var ids []int64
for _, a := range args {
id, ok := byName[strings.ToLower(a)]
if !ok {
return fmt.Errorf("no view %q", a)
}
ids = append(ids, id)
}
resp, err := c.Mutate(ctx(), "POST", "/views/order", nil, map[string]any{"ids": ids})
if err != nil {
return err
}
return report(resp)
},
}
color := &cobra.Command{
Use: "color VIEW COLOR",
Short: "Set the colour the web UI shows a view in",
Long: "Set the colour the web UI shows a view in, so zones of the same view are easy to spot.\n" +
"Colours: default (the link colour), " + strings.Join(model.ViewColors, ", ") + ".\n" +
"This only changes the web UI: it applies at once, with no changeset or deploy.",
Example: ` jarl view color internal green
jarl view color internal default`,
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
c, err := newClient()
if err != nil {
return err
}
col := strings.ToLower(args[1])
if col == "default" {
col = ""
} else if !slices.Contains(model.ViewColors, col) {
return fmt.Errorf("unknown colour %q: use default, %s", args[1], strings.Join(model.ViewColors, ", "))
}
var v api.View
if err := c.Do(ctx(), "PUT", "/views/"+url.PathEscape(args[0])+"/color", nil, api.ViewColor{Color: col}, &v); err != nil {
return err
}
fmt.Printf("View %s: colour %s\n", v.Name, orDefault(v.Color))
return nil
},
}
var keyNamed bool
key := &cobra.Command{
Use: "key VIEW",
Short: "Show the TSIG key that selects a view, to query it from anywhere",
Long: `Every view has a TSIG key (jarl-view-NAME) at the front of its
match-clients: a query signed with it lands in that view, whatever address
it comes from. Jarl signs its own checks of this server with it; use it to
see what a view answers:
jarl view key external # shows a ready dig -y command
jarl view key external --named-conf > external.key
dig -k external.key @SERVER adbraun.lol SOA
Showing it needs tsig:write and is recorded in the audit log.`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
c, err := newClient()
if err != nil {
return err
}
var k struct {
Name string `json:"name"`
Algorithm string `json:"algorithm"`
Secret string `json:"secret"`
}
if err := c.Get(ctx(), "/views/"+url.PathEscape(args[0])+"/key", nil, &k); err != nil {
return err
}
if keyNamed {
fmt.Printf("key %q {\n\talgorithm %s;\n\tsecret %q;\n};\n", k.Name, k.Algorithm, k.Secret)
return nil
}
return output(k, func(w io.Writer) {
fmt.Fprintf(w, "Key:\t%s\nAlgorithm:\t%s\nSecret:\t%s\ndig:\tdig -y %s:%s:%s @SERVER NAME\n", k.Name, k.Algorithm, k.Secret, k.Algorithm, k.Name, k.Secret)
})
},
}
key.Flags().BoolVar(&keyNamed, "named-conf", false, "print a key statement (for dig -k FILE)")
cmd.AddCommand(list, create, update, del, order, color, key)
return cmd
}
func orDefault(s string) string {
if s == "" {
return "default"
}
return s
}
func orAny(list []string) string {
if len(list) == 0 {
return "any"
}
return strings.Join(list, "; ")
}
func aclCmd() *cobra.Command {
cmd := &cobra.Command{Use: "acl", Aliases: []string{"acls"}, Short: "Manage ACLs (named address match lists)"}
list := &cobra.Command{
Use: "list",
Short: "List ACLs",
RunE: func(cmd *cobra.Command, args []string) error {
c, err := newClient()
if err != nil {
return err
}
var acls []api.ACL
if err := c.Get(ctx(), "/acls", nil, &acls); err != nil {
return err
}
return output(acls, func(w io.Writer) {
fmt.Fprintln(w, "ID\tACL\tELEMENTS\tUSED BY")
for _, a := range acls {
fmt.Fprintf(w, "%d\t%s\t%s\t%s\n", a.ID, a.Name, strings.Join(a.Elements, "; "), strings.Join(a.UsedBy, ", "))
}
})
},
}
var comment, rename string
var add, remove []string
create := &cobra.Command{
Use: "create NAME ELEMENT...",
Short: "Create an ACL",
Example: ` jarl acl create homelab 192.168.1.0/24 10.10.0.0/16 localhost`,
Args: cobra.MinimumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
c, err := newClient()
if err != nil {
return err
}
body := map[string]any{"name": args[0], "elements": args[1:], "comment": comment}
resp, err := c.Mutate(ctx(), "POST", "/acls", nil, body)
if err != nil {
return err
}
return report(resp)
},
}
create.Flags().StringVar(&comment, "comment", "", "comment")
var set []string
update := &cobra.Command{
Use: "update ACL",
Short: "Change an ACL (--add/--remove elements, --set all elements, --rename)",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
c, err := newClient()
if err != nil {
return err
}
var cur api.ACL
if err := c.Get(ctx(), "/acls/"+url.PathEscape(args[0]), nil, &cur); err != nil {
return err
}
body := map[string]any{}
elems := cur.Elements
if cmd.Flags().Changed("set") {
elems = set
}
for _, a := range add {
elems = append(elems, a)
}
if len(remove) > 0 {
drop := map[string]bool{}
for _, r := range remove {
drop[r] = true
}
var kept []string
for _, e := range elems {
if !drop[e] {
kept = append(kept, e)
}
}
elems = kept
}
if elems == nil {
elems = []string{}
}
if cmd.Flags().Changed("set") || len(add) > 0 || len(remove) > 0 {
body["elements"] = elems
}
if rename != "" {
body["name"] = rename
}
if cmd.Flags().Changed("comment") {
body["comment"] = comment
}
if len(body) == 0 {
return fmt.Errorf("nothing to change (see --help)")
}
resp, err := c.Mutate(ctx(), "PATCH", "/acls/"+url.PathEscape(args[0]), nil, body)
if err != nil {
return err
}
return report(resp)
},
}
update.Flags().StringSliceVar(&add, "add", nil, "add elements")
update.Flags().StringSliceVar(&remove, "remove", nil, "remove elements (exact text)")
update.Flags().StringSliceVar(&set, "set", nil, "replace all elements")
update.Flags().StringVar(&rename, "rename", "", "new name (references are updated)")
update.Flags().StringVar(&comment, "comment", "", "comment")
del := &cobra.Command{
Use: "delete ACL",
Short: "Delete an ACL (refused while it is in use)",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
c, err := newClient()
if err != nil {
return err
}
resp, err := c.Mutate(ctx(), "DELETE", "/acls/"+url.PathEscape(args[0]), nil, nil)
if err != nil {
return err
}
return report(resp)
},
}
cmd.AddCommand(list, create, update, del)
return cmd
}
func tsigCmd() *cobra.Command {
cmd := &cobra.Command{Use: "tsig", Aliases: []string{"key", "keys"}, Short: "Manage TSIG keys"}
list := &cobra.Command{
Use: "list",
Short: "List TSIG keys",
RunE: func(cmd *cobra.Command, args []string) error {
c, err := newClient()
if err != nil {
return err
}
var keys []api.TSIGKey
if err := c.Get(ctx(), "/tsig-keys", nil, &keys); err != nil {
return err
}
return output(keys, func(w io.Writer) {
fmt.Fprintln(w, "ID\tKEY\tALGORITHM\tUSED BY")
for _, k := range keys {
fmt.Fprintf(w, "%d\t%s\t%s\t%s\n", k.ID, k.Name, k.Algorithm, strings.Join(k.UsedBy, ", "))
}
})
},
}
var secret, algorithm, comment string
var named bool
show := &cobra.Command{
Use: "show KEY",
Short: "Show a key and its secret",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
c, err := newClient()
if err != nil {
return err
}
var k api.TSIGKey
if err := c.Get(ctx(), "/tsig-keys/"+url.PathEscape(args[0]), q("secret", "1"), &k); err != nil {
return err
}
if named {
fmt.Printf("key %q {\n\talgorithm %s;\n\tsecret %q;\n};\n", k.Name, k.Algorithm, k.Secret)
return nil
}
return output(k, func(w io.Writer) {
fmt.Fprintf(w, "Key:\t%s\nAlgorithm:\t%s\nSecret:\t%s\nUsed by:\t%s\n", k.Name, k.Algorithm, k.Secret, strings.Join(k.UsedBy, ", "))
})
},
}
show.Flags().BoolVar(&named, "named-conf", false, "print a key statement for another server's named.conf")
create := &cobra.Command{
Use: "create NAME",
Short: "Create a TSIG key (the secret is generated unless given)",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
c, err := newClient()
if err != nil {
return err
}
body := map[string]any{"name": args[0], "algorithm": algorithm, "comment": comment}
if secret != "" {
body["secret"] = secret
}
resp, err := c.Mutate(ctx(), "POST", "/tsig-keys", nil, body)
if err != nil {
return err
}
if err := report(resp); err != nil {
return err
}
if resp.Applied && flagOutput == "table" {
var k api.TSIGKey
if jsonUnmarshal(resp.Object, &k) == nil && k.Secret != "" {
fmt.Printf("secret: %s\n", k.Secret)
}
}
return nil
},
}
create.Flags().StringVar(&algorithm, "algorithm", "hmac-sha256", "algorithm")
create.Flags().StringVar(&secret, "secret", "", "base64 secret (default: generated)")
create.Flags().StringVar(&comment, "comment", "", "comment")
rotate := &cobra.Command{
Use: "rotate KEY",
Short: "Give a key a new random secret (or --secret)",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
c, err := newClient()
if err != nil {
return err
}
if secret == "" {
var k api.TSIGKey
if err := c.Get(ctx(), "/tsig-keys/"+url.PathEscape(args[0]), nil, &k); err != nil {
return err
}
secret = randomSecret(k.Algorithm)
}
resp, err := c.Mutate(ctx(), "PATCH", "/tsig-keys/"+url.PathEscape(args[0]), nil, map[string]any{"secret": secret})
if err != nil {
return err
}
if err := report(resp); err != nil {
return err
}
fmt.Printf("new secret: %s\n", secret)
return nil
},
}
rotate.Flags().StringVar(&secret, "secret", "", "the new secret (default: generated)")
del := &cobra.Command{
Use: "delete KEY",
Short: "Delete a key (refused while it is in use)",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
c, err := newClient()
if err != nil {
return err
}
resp, err := c.Mutate(ctx(), "DELETE", "/tsig-keys/"+url.PathEscape(args[0]), nil, nil)
if err != nil {
return err
}
return report(resp)
},
}
cmd.AddCommand(list, show, create, rotate, del)
return cmd
}
func optionCmd() *cobra.Command {
cmd := &cobra.Command{Use: "option", Aliases: []string{"options", "opt"}, Short: "Manage named options (server, view, zone, top-level)"}
var view, zone, zoneView string
var toplevel bool
scopeArgs := func(c *cobra.Command) {
c.Flags().StringVar(&view, "view", "", "options of this view")
c.Flags().StringVar(&zone, "zone", "", "options of this zone")
c.Flags().StringVar(&zoneView, "zone-view", "", "view of --zone")
c.Flags().BoolVar(&toplevel, "toplevel", false, "top-level statements (logging, controls, ...)")
}
scope := func() (string, int64, error) {
c, err := newClient()
if err != nil {
return "", 0, err
}
switch {
case zone != "":
id, err := resolveZoneID(c, zone, zoneView)
return model.ScopeZone, id, err
case view != "":
var v api.View
if err := c.Get(ctx(), "/views/"+url.PathEscape(view), nil, &v); err != nil {
return "", 0, err
}
return model.ScopeView, v.ID, nil
case toplevel:
return model.ScopeTopLevel, 0, nil
}
return model.ScopeOptions, 0, nil
}
list := &cobra.Command{
Use: "list",
Short: "List options (server options by default)",
RunE: func(cmd *cobra.Command, args []string) error {
c, err := newClient()
if err != nil {
return err
}
qv := q()
if view != "" || zone != "" || toplevel || !cmd.Flags().Changed("all") {
sc, id, err := scope()
if err != nil {
return err
}
qv = q("scope", sc, "scope_id", fmt.Sprint(id))
}
if all {
qv = q()
}
var opts []api.Option
if err := c.Get(ctx(), "/options", qv, &opts); err != nil {
return err
}
return output(opts, func(w io.Writer) {
fmt.Fprintln(w, "ID\tWHERE\tOPTION\tVALUE")
for _, o := range opts {
fmt.Fprintf(w, "%d\t%s\t%s\t%s\n", o.ID, o.Where, o.Name, o.Value)
}
})
},
}
scopeArgs(list)
list.Flags().BoolVar(&all, "all", false, "options of every scope")
var comment string
set := &cobra.Command{
Use: "set NAME [VALUE...]",
Short: "Set an option (replaces an existing option of the same name)",
Long: `Set an option; VALUE is named.conf syntax without the trailing semicolon.
jarl option set recursion no
jarl option set allow-recursion '{ homelab; localhost; }'
jarl option set forwarders '{ 1.1.1.1; 9.9.9.9; }'
jarl option set allow-transfer '{ key xfer; }' --zone example.com
jarl option set logging '{ channel q { file "/var/log/named/q.log"; }; category queries { q; }; }' --toplevel`,
Args: cobra.MinimumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
c, err := newClient()
if err != nil {
return err
}
sc, id, err := scope()
if err != nil {
return err
}
value := strings.Join(args[1:], " ")
var opts []api.Option
if err := c.Get(ctx(), "/options", q("scope", sc, "scope_id", fmt.Sprint(id)), &opts); err != nil {
return err
}
body := map[string]any{"value": value}
if cmd.Flags().Changed("comment") {
body["comment"] = comment
}
for _, o := range opts {
if o.Name == strings.ToLower(args[0]) && !multiple {
resp, err := c.Mutate(ctx(), "PATCH", fmt.Sprintf("/options/%d", o.ID), nil, body)
if err != nil {
return err
}
return report(resp)
}
}
body["scope"], body["scope_id"], body["name"] = sc, id, args[0]
resp, err := c.Mutate(ctx(), "POST", "/options", nil, body)
if err != nil {
return err
}
return report(resp)
},
}
scopeArgs(set)
set.Flags().StringVar(&comment, "comment", "", "comment")
set.Flags().BoolVar(&multiple, "add", false, "add another instance instead of replacing (for options that may occur several times)")
unset := &cobra.Command{
Use: "unset NAME | unset --id ID",
Short: "Remove an option (BIND's default applies again)",
RunE: func(cmd *cobra.Command, args []string) error {
c, err := newClient()
if err != nil {
return err
}
if optID > 0 {
resp, err := c.Mutate(ctx(), "DELETE", fmt.Sprintf("/options/%d", optID), nil, nil)
if err != nil {
return err
}
return report(resp)
}
if len(args) != 1 {
return fmt.Errorf("give the option name or --id")
}
sc, id, err := scope()
if err != nil {
return err
}
var opts []api.Option
if err := c.Get(ctx(), "/options", q("scope", sc, "scope_id", fmt.Sprint(id)), &opts); err != nil {
return err
}
var matches []api.Option
for _, o := range opts {
if o.Name == strings.ToLower(args[0]) {
matches = append(matches, o)
}
}
switch len(matches) {
case 0:
return fmt.Errorf("option %s is not set here", args[0])
case 1:
default:
return fmt.Errorf("option %s is set %d times; remove one with --id", args[0], len(matches))
}
resp, err := c.Mutate(ctx(), "DELETE", fmt.Sprintf("/options/%d", matches[0].ID), nil, nil)
if err != nil {
return err
}
return report(resp)
},
}
scopeArgs(unset)
unset.Flags().Int64Var(&optID, "id", 0, "remove the option with this ID")
describe := &cobra.Command{
Use: "describe [NAME]",
Short: "Describe options from the BIND 9.20 catalog",
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
c, err := newClient()
if err != nil {
return err
}
cs := "options"
switch {
case toplevel:
cs = "toplevel"
case view != "":
cs = "view"
case zone != "":
cs = "zone:" + zoneType
}
var stmts []struct {
Name string `json:"name"`
Grammar string `json:"grammar"`
Kind string `json:"kind"`
Status string `json:"status"`
Description string `json:"description"`
}
if err := c.Get(ctx(), "/catalog/options", q("scope", cs), &stmts); err != nil {
return err
}
for _, s := range stmts {
if len(args) == 1 && s.Name != args[0] {
continue
}
if len(args) == 0 {
fmt.Printf("%-32s %s\n", s.Name, s.Description)
continue
}
fmt.Printf("%s (%s)%s\n %s\n\n%s\n", s.Name, s.Kind, statusText(s.Status), s.Description, s.Grammar)
return nil
}
if len(args) == 1 {
return fmt.Errorf("%s is not an option in scope %s", args[0], cs)
}
return nil
},
}
scopeArgs(describe)
describe.Flags().StringVar(&zoneType, "zone-type", "primary", "with --zone: the zone type")
cmd.AddCommand(list, set, unset, describe)
return cmd
}
var (
all bool
multiple bool
optID int64
zoneType string
)
func statusText(s string) string {
if s == "" {
return ""
}
return " [" + s + "]"
}
func settingsCmd() *cobra.Command {
cmd := &cobra.Command{Use: "settings", Short: "Show or change Jarl's defaults for new zones"}
show := &cobra.Command{
Use: "show",
Short: "Show settings",
RunE: func(cmd *cobra.Command, args []string) error {
c, err := newClient()
if err != nil {
return err
}
var s model.Settings
if err := c.Get(ctx(), "/settings", nil, &s); err != nil {
return err
}
return output(s, func(w io.Writer) {
fmt.Fprintf(w, "Name servers:\t%s\nContact:\t%s\nDefault TTL:\t%d\nRefresh/retry/expire:\t%d/%d/%d\nNegative TTL:\t%d\nSerial policy:\t%s\n",
strings.Join(s.Nameservers, ", "), s.RName, s.DefaultTTL, s.Refresh, s.Retry, s.Expire, s.NegativeTTL, s.SerialPolicy)
})
},
}
var ns []string
var contact, policy string
var ttl, refresh, retry, expire, neg uint32
set := &cobra.Command{
Use: "set",
Short: "Change settings",
RunE: func(cmd *cobra.Command, args []string) error {
c, err := newClient()
if err != nil {
return err
}
body := map[string]any{}
f := cmd.Flags()
if f.Changed("ns") {
body["nameservers"] = ns
}
if f.Changed("contact") {
body["rname"] = contact
}
if f.Changed("serial-policy") {
body["serial_policy"] = policy
}
for name, v := range map[string]uint32{"ttl": ttl, "refresh": refresh, "retry": retry, "expire": expire, "negative-ttl": neg} {
if f.Changed(name) {
key := map[string]string{"ttl": "default_ttl", "negative-ttl": "negative_ttl"}[name]
if key == "" {
key = name
}
body[key] = v
}
}
if len(body) == 0 {
return fmt.Errorf("nothing to change (see --help)")
}
resp, err := c.Mutate(ctx(), "PATCH", "/settings", nil, body)
if err != nil {
return err
}
return report(resp)
},
}
set.Flags().StringSliceVar(&ns, "ns", nil, "default name servers")
set.Flags().StringVar(&contact, "contact", "", "default SOA contact")
set.Flags().StringVar(&policy, "serial-policy", "", "date, increment or unixtime")
set.Flags().Uint32Var(&ttl, "ttl", 0, "default TTL")
set.Flags().Uint32Var(&refresh, "refresh", 0, "SOA refresh")
set.Flags().Uint32Var(&retry, "retry", 0, "SOA retry")
set.Flags().Uint32Var(&expire, "expire", 0, "SOA expire")
set.Flags().Uint32Var(&neg, "negative-ttl", 0, "SOA minimum")
cmd.AddCommand(show, set)
return cmd
}