diff --git a/go.mod b/go.mod index 6ebc1b8..e5fb523 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/cilium/statedb go 1.23 require ( - github.com/cilium/hive v0.0.0-20241025140746-d66ad09f4384 + github.com/cilium/hive v0.0.0-20241213121623-605c1412b9b3 github.com/cilium/stream v0.0.0-20240209152734-a0792b51812d github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de github.com/spf13/cobra v1.8.0 diff --git a/go.sum b/go.sum index b92c418..f095e82 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,7 @@ -github.com/cilium/hive v0.0.0-20241011093954-8df06c41a157 h1:8UuDJ7JPPoCaDfZ/WkU/aP3FtNCwdNQe+7fbzP+lZrk= -github.com/cilium/hive v0.0.0-20241011093954-8df06c41a157/go.mod h1:pI2GJ1n3SLKIQVFrKF7W6A6gb6BQkZ+3Hp4PAEo5SuI= -github.com/cilium/hive v0.0.0-20241025140746-d66ad09f4384 h1:MAkG2lk4v0Z8J2X4+fFnhuCEsIJGPdCCrWzL41S2Z/I= -github.com/cilium/hive v0.0.0-20241025140746-d66ad09f4384/go.mod h1:pI2GJ1n3SLKIQVFrKF7W6A6gb6BQkZ+3Hp4PAEo5SuI= +github.com/cilium/hive v0.0.0-20241213101835-553aca42f74a h1:KuDVdRWFhuntkXMuXBraKvsJ4o6HuPf3iF2hETefRtE= +github.com/cilium/hive v0.0.0-20241213101835-553aca42f74a/go.mod h1:pI2GJ1n3SLKIQVFrKF7W6A6gb6BQkZ+3Hp4PAEo5SuI= +github.com/cilium/hive v0.0.0-20241213121623-605c1412b9b3 h1:RfmUH1ouzj0LzORYJRhp43e1rlGpx6GNv4NIRUakU2w= +github.com/cilium/hive v0.0.0-20241213121623-605c1412b9b3/go.mod h1:pI2GJ1n3SLKIQVFrKF7W6A6gb6BQkZ+3Hp4PAEo5SuI= github.com/cilium/stream v0.0.0-20240209152734-a0792b51812d h1:p6MgATaKEB9o7iAsk9rlzXNDMNCeKPAkx4Y8f+Zq8X8= github.com/cilium/stream v0.0.0-20240209152734-a0792b51812d/go.mod h1:3VLiLgs8wfjirkuYqos4t0IBPQ+sXtf3tFkChLm6ARM= github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= diff --git a/reconciler/testdata/batching.txtar b/reconciler/testdata/batching.txtar index 4dc5835..efa21be 100644 --- a/reconciler/testdata/batching.txtar +++ b/reconciler/testdata/batching.txtar @@ -49,11 +49,11 @@ health 'job-reconcile.*level=OK.*message=OK, 0 object' # Check metrics expvar -! grep 'reconciliation_count.test: 0$' -grep 'reconciliation_current_errors.test: 0$' -! grep 'reconciliation_total_errors.test: 0$' -! grep 'reconciliation_duration.test/update: 0$' -! grep 'reconciliation_duration.test/delete: 0$' +! stdout 'reconciliation_count.test: 0$' +stdout 'reconciliation_current_errors.test: 0$' +! stdout 'reconciliation_total_errors.test: 0$' +! stdout 'reconciliation_duration.test/update: 0$' +! stdout 'reconciliation_duration.test/delete: 0$' # ------------ diff --git a/reconciler/testdata/incremental.txtar b/reconciler/testdata/incremental.txtar index f48f4b6..6d9226f 100644 --- a/reconciler/testdata/incremental.txtar +++ b/reconciler/testdata/incremental.txtar @@ -48,11 +48,11 @@ health 'job-reconcile.*level=OK.*message=OK, 0 object' # Check metrics expvar -! grep 'reconciliation_count.test: 0$' -grep 'reconciliation_current_errors.test: 0$' -! grep 'reconciliation_total_errors.test: 0$' -! grep 'reconciliation_duration.test/update: 0$' -! grep 'reconciliation_duration.test/delete: 0$' +! stdout 'reconciliation_count.test: 0$' +stdout 'reconciliation_current_errors.test: 0$' +! stdout 'reconciliation_total_errors.test: 0$' +! stdout 'reconciliation_duration.test/update: 0$' +! stdout 'reconciliation_duration.test/delete: 0$' # ------------ diff --git a/reconciler/testdata/pruning.txtar b/reconciler/testdata/pruning.txtar index 431373d..533cc5f 100644 --- a/reconciler/testdata/pruning.txtar +++ b/reconciler/testdata/pruning.txtar @@ -14,7 +14,7 @@ mark-init expect-ops prune(n=2) health 'job-reconcile.*level=OK' expvar -! grep 'prune_count.test: 0' +! stdout 'prune_count.test: 0' # Pruning with faulty ops will mark status as degraded set-faulty true @@ -22,7 +22,7 @@ prune expect-ops 'prune(n=2) fail' health 'job-reconcile.*level=Degraded.*message=.*prune fail' expvar -grep 'prune_current_errors.test: 1' +stdout 'prune_current_errors.test: 1' # Pruning again with healthy ops fixes the status. set-faulty false @@ -30,7 +30,7 @@ prune expect-ops 'prune(n=2)' health 'job-reconcile.*level=OK' expvar -grep 'prune_current_errors.test: 0' +stdout 'prune_current_errors.test: 0' # Delete an object and check pruning happens without it db/delete test-objects obj1.yaml @@ -44,15 +44,15 @@ expect-ops prune(n=0) delete(2) prune(n=1) # Check metrics expvar -! grep 'prune_count.test: 0' -grep 'prune_current_errors.test: 0' -grep 'prune_total_errors.test: 1' -! grep 'prune_duration.test: 0$' -! grep 'reconciliation_count.test: 0$' -grep 'reconciliation_current_errors.test: 0$' -grep 'reconciliation_total_errors.test: 0$' -! grep 'reconciliation_duration.test/update: 0$' -! grep 'reconciliation_duration.test/delete: 0$' +! stdout 'prune_count.test: 0' +stdout 'prune_current_errors.test: 0' +stdout 'prune_total_errors.test: 1' +! stdout 'prune_duration.test: 0$' +! stdout 'reconciliation_count.test: 0$' +stdout 'reconciliation_current_errors.test: 0$' +stdout 'reconciliation_total_errors.test: 0$' +! stdout 'reconciliation_duration.test/update: 0$' +! stdout 'reconciliation_duration.test/delete: 0$' -- obj1.yaml -- id: 1 diff --git a/script.go b/script.go index 6e94b27..c0e805f 100644 --- a/script.go +++ b/script.go @@ -6,7 +6,6 @@ package statedb import ( "bytes" "encoding/json" - "flag" "fmt" "io" "iter" @@ -19,6 +18,7 @@ import ( "github.com/cilium/hive" "github.com/cilium/hive/script" "github.com/liggitt/tabwriter" + "github.com/spf13/pflag" "golang.org/x/time/rate" "gopkg.in/yaml.v3" ) @@ -90,17 +90,14 @@ func DBCmd(db *DB) script.Cmd { ) } -func newCmdFlagSet(w io.Writer) *flag.FlagSet { - fs := flag.NewFlagSet("", flag.ContinueOnError) - fs.SetOutput(w) - return fs -} - func InitializedCmd(db *DB) script.Cmd { return script.Command( script.CmdUsage{ Summary: "Wait until all or specific tables have been initialized", - Args: "[-timeout=<duration>] table...", + Args: "table...", + Flags: func(fs *pflag.FlagSet) { + fs.Duration("timeout", 5*time.Second, "Maximum amount of time to wait for the table contents to match") + }, Detail: []string{ "Waits until all or specific tables have been marked", "initialized. The default timeout is 5 seconds.", @@ -111,15 +108,13 @@ func InitializedCmd(db *DB) script.Cmd { }, }, func(s *script.State, args ...string) (script.WaitFunc, error) { - flags := newCmdFlagSet(s.LogWriter()) - timeout := flags.Duration("timeout", 5*time.Second, "Maximum amount of time to wait for the table contents to match") - if err := flags.Parse(args); err != nil { - return nil, fmt.Errorf("%w: %w", script.ErrUsage, err) + timeout, err := s.Flags.GetDuration("timeout") + if err != nil { + return nil, err } - args = flags.Args() txn := db.ReadTxn() - timeoutChan := time.After(*timeout) + timeoutChan := time.After(timeout) allTbls := db.GetTables(txn) tbls := allTbls @@ -166,7 +161,12 @@ func ShowCmd(db *DB) script.Cmd { return script.Command( script.CmdUsage{ Summary: "Show the contents of a table", - Args: "[-o=<file>] [-columns=col1,...] [-format={table,yaml,json}] table", + Args: "table", + Flags: func(fs *pflag.FlagSet) { + fs.StringP("out", "o", "", "File to write to instead of stdout") + fs.StringSlice("columns", nil, "Columns to write") + fs.StringP("format", "f", "table", "Format to write in (table, yaml or json)") + }, Detail: []string{ "Show the contents of a table.", "", @@ -182,20 +182,19 @@ func ShowCmd(db *DB) script.Cmd { }, }, func(s *script.State, args ...string) (script.WaitFunc, error) { - flags := newCmdFlagSet(s.LogWriter()) - file := flags.String("o", "", "File to write to instead of stdout") - columns := flags.String("columns", "", "Comma-separated list of columns to write") - format := flags.String("format", "table", "Format to write in (table, yaml, json)") - if err := flags.Parse(args); err != nil { - return nil, fmt.Errorf("%w: %w", script.ErrUsage, err) + file, err := s.Flags.GetString("out") + if err != nil { + return nil, err } - - var cols []string - if len(*columns) > 0 { - cols = strings.Split(*columns, ",") + columns, err := s.Flags.GetStringSlice("columns") + if err != nil { + return nil, err + } + format, err := s.Flags.GetString("format") + if err != nil { + return nil, err } - args = flags.Args() if len(args) < 1 { return nil, fmt.Errorf("missing table name") } @@ -203,12 +202,12 @@ func ShowCmd(db *DB) script.Cmd { return func(*script.State) (stdout, stderr string, err error) { var buf strings.Builder var w io.Writer - if *file == "" { + if file == "" { w = &buf } else { - f, err := os.OpenFile(s.Path(*file), os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644) + f, err := os.OpenFile(s.Path(file), os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644) if err != nil { - return "", "", fmt.Errorf("OpenFile(%s): %w", *file, err) + return "", "", fmt.Errorf("OpenFile(%s): %w", file, err) } defer f.Close() w = f @@ -217,7 +216,7 @@ func ShowCmd(db *DB) script.Cmd { if err != nil { return "", "", err } - err = writeObjects(tbl, tbl.All(txn), w, cols, *format) + err = writeObjects(tbl, tbl.All(txn), w, columns, format) return buf.String(), "", err }, nil }) @@ -227,7 +226,11 @@ func CompareCmd(db *DB) script.Cmd { return script.Command( script.CmdUsage{ Summary: "Compare table", - Args: "[-timeout=<dur>] [-grep=<pattern>] table file", + Args: "table file", + Flags: func(fs *pflag.FlagSet) { + fs.Duration("timeout", 5*time.Second, "Maximum amount of time to wait for the table contents to match") + fs.String("grep", "", "Grep the result rows and only compare matching ones") + }, Detail: []string{ "Compare the contents of a table against a file.", "The comparison is retried until a timeout (1s default).", @@ -243,21 +246,22 @@ func CompareCmd(db *DB) script.Cmd { }, }, func(s *script.State, args ...string) (script.WaitFunc, error) { - flags := newCmdFlagSet(s.LogWriter()) - timeout := flags.Duration("timeout", time.Second, "Maximum amount of time to wait for the table contents to match") - grep := flags.String("grep", "", "Grep the result rows and only compare matching ones") - err := flags.Parse(args) + timeout, err := s.Flags.GetDuration("timeout") + if err != nil { + return nil, err + } + grep, err := s.Flags.GetString("grep") if err != nil { - return nil, fmt.Errorf("%w: %w", script.ErrUsage, err) + return nil, err } - args = flags.Args() + if len(args) != 2 { return nil, fmt.Errorf("expected table and filename") } var grepRe *regexp.Regexp - if *grep != "" { - grepRe, err = regexp.Compile(*grep) + if grep != "" { + grepRe, err = regexp.Compile(grep) if err != nil { return nil, fmt.Errorf("bad grep: %w", err) } @@ -292,7 +296,7 @@ func CompareCmd(db *DB) script.Cmd { } lines = lines[1:] origLines := lines - timeoutChan := time.After(*timeout) + timeoutChan := time.After(timeout) for { lines = origLines @@ -485,8 +489,15 @@ func queryCmd(db *DB, query int, summary string, detail []string) script.Cmd { return script.Command( script.CmdUsage{ Summary: summary, - Args: "[-o=<file>] [-columns=col1,...] [-format={table*,yaml,json}] [-index=<index>] table key", - Detail: detail, + Args: "table key", + Flags: func(fs *pflag.FlagSet) { + fs.StringP("out", "o", "", "File to write to instead of stdout") + fs.StringSlice("columns", nil, "Columns to write") + fs.StringP("format", "f", "table", "Format to write in (table, yaml or json)") + fs.StringP("index", "i", "", "Index to query") + fs.Bool("delete", false, "Delete all matching objects") + }, + Detail: detail, }, func(s *script.State, args ...string) (script.WaitFunc, error) { return runQueryCmd(query, db, s, args) @@ -495,22 +506,27 @@ func queryCmd(db *DB, query int, summary string, detail []string) script.Cmd { } func runQueryCmd(query int, db *DB, s *script.State, args []string) (script.WaitFunc, error) { - flags := newCmdFlagSet(s.LogWriter()) - file := flags.String("o", "", "File to write results to instead of stdout") - index := flags.String("index", "", "Index to query") - format := flags.String("format", "table", "Format to write in (table, yaml, json)") - columns := flags.String("columns", "", "Comma-separated list of columns to write") - delete := flags.Bool("delete", false, "Delete all matching objects") - if err := flags.Parse(args); err != nil { - return nil, fmt.Errorf("%w: %w", script.ErrUsage, err) + file, err := s.Flags.GetString("out") + if err != nil { + return nil, err } - - var cols []string - if len(*columns) > 0 { - cols = strings.Split(*columns, ",") + columns, err := s.Flags.GetStringSlice("columns") + if err != nil { + return nil, err + } + format, err := s.Flags.GetString("format") + if err != nil { + return nil, err + } + index, err := s.Flags.GetString("index") + if err != nil { + return nil, err + } + delete, err := s.Flags.GetBool("delete") + if err != nil { + return nil, err } - args = flags.Args() if len(args) < 2 { return nil, fmt.Errorf("expected table and key") } @@ -523,12 +539,12 @@ func runQueryCmd(query int, db *DB, s *script.State, args []string) (script.Wait var buf strings.Builder var w io.Writer - if *file == "" { + if file == "" { w = &buf } else { - f, err := os.OpenFile(s.Path(*file), os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644) + f, err := os.OpenFile(s.Path(file), os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644) if err != nil { - return "", "", fmt.Errorf("OpenFile(%s): %s", *file, err) + return "", "", fmt.Errorf("OpenFile(%s): %s", file, err) } defer f.Close() w = f @@ -537,13 +553,13 @@ func runQueryCmd(query int, db *DB, s *script.State, args []string) (script.Wait var it iter.Seq2[any, uint64] switch query { case queryCmdList: - it, err = tbl.List(txn, *index, args[1]) + it, err = tbl.List(txn, index, args[1]) case queryCmdLowerBound: - it, err = tbl.LowerBound(txn, *index, args[1]) + it, err = tbl.LowerBound(txn, index, args[1]) case queryCmdPrefix: - it, err = tbl.Prefix(txn, *index, args[1]) + it, err = tbl.Prefix(txn, index, args[1]) case queryCmdGet: - it, err = tbl.List(txn, *index, args[1]) + it, err = tbl.List(txn, index, args[1]) if err == nil { it = firstOfSeq2(it) } @@ -554,12 +570,12 @@ func runQueryCmd(query int, db *DB, s *script.State, args []string) (script.Wait return "", "", fmt.Errorf("query: %w", err) } - err = writeObjects(tbl, it, w, cols, *format) + err = writeObjects(tbl, it, w, columns, format) if err != nil { return "", "", err } - if *delete { + if delete { wtxn := db.WriteTxn(tbl.Meta) count := 0 for obj := range it { diff --git a/testdata/db.txtar b/testdata/db.txtar index 286e29d..d48d5bc 100644 --- a/testdata/db.txtar +++ b/testdata/db.txtar @@ -24,44 +24,44 @@ db/insert test2 obj2.yaml # Show (non-empty) db/show test1 -grep ^ID.*Tags -grep 1.*bar -grep 2.*baz +stdout ^ID.*Tags +stdout 1.*bar +stdout 2.*baz db/show test2 -db/show -format=table test1 -grep ^ID.*Tags -grep 1.*bar -grep 2.*baz +db/show --format=table test1 +stdout ^ID.*Tags +stdout 1.*bar +stdout 2.*baz -db/show -format=table -columns=Tags test1 -grep ^Tags$ -grep '^bar, foo$' -grep '^baz, foo$' +db/show --format=table --columns=Tags test1 +stdout ^Tags$ +stdout '^bar, foo$' +stdout '^baz, foo$' -db/show -format=table -o=test1_show.table test1 +db/show -f table -o test1_show.table test1 cmp test1.table test1_show.table -db/show -format=yaml -o=test1_show.yaml test1 +db/show --format=yaml --out=test1_show.yaml test1 cmp test1.yaml test1_show.yaml -db/show -format=json -o=test1_show.json test1 +db/show --format=json -o=test1_show.json test1 cmp test1.json test1_show.json # Get db/get test2 2 -db/get -format=table test2 2 -grep '^ID.*Tags$' -grep ^2.*baz -db/get -format=table -columns=Tags test2 2 -grep ^Tags$ -grep '^baz, foo$' -db/get -format=json test2 2 -db/get -format=yaml test2 2 -db/get -format=yaml -o=obj2_get.yaml test2 2 +db/get --format=table test2 2 +stdout '^ID.*Tags$' +stdout ^2.*baz +db/get --format=table --columns=Tags test2 2 +stdout ^Tags$ +stdout '^baz, foo$' +db/get --format=json test2 2 +db/get --format=yaml test2 2 +db/get --format=yaml -o=obj2_get.yaml test2 2 cmp obj2.yaml obj2_get.yaml -db/get -index=tags -format=yaml -o=obj1_get.yaml test1 bar +db/get -i tags -f yaml -o obj1_get.yaml test1 bar cmp obj1.yaml obj1_get.yaml # List @@ -70,24 +70,24 @@ cmp obj1.table list.table db/list -o=list.table test1 2 cmp obj2.table list.table -db/list -o=list.table -index=tags test1 bar +db/list -o list.table -i tags test1 bar cmp obj1.table list.table -db/list -o=list.table -index=tags test1 baz +db/list -o=list.table -i=tags test1 baz cmp obj2.table list.table -db/list -o=list.table -index=tags test1 foo +db/list --out=list.table --index=tags test1 foo cmp objs.table list.table -db/list -format=table -index=tags -columns=Tags test1 foo -grep ^Tags$ -grep '^bar, foo$' -grep '^baz, foo$' +db/list --format=table --index=tags --columns=Tags test1 foo +stdout ^Tags$ +stdout '^bar, foo$' +stdout '^baz, foo$' # Prefix # uint64 so can't really prefix search meaningfully, unless # FromString() accomodates partial keys. db/prefix test1 1 -db/prefix -o=prefix.table -index=tags test1 ba +db/prefix -o=prefix.table --index=tags test1 ba cmp objs.table prefix.table # LowerBound @@ -103,8 +103,8 @@ cmp empty.table lb.table # Compare db/cmp test1 objs.table db/cmp test1 objs_ids.table -db/cmp -grep=bar test1 obj1.table -db/cmp -grep=baz test1 obj2.table +db/cmp --grep=bar test1 obj1.table +db/cmp --grep=baz test1 obj2.table # Delete db/delete test1 obj1.yaml @@ -116,21 +116,21 @@ db/cmp test1 empty.table # Delete with get db/insert test1 obj1.yaml db/cmp test1 obj1.table -db/get -delete test1 1 +db/get --delete test1 1 db/cmp test1 empty.table # Delete with prefix db/insert test1 obj1.yaml db/insert test1 obj2.yaml db/cmp test1 objs.table -db/prefix -index=tags -delete test1 fo +db/prefix --index=tags --delete test1 fo db/cmp test1 empty.table # Delete with lowerbound db/insert test1 obj1.yaml db/insert test1 obj2.yaml db/cmp test1 objs.table -db/lowerbound -index=id -delete test1 2 +db/lowerbound --index=id --delete test1 2 db/cmp test1 obj1.table # Tables