Script-friendly CLI output, and keep record list rows on one line
-o id and -o field=NAME,... print just the IDs or chosen fields; -q on show and list prints the IDs. jarl record list shortens data too long for the terminal (--wide shows it in full). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
113a6913a2
commit
f716fb8dd1
7 files changed
+224
-11
No files matched your search
@@ -29,6 +29,12 @@ All notable changes to Jarl are documented here. The format follows
|
|||||||
the command too, and turning signing off gives the matching `jarl record
|
the command too, and turning signing off gives the matching `jarl record
|
||||||
delete`.
|
delete`.
|
||||||
|
|
||||||
|
- CLI output for scripts: `-o id` prints just the IDs of what a command
|
||||||
|
shows or lists (`jarl zone show adbraun.lol -o id`), `-o
|
||||||
|
field=view,soa.serial` just those fields, tab separated. `-q` on `show`
|
||||||
|
and `list` prints the IDs too. With a change (`jarl record add ... -o
|
||||||
|
id`) they apply to the changed object.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- The web UI and API can listen on ports below 1024, such as 443:
|
- The web UI and API can listen on ports below 1024, such as 443:
|
||||||
@@ -40,6 +46,13 @@ All notable changes to Jarl are documented here. The format follows
|
|||||||
are separated by a faint line instead, and a full line marks where the
|
are separated by a faint line instead, and a full line marks where the
|
||||||
next name starts.
|
next name starts.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- `jarl record list`: one long value (a DKIM key) padded every row to its
|
||||||
|
length, so on a terminal each row wrapped onto a second, blank-looking
|
||||||
|
line. Long data is now shortened to fit the terminal; `--wide` shows it
|
||||||
|
in full, as does output to a pipe or file.
|
||||||
|
|
||||||
## [0.2.0] - 2026-10-09
|
## [0.2.0] - 2026-10-09
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ const completeTimeout = 3 * time.Second
|
|||||||
// setupCompletion adds value completion to the command tree.
|
// setupCompletion adds value completion to the command tree.
|
||||||
func setupCompletion(root *cobra.Command) {
|
func setupCompletion(root *cobra.Command) {
|
||||||
root.RegisterFlagCompletionFunc("server", completeServers(true))
|
root.RegisterFlagCompletionFunc("server", completeServers(true))
|
||||||
root.RegisterFlagCompletionFunc("output", cobra.FixedCompletions([]string{"table", "json", "yaml"}, cobra.ShellCompDirectiveNoFileComp))
|
root.RegisterFlagCompletionFunc("output", cobra.FixedCompletions([]string{"table", "json", "yaml", "id", "field="}, cobra.ShellCompDirectiveNoFileComp|cobra.ShellCompDirectiveNoSpace))
|
||||||
var walk func(c *cobra.Command)
|
var walk func(c *cobra.Command)
|
||||||
walk = func(c *cobra.Command) {
|
walk = func(c *cobra.Command) {
|
||||||
for _, name := range []string{"view", "user", "zone"} {
|
for _, name := range []string{"view", "user", "zone"} {
|
||||||
|
|||||||
+94
-4
@@ -3,6 +3,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
@@ -11,6 +12,7 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -237,10 +239,10 @@ Exit codes: 0 ok, 1 error, 3 not authenticated or not allowed, 4 not found,
|
|||||||
pf.Int64VarP(&flagChangeset, "changeset", "C", 0, "stage changes into this changeset instead of applying them (env JARL_CHANGESET)")
|
pf.Int64VarP(&flagChangeset, "changeset", "C", 0, "stage changes into this changeset instead of applying them (env JARL_CHANGESET)")
|
||||||
pf.BoolVarP(&flagDryRun, "dry-run", "n", false, "validate and show the changes without applying them")
|
pf.BoolVarP(&flagDryRun, "dry-run", "n", false, "validate and show the changes without applying them")
|
||||||
pf.BoolVar(&flagForce, "force", false, "overwrite BIND files that were changed outside Jarl")
|
pf.BoolVar(&flagForce, "force", false, "overwrite BIND files that were changed outside Jarl")
|
||||||
pf.StringVarP(&flagOutput, "output", "o", "table", "output format: table, json or yaml")
|
pf.StringVarP(&flagOutput, "output", "o", "table", "output format: table, json, yaml, id (just the IDs) or field=NAME[,NAME...] (just those fields, e.g. field=view or field=soa.serial)")
|
||||||
pf.BoolVar(&flagInsecure, "insecure", false, "don't verify the server's TLS certificate")
|
pf.BoolVar(&flagInsecure, "insecure", false, "don't verify the server's TLS certificate")
|
||||||
pf.StringVar(&flagCACert, "ca-cert", "", "PEM file with a CA to trust for the server's certificate")
|
pf.StringVar(&flagCACert, "ca-cert", "", "PEM file with a CA to trust for the server's certificate")
|
||||||
pf.BoolVarP(&flagQuiet, "quiet", "q", false, "only print errors (and IDs where useful)")
|
pf.BoolVarP(&flagQuiet, "quiet", "q", false, "only print errors and IDs (with show and list: just the IDs, like -o id)")
|
||||||
root.AddGroup(&cobra.Group{ID: "dns", Title: "DNS commands:"}, &cobra.Group{ID: "changes", Title: "Changes:"}, &cobra.Group{ID: "admin", Title: "Administration:"})
|
root.AddGroup(&cobra.Group{ID: "dns", Title: "DNS commands:"}, &cobra.Group{ID: "changes", Title: "Changes:"}, &cobra.Group{ID: "admin", Title: "Administration:"})
|
||||||
add := func(group string, cmds ...*cobra.Command) {
|
add := func(group string, cmds ...*cobra.Command) {
|
||||||
for _, c := range cmds {
|
for _, c := range cmds {
|
||||||
@@ -262,7 +264,15 @@ func ctx() context.Context { return context.Background() }
|
|||||||
// ---- output
|
// ---- output
|
||||||
|
|
||||||
func output(v any, table func(w io.Writer)) error {
|
func output(v any, table func(w io.Writer)) error {
|
||||||
switch flagOutput {
|
format := flagOutput
|
||||||
|
quietIDs := flagQuiet && (format == "table" || format == "")
|
||||||
|
if quietIDs {
|
||||||
|
format = "id"
|
||||||
|
}
|
||||||
|
if fieldFormat(format) {
|
||||||
|
return outputFields(v, format, quietIDs)
|
||||||
|
}
|
||||||
|
switch format {
|
||||||
case "json":
|
case "json":
|
||||||
enc := json.NewEncoder(os.Stdout)
|
enc := json.NewEncoder(os.Stdout)
|
||||||
enc.SetIndent("", " ")
|
enc.SetIndent("", " ")
|
||||||
@@ -286,7 +296,79 @@ func output(v any, table func(w io.Writer)) error {
|
|||||||
table(tw)
|
table(tw)
|
||||||
return tw.Flush()
|
return tw.Flush()
|
||||||
}
|
}
|
||||||
return fmt.Errorf("unknown output format %q (use table, json or yaml)", flagOutput)
|
return fmt.Errorf("unknown output format %q (use table, json, yaml, id or field=NAME)", flagOutput)
|
||||||
|
}
|
||||||
|
|
||||||
|
// outputFields prints fields of each object (or of the one object), one
|
||||||
|
// line per object with the fields separated by tabs, for scripts:
|
||||||
|
// "id" prints the IDs, "field=view,soa.serial" those fields. Names are the
|
||||||
|
// JSON field names (see -o json); a dot reaches into nested objects. A
|
||||||
|
// field an object doesn't have prints as empty (the API leaves out empty
|
||||||
|
// fields).
|
||||||
|
func outputFields(v any, format string, quiet bool) error {
|
||||||
|
fields := []string{"id"}
|
||||||
|
if f, ok := strings.CutPrefix(format, "field="); ok {
|
||||||
|
fields = strings.Split(f, ",")
|
||||||
|
}
|
||||||
|
b, err := json.Marshal(v)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
dec := json.NewDecoder(bytes.NewReader(b))
|
||||||
|
dec.UseNumber()
|
||||||
|
var x any
|
||||||
|
if err := dec.Decode(&x); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
items, ok := x.([]any)
|
||||||
|
if !ok {
|
||||||
|
items = []any{x}
|
||||||
|
}
|
||||||
|
if quiet && !slices.ContainsFunc(items, func(it any) bool { _, ok := lookupField(it, "id"); return ok }) {
|
||||||
|
return nil // -q on something without IDs: only errors
|
||||||
|
}
|
||||||
|
var out strings.Builder
|
||||||
|
for _, it := range items {
|
||||||
|
var vals []string
|
||||||
|
for _, f := range fields {
|
||||||
|
val, _ := lookupField(it, strings.TrimSpace(f))
|
||||||
|
vals = append(vals, fieldText(val))
|
||||||
|
}
|
||||||
|
out.WriteString(strings.Join(vals, "\t") + "\n")
|
||||||
|
}
|
||||||
|
_, err = io.WriteString(os.Stdout, out.String())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// fieldFormat reports whether an -o format prints fields (id, field=...).
|
||||||
|
func fieldFormat(format string) bool {
|
||||||
|
return format == "id" || strings.HasPrefix(format, "field=")
|
||||||
|
}
|
||||||
|
|
||||||
|
func lookupField(v any, path string) (any, bool) {
|
||||||
|
for _, part := range strings.Split(path, ".") {
|
||||||
|
m, ok := v.(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
if v, ok = m[part]; !ok {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return v, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func fieldText(v any) string {
|
||||||
|
switch x := v.(type) {
|
||||||
|
case nil:
|
||||||
|
return ""
|
||||||
|
case string:
|
||||||
|
return x
|
||||||
|
case json.Number, bool:
|
||||||
|
return fmt.Sprint(x)
|
||||||
|
}
|
||||||
|
b, _ := json.Marshal(v)
|
||||||
|
return string(b)
|
||||||
}
|
}
|
||||||
|
|
||||||
func dot(name string) string {
|
func dot(name string) string {
|
||||||
@@ -391,6 +473,14 @@ func report(resp *api.MutationResponse) error {
|
|||||||
if flagOutput == "json" || flagOutput == "yaml" {
|
if flagOutput == "json" || flagOutput == "yaml" {
|
||||||
return output(resp, nil)
|
return output(resp, nil)
|
||||||
}
|
}
|
||||||
|
if fieldFormat(flagOutput) {
|
||||||
|
// The changed object, if the response has it ("jarl record add
|
||||||
|
// ... -o id" prints the new record's ID).
|
||||||
|
if len(resp.Object) > 0 {
|
||||||
|
return outputFields(resp.Object, flagOutput, false)
|
||||||
|
}
|
||||||
|
return outputFields(resp, flagOutput, false)
|
||||||
|
}
|
||||||
w := os.Stdout
|
w := os.Stdout
|
||||||
r := client.ParseResult(resp.Result)
|
r := client.ParseResult(resp.Result)
|
||||||
switch {
|
switch {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -190,3 +191,44 @@ func TestListQueries(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestOutputFields(t *testing.T) {
|
||||||
|
capture := func(f func() error) string {
|
||||||
|
t.Helper()
|
||||||
|
r, w, _ := os.Pipe()
|
||||||
|
old := os.Stdout
|
||||||
|
os.Stdout = w
|
||||||
|
err := f()
|
||||||
|
os.Stdout = old
|
||||||
|
w.Close()
|
||||||
|
b, _ := io.ReadAll(r)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
|
zones := []map[string]any{
|
||||||
|
{"id": 12, "name": "adbraun.lol.", "view": "external", "soa": map[string]any{"serial": 2026100901}},
|
||||||
|
{"id": 3, "name": "lab.adbraun.lol.", "soa": map[string]any{"serial": 7}},
|
||||||
|
}
|
||||||
|
defer func(o string, q bool) { flagOutput, flagQuiet = o, q }(flagOutput, flagQuiet)
|
||||||
|
for _, tc := range []struct {
|
||||||
|
format string
|
||||||
|
quiet bool
|
||||||
|
v any
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{"id", false, zones, "12\n3\n"},
|
||||||
|
{"id", false, zones[0], "12\n"},
|
||||||
|
{"field=name,view,soa.serial", false, zones, "adbraun.lol.\texternal\t2026100901\nlab.adbraun.lol.\t\t7\n"},
|
||||||
|
{"field=soa", false, zones[1], "{\"serial\":7}\n"},
|
||||||
|
{"table", true, zones, "12\n3\n"},
|
||||||
|
{"table", true, map[string]any{"status": "ok"}, ""}, // -q without IDs: nothing
|
||||||
|
} {
|
||||||
|
flagOutput, flagQuiet = tc.format, tc.quiet
|
||||||
|
got := capture(func() error { return output(tc.v, func(io.Writer) { t.Error("table printed") }) })
|
||||||
|
if got != tc.want {
|
||||||
|
t.Errorf("-o %s (quiet %v) = %q, want %q", tc.format, tc.quiet, got, tc.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+48
-3
@@ -3,12 +3,14 @@ package main
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"golang.org/x/term"
|
||||||
"io"
|
"io"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
@@ -407,7 +409,7 @@ func jsonUnmarshal(b []byte, v any) error { return json.Unmarshal(b, v) }
|
|||||||
func recordCmd() *cobra.Command {
|
func recordCmd() *cobra.Command {
|
||||||
cmd := &cobra.Command{Use: "record", Aliases: []string{"records", "rr"}, Short: "Manage records in primary zones"}
|
cmd := &cobra.Command{Use: "record", Aliases: []string{"records", "rr"}, Short: "Manage records in primary zones"}
|
||||||
var view, name, typ, search, comment, ttlArg string
|
var view, name, typ, search, comment, ttlArg string
|
||||||
var ptr, missingOK, yes, onlyDisabled bool
|
var ptr, missingOK, yes, onlyDisabled, wide bool
|
||||||
var id int64
|
var id int64
|
||||||
|
|
||||||
list := &cobra.Command{
|
list := &cobra.Command{
|
||||||
@@ -431,7 +433,7 @@ func recordCmd() *cobra.Command {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return output(recs, func(w io.Writer) {
|
return output(recs, func(w io.Writer) {
|
||||||
fmt.Fprintln(w, "ID\tNAME\tTTL\tTYPE\tDATA\tCOMMENT")
|
rows := [][]string{{"ID", "NAME", "TTL", "TYPE", "DATA", "COMMENT"}}
|
||||||
for _, r := range recs {
|
for _, r := range recs {
|
||||||
ttl := fmt.Sprint(r.EffectiveTTL)
|
ttl := fmt.Sprint(r.EffectiveTTL)
|
||||||
if r.TTL == nil {
|
if r.TTL == nil {
|
||||||
@@ -441,7 +443,13 @@ func recordCmd() *cobra.Command {
|
|||||||
if r.Disabled {
|
if r.Disabled {
|
||||||
comment = strings.TrimSpace("[disabled] " + comment)
|
comment = strings.TrimSpace("[disabled] " + comment)
|
||||||
}
|
}
|
||||||
fmt.Fprintf(w, "%d\t%s\t%s\t%s\t%s\t%s\n", r.ID, r.RelativeName, ttl, r.Type, r.Data, comment)
|
rows = append(rows, []string{fmt.Sprint(r.ID), r.RelativeName, ttl, r.Type, r.Data, comment})
|
||||||
|
}
|
||||||
|
if !wide {
|
||||||
|
fitColumn(rows, 4)
|
||||||
|
}
|
||||||
|
for _, row := range rows {
|
||||||
|
fmt.Fprintln(w, strings.Join(row, "\t"))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@@ -451,6 +459,7 @@ func recordCmd() *cobra.Command {
|
|||||||
list.Flags().StringVarP(&typ, "type", "t", "", "only this type")
|
list.Flags().StringVarP(&typ, "type", "t", "", "only this type")
|
||||||
list.Flags().StringVar(&search, "search", "", "only records whose name, data or comment contains this")
|
list.Flags().StringVar(&search, "search", "", "only records whose name, data or comment contains this")
|
||||||
list.Flags().BoolVar(&onlyDisabled, "disabled", false, "only disabled records")
|
list.Flags().BoolVar(&onlyDisabled, "disabled", false, "only disabled records")
|
||||||
|
list.Flags().BoolVarP(&wide, "wide", "w", false, "show long data in full (on a terminal it is shortened to fit)")
|
||||||
|
|
||||||
add := &cobra.Command{
|
add := &cobra.Command{
|
||||||
Use: "add ZONE NAME TYPE DATA...",
|
Use: "add ZONE NAME TYPE DATA...",
|
||||||
@@ -724,3 +733,39 @@ func setTTL(body map[string]any, ttl string) error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// fitColumn shortens column col of a table (header row first) so that the
|
||||||
|
// rows fit the terminal: the table pads every row to its longest cell, so
|
||||||
|
// one long TXT record would make every row wrap. Nothing is shortened when
|
||||||
|
// standard output isn't a terminal.
|
||||||
|
func fitColumn(rows [][]string, col int) {
|
||||||
|
fd := int(os.Stdout.Fd())
|
||||||
|
if !term.IsTerminal(fd) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
width, _, err := term.GetSize(fd)
|
||||||
|
if err != nil || width <= 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
widths := make([]int, len(rows[0]))
|
||||||
|
for _, row := range rows {
|
||||||
|
for i, c := range row {
|
||||||
|
widths[i] = max(widths[i], utf8.RuneCountInString(c))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
used := 0
|
||||||
|
for i, w := range widths {
|
||||||
|
if i != col {
|
||||||
|
used += w + 2 // the table's padding
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if col == len(widths)-1 {
|
||||||
|
used -= 2
|
||||||
|
}
|
||||||
|
limit := max(width-used-2, 24)
|
||||||
|
for _, row := range rows[1:] {
|
||||||
|
if r := []rune(row[col]); len(r) > limit {
|
||||||
|
row[col] = string(r[:limit-1]) + "…"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+20
-3
@@ -139,10 +139,11 @@ to stderr with `-o json` or `-o yaml`, so stdout holds just the documents.
|
|||||||
| Flag | Meaning |
|
| Flag | Meaning |
|
||||||
|------|---------|
|
|------|---------|
|
||||||
| `-o table\|json\|yaml` | Output format. JSON and YAML have the API's full objects, for scripts. |
|
| `-o table\|json\|yaml` | Output format. JSON and YAML have the API's full objects, for scripts. |
|
||||||
|
| `-o id`, `-o field=NAME[,NAME...]` | Just the IDs, or just those fields, one line per object (fields separated by tabs). See [Scripting tips](#scripting-tips). |
|
||||||
| `-n, --dry-run` | Validate and show the diff without changing anything. |
|
| `-n, --dry-run` | Validate and show the diff without changing anything. |
|
||||||
| `-C, --changeset ID` | Stage the change into an open changeset instead of applying it. |
|
| `-C, --changeset ID` | Stage the change into an open changeset instead of applying it. |
|
||||||
| `--force` | Overwrite BIND files that were changed outside Jarl (drift). |
|
| `--force` | Overwrite BIND files that were changed outside Jarl (drift). |
|
||||||
| `-q, --quiet` | Print only errors, or just the ID where a command creates something. |
|
| `-q, --quiet` | Print only errors, or just the IDs: of what a command creates, or what `show` and `list` find (like `-o id`). |
|
||||||
|
|
||||||
## Exit codes
|
## Exit codes
|
||||||
|
|
||||||
@@ -440,8 +441,24 @@ See [acme.md](acme.md) for certbot, acme.sh and a dedicated ACME zone.
|
|||||||
|
|
||||||
## Scripting tips
|
## Scripting tips
|
||||||
|
|
||||||
* `-o json` gives full API objects. Pipe them to `jq`.
|
* `-o id` prints just the IDs of what a `show` or `list` command finds, one
|
||||||
* `-q` with `create` commands prints just the ID or token.
|
per line, and `-o field=...` picks fields (their names are those of
|
||||||
|
`-o json`; a dot reaches inside, as in `soa.serial`; a field an object
|
||||||
|
doesn't have prints as empty). For changes, they apply to the changed
|
||||||
|
object:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
ZONE=$(jarl zone show adbraun.lol --view external -o id)
|
||||||
|
jarl record list adbraun.lol --name www -t A -o id | xargs -n1 jarl record disable --id
|
||||||
|
jarl zone show adbraun.lol -o field=view,soa.serial # external 2026100901
|
||||||
|
jarl zone list -o field=id,name,view
|
||||||
|
ID=$(jarl record add adbraun.lol www A 192.0.2.10 -o id) # the new record's ID
|
||||||
|
```
|
||||||
|
* `-q` with `create` commands prints just the ID or token, and with `show`
|
||||||
|
and `list` just the IDs. Commands without IDs (status, checks) print
|
||||||
|
nothing but errors.
|
||||||
|
* `jarl record list` on a terminal shortens data too long for a line
|
||||||
|
(`…`); `--wide` shows it in full, and so does output to a pipe or file.
|
||||||
* Check `$?` against the exit code table.
|
* Check `$?` against the exit code table.
|
||||||
* `jarl record delete ... --missing-ok` succeeds when nothing matches, which
|
* `jarl record delete ... --missing-ok` succeeds when nothing matches, which
|
||||||
keeps cleanup scripts idempotent. So does `record disable|enable
|
keeps cleanup scripts idempotent. So does `record disable|enable
|
||||||
|
|||||||
@@ -170,6 +170,12 @@ contains "\[disabled\]"
|
|||||||
run "disabling twice says so (exit 5)" 5 $J record disable adbraun.test web A
|
run "disabling twice says so (exit 5)" 5 $J record disable adbraun.test web A
|
||||||
contains "already disabled"
|
contains "already disabled"
|
||||||
run "record enable with PTR" 0 $J record enable adbraun.test web A --ptr
|
run "record enable with PTR" 0 $J record enable adbraun.test web A --ptr
|
||||||
|
run "record list -o id" 0 $J record list adbraun.test --name web -t A -o id
|
||||||
|
if echo "$OUT" | grep -qx '[0-9][0-9]*'; then ok "-o id is just the ID"; else bad "-o id gave '$OUT'"; fi
|
||||||
|
run "disable by the listed ID" 0 $J record disable --id "$OUT"
|
||||||
|
run "zone show -o field" 0 $J zone show adbraun.test -o field=name,record_count,disabled_count
|
||||||
|
contains "^adbraun.test. [0-9]* 1$"
|
||||||
|
run "enable by ID" 0 $J record enable --id "$($J record list adbraun.test --name web -t A -o id)"
|
||||||
expect_dig "enabled web A back" "192.168.1.30" web.adbraun.test A
|
expect_dig "enabled web A back" "192.168.1.30" web.adbraun.test A
|
||||||
expect_dig "enabled PTR 30 back" "web.adbraun.test." 30.1.168.192.in-addr.arpa PTR
|
expect_dig "enabled PTR 30 back" "web.adbraun.test." 30.1.168.192.in-addr.arpa PTR
|
||||||
run "CNAME conflict rejected (exit 6)" 6 $J record add adbraun.test www A 192.168.1.99
|
run "CNAME conflict rejected (exit 6)" 6 $J record add adbraun.test www A 192.168.1.99
|
||||||
|
|||||||
Reference in new issue
Block a user