Add 'validate' command
This commit is contained in:
@@ -58,6 +58,8 @@ func (m *Main) Run(ctx context.Context, args []string) (err error) {
|
||||
return (&RestoreCommand{}).Run(ctx, args)
|
||||
case "snapshots":
|
||||
return (&SnapshotsCommand{}).Run(ctx, args)
|
||||
case "validate":
|
||||
return (&ValidateCommand{}).Run(ctx, args)
|
||||
case "version":
|
||||
return (&VersionCommand{}).Run(ctx, args)
|
||||
case "wal":
|
||||
@@ -85,6 +87,7 @@ The commands are:
|
||||
replicate runs a server to replicate databases
|
||||
restore recovers database backup from a replica
|
||||
snapshots list available snapshots for a database
|
||||
validate checks replica to ensure a consistent state with primary
|
||||
version prints the version
|
||||
wal list available WAL files for a database
|
||||
`[1:])
|
||||
|
||||
@@ -14,17 +14,17 @@ import (
|
||||
)
|
||||
|
||||
type RestoreCommand struct {
|
||||
DBPath string
|
||||
}
|
||||
|
||||
func (c *RestoreCommand) Run(ctx context.Context, args []string) (err error) {
|
||||
var configPath string
|
||||
var opt litestream.RestoreOptions
|
||||
opt := litestream.NewRestoreOptions()
|
||||
fs := flag.NewFlagSet("litestream-restore", flag.ContinueOnError)
|
||||
registerConfigFlag(fs, &configPath)
|
||||
fs.StringVar(&opt.OutputPath, "o", "", "output path")
|
||||
fs.StringVar(&opt.ReplicaName, "replica", "", "replica name")
|
||||
fs.StringVar(&opt.Generation, "generation", "", "generation name")
|
||||
fs.IntVar(&opt.Index, "index", opt.Index, "wal index")
|
||||
fs.BoolVar(&opt.DryRun, "dry-run", false, "dry run")
|
||||
timestampStr := fs.String("timestamp", "", "timestamp")
|
||||
verbose := fs.Bool("v", false, "verbose output")
|
||||
@@ -63,15 +63,16 @@ func (c *RestoreCommand) Run(ctx context.Context, args []string) (err error) {
|
||||
opt.Logger = log.New(os.Stderr, "", log.LstdFlags)
|
||||
}
|
||||
|
||||
// Determine absolute path for database, if specified.
|
||||
if c.DBPath, err = filepath.Abs(fs.Arg(0)); err != nil {
|
||||
// Determine absolute path for database.
|
||||
dbPath, err := filepath.Abs(fs.Arg(0))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Instantiate DB.
|
||||
dbConfig := config.DBConfig(c.DBPath)
|
||||
dbConfig := config.DBConfig(dbPath)
|
||||
if dbConfig == nil {
|
||||
return fmt.Errorf("database not found in config: %s", c.DBPath)
|
||||
return fmt.Errorf("database not found in config: %s", dbPath)
|
||||
}
|
||||
db, err := newDBFromConfig(dbConfig)
|
||||
if err != nil {
|
||||
@@ -103,6 +104,10 @@ Arguments:
|
||||
Restore from a specific generation.
|
||||
Defaults to generation with latest data.
|
||||
|
||||
-index NUM
|
||||
Restore up to a specific WAL index (inclusive).
|
||||
Defaults to use the highest available index.
|
||||
|
||||
-timestamp TIMESTAMP
|
||||
Restore to a specific point-in-time.
|
||||
Defaults to use the latest available backup.
|
||||
|
||||
136
cmd/litestream/validate.go
Normal file
136
cmd/litestream/validate.go
Normal file
@@ -0,0 +1,136 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/benbjohnson/litestream"
|
||||
)
|
||||
|
||||
type ValidateCommand struct{}
|
||||
|
||||
func (c *ValidateCommand) Run(ctx context.Context, args []string) (err error) {
|
||||
var configPath string
|
||||
opt := litestream.NewRestoreOptions()
|
||||
fs := flag.NewFlagSet("litestream-validate", flag.ContinueOnError)
|
||||
registerConfigFlag(fs, &configPath)
|
||||
fs.StringVar(&opt.ReplicaName, "replica", "", "replica name")
|
||||
fs.BoolVar(&opt.DryRun, "dry-run", false, "dry run")
|
||||
verbose := fs.Bool("v", false, "verbose output")
|
||||
fs.Usage = c.Usage
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return err
|
||||
} else if fs.NArg() == 0 || fs.Arg(0) == "" {
|
||||
return fmt.Errorf("database path required")
|
||||
} else if fs.NArg() > 1 {
|
||||
return fmt.Errorf("too many arguments")
|
||||
}
|
||||
|
||||
// Load configuration.
|
||||
if configPath == "" {
|
||||
return errors.New("-config required")
|
||||
}
|
||||
config, err := ReadConfigFile(configPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Verbose output is automatically enabled if dry run is specified.
|
||||
if opt.DryRun {
|
||||
*verbose = true
|
||||
}
|
||||
|
||||
// Instantiate logger if verbose output is enabled.
|
||||
if *verbose {
|
||||
opt.Logger = log.New(os.Stderr, "", log.LstdFlags)
|
||||
}
|
||||
|
||||
// Determine absolute path for database.
|
||||
dbPath, err := filepath.Abs(fs.Arg(0))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Instantiate DB.
|
||||
dbConfig := config.DBConfig(dbPath)
|
||||
if dbConfig == nil {
|
||||
return fmt.Errorf("database not found in config: %s", dbPath)
|
||||
}
|
||||
db, err := newDBFromConfig(dbConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Ensure replica exists, if specified.
|
||||
if opt.ReplicaName != "" && db.Replica(opt.ReplicaName) == nil {
|
||||
return fmt.Errorf("replica not found: %s", opt.ReplicaName)
|
||||
}
|
||||
|
||||
// Validate all matching replicas.
|
||||
var hasInvalidReplica bool
|
||||
for _, r := range db.Replicas {
|
||||
if opt.ReplicaName != "" && opt.ReplicaName != r.Name() {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := db.Validate(ctx, r.Name(), opt); err != nil {
|
||||
fmt.Printf("%s: replica invalid: %s\n", r.Name(), err)
|
||||
}
|
||||
}
|
||||
|
||||
if hasInvalidReplica {
|
||||
return fmt.Errorf("one or more invalid replicas found")
|
||||
}
|
||||
|
||||
fmt.Println("ok")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *ValidateCommand) Usage() {
|
||||
fmt.Printf(`
|
||||
The validate command compares a checksum of the primary database with a
|
||||
checksum of the replica at the same point in time. Returns an error if the
|
||||
databases are not equal.
|
||||
|
||||
The restored database must be written to a temporary file so you must ensure
|
||||
you have enough disk space before performing this operation.
|
||||
|
||||
Usage:
|
||||
|
||||
litestream validate [arguments] DB
|
||||
|
||||
Arguments:
|
||||
|
||||
-config PATH
|
||||
Specifies the configuration file.
|
||||
Defaults to %s
|
||||
|
||||
-replica NAME
|
||||
Validate a specific replica.
|
||||
Defaults to validating all replicas.
|
||||
|
||||
-dry-run
|
||||
Prints all log output as if it were running but does
|
||||
not perform actual validation.
|
||||
|
||||
-v
|
||||
Verbose output.
|
||||
|
||||
|
||||
Examples:
|
||||
|
||||
# Validate all replicas for the given database.
|
||||
$ litestream validate /path/to/db
|
||||
|
||||
# Validate only the S3 replica.
|
||||
$ litestream restore -replica s3 /path/to/db
|
||||
|
||||
`[1:],
|
||||
DefaultConfigPath,
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user