Merge pull request #15965 from cenkalti/hashkv

cli: Add etcdutl snapshot hashkv command
This commit is contained in:
Benjamin Wang 2024-06-03 20:58:27 +01:00 committed by GitHub
commit dc8510ce47
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 159 additions and 7 deletions

View File

@ -119,6 +119,44 @@ Prints a line of JSON encoding the database hash, revision, total keys, and size
+----------+----------+------------+------------+
```
### HASHKV [options] \<filename\>
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.

View File

@ -43,6 +43,7 @@ func init() {
rootCmd.AddCommand(
etcdutl.NewDefragCommand(),
etcdutl.NewSnapshotCommand(),
etcdutl.NewHashKVCommand(),
etcdutl.NewVersionCommand(),
etcdutl.NewCompletionCommand(),
etcdutl.NewMigrateCommand(),

View File

@ -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 <filename>",
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
}

View File

@ -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 {

View File

@ -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)
}

View File

@ -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) {

View File

@ -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, ", "))
}
}

View File

@ -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()
}

View File

@ -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,

View File

@ -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

View File

@ -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 {

View File

@ -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
}