diff --git a/CHANGELOG-3.5.md b/CHANGELOG-3.5.md index ae4745907e1..b60a08e5c79 100644 --- a/CHANGELOG-3.5.md +++ b/CHANGELOG-3.5.md @@ -225,6 +225,8 @@ Note that any `etcd_debugging_*` metrics are experimental and subject to change. - Add [`etcdctl get --count-only`](https://github.com/etcd-io/etcd/pull/11743) flag for output type `fields`. - Add [`etcdctl member list -w=json --hex`](https://github.com/etcd-io/etcd/pull/11812) flag to print memberListResponse in hex format json. - Changed [`etcdctl lock exec-command`](https://github.com/etcd-io/etcd/pull/12829) to return exit code of exec-command. +- [New tool: `etcdutl`](https://github.com/etcd-io/etcd/pull/12971) incorporated functionality of: `etcdctl snapshot status|restore`, `etcdctl backup`, `etcdctl defrag --data-dir ...`. +- [ETCDCTL_API=2 `etcdctl migrate`](https://github.com/etcd-io/etcd/pull/12971) has been decomissioned. Use etcd <=v3.4 to restore v2 storage. ### gRPC gateway diff --git a/Dockerfile-release.amd64 b/Dockerfile-release.amd64 index 11764051236..9bd425887cd 100644 --- a/Dockerfile-release.amd64 +++ b/Dockerfile-release.amd64 @@ -2,6 +2,7 @@ FROM k8s.gcr.io/build-image/debian-base:buster-v1.4.0 ADD etcd /usr/local/bin/ ADD etcdctl /usr/local/bin/ +ADD etcdutl /usr/local/bin/ RUN mkdir -p /var/etcd/ RUN mkdir -p /var/lib/etcd/ diff --git a/Dockerfile-release.arm64 b/Dockerfile-release.arm64 index 117d5e14d1d..d04d79041a8 100644 --- a/Dockerfile-release.arm64 +++ b/Dockerfile-release.arm64 @@ -2,6 +2,7 @@ FROM k8s.gcr.io/build-image/debian-base-arm64:buster-v1.4.0 ADD etcd /usr/local/bin/ ADD etcdctl /usr/local/bin/ +ADD etcdutl /usr/local/bin/ ADD var/etcd /var/etcd ADD var/lib/etcd /var/lib/etcd ENV ETCD_UNSUPPORTED_ARCH=arm64 diff --git a/Dockerfile-release.ppc64le b/Dockerfile-release.ppc64le index 1b0c721857f..51adb7ae3af 100644 --- a/Dockerfile-release.ppc64le +++ b/Dockerfile-release.ppc64le @@ -2,6 +2,7 @@ FROM k8s.gcr.io/build-image/debian-base-ppc64le:buster-v1.4.0 ADD etcd /usr/local/bin/ ADD etcdctl /usr/local/bin/ +ADD etcdutl /usr/local/bin/ ADD var/etcd /var/etcd ADD var/lib/etcd /var/lib/etcd diff --git a/Dockerfile-release.s390x b/Dockerfile-release.s390x index d16f0c18301..a96d45534c0 100644 --- a/Dockerfile-release.s390x +++ b/Dockerfile-release.s390x @@ -2,6 +2,7 @@ FROM k8s.gcr.io/build-image/debian-base-s390x:buster-v1.4.0 ADD etcd /usr/local/bin/ ADD etcdctl /usr/local/bin/ +ADD etcdutl /usr/local/bin/ ADD var/etcd /var/etcd ADD var/lib/etcd /var/lib/etcd diff --git a/Makefile b/Makefile index 94488b85b4c..bfd6741c601 100644 --- a/Makefile +++ b/Makefile @@ -25,6 +25,7 @@ build: GO_BUILD_FLAGS="-v" ./build.sh ./bin/etcd --version ./bin/etcdctl version + ./bin/etcdutl version clean: rm -f ./codecov @@ -210,7 +211,7 @@ build-docker-release-main: docker run \ --rm \ gcr.io/etcd-development/etcd:$(ETCD_VERSION) \ - /bin/sh -c "/usr/local/bin/etcd --version && /usr/local/bin/etcdctl version" + /bin/sh -c "/usr/local/bin/etcd --version && /usr/local/bin/etcdctl version && /usr/local/bin/etcdutl version" push-docker-release-main: $(info ETCD_VERSION: $(ETCD_VERSION)) @@ -529,6 +530,7 @@ build-docker-functional: /bin/bash -c "./bin/etcd --version && \ ./bin/etcd-failpoints --version && \ ./bin/etcdctl version && \ + ./bin/etcdutl version && \ ./bin/etcd-agent -help || true && \ ./bin/etcd-proxy -help || true && \ ./bin/etcd-runner --help || true && \ diff --git a/bill-of-materials.json b/bill-of-materials.json index 42f104d8a5e..7317583b4d1 100644 --- a/bill-of-materials.json +++ b/bill-of-materials.json @@ -485,6 +485,15 @@ } ] }, + { + "project": "go.etcd.io/etcd/etcdutl/v3", + "licenses": [ + { + "type": "Apache License 2.0", + "confidence": 1 + } + ] + }, { "project": "go.etcd.io/etcd/pkg/v3", "licenses": [ diff --git a/build.sh b/build.sh index b8d1709dafd..7a4f8670e6f 100755 --- a/build.sh +++ b/build.sh @@ -47,6 +47,16 @@ etcd_build() { -o="../${out}/etcd" . || return 2 ) || return 2 + run rm -f "${out}/etcdutl" + # shellcheck disable=SC2086 + ( + cd ./etcdutl + run env GO_BUILD_FLAGS="${GO_BUILD_FLAGS}" "${GO_BUILD_ENV[@]}" go build $GO_BUILD_FLAGS \ + -installsuffix=cgo \ + "-ldflags=${GO_LDFLAGS[*]}" \ + -o="../${out}/etcdutl" . || return 2 + ) || return 2 + run rm -f "${out}/etcdctl" # shellcheck disable=SC2086 ( diff --git a/dummy.go b/dummy.go index d00a878233c..48db9af9350 100644 --- a/dummy.go +++ b/dummy.go @@ -18,6 +18,8 @@ package main_test // module (e.g. for sake of 'bom' generation). // Thanks to this 'go mod tidy' is not removing that dependencies from go.mod. import ( - _ "go.etcd.io/etcd/client/v2" // keep - _ "go.etcd.io/etcd/tests/v3/integration" // keep + _ "go.etcd.io/etcd/client/v2" // keep + _ "go.etcd.io/etcd/etcdctl/v3/ctlv3/command" // keep + _ "go.etcd.io/etcd/etcdutl/v3/etcdutl" // keep + _ "go.etcd.io/etcd/tests/v3/integration" // keep ) diff --git a/etcdctl/README.md b/etcdctl/README.md index d9d3f54d834..8dc9d2a13d7 100644 --- a/etcdctl/README.md +++ b/etcdctl/README.md @@ -913,7 +913,9 @@ If NOSPACE alarm is present: ### DEFRAG [options] -DEFRAG defragments the backend database file for a set of given endpoints while etcd is running, or directly defragments an etcd data directory while etcd is not running. When an etcd member reclaims storage space from deleted and compacted keys, the space is kept in a free list and the database file remains the same size. By defragmenting the database, the etcd member releases this free space back to the file system. +DEFRAG defragments the backend database file for a set of given endpoints while etcd is running, ~~or directly defragments an etcd data directory while etcd is not running~~. When an etcd member reclaims storage space from deleted and compacted keys, the space is kept in a free list and the database file remains the same size. By defragmenting the database, the etcd member releases this free space back to the file system. + +**Note: to defragment offline (`--data-dir` flag), use: `etcutl defrag` instead** **Note that defragmentation to a live member blocks the system from reading and writing data while rebuilding its states.** @@ -921,7 +923,7 @@ DEFRAG defragments the backend database file for a set of given endpoints while #### Options -- data-dir -- Optional. If present, defragments a data directory not in use by etcd. +- data-dir -- Optional. **Deprecated**. If present, defragments a data directory not in use by etcd. To be removed in v3.6. #### Output @@ -944,11 +946,12 @@ Finished defragmenting etcd member[http://127.0.0.1:22379] Finished defragmenting etcd member[http://127.0.0.1:32379] ``` -To defragment a data directory directly, use the `--data-dir` flag: +To defragment a data directory directly, use the `etcdutl` with `--data-dir` flag +(`etcdctl` will remove this flag in v3.6): ``` bash # Defragment while etcd is not running -./etcdctl defrag --data-dir default.etcd +./etcdutl defrag --data-dir default.etcd # success (exit status 0) # Error: cannot open database at default.etcd/member/snap/db ``` @@ -978,6 +981,8 @@ Save a snapshot to "snapshot.db": ### SNAPSHOT RESTORE [options] \ +Note: Deprecated. Use `etcdutl snapshot restore` instead. To be removed in v3.6. + SNAPSHOT RESTORE creates an etcd data directory for an etcd cluster member from a backend database snapshot and a new cluster configuration. Restoring the snapshot into each member for a new cluster configuration will initialize a new etcd cluster preloaded by the snapshot data. #### Options @@ -1021,6 +1026,8 @@ bin/etcd --name sshot3 --listen-client-urls http://127.0.0.1:32379 --advertise-c ### SNAPSHOT STATUS \ +Note: Deprecated. Use `etcdutl snapshot restore` instead. To be removed in v3.6. + SNAPSHOT STATUS lists information about a given backend database snapshot file. #### Output @@ -1495,54 +1502,6 @@ The approximate total number of keys transferred to the destination cluster, upd [mirror]: ./doc/mirror_maker.md -### MIGRATE [options] - -Migrates keys in a v2 store to a v3 mvcc store. Users should run migration command for all members in the cluster. - -#### Options - -- data-dir -- Path to the data directory - -- wal-dir -- Path to the WAL directory - -- transformer -- Path to the user-provided transformer program (default if not provided) - -#### Output - -No output on success. - -#### Default transformer - -If user does not provide a transformer program, migrate command will use the default transformer. The default transformer transforms `storev2` formatted keys into `mvcc` formatted keys according to the following Go program: - -```go -func transform(n *storev2.Node) *mvccpb.KeyValue { - if n.Dir { - return nil - } - kv := &mvccpb.KeyValue{ - Key: []byte(n.Key), - Value: []byte(n.Value), - CreateRevision: int64(n.CreatedIndex), - ModRevision: int64(n.ModifiedIndex), - Version: 1, - } - return kv -} -``` - -#### User-provided transformer - -Users can provide a customized 1:n transformer function that transforms a key from the v2 store to any number of keys in the mvcc store. The migration program writes JSON formatted [v2 store keys][v2key] to the transformer program's stdin, reads protobuf formatted [mvcc keys][v3key] back from the transformer program's stdout, and finishes migration by saving the transformed keys into the mvcc store. - -The provided transformer should read until EOF and flush the stdout before exiting to ensure data integrity. - -#### Example - -``` -./etcdctl migrate --data-dir=/var/etcd --transformer=k8s-transformer -# finished transforming keys -``` ### VERSION diff --git a/etcdctl/ctlv2/command/backup_command.go b/etcdctl/ctlv2/command/backup_command.go index 1549fdd6f71..be77ea7a906 100644 --- a/etcdctl/ctlv2/command/backup_command.go +++ b/etcdctl/ctlv2/command/backup_command.go @@ -15,37 +15,22 @@ package command import ( - "os" - "path" - "regexp" - "time" - - "go.etcd.io/etcd/api/v3/etcdserverpb" - "go.etcd.io/etcd/client/pkg/v3/fileutil" - "go.etcd.io/etcd/client/pkg/v3/types" - "go.etcd.io/etcd/pkg/v3/idutil" - "go.etcd.io/etcd/pkg/v3/pbutil" - "go.etcd.io/etcd/raft/v3/raftpb" - "go.etcd.io/etcd/server/v3/datadir" - "go.etcd.io/etcd/server/v3/etcdserver/api/membership" - "go.etcd.io/etcd/server/v3/etcdserver/api/snap" - "go.etcd.io/etcd/server/v3/etcdserver/api/v2store" - "go.etcd.io/etcd/server/v3/etcdserver/cindex" - "go.etcd.io/etcd/server/v3/mvcc/backend" - "go.etcd.io/etcd/server/v3/verify" - "go.etcd.io/etcd/server/v3/wal" - "go.etcd.io/etcd/server/v3/wal/walpb" - "github.com/urfave/cli" - bolt "go.etcd.io/bbolt" - "go.uber.org/zap" + "go.etcd.io/etcd/etcdutl/v3/etcdutl" +) + +const ( + description = "Performs an offline backup of etcd directory.\n\n" + + "Moved to `./etcdutl backup` and going to be decomissioned in v3.5\n\n" + + "The recommended (online) backup command is: `./etcdctl snapshot save ...`.\n\n" ) func NewBackupCommand() cli.Command { return cli.Command{ - Name: "backup", - Usage: "backup an etcd directory", - ArgsUsage: " ", + Name: "backup", + Usage: "--data-dir=... --backup-dir={output}", + UsageText: "[deprecated] offline backup an etcd directory.", + Description: description, Flags: []cli.Flag{ cli.StringFlag{Name: "data-dir", Value: "", Usage: "Path to the etcd data dir"}, cli.StringFlag{Name: "wal-dir", Value: "", Usage: "Path to the etcd wal dir"}, @@ -57,275 +42,13 @@ func NewBackupCommand() cli.Command { } } -type desiredCluster struct { - clusterId types.ID - nodeId types.ID - members []*membership.Member - confState raftpb.ConfState -} - -func newDesiredCluster() desiredCluster { - idgen := idutil.NewGenerator(0, time.Now()) - nodeID := idgen.Next() - clusterID := idgen.Next() - - return desiredCluster{ - clusterId: types.ID(clusterID), - nodeId: types.ID(nodeID), - members: []*membership.Member{ - { - ID: types.ID(nodeID), - Attributes: membership.Attributes{ - Name: "etcdctl-v2-backup", - }, - RaftAttributes: membership.RaftAttributes{ - PeerURLs: []string{"http://use-flag--force-new-cluster:2080"}, - }}}, - confState: raftpb.ConfState{Voters: []uint64{nodeID}}, - } -} - -// handleBackup handles a request that intends to do a backup. func handleBackup(c *cli.Context) error { - var srcWAL string - var destWAL string - - lg, err := zap.NewProduction() - if err != nil { - return err - } - - withV3 := c.Bool("with-v3") - srcDir := c.String("data-dir") - destDir := c.String("backup-dir") - - srcSnap := datadir.ToSnapDir(srcDir) - destSnap := datadir.ToSnapDir(destDir) - - if c.String("wal-dir") != "" { - srcWAL = c.String("wal-dir") - } else { - srcWAL = datadir.ToWalDir(srcDir) - } - - if c.String("backup-wal-dir") != "" { - destWAL = c.String("backup-wal-dir") - } else { - destWAL = datadir.ToWalDir(destDir) - } - - if err := fileutil.CreateDirAll(destSnap); err != nil { - lg.Fatal("failed creating backup snapshot dir", zap.String("dest-snap", destSnap), zap.Error(err)) - } - - destDbPath := datadir.ToBackendFileName(destDir) - srcDbPath := datadir.ToBackendFileName(srcDir) - desired := newDesiredCluster() - - walsnap := saveSnap(lg, destSnap, srcSnap, &desired) - metadata, state, ents := translateWAL(lg, srcWAL, walsnap, withV3) - saveDB(lg, destDbPath, srcDbPath, state.Commit, state.Term, &desired, withV3) - - neww, err := wal.Create(lg, destWAL, pbutil.MustMarshal(&metadata)) - if err != nil { - lg.Fatal("wal.Create failed", zap.Error(err)) - } - defer neww.Close() - if err := neww.Save(state, ents); err != nil { - lg.Fatal("wal.Save failed ", zap.Error(err)) - } - if err := neww.SaveSnapshot(walsnap); err != nil { - lg.Fatal("SaveSnapshot", zap.Error(err)) - } - - verify.MustVerifyIfEnabled(verify.Config{ - Logger: lg, - DataDir: destDir, - ExactIndex: false, - }) - + etcdutl.HandleBackup( + c.Bool("with-v3"), + c.String("data-dir"), + c.String("backup-dir"), + c.String("wal-dir"), + c.String("backup-wal-dir"), + ) return nil } - -func saveSnap(lg *zap.Logger, destSnap, srcSnap string, desired *desiredCluster) (walsnap walpb.Snapshot) { - ss := snap.New(lg, srcSnap) - snapshot, err := ss.Load() - if err != nil && err != snap.ErrNoSnapshot { - lg.Fatal("saveSnap(Snapshoter.Load) failed", zap.Error(err)) - } - if snapshot != nil { - walsnap.Index, walsnap.Term, walsnap.ConfState = snapshot.Metadata.Index, snapshot.Metadata.Term, &desired.confState - newss := snap.New(lg, destSnap) - snapshot.Metadata.ConfState = desired.confState - snapshot.Data = mustTranslateV2store(lg, snapshot.Data, desired) - if err = newss.SaveSnap(*snapshot); err != nil { - lg.Fatal("saveSnap(Snapshoter.SaveSnap) failed", zap.Error(err)) - } - } - return walsnap -} - -// mustTranslateV2store processes storeData such that they match 'desiredCluster'. -// In particular the method overrides membership information. -func mustTranslateV2store(lg *zap.Logger, storeData []byte, desired *desiredCluster) []byte { - st := v2store.New() - if err := st.Recovery(storeData); err != nil { - lg.Panic("cannot translate v2store", zap.Error(err)) - } - - raftCluster := membership.NewClusterFromMembers(lg, desired.clusterId, desired.members) - raftCluster.SetID(desired.nodeId, desired.clusterId) - raftCluster.SetStore(st) - raftCluster.PushMembershipToStorage() - - outputData, err := st.Save() - if err != nil { - lg.Panic("cannot save v2store", zap.Error(err)) - } - return outputData -} - -func translateWAL(lg *zap.Logger, srcWAL string, walsnap walpb.Snapshot, v3 bool) (etcdserverpb.Metadata, raftpb.HardState, []raftpb.Entry) { - w, err := wal.OpenForRead(lg, srcWAL, walsnap) - if err != nil { - lg.Fatal("wal.OpenForRead failed", zap.Error(err)) - } - defer w.Close() - wmetadata, state, ents, err := w.ReadAll() - switch err { - case nil: - case wal.ErrSnapshotNotFound: - lg.Warn("failed to find the match snapshot record", zap.Any("walsnap", walsnap), zap.String("srcWAL", srcWAL)) - lg.Warn("etcdctl will add it back. Start auto fixing...") - default: - lg.Fatal("unexpected error while reading WAL", zap.Error(err)) - } - - re := path.Join(membership.StoreMembersPrefix, "[[:xdigit:]]{1,16}", "attributes") - memberAttrRE := regexp.MustCompile(re) - - for i := 0; i < len(ents); i++ { - - // Replacing WAL entries with 'dummy' entries allows to avoid - // complicated entries shifting and risk of other data (like consistent_index) - // running out of sync. - // Also moving entries and computing offsets would get complicated if - // TERM changes (so there are superflous entries from previous term). - - if ents[i].Type == raftpb.EntryConfChange { - lg.Info("ignoring EntryConfChange raft entry") - raftEntryToNoOp(&ents[i]) - continue - } - - var raftReq etcdserverpb.InternalRaftRequest - var v2Req *etcdserverpb.Request - if pbutil.MaybeUnmarshal(&raftReq, ents[i].Data) { - v2Req = raftReq.V2 - } else { - v2Req = &etcdserverpb.Request{} - pbutil.MustUnmarshal(v2Req, ents[i].Data) - } - - if v2Req != nil && v2Req.Method == "PUT" && memberAttrRE.MatchString(v2Req.Path) { - lg.Info("ignoring member attribute update on", - zap.Stringer("entry", &ents[i]), - zap.String("v2Req.Path", v2Req.Path)) - raftEntryToNoOp(&ents[i]) - continue - } - - if v2Req != nil { - lg.Debug("preserving log entry", zap.Stringer("entry", &ents[i])) - } - - if raftReq.ClusterMemberAttrSet != nil { - lg.Info("ignoring cluster_member_attr_set") - raftEntryToNoOp(&ents[i]) - continue - } - - if v3 || raftReq.Header == nil { - lg.Debug("preserving log entry", zap.Stringer("entry", &ents[i])) - continue - } - lg.Info("ignoring v3 raft entry") - raftEntryToNoOp(&ents[i]) - } - var metadata etcdserverpb.Metadata - pbutil.MustUnmarshal(&metadata, wmetadata) - return metadata, state, ents -} - -func raftEntryToNoOp(entry *raftpb.Entry) { - // Empty (dummy) entries are send by RAFT when new leader is getting elected. - // They do not cary any change to data-model so its safe to replace entries - // to be ignored with them. - *entry = raftpb.Entry{Term: entry.Term, Index: entry.Index, Type: raftpb.EntryNormal, Data: nil} -} - -// saveDB copies the v3 backend and strips cluster information. -func saveDB(lg *zap.Logger, destDB, srcDB string, idx uint64, term uint64, desired *desiredCluster, v3 bool) { - - // open src db to safely copy db state - if v3 { - var src *bolt.DB - ch := make(chan *bolt.DB, 1) - go func() { - db, err := bolt.Open(srcDB, 0444, &bolt.Options{ReadOnly: true}) - if err != nil { - lg.Fatal("bolt.Open FAILED", zap.Error(err)) - } - ch <- db - }() - select { - case src = <-ch: - case <-time.After(time.Second): - lg.Fatal("timed out waiting to acquire lock on", zap.String("srcDB", srcDB)) - src = <-ch - } - defer src.Close() - - tx, err := src.Begin(false) - if err != nil { - lg.Fatal("bbolt.BeginTx failed", zap.Error(err)) - } - - // copy srcDB to destDB - dest, err := os.Create(destDB) - if err != nil { - lg.Fatal("creation of destination file failed", zap.String("dest", destDB), zap.Error(err)) - } - if _, err := tx.WriteTo(dest); err != nil { - lg.Fatal("bbolt write to destination file failed", zap.String("dest", destDB), zap.Error(err)) - } - dest.Close() - if err := tx.Rollback(); err != nil { - lg.Fatal("bbolt tx.Rollback failed", zap.String("dest", destDB), zap.Error(err)) - } - } - - be := backend.NewDefaultBackend(destDB) - defer be.Close() - - if err := membership.TrimClusterFromBackend(be); err != nil { - lg.Fatal("bbolt tx.Membership failed", zap.Error(err)) - } - - raftCluster := membership.NewClusterFromMembers(lg, desired.clusterId, desired.members) - raftCluster.SetID(desired.nodeId, desired.clusterId) - raftCluster.SetBackend(be) - raftCluster.PushMembershipToStorage() - - if !v3 { - tx := be.BatchTx() - tx.Lock() - defer tx.Unlock() - cindex.UnsafeCreateMetaBucket(tx) - cindex.UnsafeUpdateConsistentIndex(tx, idx, term, false) - } else { - // Thanks to translateWAL not moving entries, but just replacing them with - // 'empty', there is no need to update the consistency index. - } - -} diff --git a/etcdctl/ctlv3/command/defrag_command.go b/etcdctl/ctlv3/command/defrag_command.go index 2d92dc45843..42e47cbb905 100644 --- a/etcdctl/ctlv3/command/defrag_command.go +++ b/etcdctl/ctlv3/command/defrag_command.go @@ -17,12 +17,10 @@ package command import ( "fmt" "os" - "path/filepath" - "time" "github.com/spf13/cobra" + "go.etcd.io/etcd/etcdutl/v3/etcdutl" "go.etcd.io/etcd/pkg/v3/cobrautl" - "go.etcd.io/etcd/server/v3/mvcc/backend" ) var ( @@ -43,12 +41,11 @@ func NewDefragCommand() *cobra.Command { func defragCommandFunc(cmd *cobra.Command, args []string) { if len(defragDataDir) > 0 { - err := defragData(defragDataDir) + fmt.Fprintf(os.Stderr, "Use `etcdutl defrag` instead. The --data-dir is going to be decomissioned in v3.6.\n\n") + err := etcdutl.DefragData(defragDataDir) if err != nil { - fmt.Fprintf(os.Stderr, "Failed to defragment etcd data[%s] (%v)\n", defragDataDir, err) - os.Exit(cobrautl.ExitError) + cobrautl.ExitWithError(cobrautl.ExitError, err) } - return } failures := 0 @@ -69,23 +66,3 @@ func defragCommandFunc(cmd *cobra.Command, args []string) { os.Exit(cobrautl.ExitError) } } - -func defragData(dataDir string) error { - var be backend.Backend - - bch := make(chan struct{}) - dbDir := filepath.Join(dataDir, "member", "snap", "db") - go func() { - defer close(bch) - be = backend.NewDefaultBackend(dbDir) - - }() - select { - case <-bch: - case <-time.After(time.Second): - fmt.Fprintf(os.Stderr, "waiting for etcd to close and release its lock on %q. "+ - "To defrag a running etcd instance, omit --data-dir.\n", dbDir) - <-bch - } - return be.Defrag() -} diff --git a/etcdctl/ctlv3/command/migrate_command.go b/etcdctl/ctlv3/command/migrate_command.go deleted file mode 100644 index 0ebdc95ea5b..00000000000 --- a/etcdctl/ctlv3/command/migrate_command.go +++ /dev/null @@ -1,407 +0,0 @@ -// Copyright 2016 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 command - -import ( - "encoding/binary" - "encoding/json" - "fmt" - "io" - "os" - "os/exec" - "path/filepath" - "time" - - pb "go.etcd.io/etcd/api/v3/etcdserverpb" - "go.etcd.io/etcd/api/v3/mvccpb" - "go.etcd.io/etcd/client/pkg/v3/types" - "go.etcd.io/etcd/client/v2" - "go.etcd.io/etcd/pkg/v3/cobrautl" - "go.etcd.io/etcd/pkg/v3/pbutil" - "go.etcd.io/etcd/raft/v3/raftpb" - "go.etcd.io/etcd/server/v3/etcdserver" - "go.etcd.io/etcd/server/v3/etcdserver/api" - "go.etcd.io/etcd/server/v3/etcdserver/api/membership" - "go.etcd.io/etcd/server/v3/etcdserver/api/snap" - "go.etcd.io/etcd/server/v3/etcdserver/api/v2error" - "go.etcd.io/etcd/server/v3/etcdserver/api/v2store" - "go.etcd.io/etcd/server/v3/etcdserver/cindex" - "go.etcd.io/etcd/server/v3/mvcc" - "go.etcd.io/etcd/server/v3/mvcc/backend" - "go.etcd.io/etcd/server/v3/wal" - "go.etcd.io/etcd/server/v3/wal/walpb" - - "github.com/gogo/protobuf/proto" - "github.com/spf13/cobra" - "go.uber.org/zap" -) - -var ( - migrateExcludeTTLKey bool - migrateDatadir string - migrateWALdir string - migrateTransformer string -) - -// NewMigrateCommand returns the cobra command for "migrate". -func NewMigrateCommand() *cobra.Command { - mc := &cobra.Command{ - Use: "migrate", - Short: "Migrates keys in a v2 store to a mvcc store", - Run: migrateCommandFunc, - } - - mc.Flags().BoolVar(&migrateExcludeTTLKey, "no-ttl", false, "Do not convert TTL keys") - mc.Flags().StringVar(&migrateDatadir, "data-dir", "", "Path to the data directory") - mc.Flags().StringVar(&migrateWALdir, "wal-dir", "", "Path to the WAL directory") - mc.Flags().StringVar(&migrateTransformer, "transformer", "", "Path to the user-provided transformer program") - return mc -} - -func migrateCommandFunc(cmd *cobra.Command, args []string) { - var ( - writer io.WriteCloser - reader io.ReadCloser - errc chan error - ) - if migrateTransformer != "" { - writer, reader, errc = startTransformer() - } else { - fmt.Println("using default transformer") - writer, reader, errc = defaultTransformer() - } - - st, index, term := rebuildStoreV2() - be := prepareBackend() - defer be.Close() - - go func() { - writeStore(writer, st) - writer.Close() - }() - - readKeys(reader, be) - cindex.UpdateConsistentIndex(be.BatchTx(), index, term, true) - err := <-errc - if err != nil { - fmt.Println("failed to transform keys") - cobrautl.ExitWithError(cobrautl.ExitError, err) - } - - fmt.Println("finished transforming keys") -} - -func prepareBackend() backend.Backend { - var be backend.Backend - - bch := make(chan struct{}) - dbpath := filepath.Join(migrateDatadir, "member", "snap", "db") - go func() { - defer close(bch) - be = backend.NewDefaultBackend(dbpath) - - }() - select { - case <-bch: - case <-time.After(time.Second): - fmt.Fprintf(os.Stderr, "waiting for etcd to close and release its lock on %q\n", dbpath) - <-bch - } - - tx := be.BatchTx() - tx.Lock() - tx.UnsafeCreateBucket([]byte("key")) - tx.UnsafeCreateBucket([]byte("meta")) - tx.Unlock() - return be -} - -func rebuildStoreV2() (st v2store.Store, index uint64, term uint64) { - cl := membership.NewCluster(zap.NewExample()) - - waldir := migrateWALdir - if len(waldir) == 0 { - waldir = filepath.Join(migrateDatadir, "member", "wal") - } - snapdir := filepath.Join(migrateDatadir, "member", "snap") - - ss := snap.New(zap.NewExample(), snapdir) - snapshot, err := ss.Load() - if err != nil && err != snap.ErrNoSnapshot { - cobrautl.ExitWithError(cobrautl.ExitError, err) - } - - var walsnap walpb.Snapshot - if snapshot != nil { - walsnap.Index, walsnap.Term = snapshot.Metadata.Index, snapshot.Metadata.Term - index = snapshot.Metadata.Index - term = snapshot.Metadata.Term - } - - w, err := wal.OpenForRead(zap.NewExample(), waldir, walsnap) - if err != nil { - cobrautl.ExitWithError(cobrautl.ExitError, err) - } - defer w.Close() - - _, _, ents, err := w.ReadAll() - if err != nil { - cobrautl.ExitWithError(cobrautl.ExitError, err) - } - - st = v2store.New() - if snapshot != nil { - err := st.Recovery(snapshot.Data) - if err != nil { - cobrautl.ExitWithError(cobrautl.ExitError, err) - } - } - - cl.SetStore(st) - cl.Recover(api.UpdateCapability) - - applier := etcdserver.NewApplierV2(zap.NewExample(), st, cl) - for _, ent := range ents { - if ent.Type == raftpb.EntryConfChange { - var cc raftpb.ConfChange - pbutil.MustUnmarshal(&cc, ent.Data) - applyConf(cc, cl) - continue - } - - var raftReq pb.InternalRaftRequest - if !pbutil.MaybeUnmarshal(&raftReq, ent.Data) { // backward compatible - var r pb.Request - pbutil.MustUnmarshal(&r, ent.Data) - applyRequest(&r, applier) - } else { - if raftReq.V2 != nil { - req := raftReq.V2 - applyRequest(req, applier) - } - } - if ent.Index >= index { - index = ent.Index - term = ent.Term - } - } - - return st, index, term -} - -func applyConf(cc raftpb.ConfChange, cl *membership.RaftCluster) { - if err := cl.ValidateConfigurationChange(cc); err != nil { - return - } - switch cc.Type { - case raftpb.ConfChangeAddNode: - m := new(membership.Member) - if err := json.Unmarshal(cc.Context, m); err != nil { - panic(err) - } - cl.AddMember(m, true) - case raftpb.ConfChangeRemoveNode: - cl.RemoveMember(types.ID(cc.NodeID), true) - case raftpb.ConfChangeUpdateNode: - m := new(membership.Member) - if err := json.Unmarshal(cc.Context, m); err != nil { - panic(err) - } - cl.UpdateRaftAttributes(m.ID, m.RaftAttributes, true) - } -} - -func applyRequest(req *pb.Request, applyV2 etcdserver.ApplierV2) { - r := (*etcdserver.RequestV2)(req) - r.TTLOptions() - switch r.Method { - case "POST": - applyV2.Post(r) - case "PUT": - applyV2.Put(r) - case "DELETE": - applyV2.Delete(r) - case "QGET": - applyV2.QGet(r) - case "SYNC": - applyV2.Sync(r) - default: - panic("unknown command") - } -} - -func writeStore(w io.Writer, st v2store.Store) uint64 { - all, err := st.Get("/1", true, true) - if err != nil { - if eerr, ok := err.(*v2error.Error); ok && eerr.ErrorCode == v2error.EcodeKeyNotFound { - fmt.Println("no v2 keys to migrate") - os.Exit(0) - } - cobrautl.ExitWithError(cobrautl.ExitError, err) - } - return writeKeys(w, all.Node) -} - -func writeKeys(w io.Writer, n *v2store.NodeExtern) uint64 { - maxIndex := n.ModifiedIndex - - nodes := n.Nodes - // remove store v2 bucket prefix - n.Key = n.Key[2:] - if n.Key == "" { - n.Key = "/" - } - if n.Dir { - n.Nodes = nil - } - if !migrateExcludeTTLKey || n.TTL == 0 { - b, err := json.Marshal(n) - if err != nil { - cobrautl.ExitWithError(cobrautl.ExitError, err) - } - fmt.Fprint(w, string(b)) - } - for _, nn := range nodes { - max := writeKeys(w, nn) - if max > maxIndex { - maxIndex = max - } - } - return maxIndex -} - -func readKeys(r io.Reader, be backend.Backend) error { - for { - length64, err := readInt64(r) - if err != nil { - if err == io.EOF { - return nil - } - return err - } - - buf := make([]byte, int(length64)) - if _, err = io.ReadFull(r, buf); err != nil { - return err - } - - var kv mvccpb.KeyValue - err = proto.Unmarshal(buf, &kv) - if err != nil { - return err - } - - mvcc.WriteKV(be, kv) - } -} - -func readInt64(r io.Reader) (int64, error) { - var n int64 - err := binary.Read(r, binary.LittleEndian, &n) - return n, err -} - -func startTransformer() (io.WriteCloser, io.ReadCloser, chan error) { - cmd := exec.Command(migrateTransformer) - cmd.Stderr = os.Stderr - - writer, err := cmd.StdinPipe() - if err != nil { - cobrautl.ExitWithError(cobrautl.ExitError, err) - } - - reader, rerr := cmd.StdoutPipe() - if rerr != nil { - cobrautl.ExitWithError(cobrautl.ExitError, rerr) - } - - if err := cmd.Start(); err != nil { - cobrautl.ExitWithError(cobrautl.ExitError, err) - } - - errc := make(chan error, 1) - - go func() { - errc <- cmd.Wait() - }() - - return writer, reader, errc -} - -func defaultTransformer() (io.WriteCloser, io.ReadCloser, chan error) { - // transformer decodes v2 keys from sr - sr, sw := io.Pipe() - // transformer encodes v3 keys into dw - dr, dw := io.Pipe() - - decoder := json.NewDecoder(sr) - - errc := make(chan error, 1) - - go func() { - defer func() { - sr.Close() - dw.Close() - }() - - for decoder.More() { - node := &client.Node{} - if err := decoder.Decode(node); err != nil { - errc <- err - return - } - - kv := transform(node) - if kv == nil { - continue - } - - data, err := proto.Marshal(kv) - if err != nil { - errc <- err - return - } - buf := make([]byte, 8) - binary.LittleEndian.PutUint64(buf, uint64(len(data))) - if _, err := dw.Write(buf); err != nil { - errc <- err - return - } - if _, err := dw.Write(data); err != nil { - errc <- err - return - } - } - - errc <- nil - }() - - return sw, dr, errc -} - -func transform(n *client.Node) *mvccpb.KeyValue { - const unKnownVersion = 1 - if n.Dir { - return nil - } - kv := &mvccpb.KeyValue{ - Key: []byte(n.Key), - Value: []byte(n.Value), - CreateRevision: int64(n.CreatedIndex), - ModRevision: int64(n.ModifiedIndex), - Version: unKnownVersion, - } - return kv -} diff --git a/etcdctl/ctlv3/command/printer.go b/etcdctl/ctlv3/command/printer.go index 7a77a968c7e..2d31d9ec8c6 100644 --- a/etcdctl/ctlv3/command/printer.go +++ b/etcdctl/ctlv3/command/printer.go @@ -21,7 +21,6 @@ import ( pb "go.etcd.io/etcd/api/v3/etcdserverpb" v3 "go.etcd.io/etcd/client/v3" - "go.etcd.io/etcd/etcdctl/v3/snapshot" "go.etcd.io/etcd/pkg/v3/cobrautl" "github.com/dustin/go-humanize" @@ -52,7 +51,6 @@ type printer interface { MoveLeader(leader, target uint64, r v3.MoveLeaderResponse) Alarm(v3.AlarmResponse) - DBStatus(snapshot.Status) RoleAdd(role string, r v3.AuthRoleAddResponse) RoleGet(role string, r v3.AuthRoleGetResponse) @@ -160,7 +158,6 @@ func newPrinterUnsupported(n string) printer { func (p *printerUnsupported) EndpointHealth([]epHealth) { p.p(nil) } func (p *printerUnsupported) EndpointStatus([]epStatus) { p.p(nil) } func (p *printerUnsupported) EndpointHashKV([]epHashKV) { p.p(nil) } -func (p *printerUnsupported) DBStatus(snapshot.Status) { p.p(nil) } func (p *printerUnsupported) MoveLeader(leader, target uint64, r v3.MoveLeaderResponse) { p.p(nil) } @@ -230,14 +227,3 @@ func makeEndpointHashKVTable(hashList []epHashKV) (hdr []string, rows [][]string } return hdr, rows } - -func makeDBStatusTable(ds snapshot.Status) (hdr []string, rows [][]string) { - hdr = []string{"hash", "revision", "total keys", "total size"} - rows = append(rows, []string{ - fmt.Sprintf("%x", ds.Hash), - fmt.Sprint(ds.Revision), - fmt.Sprint(ds.TotalKey), - humanize.Bytes(uint64(ds.TotalSize)), - }) - return hdr, rows -} diff --git a/etcdctl/ctlv3/command/printer_fields.go b/etcdctl/ctlv3/command/printer_fields.go index 687fbba48ca..ca4611c735c 100644 --- a/etcdctl/ctlv3/command/printer_fields.go +++ b/etcdctl/ctlv3/command/printer_fields.go @@ -20,7 +20,6 @@ import ( pb "go.etcd.io/etcd/api/v3/etcdserverpb" spb "go.etcd.io/etcd/api/v3/mvccpb" v3 "go.etcd.io/etcd/client/v3" - "go.etcd.io/etcd/etcdctl/v3/snapshot" ) type fieldsPrinter struct{ printer } @@ -186,13 +185,6 @@ func (p *fieldsPrinter) Alarm(r v3.AlarmResponse) { } } -func (p *fieldsPrinter) DBStatus(r snapshot.Status) { - fmt.Println(`"Hash" :`, r.Hash) - fmt.Println(`"Revision" :`, r.Revision) - fmt.Println(`"Keys" :`, r.TotalKey) - fmt.Println(`"Size" :`, r.TotalSize) -} - func (p *fieldsPrinter) RoleAdd(role string, r v3.AuthRoleAddResponse) { p.hdr(r.Header) } func (p *fieldsPrinter) RoleGet(role string, r v3.AuthRoleGetResponse) { p.hdr(r.Header) diff --git a/etcdctl/ctlv3/command/printer_json.go b/etcdctl/ctlv3/command/printer_json.go index 22bc4f482c1..ca90a4a311c 100644 --- a/etcdctl/ctlv3/command/printer_json.go +++ b/etcdctl/ctlv3/command/printer_json.go @@ -22,7 +22,6 @@ import ( "strconv" "go.etcd.io/etcd/client/v3" - "go.etcd.io/etcd/etcdctl/v3/snapshot" ) type jsonPrinter struct { @@ -40,7 +39,6 @@ func newJSONPrinter(isHex bool) printer { func (p *jsonPrinter) EndpointHealth(r []epHealth) { printJSON(r) } func (p *jsonPrinter) EndpointStatus(r []epStatus) { printJSON(r) } func (p *jsonPrinter) EndpointHashKV(r []epHashKV) { printJSON(r) } -func (p *jsonPrinter) DBStatus(r snapshot.Status) { printJSON(r) } func (p *jsonPrinter) MemberList(r clientv3.MemberListResponse) { if p.isHex { diff --git a/etcdctl/ctlv3/command/printer_simple.go b/etcdctl/ctlv3/command/printer_simple.go index 96be1cab360..c5939fa4728 100644 --- a/etcdctl/ctlv3/command/printer_simple.go +++ b/etcdctl/ctlv3/command/printer_simple.go @@ -22,7 +22,6 @@ import ( pb "go.etcd.io/etcd/api/v3/etcdserverpb" "go.etcd.io/etcd/client/pkg/v3/types" v3 "go.etcd.io/etcd/client/v3" - "go.etcd.io/etcd/etcdctl/v3/snapshot" ) type simplePrinter struct { @@ -171,13 +170,6 @@ func (s *simplePrinter) EndpointHashKV(hashList []epHashKV) { } } -func (s *simplePrinter) DBStatus(ds snapshot.Status) { - _, rows := makeDBStatusTable(ds) - for _, row := range rows { - fmt.Println(strings.Join(row, ", ")) - } -} - func (s *simplePrinter) MoveLeader(leader, target uint64, r v3.MoveLeaderResponse) { fmt.Printf("Leadership transferred from %s to %s\n", types.ID(leader), types.ID(target)) } diff --git a/etcdctl/ctlv3/command/printer_table.go b/etcdctl/ctlv3/command/printer_table.go index d9d49a2ec2c..2bc6cfcf603 100644 --- a/etcdctl/ctlv3/command/printer_table.go +++ b/etcdctl/ctlv3/command/printer_table.go @@ -18,7 +18,6 @@ import ( "os" v3 "go.etcd.io/etcd/client/v3" - "go.etcd.io/etcd/etcdctl/v3/snapshot" "github.com/olekukonko/tablewriter" ) @@ -65,13 +64,3 @@ func (tp *tablePrinter) EndpointHashKV(r []epHashKV) { table.SetAlignment(tablewriter.ALIGN_RIGHT) table.Render() } -func (tp *tablePrinter) DBStatus(r snapshot.Status) { - hdr, rows := makeDBStatusTable(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/etcdctl/ctlv3/command/snapshot_command.go b/etcdctl/ctlv3/command/snapshot_command.go index 181626d33cc..e5d3f3f1c56 100644 --- a/etcdctl/ctlv3/command/snapshot_command.go +++ b/etcdctl/ctlv3/command/snapshot_command.go @@ -17,13 +17,12 @@ package command import ( "context" "fmt" - "path/filepath" - "strings" - - "go.etcd.io/etcd/etcdctl/v3/snapshot" - "go.etcd.io/etcd/pkg/v3/cobrautl" + "os" "github.com/spf13/cobra" + snapshot "go.etcd.io/etcd/client/v3/snapshot" + "go.etcd.io/etcd/etcdutl/v3/etcdutl" + "go.etcd.io/etcd/pkg/v3/cobrautl" "go.uber.org/zap" ) @@ -65,9 +64,11 @@ func NewSnapshotSaveCommand() *cobra.Command { func newSnapshotStatusCommand() *cobra.Command { return &cobra.Command{ Use: "status ", - Short: "Gets backend snapshot status of a given file", + Short: "[deprecated] Gets backend snapshot status of a given file", Long: `When --write-out is set to simple, this command prints out comma-separated status lists for each endpoint. The items in the lists are hash, revision, total keys, total size. + +Moved to 'etcdctl snapshot status ...' `, Run: snapshotStatusCommandFunc, } @@ -78,6 +79,7 @@ func NewSnapshotRestoreCommand() *cobra.Command { Use: "restore [options]", Short: "Restores an etcd member snapshot to an etcd directory", Run: snapshotRestoreCommandFunc, + Long: "Moved to `etcdctl snapshot restore ...`\n", } cmd.Flags().StringVar(&restoreDataDir, "data-dir", "", "Path to the data directory") cmd.Flags().StringVar(&restoreWalDir, "wal-dir", "", "Path to the WAL directory (use --data-dir if none given)") @@ -100,7 +102,6 @@ func snapshotSaveCommandFunc(cmd *cobra.Command, args []string) { if err != nil { cobrautl.ExitWithError(cobrautl.ExitError, err) } - sp := snapshot.NewV3(lg) cfg := mustClientCfgFromCmd(cmd) // if user does not specify "--command-timeout" flag, there will be no timeout for snapshot save command @@ -111,65 +112,21 @@ func snapshotSaveCommandFunc(cmd *cobra.Command, args []string) { defer cancel() path := args[0] - if err := sp.Save(ctx, *cfg, path); err != nil { + if err := snapshot.Save(ctx, lg, *cfg, path); err != nil { cobrautl.ExitWithError(cobrautl.ExitInterrupted, err) } fmt.Printf("Snapshot saved at %s\n", path) } func snapshotStatusCommandFunc(cmd *cobra.Command, args []string) { - if len(args) != 1 { - err := fmt.Errorf("snapshot status requires exactly one argument") - cobrautl.ExitWithError(cobrautl.ExitBadArgs, err) - } - initDisplayFromCmd(cmd) - - lg, err := zap.NewProduction() - if err != nil { - cobrautl.ExitWithError(cobrautl.ExitError, err) - } - sp := snapshot.NewV3(lg) - ds, err := sp.Status(args[0]) - if err != nil { - cobrautl.ExitWithError(cobrautl.ExitError, err) - } - display.DBStatus(ds) + fmt.Fprintf(os.Stderr, "Deprecated: Use `etcdutl snapshot status` instead.\n\n") + etcdutl.SnapshotStatusCommandFunc(cmd, args) } func snapshotRestoreCommandFunc(cmd *cobra.Command, args []string) { - if len(args) != 1 { - err := fmt.Errorf("snapshot restore requires exactly one argument") - cobrautl.ExitWithError(cobrautl.ExitBadArgs, err) - } - - dataDir := restoreDataDir - if dataDir == "" { - dataDir = restoreName + ".etcd" - } - - walDir := restoreWalDir - if walDir == "" { - walDir = filepath.Join(dataDir, "member", "wal") - } - - lg, err := zap.NewProduction() - if err != nil { - cobrautl.ExitWithError(cobrautl.ExitError, err) - } - sp := snapshot.NewV3(lg) - - if err := sp.Restore(snapshot.RestoreConfig{ - SnapshotPath: args[0], - Name: restoreName, - OutputDataDir: dataDir, - OutputWALDir: walDir, - PeerURLs: strings.Split(restorePeerURLs, ","), - InitialCluster: restoreCluster, - InitialClusterToken: restoreClusterToken, - SkipHashCheck: skipHashCheck, - }); err != nil { - cobrautl.ExitWithError(cobrautl.ExitError, err) - } + fmt.Fprintf(os.Stderr, "Deprecated: Use `etcdutl snapshot restore` instead.\n\n") + etcdutl.SnapshotRestoreCommandFunc(restoreCluster, restoreClusterToken, restoreDataDir, restoreWalDir, + restorePeerURLs, restoreName, skipHashCheck, args) } func initialClusterFromName(name string) string { diff --git a/etcdctl/ctlv3/ctl.go b/etcdctl/ctlv3/ctl.go index 5dc7ca8c77d..d25263c734d 100644 --- a/etcdctl/ctlv3/ctl.go +++ b/etcdctl/ctlv3/ctl.go @@ -87,7 +87,6 @@ func init() { command.NewMemberCommand(), command.NewSnapshotCommand(), command.NewMakeMirrorCommand(), - command.NewMigrateCommand(), command.NewLockCommand(), command.NewElectCommand(), command.NewAuthCommand(), diff --git a/etcdctl/go.mod b/etcdctl/go.mod index 6899a8c21ee..04001e5cee8 100644 --- a/etcdctl/go.mod +++ b/etcdctl/go.mod @@ -5,19 +5,16 @@ go 1.16 require ( github.com/bgentry/speakeasy v0.1.0 github.com/dustin/go-humanize v1.0.0 - github.com/gogo/protobuf v1.3.2 - github.com/olekukonko/tablewriter v0.0.4 + github.com/olekukonko/tablewriter v0.0.5 github.com/spf13/cobra v1.1.3 github.com/spf13/pflag v1.0.5 github.com/urfave/cli v1.22.4 - go.etcd.io/bbolt v1.3.6-0.20210426205525-9c92be978ae0 go.etcd.io/etcd/api/v3 v3.5.0-alpha.0 go.etcd.io/etcd/client/pkg/v3 v3.5.0-alpha.0 go.etcd.io/etcd/client/v2 v2.305.0-alpha.0 go.etcd.io/etcd/client/v3 v3.5.0-alpha.0 + go.etcd.io/etcd/etcdutl/v3 v3.5.0-alpha.0 go.etcd.io/etcd/pkg/v3 v3.5.0-alpha.0 - go.etcd.io/etcd/raft/v3 v3.5.0-alpha.0 - go.etcd.io/etcd/server/v3 v3.5.0-alpha.0 go.uber.org/zap v1.16.1-0.20210329175301-c23abee72d19 golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba google.golang.org/grpc v1.37.0 @@ -29,6 +26,7 @@ replace ( go.etcd.io/etcd/client/pkg/v3 => ../client/pkg go.etcd.io/etcd/client/v2 => ../client/v2 go.etcd.io/etcd/client/v3 => ../client/v3 + go.etcd.io/etcd/etcdutl/v3 => ../etcdutl go.etcd.io/etcd/pkg/v3 => ../pkg go.etcd.io/etcd/raft/v3 => ../raft go.etcd.io/etcd/server/v3 => ../server diff --git a/etcdctl/go.sum b/etcdctl/go.sum index a0eeaa3c25d..90c5668201b 100644 --- a/etcdctl/go.sum +++ b/etcdctl/go.sum @@ -190,8 +190,8 @@ github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRU github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-isatty v0.0.3 h1:ns/ykhmWi7G9O+8a448SecJU3nSMBXJfqQkl0upE1jI= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-runewidth v0.0.7 h1:Ei8KR0497xHyKJPAv59M1dkC+rOZCMBJ+t3fZ+twI54= -github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= @@ -211,8 +211,8 @@ github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9 github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= -github.com/olekukonko/tablewriter v0.0.4 h1:vHD/YYe1Wolo78koG299f7V/VAS08c6IpCLn+Ejf/w8= -github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= diff --git a/etcdutl/LICENSE b/etcdutl/LICENSE new file mode 100644 index 00000000000..d6456956733 --- /dev/null +++ b/etcdutl/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/etcdutl/README.md b/etcdutl/README.md new file mode 100644 index 00000000000..966a615110e --- /dev/null +++ b/etcdutl/README.md @@ -0,0 +1,196 @@ +etcdutl +======== + +`etcdutl` is a command line administration utility for [etcd][etcd]. + +It's designed to operate directly on etcd data files. +For operations over a network, please use `etcdctl`. + +### DEFRAG [options] + +DEFRAG directly defragments an etcd data directory while etcd is not running. +When an etcd member reclaims storage space from deleted and compacted keys, the space is kept in a free list and the database file remains the same size. By defragmenting the database, the etcd member releases this free space back to the file system. + +In order to defrag a live etcd instances over the network, please use `etcdctl defrag` instead. + +#### Options + +- data-dir -- Optional. If present, defragments a data directory not in use by etcd. + +#### Output + +Exit status '0' when the process was successful. + +#### Example + +To defragment a data directory directly, use the `--data-dir` flag: + +``` bash +# Defragment while etcd is not running +./etcdutl defrag --data-dir default.etcd +# success (exit status 0) +# Error: cannot open database at default.etcd/member/snap/db +``` + +#### Remarks + +DEFRAG returns a zero exit code only if it succeeded in defragmenting all given endpoints. + + +### SNAPSHOT RESTORE [options] \ + +SNAPSHOT RESTORE creates an etcd data directory for an etcd cluster member from a backend database snapshot and a new cluster configuration. Restoring the snapshot into each member for a new cluster configuration will initialize a new etcd cluster preloaded by the snapshot data. + +#### Options + +The snapshot restore options closely resemble to those used in the `etcd` command for defining a cluster. + +- data-dir -- Path to the data directory. Uses \.etcd if none given. + +- wal-dir -- Path to the WAL directory. Uses data directory if none given. + +- initial-cluster -- The initial cluster configuration for the restored etcd cluster. + +- initial-cluster-token -- Initial cluster token for the restored etcd cluster. + +- initial-advertise-peer-urls -- List of peer URLs for the member being restored. + +- name -- Human-readable name for the etcd cluster member being restored. + +- skip-hash-check -- Ignore snapshot integrity hash value (required if copied from data directory) + +#### Output + +A new etcd data directory initialized with the snapshot. + +#### Example + +Save a snapshot, restore into a new 3 node cluster, and start the cluster: +``` +./etcdutl snapshot save snapshot.db + +# restore members +bin/etcdutl snapshot restore snapshot.db --initial-cluster-token etcd-cluster-1 --initial-advertise-peer-urls http://127.0.0.1:12380 --name sshot1 --initial-cluster 'sshot1=http://127.0.0.1:12380,sshot2=http://127.0.0.1:22380,sshot3=http://127.0.0.1:32380' +bin/etcdutl snapshot restore snapshot.db --initial-cluster-token etcd-cluster-1 --initial-advertise-peer-urls http://127.0.0.1:22380 --name sshot2 --initial-cluster 'sshot1=http://127.0.0.1:12380,sshot2=http://127.0.0.1:22380,sshot3=http://127.0.0.1:32380' +bin/etcdutl snapshot restore snapshot.db --initial-cluster-token etcd-cluster-1 --initial-advertise-peer-urls http://127.0.0.1:32380 --name sshot3 --initial-cluster 'sshot1=http://127.0.0.1:12380,sshot2=http://127.0.0.1:22380,sshot3=http://127.0.0.1:32380' + +# launch members +bin/etcd --name sshot1 --listen-client-urls http://127.0.0.1:2379 --advertise-client-urls http://127.0.0.1:2379 --listen-peer-urls http://127.0.0.1:12380 & +bin/etcd --name sshot2 --listen-client-urls http://127.0.0.1:22379 --advertise-client-urls http://127.0.0.1:22379 --listen-peer-urls http://127.0.0.1:22380 & +bin/etcd --name sshot3 --listen-client-urls http://127.0.0.1:32379 --advertise-client-urls http://127.0.0.1:32379 --listen-peer-urls http://127.0.0.1:32380 & +``` + +### SNAPSHOT STATUS \ + +SNAPSHOT STATUS lists information about a given backend database snapshot file. + +#### Output + +##### Simple format + +Prints a humanized table of the database hash, revision, total keys, and size. + +##### JSON format + +Prints a line of JSON encoding the database hash, revision, total keys, and size. + +#### Examples +```bash +./etcdutl snapshot status file.db +# cf1550fb, 3, 3, 25 kB +``` + +```bash +./etcdutl --write-out=json snapshot status file.db +# {"hash":3474280699,"revision":3,"totalKey":3,"totalSize":24576} +``` + +```bash +./etcdutl --write-out=table snapshot status file.db ++----------+----------+------------+------------+ +| HASH | REVISION | TOTAL KEYS | TOTAL SIZE | ++----------+----------+------------+------------+ +| cf1550fb | 3 | 3 | 25 kB | ++----------+----------+------------+------------+ +``` + +### VERSION + +Prints the version of etcdutl. + +#### Output + +Prints etcd version and API version. + +#### Examples + +```bash +./etcdutl version +# etcdutl version: 3.1.0-alpha.0+git +# API version: 3.1 +``` + +### VERSION + +Prints the version of etcdctl. + +#### Output + +Prints etcd version and API version. + +#### Examples + +```bash +./etcdutl version +# etcdutl version: 3.5.0 +# API version: 3.1 +``` + + +## Exit codes + +For all commands, a successful execution returns a zero exit code. All failures will return non-zero exit codes. + +## Output formats + +All commands accept an output format by setting `-w` or `--write-out`. All commands default to the "simple" output format, which is meant to be human-readable. The simple format is listed in each command's `Output` description since it is customized for each command. If a command has a corresponding RPC, it will respect all output formats. + +If a command fails, returning a non-zero exit code, an error string will be written to standard error regardless of output format. + +### Simple + +A format meant to be easy to parse and human-readable. Specific to each command. + +### JSON + +The JSON encoding of the command's [RPC response][etcdrpc]. Since etcd's RPCs use byte strings, the JSON output will encode keys and values in base64. + +Some commands without an RPC also support JSON; see the command's `Output` description. + +### Protobuf + +The protobuf encoding of the command's [RPC response][etcdrpc]. If an RPC is streaming, the stream messages will be concatenated. If an RPC is not given for a command, the protobuf output is not defined. + +### Fields + +An output format similar to JSON but meant to parse with coreutils. For an integer field named `Field`, it writes a line in the format `"Field" : %d` where `%d` is go's integer formatting. For byte array fields, it writes `"Field" : %q` where `%q` is go's quoted string formatting (e.g., `[]byte{'a', '\n'}` is written as `"a\n"`). + +## Compatibility Support + +etcdutl is still in its early stage. We try out best to ensure fully compatible releases, however we might break compatibility to fix bugs or improve commands. If we intend to release a version of etcdutl with backward incompatibilities, we will provide notice prior to release and have instructions on how to upgrade. + +### Input Compatibility + +Input includes the command name, its flags, and its arguments. We ensure backward compatibility of the input of normal commands in non-interactive mode. + +### Output Compatibility +Currently, we do not ensure backward compatibility of utility commands. + +### TODO: compatibility with etcd server + +[etcd]: https://github.com/coreos/etcd +[READMEv2]: READMEv2.md +[v2key]: ../store/node_extern.go#L28-L37 +[v3key]: ../api/mvccpb/kv.proto#L12-L29 +[etcdrpc]: ../api/etcdserverpb/rpc.proto +[storagerpc]: ../api/mvccpb/kv.proto diff --git a/etcdutl/ctl.go b/etcdutl/ctl.go new file mode 100644 index 00000000000..a044547c63c --- /dev/null +++ b/etcdutl/ctl.go @@ -0,0 +1,55 @@ +// Copyright 2021 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 contains the main entry point for the etcdutl. +package main + +import ( + "github.com/spf13/cobra" + "go.etcd.io/etcd/etcdutl/v3/etcdutl" +) + +const ( + cliName = "etcdutl" + cliDescription = "An administrative command line tool for etcd3." +) + +var ( + rootCmd = &cobra.Command{ + Use: cliName, + Short: cliDescription, + SuggestFor: []string{"etcdutl"}, + } +) + +func init() { + rootCmd.PersistentFlags().StringVarP(&etcdutl.OutputFormat, "write-out", "w", "simple", "set the output format (fields, json, protobuf, simple, table)") + + rootCmd.AddCommand( + etcdutl.NewBackupCommand(), + etcdutl.NewDefragCommand(), + etcdutl.NewSnapshotCommand(), + etcdutl.NewVersionCommand(), + ) +} + +func Start() error { + // Make help just show the usage + rootCmd.SetHelpTemplate(`{{.UsageString}}`) + return rootCmd.Execute() +} + +func init() { + cobra.EnablePrefixMatching = true +} diff --git a/etcdutl/etcdutl/backup_command.go b/etcdutl/etcdutl/backup_command.go new file mode 100644 index 00000000000..c09bcf14a79 --- /dev/null +++ b/etcdutl/etcdutl/backup_command.go @@ -0,0 +1,331 @@ +// Copyright 2021 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 ( + "os" + "path" + "regexp" + "time" + + "github.com/spf13/cobra" + "go.etcd.io/etcd/api/v3/etcdserverpb" + "go.etcd.io/etcd/client/pkg/v3/fileutil" + "go.etcd.io/etcd/client/pkg/v3/types" + "go.etcd.io/etcd/pkg/v3/idutil" + "go.etcd.io/etcd/pkg/v3/pbutil" + "go.etcd.io/etcd/raft/v3/raftpb" + "go.etcd.io/etcd/server/v3/datadir" + "go.etcd.io/etcd/server/v3/etcdserver/api/membership" + "go.etcd.io/etcd/server/v3/etcdserver/api/snap" + "go.etcd.io/etcd/server/v3/etcdserver/api/v2store" + "go.etcd.io/etcd/server/v3/etcdserver/cindex" + "go.etcd.io/etcd/server/v3/mvcc/backend" + "go.etcd.io/etcd/server/v3/verify" + "go.etcd.io/etcd/server/v3/wal" + "go.etcd.io/etcd/server/v3/wal/walpb" + + bolt "go.etcd.io/bbolt" + "go.uber.org/zap" +) + +var ( + withV3 bool + dataDir string + backupDir string + walDir string + backupWalDir string +) + +func NewBackupCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "backup", + Short: "[legacy] offline backup of etcd directory", + + Long: "Prefer: `etcdctl snapshot save` instead.", + Run: doBackup, + } + cmd.Flags().StringVar(&dataDir, "data-dir", "", "Path to the etcd data dir") + cmd.Flags().StringVar(&walDir, "wal-dir", "", "Path to the etcd wal dir") + cmd.Flags().StringVar(&backupDir, "backup-dir", "", "Path to the backup dir") + cmd.Flags().StringVar(&backupWalDir, "backup-wal-dir", "", "Path to the backup wal dir") + cmd.Flags().BoolVar(&withV3, "with-v3", true, "Backup v3 backend data") + cmd.MarkFlagRequired("data-dir") + cmd.MarkFlagRequired("backup-dir") + return cmd +} + +func doBackup(cmd *cobra.Command, args []string) { + HandleBackup(withV3, dataDir, backupDir, walDir, backupWalDir) +} + +type desiredCluster struct { + clusterId types.ID + nodeId types.ID + members []*membership.Member + confState raftpb.ConfState +} + +func newDesiredCluster() desiredCluster { + idgen := idutil.NewGenerator(0, time.Now()) + nodeID := idgen.Next() + clusterID := idgen.Next() + + return desiredCluster{ + clusterId: types.ID(clusterID), + nodeId: types.ID(nodeID), + members: []*membership.Member{ + { + ID: types.ID(nodeID), + Attributes: membership.Attributes{ + Name: "etcdctl-v2-backup", + }, + RaftAttributes: membership.RaftAttributes{ + PeerURLs: []string{"http://use-flag--force-new-cluster:2080"}, + }}}, + confState: raftpb.ConfState{Voters: []uint64{nodeID}}, + } +} + +// HandleBackup handles a request that intends to do a backup. +func HandleBackup(withV3 bool, srcDir string, destDir string, srcWAL string, destWAL string) error { + lg := GetLogger() + + srcSnap := datadir.ToSnapDir(srcDir) + destSnap := datadir.ToSnapDir(destDir) + + if srcWAL == "" { + srcWAL = datadir.ToWalDir(srcDir) + } + + if destWAL == "" { + destWAL = datadir.ToWalDir(destDir) + } + + if err := fileutil.CreateDirAll(destSnap); err != nil { + lg.Fatal("failed creating backup snapshot dir", zap.String("dest-snap", destSnap), zap.Error(err)) + } + + destDbPath := datadir.ToBackendFileName(destDir) + srcDbPath := datadir.ToBackendFileName(srcDir) + desired := newDesiredCluster() + + walsnap := saveSnap(lg, destSnap, srcSnap, &desired) + metadata, state, ents := translateWAL(lg, srcWAL, walsnap, withV3) + saveDB(lg, destDbPath, srcDbPath, state.Commit, state.Term, &desired, withV3) + + neww, err := wal.Create(lg, destWAL, pbutil.MustMarshal(&metadata)) + if err != nil { + lg.Fatal("wal.Create failed", zap.Error(err)) + } + defer neww.Close() + if err := neww.Save(state, ents); err != nil { + lg.Fatal("wal.Save failed ", zap.Error(err)) + } + if err := neww.SaveSnapshot(walsnap); err != nil { + lg.Fatal("SaveSnapshot", zap.Error(err)) + } + + verify.MustVerifyIfEnabled(verify.Config{ + Logger: lg, + DataDir: destDir, + ExactIndex: false, + }) + + return nil +} + +func saveSnap(lg *zap.Logger, destSnap, srcSnap string, desired *desiredCluster) (walsnap walpb.Snapshot) { + ss := snap.New(lg, srcSnap) + snapshot, err := ss.Load() + if err != nil && err != snap.ErrNoSnapshot { + lg.Fatal("saveSnap(Snapshoter.Load) failed", zap.Error(err)) + } + if snapshot != nil { + walsnap.Index, walsnap.Term, walsnap.ConfState = snapshot.Metadata.Index, snapshot.Metadata.Term, &desired.confState + newss := snap.New(lg, destSnap) + snapshot.Metadata.ConfState = desired.confState + snapshot.Data = mustTranslateV2store(lg, snapshot.Data, desired) + if err = newss.SaveSnap(*snapshot); err != nil { + lg.Fatal("saveSnap(Snapshoter.SaveSnap) failed", zap.Error(err)) + } + } + return walsnap +} + +// mustTranslateV2store processes storeData such that they match 'desiredCluster'. +// In particular the method overrides membership information. +func mustTranslateV2store(lg *zap.Logger, storeData []byte, desired *desiredCluster) []byte { + st := v2store.New() + if err := st.Recovery(storeData); err != nil { + lg.Panic("cannot translate v2store", zap.Error(err)) + } + + raftCluster := membership.NewClusterFromMembers(lg, desired.clusterId, desired.members) + raftCluster.SetID(desired.nodeId, desired.clusterId) + raftCluster.SetStore(st) + raftCluster.PushMembershipToStorage() + + outputData, err := st.Save() + if err != nil { + lg.Panic("cannot save v2store", zap.Error(err)) + } + return outputData +} + +func translateWAL(lg *zap.Logger, srcWAL string, walsnap walpb.Snapshot, v3 bool) (etcdserverpb.Metadata, raftpb.HardState, []raftpb.Entry) { + w, err := wal.OpenForRead(lg, srcWAL, walsnap) + if err != nil { + lg.Fatal("wal.OpenForRead failed", zap.Error(err)) + } + defer w.Close() + wmetadata, state, ents, err := w.ReadAll() + switch err { + case nil: + case wal.ErrSnapshotNotFound: + lg.Warn("failed to find the match snapshot record", zap.Any("walsnap", walsnap), zap.String("srcWAL", srcWAL)) + lg.Warn("etcdctl will add it back. Start auto fixing...") + default: + lg.Fatal("unexpected error while reading WAL", zap.Error(err)) + } + + re := path.Join(membership.StoreMembersPrefix, "[[:xdigit:]]{1,16}", "attributes") + memberAttrRE := regexp.MustCompile(re) + + for i := 0; i < len(ents); i++ { + + // Replacing WAL entries with 'dummy' entries allows to avoid + // complicated entries shifting and risk of other data (like consistent_index) + // running out of sync. + // Also moving entries and computing offsets would get complicated if + // TERM changes (so there are superflous entries from previous term). + + if ents[i].Type == raftpb.EntryConfChange { + lg.Info("ignoring EntryConfChange raft entry") + raftEntryToNoOp(&ents[i]) + continue + } + + var raftReq etcdserverpb.InternalRaftRequest + var v2Req *etcdserverpb.Request + if pbutil.MaybeUnmarshal(&raftReq, ents[i].Data) { + v2Req = raftReq.V2 + } else { + v2Req = &etcdserverpb.Request{} + pbutil.MustUnmarshal(v2Req, ents[i].Data) + } + + if v2Req != nil && v2Req.Method == "PUT" && memberAttrRE.MatchString(v2Req.Path) { + lg.Info("ignoring member attribute update on", + zap.Stringer("entry", &ents[i]), + zap.String("v2Req.Path", v2Req.Path)) + raftEntryToNoOp(&ents[i]) + continue + } + + if v2Req != nil { + lg.Debug("preserving log entry", zap.Stringer("entry", &ents[i])) + } + + if raftReq.ClusterMemberAttrSet != nil { + lg.Info("ignoring cluster_member_attr_set") + raftEntryToNoOp(&ents[i]) + continue + } + + if v3 || raftReq.Header == nil { + lg.Debug("preserving log entry", zap.Stringer("entry", &ents[i])) + continue + } + lg.Info("ignoring v3 raft entry") + raftEntryToNoOp(&ents[i]) + } + var metadata etcdserverpb.Metadata + pbutil.MustUnmarshal(&metadata, wmetadata) + return metadata, state, ents +} + +func raftEntryToNoOp(entry *raftpb.Entry) { + // Empty (dummy) entries are send by RAFT when new leader is getting elected. + // They do not cary any change to data-model so its safe to replace entries + // to be ignored with them. + *entry = raftpb.Entry{Term: entry.Term, Index: entry.Index, Type: raftpb.EntryNormal, Data: nil} +} + +// saveDB copies the v3 backend and strips cluster information. +func saveDB(lg *zap.Logger, destDB, srcDB string, idx uint64, term uint64, desired *desiredCluster, v3 bool) { + + // open src db to safely copy db state + if v3 { + var src *bolt.DB + ch := make(chan *bolt.DB, 1) + go func() { + db, err := bolt.Open(srcDB, 0444, &bolt.Options{ReadOnly: true}) + if err != nil { + lg.Fatal("bolt.Open FAILED", zap.Error(err)) + } + ch <- db + }() + select { + case src = <-ch: + case <-time.After(time.Second): + lg.Fatal("timed out waiting to acquire lock on", zap.String("srcDB", srcDB)) + src = <-ch + } + defer src.Close() + + tx, err := src.Begin(false) + if err != nil { + lg.Fatal("bbolt.BeginTx failed", zap.Error(err)) + } + + // copy srcDB to destDB + dest, err := os.Create(destDB) + if err != nil { + lg.Fatal("creation of destination file failed", zap.String("dest", destDB), zap.Error(err)) + } + if _, err := tx.WriteTo(dest); err != nil { + lg.Fatal("bbolt write to destination file failed", zap.String("dest", destDB), zap.Error(err)) + } + dest.Close() + if err := tx.Rollback(); err != nil { + lg.Fatal("bbolt tx.Rollback failed", zap.String("dest", destDB), zap.Error(err)) + } + } + + be := backend.NewDefaultBackend(destDB) + defer be.Close() + + if err := membership.TrimClusterFromBackend(be); err != nil { + lg.Fatal("bbolt tx.Membership failed", zap.Error(err)) + } + + raftCluster := membership.NewClusterFromMembers(lg, desired.clusterId, desired.members) + raftCluster.SetID(desired.nodeId, desired.clusterId) + raftCluster.SetBackend(be) + raftCluster.PushMembershipToStorage() + + if !v3 { + tx := be.BatchTx() + tx.Lock() + defer tx.Unlock() + cindex.UnsafeCreateMetaBucket(tx) + cindex.UnsafeUpdateConsistentIndex(tx, idx, term, false) + } else { + // Thanks to translateWAL not moving entries, but just replacing them with + // 'empty', there is no need to update the consistency index. + } + +} diff --git a/etcdutl/etcdutl/common.go b/etcdutl/etcdutl/common.go new file mode 100644 index 00000000000..4b4a198aaf8 --- /dev/null +++ b/etcdutl/etcdutl/common.go @@ -0,0 +1,32 @@ +// Copyright 2021 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 ( + "go.etcd.io/etcd/pkg/v3/cobrautl" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +func GetLogger() *zap.Logger { + config := zap.NewProductionConfig() + config.Encoding = "console" + config.EncoderConfig.EncodeTime = zapcore.RFC3339TimeEncoder + lg, err := config.Build() + if err != nil { + cobrautl.ExitWithError(cobrautl.ExitBadArgs, err) + } + return lg +} diff --git a/etcdutl/etcdutl/defrag_command.go b/etcdutl/etcdutl/defrag_command.go new file mode 100644 index 00000000000..1660dd7071a --- /dev/null +++ b/etcdutl/etcdutl/defrag_command.go @@ -0,0 +1,72 @@ +// Copyright 2021 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 ( + "fmt" + "os" + "time" + + "github.com/spf13/cobra" + "go.etcd.io/etcd/pkg/v3/cobrautl" + "go.etcd.io/etcd/server/v3/datadir" + "go.etcd.io/etcd/server/v3/mvcc/backend" +) + +var ( + defragDataDir string +) + +// NewDefragCommand returns the cobra command for "Defrag". +func NewDefragCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "defrag", + Short: "Defragments the storage of the etcd", + Run: defragCommandFunc, + } + cmd.Flags().StringVar(&defragDataDir, "data-dir", "", "Required. Defragments a data directory not in use by etcd.") + cmd.MarkFlagRequired("data-dir") + return cmd +} + +func defragCommandFunc(cmd *cobra.Command, args []string) { + err := DefragData(defragDataDir) + if err != nil { + cobrautl.ExitWithError(cobrautl.ExitError, + fmt.Errorf("Failed to defragment etcd data[%s] (%v)", defragDataDir, err)) + } +} + +func DefragData(dataDir string) error { + var be backend.Backend + lg := GetLogger() + bch := make(chan struct{}) + dbDir := datadir.ToBackendFileName(dataDir) + go func() { + defer close(bch) + cfg := backend.DefaultBackendConfig() + cfg.Logger = lg + cfg.Path = dbDir + be = backend.New(cfg) + }() + select { + case <-bch: + case <-time.After(time.Second): + fmt.Fprintf(os.Stderr, "waiting for etcd to close and release its lock on %q. "+ + "To defrag a running etcd instance, omit --data-dir.\n", dbDir) + <-bch + } + return be.Defrag() +} diff --git a/etcdutl/etcdutl/printer.go b/etcdutl/etcdutl/printer.go new file mode 100644 index 00000000000..ad4e60246e6 --- /dev/null +++ b/etcdutl/etcdutl/printer.go @@ -0,0 +1,88 @@ +// Copyright 2021 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 ( + "errors" + "fmt" + + "github.com/spf13/cobra" + "go.etcd.io/etcd/etcdutl/v3/snapshot" + "go.etcd.io/etcd/pkg/v3/cobrautl" + + "github.com/dustin/go-humanize" +) + +var ( + OutputFormat string +) + +type printer interface { + DBStatus(snapshot.Status) +} + +func NewPrinter(printerType string) printer { + switch printerType { + case "simple": + return &simplePrinter{} + case "fields": + return &fieldsPrinter{newPrinterUnsupported("fields")} + case "json": + return newJSONPrinter() + case "protobuf": + return newPBPrinter() + case "table": + return &tablePrinter{newPrinterUnsupported("table")} + } + return nil +} + +type printerRPC struct { + printer + p func(interface{}) +} + +type printerUnsupported struct{ printerRPC } + +func newPrinterUnsupported(n string) printer { + f := func(interface{}) { + cobrautl.ExitWithError(cobrautl.ExitBadFeature, errors.New(n+" not supported as output format")) + } + return &printerUnsupported{printerRPC{nil, f}} +} + +func (p *printerUnsupported) DBStatus(snapshot.Status) { p.p(nil) } + +func makeDBStatusTable(ds snapshot.Status) (hdr []string, rows [][]string) { + hdr = []string{"hash", "revision", "total keys", "total size"} + rows = append(rows, []string{ + fmt.Sprintf("%x", ds.Hash), + fmt.Sprint(ds.Revision), + fmt.Sprint(ds.TotalKey), + humanize.Bytes(uint64(ds.TotalSize)), + }) + return hdr, rows +} + +func initPrinterFromCmd(cmd *cobra.Command) (p printer) { + outputType, err := cmd.Flags().GetString("write-out") + if err != nil { + cobrautl.ExitWithError(cobrautl.ExitError, err) + } + if p = NewPrinter(outputType); p == nil { + cobrautl.ExitWithError(cobrautl.ExitBadFeature, errors.New("unsupported output format")) + } + return p +} diff --git a/etcdutl/etcdutl/printer_fields.go b/etcdutl/etcdutl/printer_fields.go new file mode 100644 index 00000000000..374312cf5d9 --- /dev/null +++ b/etcdutl/etcdutl/printer_fields.go @@ -0,0 +1,30 @@ +// Copyright 2021 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 ( + "fmt" + + "go.etcd.io/etcd/etcdutl/v3/snapshot" +) + +type fieldsPrinter struct{ printer } + +func (p *fieldsPrinter) DBStatus(r snapshot.Status) { + fmt.Println(`"Hash" :`, r.Hash) + fmt.Println(`"Revision" :`, r.Revision) + fmt.Println(`"Keys" :`, r.TotalKey) + fmt.Println(`"Size" :`, r.TotalSize) +} diff --git a/etcdutl/etcdutl/printer_json.go b/etcdutl/etcdutl/printer_json.go new file mode 100644 index 00000000000..38fe3e4548e --- /dev/null +++ b/etcdutl/etcdutl/printer_json.go @@ -0,0 +1,45 @@ +// Copyright 2021 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 ( + "encoding/json" + "fmt" + "os" + + "go.etcd.io/etcd/etcdutl/v3/snapshot" +) + +type jsonPrinter struct { + printer +} + +func newJSONPrinter() printer { + return &jsonPrinter{ + printer: &printerRPC{newPrinterUnsupported("json"), printJSON}, + } +} + +func (p *jsonPrinter) DBStatus(r snapshot.Status) { printJSON(r) } + +// !!! Share ?? +func printJSON(v interface{}) { + b, err := json.Marshal(v) + if err != nil { + fmt.Fprintf(os.Stderr, "%v\n", err) + return + } + fmt.Println(string(b)) +} diff --git a/etcdutl/etcdutl/printer_protobuf.go b/etcdutl/etcdutl/printer_protobuf.go new file mode 100644 index 00000000000..0a9003b475d --- /dev/null +++ b/etcdutl/etcdutl/printer_protobuf.go @@ -0,0 +1,47 @@ +// Copyright 2021 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 ( + "fmt" + "os" + + "go.etcd.io/etcd/pkg/v3/cobrautl" +) + +type pbPrinter struct{ printer } + +type pbMarshal interface { + Marshal() ([]byte, error) +} + +func newPBPrinter() printer { + return &pbPrinter{ + &printerRPC{newPrinterUnsupported("protobuf"), printPB}, + } +} + +func printPB(v interface{}) { + m, ok := v.(pbMarshal) + if !ok { + cobrautl.ExitWithError(cobrautl.ExitBadFeature, fmt.Errorf("marshal unsupported for type %T (%v)", v, v)) + } + b, err := m.Marshal() + if err != nil { + fmt.Fprintf(os.Stderr, "%v\n", err) + return + } + fmt.Print(string(b)) +} diff --git a/etcdutl/etcdutl/printer_simple.go b/etcdutl/etcdutl/printer_simple.go new file mode 100644 index 00000000000..306ebf0c7f3 --- /dev/null +++ b/etcdutl/etcdutl/printer_simple.go @@ -0,0 +1,32 @@ +// Copyright 2021 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 ( + "fmt" + "strings" + + "go.etcd.io/etcd/etcdutl/v3/snapshot" +) + +type simplePrinter struct { +} + +func (s *simplePrinter) DBStatus(ds snapshot.Status) { + _, rows := makeDBStatusTable(ds) + for _, row := range rows { + fmt.Println(strings.Join(row, ", ")) + } +} diff --git a/etcdutl/etcdutl/printer_table.go b/etcdutl/etcdutl/printer_table.go new file mode 100644 index 00000000000..2f8f81d4e6a --- /dev/null +++ b/etcdutl/etcdutl/printer_table.go @@ -0,0 +1,36 @@ +// Copyright 2021 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 ( + "os" + + "go.etcd.io/etcd/etcdutl/v3/snapshot" + + "github.com/olekukonko/tablewriter" +) + +type tablePrinter struct{ printer } + +func (tp *tablePrinter) DBStatus(r snapshot.Status) { + hdr, rows := makeDBStatusTable(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/etcdutl/etcdutl/snapshot_command.go b/etcdutl/etcdutl/snapshot_command.go new file mode 100644 index 00000000000..94ab2a5ac92 --- /dev/null +++ b/etcdutl/etcdutl/snapshot_command.go @@ -0,0 +1,167 @@ +// Copyright 2021 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 ( + "fmt" + "strings" + + "go.etcd.io/etcd/etcdutl/v3/snapshot" + "go.etcd.io/etcd/pkg/v3/cobrautl" + "go.etcd.io/etcd/server/v3/datadir" + + "github.com/spf13/cobra" +) + +const ( + defaultName = "default" + defaultInitialAdvertisePeerURLs = "http://localhost:2380" +) + +var ( + restoreCluster string + restoreClusterToken string + restoreDataDir string + restoreWalDir string + restorePeerURLs string + restoreName string + skipHashCheck bool +) + +// NewSnapshotCommand returns the cobra command for "snapshot". +func NewSnapshotCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "snapshot ", + Short: "Manages etcd node snapshots", + } + cmd.AddCommand(NewSnapshotSaveCommand()) + cmd.AddCommand(NewSnapshotRestoreCommand()) + cmd.AddCommand(newSnapshotStatusCommand()) + return cmd +} + +func NewSnapshotSaveCommand() *cobra.Command { + return &cobra.Command{ + Use: "save ", + Short: "Stores an etcd node backend snapshot to a given file", + Hidden: true, + DisableFlagsInUseLine: true, + Run: func(cmd *cobra.Command, args []string) { + cobrautl.ExitWithError(cobrautl.ExitBadArgs, + fmt.Errorf("In order to download snapshot use: "+ + "`etcdctl snapshot save ...`")) + }, + Deprecated: "Use `etcdctl snapshot save` to download snapshot", + } +} + +func newSnapshotStatusCommand() *cobra.Command { + return &cobra.Command{ + Use: "status ", + Short: "Gets backend snapshot status of a given file", + Long: `When --write-out is set to simple, this command prints out comma-separated status lists for each endpoint. +The items in the lists are hash, revision, total keys, total size. +`, + Run: SnapshotStatusCommandFunc, + } +} + +func NewSnapshotRestoreCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "restore --data-dir {output dir} [options]", + Short: "Restores an etcd member snapshot to an etcd directory", + Run: snapshotRestoreCommandFunc, + } + cmd.Flags().StringVar(&restoreDataDir, "data-dir", "", "Path to the output data directory") + cmd.Flags().StringVar(&restoreWalDir, "wal-dir", "", "Path to the WAL directory (use --data-dir if none given)") + cmd.Flags().StringVar(&restoreCluster, "initial-cluster", initialClusterFromName(defaultName), "Initial cluster configuration for restore bootstrap") + cmd.Flags().StringVar(&restoreClusterToken, "initial-cluster-token", "etcd-cluster", "Initial cluster token for the etcd cluster during restore bootstrap") + cmd.Flags().StringVar(&restorePeerURLs, "initial-advertise-peer-urls", defaultInitialAdvertisePeerURLs, "List of this member's peer URLs to advertise to the rest of the cluster") + cmd.Flags().StringVar(&restoreName, "name", defaultName, "Human-readable name for this member") + cmd.Flags().BoolVar(&skipHashCheck, "skip-hash-check", false, "Ignore snapshot integrity hash value (required if copied from data directory)") + + cmd.MarkFlagRequired("data-dir") + + return cmd +} + +func SnapshotStatusCommandFunc(cmd *cobra.Command, args []string) { + if len(args) != 1 { + err := fmt.Errorf("snapshot status requires exactly one argument") + cobrautl.ExitWithError(cobrautl.ExitBadArgs, err) + } + printer := initPrinterFromCmd(cmd) + + lg := GetLogger() + sp := snapshot.NewV3(lg) + ds, err := sp.Status(args[0]) + if err != nil { + cobrautl.ExitWithError(cobrautl.ExitError, err) + } + printer.DBStatus(ds) +} + +func snapshotRestoreCommandFunc(_ *cobra.Command, args []string) { + SnapshotRestoreCommandFunc(restoreCluster, restoreClusterToken, restoreDataDir, restoreWalDir, + restorePeerURLs, restoreName, skipHashCheck, args) +} + +func SnapshotRestoreCommandFunc(restoreCluster string, + restoreClusterToken string, + restoreDataDir string, + restoreWalDir string, + restorePeerURLs string, + restoreName string, + skipHashCheck bool, + args []string) { + if len(args) != 1 { + err := fmt.Errorf("snapshot restore requires exactly one argument") + cobrautl.ExitWithError(cobrautl.ExitBadArgs, err) + } + + dataDir := restoreDataDir + if dataDir == "" { + dataDir = restoreName + ".etcd" + } + + walDir := restoreWalDir + if walDir == "" { + walDir = datadir.ToWalDir(dataDir) + } + + lg := GetLogger() + sp := snapshot.NewV3(lg) + + if err := sp.Restore(snapshot.RestoreConfig{ + SnapshotPath: args[0], + Name: restoreName, + OutputDataDir: dataDir, + OutputWALDir: walDir, + PeerURLs: strings.Split(restorePeerURLs, ","), + InitialCluster: restoreCluster, + InitialClusterToken: restoreClusterToken, + SkipHashCheck: skipHashCheck, + }); err != nil { + cobrautl.ExitWithError(cobrautl.ExitError, err) + } +} + +func initialClusterFromName(name string) string { + n := name + if name == "" { + n = defaultName + } + return fmt.Sprintf("%s=http://localhost:2380", n) +} diff --git a/etcdutl/etcdutl/version_command.go b/etcdutl/etcdutl/version_command.go new file mode 100644 index 00000000000..1cb1a146b4b --- /dev/null +++ b/etcdutl/etcdutl/version_command.go @@ -0,0 +1,37 @@ +// Copyright 2021 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 ( + "fmt" + + "go.etcd.io/etcd/api/v3/version" + + "github.com/spf13/cobra" +) + +// NewVersionCommand prints out the version of etcd. +func NewVersionCommand() *cobra.Command { + return &cobra.Command{ + Use: "version", + Short: "Prints the version of etcdutl", + Run: versionCommandFunc, + } +} + +func versionCommandFunc(cmd *cobra.Command, args []string) { + fmt.Println("etcdutl version:", version.Version) + fmt.Println("API version:", version.APIVersion) +} diff --git a/etcdutl/go.mod b/etcdutl/go.mod new file mode 100644 index 00000000000..6e61a0e4ca7 --- /dev/null +++ b/etcdutl/go.mod @@ -0,0 +1,35 @@ +module go.etcd.io/etcd/etcdutl/v3 + +go 1.16 + +replace ( + go.etcd.io/etcd/api/v3 => ../api + go.etcd.io/etcd/client/pkg/v3 => ../client/pkg + go.etcd.io/etcd/client/v2 => ../client/v2 + go.etcd.io/etcd/client/v3 => ../client/v3 + go.etcd.io/etcd/pkg/v3 => ../pkg + go.etcd.io/etcd/raft/v3 => ../raft + go.etcd.io/etcd/server/v3 => ../server +) + +// Bad imports are sometimes causing attempts to pull that code. +// This makes the error more explicit. +replace ( + go.etcd.io/etcd => ./FORBIDDEN_DEPENDENCY + go.etcd.io/etcd/v3 => ./FORBIDDEN_DEPENDENCY + go.etcd.io/tests/v3 => ./FORBIDDEN_DEPENDENCY +) + +require ( + github.com/dustin/go-humanize v1.0.0 + github.com/olekukonko/tablewriter v0.0.5 + github.com/spf13/cobra v1.1.3 + go.etcd.io/bbolt v1.3.6-0.20210426205525-9c92be978ae0 + go.etcd.io/etcd/api/v3 v3.5.0-alpha.0 + go.etcd.io/etcd/client/pkg/v3 v3.5.0-alpha.0 + go.etcd.io/etcd/client/v3 v3.5.0-alpha.0 + go.etcd.io/etcd/pkg/v3 v3.5.0-alpha.0 + go.etcd.io/etcd/raft/v3 v3.5.0-alpha.0 + go.etcd.io/etcd/server/v3 v3.5.0-alpha.0 + go.uber.org/zap v1.16.1-0.20210329175301-c23abee72d19 +) diff --git a/etcdutl/go.sum b/etcdutl/go.sum new file mode 100644 index 00000000000..d44b8a0a84d --- /dev/null +++ b/etcdutl/go.sum @@ -0,0 +1,508 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3 h1:AVXDdKsrtX33oR9fbCMu/+c1o8Ofjq6Ku/MInaLVg5Y= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/benbjohnson/clock v1.0.3/go.mod h1:bGMdMPoPVvcYyt1gHDf4J2KE153Yf9BuiUKYMaxlTDM= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/certifi/gocertifi v0.0.0-20191021191039-0944d244cd40/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA= +github.com/certifi/gocertifi v0.0.0-20200922220541-2c3bb06c6054 h1:uH66TXeswKn5PW5zdZ39xEwfS9an067BirqA+P4QaLI= +github.com/certifi/gocertifi v0.0.0-20200922220541-2c3bb06c6054/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA= +github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cockroachdb/datadriven v0.0.0-20200714090401-bf6692d28da5 h1:xD/lrqdvwsc+O2bjSSi3YqY73Ke3LAiSCx49aCesA0E= +github.com/cockroachdb/datadriven v0.0.0-20200714090401-bf6692d28da5/go.mod h1:h6jFvWxBdQXxjopDMZyH2UVceIRfR84bdzbkoKrsWNo= +github.com/cockroachdb/errors v1.2.4 h1:Lap807SXTH5tri2TivECb/4abUkMZC9zRoLarvcKDqs= +github.com/cockroachdb/errors v1.2.4/go.mod h1:rQD95gz6FARkaKkQXUksEje/d9a6wBJoCr5oaCLELYA= +github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f h1:o/kfcElHqOiXqcou5a3rIlMc7oJbMQkeLk0VQJ7zgqY= +github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f/go.mod h1:i/u985jwjWRlyHXQbwatDASoW0RMlZ/3i9yJHE2xLkI= +github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e h1:Wf6HqHfScWJN9/ZjdUKyjop4mf3Qdd+1TvvltAvM3m8= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd/v22 v22.3.1 h1:7OO2CXWMYNDdaAzP51t4lCCZWwpQHmvPbm9sxWjm3So= +github.com/coreos/go-systemd/v22 v22.3.1/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/form3tech-oss/jwt-go v3.2.3+incompatible h1:7ZaBxOI7TMoYBfyA3cQHErNNyAWIKUMIwqxEtgHOs5c= +github.com/form3tech-oss/jwt-go v3.2.3+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/getsentry/raven-go v0.2.0 h1:no+xWJRb5ZI7eE8TWgIq1jLulQiIoLG0IfYxv5JYMGs= +github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= +github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= +github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= +github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/jonboulle/clockwork v0.2.2 h1:UOGuzwb1PwsrDAObMuhUnj0p5ULPj8V/xJ7Kx9qUBdQ= +github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= +github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= +github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.5.1 h1:bdHYieyGlH+6OLEk2YQha8THib30KP0/yD0YH9m6xcA= +github.com/prometheus/client_golang v1.5.1/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= +github.com/prometheus/common v0.10.0 h1:RyRA7RzGXQZiW+tGMr7sxa85G1z0yOpM1qq5c8lNawc= +github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= +github.com/prometheus/procfs v0.2.0 h1:wH4vA7pcjKuZzjF7lM8awk4fnuJO6idemZXoKnULUx4= +github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v1.1.3 h1:xghbfqPkxzxP3C/f3n5DdpAbdKLj4ZE4BWQI362l53M= +github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 h1:eY9dn8+vbi4tKz5Qo6v2eYzo7kUS51QINcR5jNpbZS8= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/bbolt v1.3.6-0.20210426205525-9c92be978ae0 h1:FPuyGXkE6qPKJ71PyS0sdXuxUvYGXAXxV0XHpx0qjHE= +go.etcd.io/bbolt v1.3.6-0.20210426205525-9c92be978ae0/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opentelemetry.io/contrib v0.20.0 h1:ubFQUn0VCZ0gPwIoJfBJVpeBlyRMxu8Mm/huKWYd9p0= +go.opentelemetry.io/contrib v0.20.0/go.mod h1:G/EtFaa6qaN7+LxqfIAT3GiZa7Wv5DTBUzl5H4LY0Kc= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.20.0 h1:sO4WKdPAudZGKPcpZT4MJn6JaDmpyLrMPDGGyA1SttE= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.20.0/go.mod h1:oVGt1LRbBOBq1A5BQLlUg9UaU/54aiHw8cgjV3aWZ/E= +go.opentelemetry.io/otel v0.20.0 h1:eaP0Fqu7SXHwvjiqDq83zImeehOHX8doTvU9AwXON8g= +go.opentelemetry.io/otel v0.20.0/go.mod h1:Y3ugLH2oa81t5QO+Lty+zXf8zC9L26ax4Nzoxm/dooo= +go.opentelemetry.io/otel/exporters/otlp v0.20.0/go.mod h1:YIieizyaN77rtLJra0buKiNBOm9XQfkPEKBeuhoMwAM= +go.opentelemetry.io/otel/metric v0.20.0 h1:4kzhXFP+btKm4jwxpjIqjs41A7MakRFUS86bqLHTIw8= +go.opentelemetry.io/otel/metric v0.20.0/go.mod h1:598I5tYlH1vzBjn+BTuhzTCSb/9debfNp6R3s7Pr1eU= +go.opentelemetry.io/otel/oteltest v0.20.0 h1:HiITxCawalo5vQzdHfKeZurV8x7ljcqAgiWzF6Vaeaw= +go.opentelemetry.io/otel/oteltest v0.20.0/go.mod h1:L7bgKf9ZB7qCwT9Up7i9/pn0PWIa9FqQ2IQ8LoxiGnw= +go.opentelemetry.io/otel/sdk v0.20.0/go.mod h1:g/IcepuwNsoiX5Byy2nNV0ySUF1em498m7hBWC279Yc= +go.opentelemetry.io/otel/sdk/export/metric v0.20.0/go.mod h1:h7RBNMsDJ5pmI1zExLi+bJK+Dr8NQCh0qGhm1KDnNlE= +go.opentelemetry.io/otel/sdk/metric v0.20.0/go.mod h1:knxiS8Xd4E/N+ZqKmUPf3gTTZ4/0TjTXukfxjzSTpHE= +go.opentelemetry.io/otel/trace v0.20.0 h1:1DL6EXUdcg95gukhuRRvLDO/4X5THh/5dIV52lqtnbw= +go.opentelemetry.io/otel/trace v0.20.0/go.mod h1:6GjCW8zgDjwGHGa6GkyeB8+/5vjT16gUEi0Nf1iBdgw= +go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.1.10 h1:z+mqJhf6ss6BSfSM671tgKyZBFPTTJM+HLxnhPC3wu0= +go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.16.1-0.20210329175301-c23abee72d19 h1:040c3dLNhgFQkoojH2AMpHCy4SrvhmxdU72d9GLGGE0= +go.uber.org/zap v1.16.1-0.20210329175301-c23abee72d19/go.mod h1:aMfIlz3TDBfB0BwTCKFU1XbEmj9zevr5S5LcBr85MXw= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0 h1:hb9wdF1z5waM+dSIICn1l0DkLVDT3hqhhQsDNUmHPRE= +golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb h1:eBmm0M9fYhWpKZLjQUUKka/LtIxf46G4fxeEz5KJr9U= +golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57 h1:F5Gozwx4I1xtr/sr/8CFbb57iKi3297KFs0QDbGN60A= +golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba h1:O8mE0/t419eoIwhTFpKVkHiTs/Igowgfkj25AcZrtiE= +golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a h1:CB3a9Nez8M13wwlr/E2YtwoU+qYHKfC+JrDa45RXXoQ= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1 h1:QzqyMA1tlu6CgqCDUtU9V+ZKhLFT2dkJuANu5QaxI3I= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 h1:+kGHl1aib/qcwaRi1CbqBZ1rk19r85MNUf8HaBghugY= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.37.0 h1:uSZWeQJX5j11bIQ4AJoj+McDBo29cY1MCoC1wO3ts+c= +google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= diff --git a/etcdutl/main.go b/etcdutl/main.go new file mode 100644 index 00000000000..bff0b1d869b --- /dev/null +++ b/etcdutl/main.go @@ -0,0 +1,26 @@ +// Copyright 2021 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. + +// etcdutl is a command line application that operates on etcd files. +package main + +import ( + "go.etcd.io/etcd/pkg/v3/cobrautl" +) + +func main() { + if err := Start(); err != nil { + cobrautl.ExitWithError(cobrautl.ExitError, err) + } +} diff --git a/etcdutl/main_test.go b/etcdutl/main_test.go new file mode 100644 index 00000000000..b54b2ba234f --- /dev/null +++ b/etcdutl/main_test.go @@ -0,0 +1,66 @@ +// Copyright 2017 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 main + +import ( + "log" + "os" + "strings" + "testing" +) + +func SplitTestArgs(args []string) (testArgs, appArgs []string) { + for i, arg := range os.Args { + switch { + case strings.HasPrefix(arg, "-test."): + testArgs = append(testArgs, arg) + case i == 0: + appArgs = append(appArgs, arg) + testArgs = append(testArgs, arg) + default: + appArgs = append(appArgs, arg) + } + } + return +} + +// Empty test to avoid no-tests warning. +func TestEmpty(t *testing.T) {} + +/** + * The purpose of this "test" is to run etcdctl with code-coverage + * collection turned on. The technique is documented here: + * + * https://www.cyphar.com/blog/post/20170412-golang-integration-coverage + */ +func TestMain(m *testing.M) { + // don't launch etcdutl when invoked via go test + if strings.HasSuffix(os.Args[0], "etcdutl.test") { + return + } + + testArgs, appArgs := SplitTestArgs(os.Args) + + os.Args = appArgs + + err := Start() + if err != nil { + log.Fatalf("etcdctl failed with: %v", err) + } + + // This will generate coverage files: + os.Args = testArgs + m.Run() +} diff --git a/etcdctl/snapshot/doc.go b/etcdutl/snapshot/doc.go similarity index 100% rename from etcdctl/snapshot/doc.go rename to etcdutl/snapshot/doc.go diff --git a/etcdctl/snapshot/util.go b/etcdutl/snapshot/util.go similarity index 100% rename from etcdctl/snapshot/util.go rename to etcdutl/snapshot/util.go diff --git a/etcdctl/snapshot/v3_snapshot.go b/etcdutl/snapshot/v3_snapshot.go similarity index 100% rename from etcdctl/snapshot/v3_snapshot.go rename to etcdutl/snapshot/v3_snapshot.go diff --git a/go.mod b/go.mod index 45ee9e8613c..4e847849da8 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ replace ( go.etcd.io/etcd/client/v2 => ./client/v2 go.etcd.io/etcd/client/v3 => ./client/v3 go.etcd.io/etcd/etcdctl/v3 => ./etcdctl + go.etcd.io/etcd/etcdutl/v3 => ./etcdutl go.etcd.io/etcd/pkg/v3 => ./pkg go.etcd.io/etcd/raft/v3 => ./raft go.etcd.io/etcd/server/v3 => ./server @@ -17,13 +18,14 @@ replace ( require ( github.com/bgentry/speakeasy v0.1.0 github.com/dustin/go-humanize v1.0.0 - github.com/mattn/go-runewidth v0.0.9 // indirect github.com/spf13/cobra v1.1.3 go.etcd.io/bbolt v1.3.6-0.20210426205525-9c92be978ae0 go.etcd.io/etcd/api/v3 v3.5.0-alpha.0 go.etcd.io/etcd/client/pkg/v3 v3.5.0-alpha.0 go.etcd.io/etcd/client/v2 v2.305.0-alpha.0 go.etcd.io/etcd/client/v3 v3.5.0-alpha.0 + go.etcd.io/etcd/etcdctl/v3 v3.0.0-00010101000000-000000000000 + go.etcd.io/etcd/etcdutl/v3 v3.5.0-alpha.0 go.etcd.io/etcd/pkg/v3 v3.5.0-alpha.0 go.etcd.io/etcd/raft/v3 v3.5.0-alpha.0 go.etcd.io/etcd/server/v3 v3.5.0-alpha.0 diff --git a/go.sum b/go.sum index bddda881561..d35fb315f0c 100644 --- a/go.sum +++ b/go.sum @@ -198,7 +198,6 @@ github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRU github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-isatty v0.0.3 h1:ns/ykhmWi7G9O+8a448SecJU3nSMBXJfqQkl0upE1jI= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= @@ -220,7 +219,8 @@ github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9 github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= -github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= diff --git a/scripts/build-binary b/scripts/build-binary index 91e8f39eb34..6186424aa9e 100755 --- a/scripts/build-binary +++ b/scripts/build-binary @@ -44,13 +44,14 @@ function package { if [ "${GOOS}" == "windows" ]; then ext=".exe" fi - for bin in etcd etcdctl; do + for bin in etcd etcdctl etcdutl; do cp "${srcdir}/${bin}" "${target}/${bin}${ext}" done cp etcd/README.md "${target}"/README.md cp etcd/etcdctl/README.md "${target}"/README-etcdctl.md cp etcd/etcdctl/READMEv2.md "${target}"/READMEv2-etcdctl.md + cp etcd/etcdutl/README.md "${target}"/README-etcdutl.md cp -R etcd/Documentation "${target}"/Documentation } diff --git a/scripts/test_lib.sh b/scripts/test_lib.sh index 0b9c60e1bda..9053f9ce8c7 100644 --- a/scripts/test_lib.sh +++ b/scripts/test_lib.sh @@ -164,7 +164,7 @@ function run_for_module { } function module_dirs() { - echo "api pkg raft client/pkg client/v2 client/v3 server etcdctl tests ." + echo "api pkg raft client/pkg client/v2 client/v3 server etcdutl etcdctl tests ." } # maybe_run [cmd...] runs given command depending on the DRY_RUN flag. @@ -185,6 +185,7 @@ function modules() { "${ROOT_MODULE}/client/v2" "${ROOT_MODULE}/client/v3" "${ROOT_MODULE}/server/v3" + "${ROOT_MODULE}/etcdutl/v3" "${ROOT_MODULE}/etcdctl/v3" "${ROOT_MODULE}/tests/v3" "${ROOT_MODULE}/v3") diff --git a/server/mvcc/backend/backend.go b/server/mvcc/backend/backend.go index ecf941f1fe7..055aedaff69 100644 --- a/server/mvcc/backend/backend.go +++ b/server/mvcc/backend/backend.go @@ -465,7 +465,7 @@ func (b *backend) defrag() error { size2, sizeInUse2 := b.Size(), b.SizeInUse() if b.lg != nil { b.lg.Info( - "defragmented", + "finished defragmenting directory", zap.String("path", dbp), zap.Int64("current-db-size-bytes-diff", size2-size1), zap.Int64("current-db-size-bytes", size2), diff --git a/test.sh b/test.sh index ee91132b6ae..308cde40215 100755 --- a/test.sh +++ b/test.sh @@ -200,6 +200,7 @@ function grpcproxy_pass { function build_cov_pass { run_for_module "server" run go test -tags cov -c -covermode=set -coverpkg="./..." -o "../bin/etcd_test" run_for_module "etcdctl" run go test -tags cov -c -covermode=set -coverpkg="./..." -o "../bin/etcdctl_test" + run_for_module "etcdutl" run go test -tags cov -c -covermode=set -coverpkg="./..." -o "../bin/etcdutl_test" } # pkg_to_coverflag [prefix] [pkgs] @@ -345,6 +346,7 @@ function cov_pass { sed --in-place -E "s|go.etcd.io/etcd/client/v2/|client/v2/|g" "${cover_out_file}" || true sed --in-place -E "s|go.etcd.io/etcd/client/pkg/v3|client/pkg/v3/|g" "${cover_out_file}" || true sed --in-place -E "s|go.etcd.io/etcd/etcdctl/v3/|etcdctl/|g" "${cover_out_file}" || true + sed --in-place -E "s|go.etcd.io/etcd/etcdutl/v3/|etcdutl/|g" "${cover_out_file}" || true sed --in-place -E "s|go.etcd.io/etcd/pkg/v3/|pkg/|g" "${cover_out_file}" || true sed --in-place -E "s|go.etcd.io/etcd/raft/v3/|raft/|g" "${cover_out_file}" || true sed --in-place -E "s|go.etcd.io/etcd/server/v3/|server/|g" "${cover_out_file}" || true diff --git a/tests/e2e/ctl_v2_test.go b/tests/e2e/ctl_v2_test.go index 075c6f8805b..107f1c2f79c 100644 --- a/tests/e2e/ctl_v2_test.go +++ b/tests/e2e/ctl_v2_test.go @@ -15,6 +15,7 @@ package e2e import ( + "fmt" "io/ioutil" "os" "strings" @@ -210,13 +211,20 @@ func TestCtlV2RoleList(t *testing.T) { } } -func TestCtlV2Backup(t *testing.T) { testCtlV2Backup(t, 0, false) } -func TestCtlV2BackupSnapshot(t *testing.T) { testCtlV2Backup(t, 1, false) } - -func TestCtlV2BackupV3(t *testing.T) { testCtlV2Backup(t, 0, true) } -func TestCtlV2BackupV3Snapshot(t *testing.T) { testCtlV2Backup(t, 1, true) } +func TestUtlCtlV2Backup(t *testing.T) { + for snap := range []int{0, 1} { + for _, v3 := range []bool{true, false} { + for _, utl := range []bool{true, false} { + t.Run(fmt.Sprintf("etcdutl:%v;snap:%v;v3:%v", utl, snap, v3), + func(t *testing.T) { + testUtlCtlV2Backup(t, snap, v3, utl) + }) + } + } + } +} -func testCtlV2Backup(t *testing.T, snapCount int, v3 bool) { +func testUtlCtlV2Backup(t *testing.T, snapCount int, v3 bool, utl bool) { BeforeTestV2(t) backupDir, err := ioutil.TempDir(t.TempDir(), "testbackup0.etcd") @@ -251,7 +259,7 @@ func testCtlV2Backup(t *testing.T, snapCount int, v3 bool) { } } t.Log("Triggering etcd backup") - if err := etcdctlBackup(t, epc1, epc1.procs[0].Config().dataDirPath, backupDir, v3); err != nil { + if err := etcdctlBackup(t, epc1, epc1.procs[0].Config().dataDirPath, backupDir, v3, utl); err != nil { t.Fatal(err) } t.Log("Closing etcd-1 backup") @@ -377,13 +385,22 @@ func TestCtlV2ClusterHealth(t *testing.T) { func etcdctlPrefixArgs(clus *etcdProcessCluster) []string { endpoints := strings.Join(clus.EndpointsV2(), ",") - cmdArgs := []string{ctlBinPath, "--endpoints", endpoints} + cmdArgs := []string{ctlBinPath} + + cmdArgs = append(cmdArgs, "--endpoints", endpoints) if clus.cfg.clientTLS == clientTLS { cmdArgs = append(cmdArgs, "--ca-file", caPath, "--cert-file", certPath, "--key-file", privateKeyPath) } return cmdArgs } +func etcductlPrefixArgs(utl bool) []string { + if utl { + return []string{utlBinPath} + } + return []string{ctlBinPath} +} + func etcdctlClusterHealth(clus *etcdProcessCluster, val string) error { cmdArgs := append(etcdctlPrefixArgs(clus), "cluster-health") return spawnWithExpect(cmdArgs, val) @@ -480,10 +497,12 @@ func etcdctlAuthEnable(clus *etcdProcessCluster) error { return spawnWithExpect(cmdArgs, "Authentication Enabled") } -func etcdctlBackup(t testing.TB, clus *etcdProcessCluster, dataDir, backupDir string, v3 bool) error { - cmdArgs := append(etcdctlPrefixArgs(clus), "backup", "--data-dir", dataDir, "--backup-dir", backupDir) +func etcdctlBackup(t testing.TB, clus *etcdProcessCluster, dataDir, backupDir string, v3 bool, utl bool) error { + cmdArgs := append(etcductlPrefixArgs(utl), "backup", "--data-dir", dataDir, "--backup-dir", backupDir) if v3 { cmdArgs = append(cmdArgs, "--with-v3") + } else if utl { + cmdArgs = append(cmdArgs, "--with-v3=false") } t.Logf("Running: %v", cmdArgs) proc, err := spawnCmd(cmdArgs) diff --git a/tests/e2e/ctl_v3_alarm_test.go b/tests/e2e/ctl_v3_alarm_test.go index e4f50060436..7b9b445b09b 100644 --- a/tests/e2e/ctl_v3_alarm_test.go +++ b/tests/e2e/ctl_v3_alarm_test.go @@ -84,7 +84,7 @@ func alarmTest(cx ctlCtx) { if err := ctlV3Compact(cx, sresp.Header.Revision, true); err != nil { cx.t.Fatal(err) } - if err := ctlV3Defrag(cx); err != nil { + if err := ctlV3OnlineDefrag(cx); err != nil { cx.t.Fatal(err) } diff --git a/tests/e2e/ctl_v3_auth_test.go b/tests/e2e/ctl_v3_auth_test.go index bee66e6d9e0..58a3b61e037 100644 --- a/tests/e2e/ctl_v3_auth_test.go +++ b/tests/e2e/ctl_v3_auth_test.go @@ -994,13 +994,13 @@ func authTestDefrag(cx ctlCtx) { // ordinary user cannot defrag cx.user, cx.pass = "test-user", "pass" - if err := ctlV3Defrag(cx); err == nil { + if err := ctlV3OnlineDefrag(cx); err == nil { cx.t.Fatal("ordinary user should not be able to issue a defrag request") } // root can defrag cx.user, cx.pass = "root", "root" - if err := ctlV3Defrag(cx); err != nil { + if err := ctlV3OnlineDefrag(cx); err != nil { cx.t.Fatal(err) } } diff --git a/tests/e2e/ctl_v3_defrag_test.go b/tests/e2e/ctl_v3_defrag_test.go index 64c3bb9f0c3..8fbe476f093 100644 --- a/tests/e2e/ctl_v3_defrag_test.go +++ b/tests/e2e/ctl_v3_defrag_test.go @@ -16,7 +16,14 @@ package e2e import "testing" -func TestCtlV3Defrag(t *testing.T) { testCtl(t, defragTest) } +func TestCtlV3DefragOnline(t *testing.T) { testCtl(t, defragOnlineTest) } + +func TestCtlV3DefragOffline(t *testing.T) { + testCtlWithOffline(t, maintenanceInitKeys, defragOfflineTest) +} +func TestCtlV3DefragOfflineEtcdutl(t *testing.T) { + testCtlWithOffline(t, maintenanceInitKeys, defragOfflineTest, withEtcdutl()) +} func maintenanceInitKeys(cx ctlCtx) { var kvs = []kv{{"key", "val1"}, {"key", "val2"}, {"key", "val3"}} @@ -27,19 +34,19 @@ func maintenanceInitKeys(cx ctlCtx) { } } -func defragTest(cx ctlCtx) { +func defragOnlineTest(cx ctlCtx) { maintenanceInitKeys(cx) if err := ctlV3Compact(cx, 4, cx.compactPhysical); err != nil { cx.t.Fatal(err) } - if err := ctlV3Defrag(cx); err != nil { + if err := ctlV3OnlineDefrag(cx); err != nil { cx.t.Fatalf("defragTest ctlV3Defrag error (%v)", err) } } -func ctlV3Defrag(cx ctlCtx) error { +func ctlV3OnlineDefrag(cx ctlCtx) error { cmdArgs := append(cx.PrefixArgs(), "defrag") lines := make([]string, cx.epc.cfg.clusterSize) for i := range lines { @@ -47,3 +54,15 @@ func ctlV3Defrag(cx ctlCtx) error { } return spawnWithExpects(cmdArgs, lines...) } + +func ctlV3OfflineDefrag(cx ctlCtx) error { + cmdArgs := append(cx.PrefixArgsUtl(), "defrag", "--data-dir", cx.dataDir) + lines := []string{"finished defragmenting directory"} + return spawnWithExpects(cmdArgs, lines...) +} + +func defragOfflineTest(cx ctlCtx) { + if err := ctlV3OfflineDefrag(cx); err != nil { + cx.t.Fatalf("defragTest ctlV3Defrag error (%v)", err) + } +} diff --git a/tests/e2e/ctl_v3_migrate_test.go b/tests/e2e/ctl_v3_migrate_test.go deleted file mode 100644 index c36b5a38c61..00000000000 --- a/tests/e2e/ctl_v3_migrate_test.go +++ /dev/null @@ -1,107 +0,0 @@ -// Copyright 2016 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 e2e - -import ( - "context" - "fmt" - "os" - "testing" - "time" - - "go.etcd.io/etcd/client/v3" -) - -func TestCtlV3Migrate(t *testing.T) { - BeforeTest(t) - - cfg := newConfigNoTLS() - cfg.enableV2 = true - epc := setupEtcdctlTest(t, cfg, false) - defer func() { - if errC := epc.Close(); errC != nil { - t.Fatalf("error closing etcd processes (%v)", errC) - } - }() - - keys := make([]string, 3) - vals := make([]string, 3) - for i := range keys { - keys[i] = fmt.Sprintf("foo_%d", i) - vals[i] = fmt.Sprintf("bar_%d", i) - } - os.Setenv("ETCDCTL_API", "2") - for i := range keys { - if err := etcdctlSet(epc, keys[i], vals[i]); err != nil { - t.Fatal(err) - } - } - - dataDir := epc.procs[0].Config().dataDirPath - if err := epc.Stop(); err != nil { - t.Fatalf("error closing etcd processes (%v)", err) - } - - os.Unsetenv("ETCDCTL_API") - cx := ctlCtx{ - t: t, - cfg: *newConfigNoTLS(), - dialTimeout: 7 * time.Second, - epc: epc, - } - if err := ctlV3Migrate(cx, dataDir, ""); err != nil { - t.Fatal(err) - } - - epc.procs[0].Config().keepDataDir = true - if err := epc.Restart(); err != nil { - t.Fatal(err) - } - - cli, err := clientv3.New(clientv3.Config{ - Endpoints: epc.EndpointsV3(), - DialTimeout: 3 * time.Second, - }) - if err != nil { - t.Fatal(err) - } - defer cli.Close() - resp, err := cli.Get(context.TODO(), "test") - if err != nil { - t.Fatal(err) - } - revAfterMigrate := resp.Header.Revision - // to ensure revision increment is continuous from migrated v2 data - if err := ctlV3Put(cx, "test", "value", ""); err != nil { - t.Fatal(err) - } - - resp, err = cli.Get(context.TODO(), "test") - if err != nil { - t.Fatal(err) - } - if len(resp.Kvs) != 1 { - t.Fatalf("len(resp.Kvs) expected 1, got %+v", resp.Kvs) - } - - if resp.Kvs[0].CreateRevision != revAfterMigrate+1 { - t.Fatalf("expected revision increment is continuous from migrated v2, got %d", resp.Kvs[0].CreateRevision) - } -} - -func ctlV3Migrate(cx ctlCtx, dataDir, walDir string) error { - cmdArgs := append(cx.PrefixArgs(), "migrate", "--data-dir", dataDir, "--wal-dir", walDir) - return spawnWithExpects(cmdArgs, "finished transforming keys") -} diff --git a/tests/e2e/ctl_v3_snapshot_test.go b/tests/e2e/ctl_v3_snapshot_test.go index 8ef76cea58b..ce172f4c18e 100644 --- a/tests/e2e/ctl_v3_snapshot_test.go +++ b/tests/e2e/ctl_v3_snapshot_test.go @@ -24,11 +24,12 @@ import ( "testing" "time" - "go.etcd.io/etcd/etcdctl/v3/snapshot" + "go.etcd.io/etcd/etcdutl/v3/snapshot" "go.etcd.io/etcd/pkg/v3/expect" ) -func TestCtlV3Snapshot(t *testing.T) { testCtl(t, snapshotTest) } +func TestCtlV3Snapshot(t *testing.T) { testCtl(t, snapshotTest) } +func TestCtlV3SnapshotEtcdutl(t *testing.T) { testCtl(t, snapshotTest, withEtcdutl()) } func snapshotTest(cx ctlCtx) { maintenanceInitKeys(cx) @@ -60,7 +61,8 @@ func snapshotTest(cx ctlCtx) { } } -func TestCtlV3SnapshotCorrupt(t *testing.T) { testCtl(t, snapshotCorruptTest) } +func TestCtlV3SnapshotCorrupt(t *testing.T) { testCtl(t, snapshotCorruptTest) } +func TestCtlV3SnapshotCorruptEtcdutl(t *testing.T) { testCtl(t, snapshotCorruptTest, withEtcdutl()) } func snapshotCorruptTest(cx ctlCtx) { fpath := filepath.Join(cx.t.TempDir(), "snapshot") @@ -81,10 +83,9 @@ func snapshotCorruptTest(cx ctlCtx) { f.Close() datadir := cx.t.TempDir() - defer os.RemoveAll(datadir) serr := spawnWithExpect( - append(cx.PrefixArgs(), "snapshot", "restore", + append(cx.PrefixArgsUtl(), "snapshot", "restore", "--data-dir", datadir, fpath), "expected sha256") @@ -96,6 +97,9 @@ func snapshotCorruptTest(cx ctlCtx) { // This test ensures that the snapshot status does not modify the snapshot file func TestCtlV3SnapshotStatusBeforeRestore(t *testing.T) { testCtl(t, snapshotStatusBeforeRestoreTest) } +func TestCtlV3SnapshotStatusBeforeRestoreEtcdutl(t *testing.T) { + testCtl(t, snapshotStatusBeforeRestoreTest, withEtcdutl()) +} func snapshotStatusBeforeRestoreTest(cx ctlCtx) { fpath := filepath.Join(cx.t.TempDir(), "snapshot") @@ -114,7 +118,7 @@ func snapshotStatusBeforeRestoreTest(cx ctlCtx) { dataDir := cx.t.TempDir() defer os.RemoveAll(dataDir) serr := spawnWithExpect( - append(cx.PrefixArgs(), "snapshot", "restore", + append(cx.PrefixArgsUtl(), "snapshot", "restore", "--data-dir", dataDir, fpath), "added member") @@ -129,7 +133,7 @@ func ctlV3SnapshotSave(cx ctlCtx, fpath string) error { } func getSnapshotStatus(cx ctlCtx, fpath string) (snapshot.Status, error) { - cmdArgs := append(cx.PrefixArgs(), "--write-out", "json", "snapshot", "status", fpath) + cmdArgs := append(cx.PrefixArgsUtl(), "--write-out", "json", "snapshot", "status", fpath) proc, err := spawnCmd(cmdArgs) if err != nil { @@ -152,9 +156,12 @@ func getSnapshotStatus(cx ctlCtx, fpath string) (snapshot.Status, error) { return resp, nil } +func TestIssue6361(t *testing.T) { testIssue6361(t, false) } +func TestIssue6361etcdutl(t *testing.T) { testIssue6361(t, true) } + // TestIssue6361 ensures new member that starts with snapshot correctly // syncs up with other members and serve correct data. -func TestIssue6361(t *testing.T) { +func testIssue6361(t *testing.T, etcdutl bool) { { // This tests is pretty flaky on semaphoreci as of 2021-01-10. // TODO: Remove when the flakiness source is identified. @@ -206,8 +213,13 @@ func TestIssue6361(t *testing.T) { newDataDir := filepath.Join(t.TempDir(), "test.data") + uctlBinPath := ctlBinPath + if etcdutl { + uctlBinPath = utlBinPath + } + t.Log("etcdctl restoring the snapshot...") - err = spawnWithExpect([]string{ctlBinPath, "snapshot", "restore", fpath, "--name", epc.procs[0].Config().name, "--initial-cluster", epc.procs[0].Config().initialCluster, "--initial-cluster-token", epc.procs[0].Config().initialToken, "--initial-advertise-peer-urls", epc.procs[0].Config().purl.String(), "--data-dir", newDataDir}, "added member") + err = spawnWithExpect([]string{uctlBinPath, "snapshot", "restore", fpath, "--name", epc.procs[0].Config().name, "--initial-cluster", epc.procs[0].Config().initialCluster, "--initial-cluster-token", epc.procs[0].Config().initialToken, "--initial-advertise-peer-urls", epc.procs[0].Config().purl.String(), "--data-dir", newDataDir}, "added member") if err != nil { t.Fatal(err) } diff --git a/tests/e2e/ctl_v3_test.go b/tests/e2e/ctl_v3_test.go index f6eeeecbfed..5c8bb2fe95a 100644 --- a/tests/e2e/ctl_v3_test.go +++ b/tests/e2e/ctl_v3_test.go @@ -21,6 +21,7 @@ import ( "testing" "time" + "github.com/stretchr/testify/assert" "go.etcd.io/etcd/api/v3/version" "go.etcd.io/etcd/client/pkg/v3/fileutil" "go.etcd.io/etcd/client/pkg/v3/testutil" @@ -142,6 +143,12 @@ type ctlCtx struct { // for compaction compactPhysical bool + + // to run etcdutl instead of etcdctl for suitable commands. + etcdutl bool + + // dir that was used during the test + dataDir string } type ctlOption func(*ctlCtx) @@ -197,7 +204,15 @@ func withFlagByEnv() ctlOption { return func(cx *ctlCtx) { cx.envMap = make(map[string]struct{}) } } +func withEtcdutl() ctlOption { + return func(cx *ctlCtx) { cx.etcdutl = true } +} + func testCtl(t *testing.T, testFunc func(ctlCtx), opts ...ctlOption) { + testCtlWithOffline(t, testFunc, nil, opts...) +} + +func testCtlWithOffline(t *testing.T, testFunc func(ctlCtx), testOfflineFunc func(ctlCtx), opts ...ctlOption) { BeforeTest(t) ret := ctlCtx{ @@ -217,12 +232,16 @@ func testCtl(t *testing.T, testFunc func(ctlCtx), opts ...ctlOption) { if ret.initialCorruptCheck { ret.cfg.initialCorruptCheck = ret.initialCorruptCheck } + if testOfflineFunc != nil { + ret.cfg.keepDataDir = true + } epc, err := newEtcdProcessCluster(t, &ret.cfg) if err != nil { t.Fatalf("could not start etcd process cluster (%v)", err) } ret.epc = epc + ret.dataDir = epc.procs[0].Config().dataDirPath defer func() { if ret.envMap != nil { @@ -230,8 +249,10 @@ func testCtl(t *testing.T, testFunc func(ctlCtx), opts ...ctlOption) { os.Unsetenv(k) } } - if errC := ret.epc.Close(); errC != nil { - t.Fatalf("error closing etcd processes (%v)", errC) + if ret.epc != nil { + if errC := ret.epc.Close(); errC != nil { + t.Fatalf("error closing etcd processes (%v)", errC) + } } }() @@ -239,6 +260,7 @@ func testCtl(t *testing.T, testFunc func(ctlCtx), opts ...ctlOption) { go func() { defer close(donec) testFunc(ret) + t.Log("---testFunc logic DONE") }() timeout := 2*ret.dialTimeout + time.Second @@ -250,7 +272,15 @@ func testCtl(t *testing.T, testFunc func(ctlCtx), opts ...ctlOption) { testutil.FatalStack(t, fmt.Sprintf("test timed out after %v", timeout)) case <-donec: } - t.Log("---Test logic DONE") + + t.Log("closing test cluster...") + assert.NoError(t, epc.Close()) + epc = nil + t.Log("closed test cluster...") + + if testOfflineFunc != nil { + testOfflineFunc(ret) + } } func (cx *ctlCtx) prefixArgs(eps []string) []string { @@ -296,6 +326,16 @@ func (cx *ctlCtx) PrefixArgs() []string { return cx.prefixArgs(cx.epc.EndpointsV3()) } +// PrefixArgsUtl returns prefix of the command that is either etcdctl or etcdutl +// depending on cx configuration. +// Please not thet 'utl' compatible commands does not consume --endpoints flag. +func (cx *ctlCtx) PrefixArgsUtl() []string { + if cx.etcdutl { + return []string{utlBinPath} + } + return []string{ctlBinPath} +} + func isGRPCTimedout(err error) bool { return strings.Contains(err.Error(), "grpc: timed out trying to connect") } diff --git a/tests/e2e/etcd_process.go b/tests/e2e/etcd_process.go index 67c293fe58f..f744fa81cd9 100644 --- a/tests/e2e/etcd_process.go +++ b/tests/e2e/etcd_process.go @@ -28,6 +28,7 @@ var ( etcdServerReadyLines = []string{"ready to serve client requests"} binPath string ctlBinPath string + utlBinPath string ) // etcdProcess is a process that serves etcd requests. @@ -143,8 +144,11 @@ func (ep *etcdServerProcess) Close() error { if err := ep.Stop(); err != nil { return err } - ep.cfg.lg.Info("removing directory", zap.String("data-dir", ep.cfg.dataDirPath)) - return os.RemoveAll(ep.cfg.dataDirPath) + if !ep.cfg.keepDataDir { + ep.cfg.lg.Info("removing directory", zap.String("data-dir", ep.cfg.dataDirPath)) + return os.RemoveAll(ep.cfg.dataDirPath) + } + return nil } func (ep *etcdServerProcess) WithStopSignal(sig os.Signal) os.Signal { diff --git a/tests/e2e/etcd_spawn_cov.go b/tests/e2e/etcd_spawn_cov.go index 3bfd8433dc4..9b24ac9d0c1 100644 --- a/tests/e2e/etcd_spawn_cov.go +++ b/tests/e2e/etcd_spawn_cov.go @@ -48,6 +48,8 @@ func spawnCmdWithLogger(lg *zap.Logger, args []string) (*expect.ExpectProcess, e cmd = cmd + "_test" case strings.HasSuffix(cmd, "/etcdctl"): cmd = cmd + "_test" + case strings.HasSuffix(cmd, "/etcdutl"): + cmd = cmd + "_test" case strings.HasSuffix(cmd, "/etcdctl3"): cmd = ctlBinPath + "_test" env = append(env, "ETCDCTL_API=3") diff --git a/tests/e2e/etcd_spawn_nocov.go b/tests/e2e/etcd_spawn_nocov.go index e753a967f01..b0e872fb220 100644 --- a/tests/e2e/etcd_spawn_nocov.go +++ b/tests/e2e/etcd_spawn_nocov.go @@ -19,6 +19,7 @@ package e2e import ( "os" + "strings" "go.etcd.io/etcd/pkg/v3/expect" "go.uber.org/zap" @@ -35,7 +36,7 @@ func spawnCmdWithLogger(lg *zap.Logger, args []string) (*expect.ExpectProcess, e if err != nil { return nil, err } - if args[0] == ctlBinPath+"3" { + if strings.HasSuffix(args[0], "/etcdctl3") { env := append(os.Environ(), "ETCDCTL_API=3") lg.Info("spawning process with ETCDCTL_API=3", zap.Strings("args", args), zap.String("working-dir", wd)) return expect.NewExpectWithEnv(ctlBinPath, args[1:], env) diff --git a/tests/e2e/main_test.go b/tests/e2e/main_test.go index ddb7ae4ba7b..41561b5501e 100644 --- a/tests/e2e/main_test.go +++ b/tests/e2e/main_test.go @@ -46,6 +46,7 @@ func TestMain(m *testing.M) { binPath = binDir + "/etcd" ctlBinPath = binDir + "/etcdctl" + utlBinPath = binDir + "/etcdutl" certPath = certDir + "/server.crt" privateKeyPath = certDir + "/server.key.insecure" caPath = certDir + "/ca.crt" diff --git a/tests/functional/rpcpb/member.go b/tests/functional/rpcpb/member.go index a3d9941e62f..a74a3b71b4a 100644 --- a/tests/functional/rpcpb/member.go +++ b/tests/functional/rpcpb/member.go @@ -26,7 +26,7 @@ import ( "go.etcd.io/etcd/client/pkg/v3/logutil" "go.etcd.io/etcd/client/pkg/v3/transport" "go.etcd.io/etcd/client/v3" - "go.etcd.io/etcd/etcdctl/v3/snapshot" + "go.etcd.io/etcd/etcdutl/v3/snapshot" "github.com/dustin/go-humanize" "go.uber.org/zap" diff --git a/tests/go.mod b/tests/go.mod index 075d25a3495..e5cc2afc581 100644 --- a/tests/go.mod +++ b/tests/go.mod @@ -8,10 +8,10 @@ replace ( go.etcd.io/etcd/client/v2 => ../client/v2 go.etcd.io/etcd/client/v3 => ../client/v3 go.etcd.io/etcd/etcdctl/v3 => ../etcdctl + go.etcd.io/etcd/etcdutl/v3 => ../etcdutl go.etcd.io/etcd/pkg/v3 => ../pkg go.etcd.io/etcd/raft/v3 => ../raft go.etcd.io/etcd/server/v3 => ../server - go.etcd.io/etcd/v3 => ../ ) require ( @@ -32,7 +32,7 @@ require ( go.etcd.io/etcd/client/pkg/v3 v3.5.0-alpha.0 go.etcd.io/etcd/client/v2 v2.305.0-alpha.0 go.etcd.io/etcd/client/v3 v3.5.0-alpha.0 - go.etcd.io/etcd/etcdctl/v3 v3.5.0-alpha.0 + go.etcd.io/etcd/etcdutl/v3 v3.5.0-alpha.0 go.etcd.io/etcd/pkg/v3 v3.5.0-alpha.0 go.etcd.io/etcd/raft/v3 v3.5.0-alpha.0 go.etcd.io/etcd/server/v3 v3.5.0-alpha.0 diff --git a/tests/go.sum b/tests/go.sum index 882fece5508..25a535a4cd7 100644 --- a/tests/go.sum +++ b/tests/go.sum @@ -58,7 +58,6 @@ github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7 github.com/coreos/go-systemd/v22 v22.3.1 h1:7OO2CXWMYNDdaAzP51t4lCCZWwpQHmvPbm9sxWjm3So= github.com/coreos/go-systemd/v22 v22.3.1/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= -github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw= @@ -196,7 +195,7 @@ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= @@ -216,7 +215,7 @@ github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9 github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= -github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= @@ -288,7 +287,6 @@ github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69 github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802 h1:uruHq4dN7GR16kFc5fp3d1RIYzJW5onx8Ybykw2YQFA= github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= -github.com/urfave/cli v1.22.4/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 h1:eY9dn8+vbi4tKz5Qo6v2eYzo7kUS51QINcR5jNpbZS8= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -505,7 +503,6 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/cheggaaa/pb.v1 v1.0.28/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= diff --git a/tests/integration/snapshot/v3_snapshot_test.go b/tests/integration/snapshot/v3_snapshot_test.go index 4b0729f0130..36886c40bf9 100644 --- a/tests/integration/snapshot/v3_snapshot_test.go +++ b/tests/integration/snapshot/v3_snapshot_test.go @@ -27,7 +27,7 @@ import ( "go.etcd.io/etcd/client/pkg/v3/testutil" "go.etcd.io/etcd/client/v3" - "go.etcd.io/etcd/etcdctl/v3/snapshot" + "go.etcd.io/etcd/etcdutl/v3/snapshot" "go.etcd.io/etcd/server/v3/embed" "go.etcd.io/etcd/tests/v3/integration" "go.uber.org/zap/zapcore"