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 { clients, dests := orAny(v.MatchClients), orAny(v.MatchDestinations) if v.Limited { // No views:read: only the zones they can see. clients, dests = "(not shown)", "(not shown)" } fmt.Fprintf(w, "%d\t%d\t%s\t%s\t%s\t%d\t%s\n", i+1, v.ID, v.Name, clients, dests, v.Zones, orDefault(v.Color)) } }) }, } var clients, dests []string var recursiveOnly, comment, exposure string var adopt, cascade, yes, reverseSuggest 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 } if f.Changed("exposure") { body["exposure"] = exposure } if f.Changed("reverse-suggestions") { body["reverse_suggestions"] = reverseSuggest } 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") c.Flags().StringVar(&exposure, "exposure", "", "reachable from the internet: auto (worked out), public or private (docs/exposure.md)") c.Flags().BoolVar(&reverseSuggest, "reverse-suggestions", true, "suggest reverse zones for the view's addresses (--reverse-suggestions=false: don't; docs/reverse.md)") } 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. It replaces the option of that name. Statements told apart by the name after the keyword (tls NAME, key NAME, remote-servers NAME, http NAME, dnssec-policy NAME, server ADDRESS, ...) replace only the one with that name: setting tls cloudflare leaves tls quad9 alone. --add adds another instance of any other option that may occur several times. 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 } label := optionLabel(strings.ToLower(args[0]), value) for _, o := range opts { if label != "" && optionLabel(o.Name, o.Value) != label { continue // another tls (key, server, ...) statement } 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 [LABEL] | 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 || len(args) > 2 { return fmt.Errorf("give the option name (and the name after it, for tls NAME and the like) 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]) && (len(args) == 1 || strings.EqualFold(optionLabel(o.Name, o.Value), strings.Trim(args[1], `"`))) { matches = append(matches, o) } } switch len(matches) { case 0: return fmt.Errorf("option %s is not set here", strings.Join(args, " ")) case 1: default: var labels []string for _, o := range matches { if l := optionLabel(o.Name, o.Value); l != "" { labels = append(labels, l) } } if len(labels) == len(matches) { return fmt.Errorf("option %s is set %d times (%s); say which: jarl option unset %s %s", args[0], len(matches), strings.Join(labels, ", "), args[0], labels[0]) } 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") generated := &cobra.Command{ Use: "generated", Short: "What Jarl adds to named.conf by itself, why, and what controls it", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { c, err := newClient() if err != nil { return err } var gen []struct { Where string `json:"where"` Text string `json:"text"` Why string `json:"why"` Setting string `json:"setting"` } if err := c.Get(ctx(), "/options/generated", nil, &gen); err != nil { return err } return output(gen, func(w io.Writer) { for i, g := range gen { if i > 0 { fmt.Fprintln(w) } fmt.Fprintf(w, "# %s: %s\n# controlled by: %s\n%s\n", g.Where, g.Why, g.Setting, g.Text) } }) }, } cmd.AddCommand(list, set, unset, describe, generated) 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, srvExposure 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 } if f.Changed("exposure") { body["exposure"] = srvExposure } 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().StringVar(&srvExposure, "exposure", "", "without views: is the server reachable from the internet? auto, public or private (docs/exposure.md)") 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 } // labelled are the statements told apart by the name after the keyword: // tls quad9 { ... }, key xfer { ... }, server 192.0.2.1 { ... }. var labelled = map[string]bool{"tls": true, "http": true, "key": true, "remote-servers": true, "primaries": true, "masters": true, "parental-agents": true, "dnssec-policy": true, "key-store": true, "server": true, "view": true, "acl": true, "zone": true} // optionLabel returns the name a labelled statement's value starts with // ("quad9" for tls quad9 { ... }), lowercase; "" for other statements. func optionLabel(name, value string) string { if !labelled[name] { return "" } v := strings.TrimSpace(value) if v == "" || strings.HasPrefix(v, "{") { return "" } if strings.HasPrefix(v, `"`) { if end := strings.Index(v[1:], `"`); end >= 0 { return strings.ToLower(v[1 : end+1]) } } end := strings.IndexAny(v, " \t\n{;") if end < 0 { end = len(v) } return strings.ToLower(v[:end]) }