diff --git a/etcdutl/README.md b/etcdutl/README.md index f4a959140ae..06cf85d7e9b 100644 --- a/etcdutl/README.md +++ b/etcdutl/README.md @@ -119,6 +119,44 @@ Prints a line of JSON encoding the database hash, revision, total keys, and size +----------+----------+------------+------------+ ``` +### HASHKV [options] \ + +HASHKV prints hash of keys and values up to given revision. + +#### Options + +- rev -- Revision number. Default is 0 which means the latest revision. + +#### Output + +##### Simple format + +Prints a humanized table of the KV hash, hash revision and compact revision. + +##### JSON format + +Prints a line of JSON encoding the KV hash, hash revision and compact revision. + +#### Examples +```bash +./etcdutl hashkv file.db +# 35c86e9b, 214, 150 +``` + +```bash +./etcdutl --write-out=json hashkv file.db +# {"hash":902327963,"hashRevision":214,"compactRevision":150} +``` + +```bash +./etcdutl --write-out=table hashkv file.db ++----------+---------------+------------------+ +| HASH | HASH REVISION | COMPACT REVISION | ++----------+---------------+------------------+ +| 35c86e9b | 214 | 150 | ++----------+---------------+------------------+ +``` + ### VERSION Prints the version of etcdutl. diff --git a/etcdutl/ctl.go b/etcdutl/ctl.go index c43b81d6845..e4106634744 100644 --- a/etcdutl/ctl.go +++ b/etcdutl/ctl.go @@ -43,6 +43,7 @@ func init() { rootCmd.AddCommand( etcdutl.NewDefragCommand(), etcdutl.NewSnapshotCommand(), + etcdutl.NewHashKVCommand(), etcdutl.NewVersionCommand(), etcdutl.NewCompletionCommand(), etcdutl.NewMigrateCommand(), diff --git a/etcdutl/etcdutl/hashkv_command.go b/etcdutl/etcdutl/hashkv_command.go new file mode 100644 index 00000000000..36db0a43f75 --- /dev/null +++ b/etcdutl/etcdutl/hashkv_command.go @@ -0,0 +1,74 @@ +// Copyright 2024 The etcd Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package etcdutl + +import ( + "github.com/spf13/cobra" + "go.uber.org/zap" + + "go.etcd.io/etcd/pkg/v3/cobrautl" + "go.etcd.io/etcd/server/v3/storage/backend" + "go.etcd.io/etcd/server/v3/storage/mvcc" +) + +var ( + hashKVRevision int64 +) + +// NewHashKVCommand returns the cobra command for "hashkv". +func NewHashKVCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "hashkv ", + Short: "Prints the KV history hash of a given file", + Args: cobra.ExactArgs(1), + Run: hashKVCommandFunc, + } + cmd.Flags().Int64Var(&hashKVRevision, "rev", 0, "maximum revision to hash (default: latest revision)") + return cmd +} + +func hashKVCommandFunc(cmd *cobra.Command, args []string) { + printer := initPrinterFromCmd(cmd) + + ds, err := calculateHashKV(args[0], hashKVRevision) + if err != nil { + cobrautl.ExitWithError(cobrautl.ExitError, err) + } + printer.DBHashKV(ds) +} + +type HashKV struct { + Hash uint32 `json:"hash"` + HashRevision int64 `json:"hashRevision"` + CompactRevision int64 `json:"compactRevision"` +} + +func calculateHashKV(dbPath string, rev int64) (HashKV, error) { + cfg := backend.DefaultBackendConfig(zap.NewNop()) + cfg.Path = dbPath + b := backend.New(cfg) + st := mvcc.NewStore(zap.NewNop(), b, nil, mvcc.StoreConfig{}) + hst := mvcc.NewHashStorage(zap.NewNop(), st) + + h, _, err := hst.HashByRev(rev) + if err != nil { + return HashKV{}, err + } + return HashKV{ + Hash: h.Hash, + HashRevision: h.Revision, + CompactRevision: h.CompactRevision, + }, nil +} diff --git a/etcdutl/etcdutl/printer.go b/etcdutl/etcdutl/printer.go index 540a7efa99a..5234891caa3 100644 --- a/etcdutl/etcdutl/printer.go +++ b/etcdutl/etcdutl/printer.go @@ -31,6 +31,7 @@ var ( type printer interface { DBStatus(snapshot.Status) + DBHashKV(HashKV) } func NewPrinter(printerType string) printer { @@ -64,6 +65,7 @@ func newPrinterUnsupported(n string) printer { } func (p *printerUnsupported) DBStatus(snapshot.Status) { p.p(nil) } +func (p *printerUnsupported) DBHashKV(HashKV) { p.p(nil) } func makeDBStatusTable(ds snapshot.Status) (hdr []string, rows [][]string) { hdr = []string{"hash", "revision", "total keys", "total size", "version"} @@ -77,6 +79,16 @@ func makeDBStatusTable(ds snapshot.Status) (hdr []string, rows [][]string) { return hdr, rows } +func makeDBHashKVTable(ds HashKV) (hdr []string, rows [][]string) { + hdr = []string{"hash", "hash revision", "compact revision"} + rows = append(rows, []string{ + fmt.Sprint(ds.Hash), + fmt.Sprint(ds.HashRevision), + fmt.Sprint(ds.CompactRevision), + }) + return hdr, rows +} + func initPrinterFromCmd(cmd *cobra.Command) (p printer) { outputType, err := cmd.Flags().GetString("write-out") if err != nil { diff --git a/etcdutl/etcdutl/printer_fields.go b/etcdutl/etcdutl/printer_fields.go index d534e396ffe..68f50014162 100644 --- a/etcdutl/etcdutl/printer_fields.go +++ b/etcdutl/etcdutl/printer_fields.go @@ -29,3 +29,9 @@ func (p *fieldsPrinter) DBStatus(r snapshot.Status) { fmt.Println(`"Size" :`, r.TotalSize) fmt.Println(`"Version" :`, r.Version) } + +func (p *fieldsPrinter) DBHashKV(r HashKV) { + fmt.Println(`"Hash" :`, r.Hash) + fmt.Println(`"Hash revision" :`, r.HashRevision) + fmt.Println(`"Compact revision" :`, r.CompactRevision) +} diff --git a/etcdutl/etcdutl/printer_json.go b/etcdutl/etcdutl/printer_json.go index b6d828962b3..ffe3a35f4c4 100644 --- a/etcdutl/etcdutl/printer_json.go +++ b/etcdutl/etcdutl/printer_json.go @@ -33,6 +33,7 @@ func newJSONPrinter() printer { } func (p *jsonPrinter) DBStatus(r snapshot.Status) { printJSON(r) } +func (p *jsonPrinter) DBHashKV(r HashKV) { printJSON(r) } // !!! Share ?? func printJSON(v any) { diff --git a/etcdutl/etcdutl/printer_simple.go b/etcdutl/etcdutl/printer_simple.go index 306ebf0c7f3..8982e366b2c 100644 --- a/etcdutl/etcdutl/printer_simple.go +++ b/etcdutl/etcdutl/printer_simple.go @@ -30,3 +30,10 @@ func (s *simplePrinter) DBStatus(ds snapshot.Status) { fmt.Println(strings.Join(row, ", ")) } } + +func (s *simplePrinter) DBHashKV(ds HashKV) { + _, rows := makeDBHashKVTable(ds) + for _, row := range rows { + fmt.Println(strings.Join(row, ", ")) + } +} diff --git a/etcdutl/etcdutl/printer_table.go b/etcdutl/etcdutl/printer_table.go index 34f0ad79a64..ec66ea38f76 100644 --- a/etcdutl/etcdutl/printer_table.go +++ b/etcdutl/etcdutl/printer_table.go @@ -34,3 +34,14 @@ func (tp *tablePrinter) DBStatus(r snapshot.Status) { table.SetAlignment(tablewriter.ALIGN_RIGHT) table.Render() } + +func (tp *tablePrinter) DBHashKV(r HashKV) { + hdr, rows := makeDBHashKVTable(r) + table := tablewriter.NewWriter(os.Stdout) + table.SetHeader(hdr) + for _, row := range rows { + table.Append(row) + } + table.SetAlignment(tablewriter.ALIGN_RIGHT) + table.Render() +} diff --git a/server/storage/mvcc/hash.go b/server/storage/mvcc/hash.go index 94e65f6a25c..56416ba4847 100644 --- a/server/storage/mvcc/hash.go +++ b/server/storage/mvcc/hash.go @@ -111,7 +111,7 @@ type hashStorage struct { lg *zap.Logger } -func newHashStorage(lg *zap.Logger, s *store) *hashStorage { +func NewHashStorage(lg *zap.Logger, s *store) HashStorage { return &hashStorage{ store: s, lg: lg, diff --git a/server/storage/mvcc/hash_test.go b/server/storage/mvcc/hash_test.go index d906d41f17a..8c3e5bf71cd 100644 --- a/server/storage/mvcc/hash_test.go +++ b/server/storage/mvcc/hash_test.go @@ -178,8 +178,9 @@ func (tc hashTestCase) Compact(ctx context.Context, rev int64) error { func TestHasherStore(t *testing.T) { lg := zaptest.NewLogger(t) - s := newHashStorage(lg, newFakeStore(lg)) - defer s.store.Close() + store := newFakeStore(lg) + s := NewHashStorage(lg, store) + defer store.Close() var hashes []KeyValueHash for i := 0; i < hashStorageMaxSize; i++ { @@ -207,8 +208,9 @@ func TestHasherStore(t *testing.T) { func TestHasherStoreFull(t *testing.T) { lg := zaptest.NewLogger(t) - s := newHashStorage(lg, newFakeStore(lg)) - defer s.store.Close() + store := newFakeStore(lg) + s := NewHashStorage(lg, store) + defer store.Close() var minRevision int64 = 100 var maxRevision = minRevision + hashStorageMaxSize diff --git a/server/storage/mvcc/kvstore.go b/server/storage/mvcc/kvstore.go index 236b09981ef..2391361fca1 100644 --- a/server/storage/mvcc/kvstore.go +++ b/server/storage/mvcc/kvstore.go @@ -106,7 +106,7 @@ func NewStore(lg *zap.Logger, b backend.Backend, le lease.Lessor, cfg StoreConfi lg: lg, } - s.hashes = newHashStorage(lg, s) + s.hashes = NewHashStorage(lg, s) s.ReadView = &readView{s} s.WriteView = &writeView{s} if s.le != nil { diff --git a/server/storage/mvcc/kvstore_test.go b/server/storage/mvcc/kvstore_test.go index 7bfdabf2659..31375dc0c4e 100644 --- a/server/storage/mvcc/kvstore_test.go +++ b/server/storage/mvcc/kvstore_test.go @@ -929,7 +929,7 @@ func newFakeStore(lg *zap.Logger) *store { lg: lg, } s.ReadView, s.WriteView = &readView{s}, &writeView{s} - s.hashes = newHashStorage(lg, s) + s.hashes = NewHashStorage(lg, s) return s }