Skip to content

Commit

Permalink
etcdctl: add etcdctl snapshot pipe command
Browse files Browse the repository at this point in the history
To improve the security of etcdctl. Added the ability to write snapshots to stdout without writing data to disk.

Signed-off-by: Ais8Ooz8 <[email protected]>
  • Loading branch information
Ais8Ooz8 committed Jul 14, 2023
1 parent 882edb3 commit 11fb917
Show file tree
Hide file tree
Showing 3 changed files with 90 additions and 0 deletions.
45 changes: 45 additions & 0 deletions client/v3/snapshot/v3_snapshot.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,5 +98,50 @@ func SaveWithVersion(ctx context.Context, lg *zap.Logger, cfg clientv3.Config, d
return resp.Version, fmt.Errorf("could not rename %s to %s (%v)", partpath, dbPath, err)
}
lg.Info("saved", zap.String("path", dbPath))
lg.Info("saved", zap.String("path", "/dev/stdout"))
return resp.Version, nil
}

func PipeWithVersion(ctx context.Context, lg *zap.Logger, cfg clientv3.Config) (string, error) {
cfg.Logger = lg.Named("client")
if len(cfg.Endpoints) != 1 {
return "", fmt.Errorf("snapshot must be requested to one selected node, not multiple %v", cfg.Endpoints)
}
cli, err := clientv3.New(cfg)
if err != nil {
return "", err
}
defer cli.Close()

start := time.Now()
resp, err := cli.SnapshotWithVersion(ctx)
if err != nil {
return "", err
}
defer resp.Snapshot.Close()
lg.Info("fetching snapshot", zap.String("endpoint", cfg.Endpoints[0]))
var size int64
f := os.Stdout
size, err = io.Copy(f, resp.Snapshot)
if err != nil {
return resp.Version, err
}
if !hasChecksum(size) {
return resp.Version, fmt.Errorf("sha256 checksum not found [bytes: %d]", size)
}
if err = fileutil.Fsync(f); err != nil {
return resp.Version, err
}
if err = f.Close(); err != nil {
return resp.Version, err
}
lg.Info("fetched snapshot",
zap.String("endpoint", cfg.Endpoints[0]),
zap.String("size", humanize.Bytes(uint64(size))),
zap.Duration("took", time.Since(start)),
zap.String("etcd-version", resp.Version),
)

lg.Info("saved", zap.String("path", "/dev/stdout"))
return resp.Version, nil
}
15 changes: 15 additions & 0 deletions etcdctl/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1012,6 +1012,21 @@ DEFRAG returns a zero exit code only if it succeeded defragmenting all given end

SNAPSHOT provides commands to restore a snapshot of a running etcd server into a fresh cluster.

### SNAPSHOT PIPE \<filename\>

SNAPSHOT PIPE writes a point-in-time snapshot of the etcd backend database to stdout.

#### Output

The backend snapshot is written to stdout.

#### Example

Write a snapshot to stdout:
```
./etcdctl snapshot pipe
```

### SNAPSHOT SAVE \<filename\>

SNAPSHOT SAVE writes a point-in-time snapshot of the etcd backend database to a file.
Expand Down
30 changes: 30 additions & 0 deletions etcdctl/ctlv3/command/snapshot_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ func NewSnapshotCommand() *cobra.Command {
Short: "Manages etcd node snapshots",
}
cmd.AddCommand(NewSnapshotSaveCommand())
cmd.AddCommand(NewSnapshotPipeCommand())
return cmd
}

Expand All @@ -44,6 +45,14 @@ func NewSnapshotSaveCommand() *cobra.Command {
}
}

func NewSnapshotPipeCommand() *cobra.Command {
return &cobra.Command{
Use: "pipe",
Short: "Streams an etcd node backend snapshot to STDOUT",
Run: snapshotPipeCommandFunc,
}
}

func snapshotSaveCommandFunc(cmd *cobra.Command, args []string) {
if len(args) != 1 {
err := fmt.Errorf("snapshot save expects one argument")
Expand Down Expand Up @@ -73,3 +82,24 @@ func snapshotSaveCommandFunc(cmd *cobra.Command, args []string) {
fmt.Printf("Server version %s\n", version)
}
}

func snapshotPipeCommandFunc(cmd *cobra.Command, args []string) {

lg, err := logutil.CreateDefaultZapLogger(zap.InfoLevel)
if err != nil {
cobrautl.ExitWithError(cobrautl.ExitError, err)
}
cfg := mustClientCfgFromCmd(cmd)

// if user does not specify "--command-timeout" flag, there will be no timeout for snapshot pipe command
ctx, cancel := context.WithCancel(context.Background())
if isCommandTimeoutFlagSet(cmd) {
ctx, cancel = commandCtx(cmd)
}
defer cancel()

_, err = snapshot.PipeWithVersion(ctx, lg, *cfg)
if err != nil {
cobrautl.ExitWithError(cobrautl.ExitInterrupted, err)
}
}

0 comments on commit 11fb917

Please sign in to comment.