Allow replication without config file.

This commit changes `litestream replicate` to accept a database
path and a replica URL instead of using the config file. This allows
people to quickly try out the tool instead of learning the config
file syntax.
This commit is contained in:
Ben Johnson
2021-01-24 10:08:44 -07:00
parent 16f79e5814
commit f7213ed35c
4 changed files with 86 additions and 49 deletions

View File

@@ -138,18 +138,13 @@ func (c *Config) DBConfig(path string) *DBConfig {
} }
// ReadConfigFile unmarshals config from filename. Expands path if needed. // ReadConfigFile unmarshals config from filename. Expands path if needed.
func ReadConfigFile(filename string) (Config, error) { func ReadConfigFile(filename string) (_ Config, err error) {
config := DefaultConfig() config := DefaultConfig()
// Expand filename, if necessary. // Expand filename, if necessary.
if prefix := "~" + string(os.PathSeparator); strings.HasPrefix(filename, prefix) { filename, err = expand(filename)
u, err := user.Current() if err != nil {
if err != nil { return config, err
return config, err
} else if u.HomeDir == "" {
return config, fmt.Errorf("home directory unset")
}
filename = filepath.Join(u.HomeDir, strings.TrimPrefix(filename, prefix))
} }
// Read & deserialize configuration. // Read & deserialize configuration.
@@ -174,7 +169,12 @@ type DBConfig struct {
} }
// Normalize expands paths and parses URL-specified replicas. // Normalize expands paths and parses URL-specified replicas.
func (c *DBConfig) Normalize() error { func (c *DBConfig) Normalize() (err error) {
c.Path, err = expand(c.Path)
if err != nil {
return err
}
for i := range c.Replicas { for i := range c.Replicas {
if err := c.Replicas[i].Normalize(); err != nil { if err := c.Replicas[i].Normalize(); err != nil {
return err return err
@@ -202,27 +202,19 @@ type ReplicaConfig struct {
// Normalize expands paths and parses URL-specified replicas. // Normalize expands paths and parses URL-specified replicas.
func (c *ReplicaConfig) Normalize() error { func (c *ReplicaConfig) Normalize() error {
// Expand path filename, if necessary.
if prefix := "~" + string(os.PathSeparator); strings.HasPrefix(c.Path, prefix) {
u, err := user.Current()
if err != nil {
return err
} else if u.HomeDir == "" {
return fmt.Errorf("cannot expand replica path, no home directory available")
}
c.Path = filepath.Join(u.HomeDir, strings.TrimPrefix(c.Path, prefix))
}
// Attempt to parse as URL. Ignore if it is not a URL or if there is no scheme. // Attempt to parse as URL. Ignore if it is not a URL or if there is no scheme.
u, err := url.Parse(c.Path) u, err := url.Parse(c.Path)
if err != nil || u.Scheme == "" { if err != nil || u.Scheme == "" {
if c.Type == "" || c.Type == "file" {
c.Path, err = expand(c.Path)
return err
}
return nil return nil
} }
switch u.Scheme { switch u.Scheme {
case "file": case "file":
u.Scheme = "" c.Type, u.Scheme = u.Scheme, ""
c.Type = u.Scheme
c.Path = path.Clean(u.String()) c.Path = path.Clean(u.String())
return nil return nil
@@ -230,10 +222,6 @@ func (c *ReplicaConfig) Normalize() error {
c.Type = u.Scheme c.Type = u.Scheme
c.Path = strings.TrimPrefix(path.Clean(u.Path), "/") c.Path = strings.TrimPrefix(path.Clean(u.Path), "/")
c.Bucket = u.Host c.Bucket = u.Host
if u := u.User; u != nil {
c.AccessKeyID = u.Username()
c.SecretAccessKey, _ = u.Password()
}
return nil return nil
default: default:
@@ -322,11 +310,7 @@ func newS3ReplicaFromConfig(db *litestream.DB, c *Config, dbc *DBConfig, rc *Rep
} }
// Ensure required settings are set. // Ensure required settings are set.
if accessKeyID == "" { if bucket == "" {
return nil, fmt.Errorf("%s: s3 access key id required", db.Path())
} else if secretAccessKey == "" {
return nil, fmt.Errorf("%s: s3 secret access key required", db.Path())
} else if bucket == "" {
return nil, fmt.Errorf("%s: s3 bucket required", db.Path()) return nil, fmt.Errorf("%s: s3 bucket required", db.Path())
} }
@@ -352,3 +336,26 @@ func newS3ReplicaFromConfig(db *litestream.DB, c *Config, dbc *DBConfig, rc *Rep
} }
return r, nil return r, nil
} }
// expand returns an absolute path for s.
func expand(s string) (string, error) {
// Just expand to absolute path if there is no home directory prefix.
prefix := "~" + string(os.PathSeparator)
if s != "~" && !strings.HasPrefix(s, prefix) {
return filepath.Abs(s)
}
// Look up home directory.
u, err := user.Current()
if err != nil {
return "", err
} else if u.HomeDir == "" {
return "", fmt.Errorf("cannot expand path %s, no home directory available", s)
}
// Return path with tilde replaced by the home directory.
if s == "~" {
return u.HomeDir, nil
}
return filepath.Join(u.HomeDir, strings.TrimPrefix(s, prefix)), nil
}

View File

@@ -36,12 +36,27 @@ func (c *ReplicateCommand) Run(ctx context.Context, args []string) (err error) {
return err return err
} }
// Load configuration. // Load configuration or use CLI args to build db/replica.
if c.ConfigPath == "" { var config Config
return errors.New("-config required") if fs.NArg() == 1 {
return fmt.Errorf("must specify at least one replica URL for %s", fs.Arg(0))
} else if fs.NArg() > 1 {
dbConfig := &DBConfig{Path: fs.Arg(0)}
for _, u := range fs.Args()[1:] {
dbConfig.Replicas = append(dbConfig.Replicas, &ReplicaConfig{Path: u})
}
config.DBs = []*DBConfig{dbConfig}
} else if c.ConfigPath != "" {
config, err = ReadConfigFile(c.ConfigPath)
if err != nil {
return err
}
} else {
return errors.New("-config flag or database/replica arguments required")
} }
config, err := ReadConfigFile(c.ConfigPath)
if err != nil { // Normalize configuration paths.
if err := config.Normalize(); err != nil {
return err return err
} }
@@ -132,13 +147,17 @@ func (c *ReplicateCommand) Close() (err error) {
// Usage prints the help screen to STDOUT. // Usage prints the help screen to STDOUT.
func (c *ReplicateCommand) Usage() { func (c *ReplicateCommand) Usage() {
fmt.Printf(` fmt.Printf(`
The replicate command starts a server to monitor & replicate databases The replicate command starts a server to monitor & replicate databases.
specified in your configuration file. You can specify your database & replicas in a configuration file or you can
replicate a single database file by specifying its path and its replicas in the
command line arguments.
Usage: Usage:
litestream replicate [arguments] litestream replicate [arguments]
litestream replicate [arguments] DB_PATH REPLICA_URL [REPLICA_URL...]
Arguments: Arguments:
-config PATH -config PATH

View File

@@ -33,7 +33,7 @@ func (c *RestoreCommand) Run(ctx context.Context, args []string) (err error) {
if err := fs.Parse(args); err != nil { if err := fs.Parse(args); err != nil {
return err return err
} else if fs.NArg() == 0 || fs.Arg(0) == "" { } else if fs.NArg() == 0 || fs.Arg(0) == "" {
return fmt.Errorf("database path required") return fmt.Errorf("database path or replica URL required")
} else if fs.NArg() > 1 { } else if fs.NArg() > 1 {
return fmt.Errorf("too many arguments") return fmt.Errorf("too many arguments")
} }
@@ -90,7 +90,9 @@ The restore command recovers a database from a previous snapshot and WAL.
Usage: Usage:
litestream restore [arguments] DB litestream restore [arguments] DB_PATH
litestream restore [arguments] REPLICA_URL
Arguments: Arguments:

View File

@@ -14,6 +14,7 @@ import (
"github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials" "github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/defaults"
"github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/s3" "github.com/aws/aws-sdk-go/service/s3"
"github.com/aws/aws-sdk-go/service/s3/s3manager" "github.com/aws/aws-sdk-go/service/s3/s3manager"
@@ -647,10 +648,9 @@ func (r *Replica) Init(ctx context.Context) (err error) {
} }
// Create new AWS session. // Create new AWS session.
sess, err := session.NewSession(&aws.Config{ config := r.config()
Credentials: credentials.NewStaticCredentials(r.AccessKeyID, r.SecretAccessKey, ""), config.Region = aws.String(region)
Region: aws.String(region), sess, err := session.NewSession(config)
})
if err != nil { if err != nil {
return fmt.Errorf("cannot create aws session: %w", err) return fmt.Errorf("cannot create aws session: %w", err)
} }
@@ -659,12 +659,21 @@ func (r *Replica) Init(ctx context.Context) (err error) {
return nil return nil
} }
// config returns the AWS configuration. Uses the default credential chain
// unless a key/secret are explicitly set.
func (r *Replica) config() *aws.Config {
config := defaults.Get().Config
if r.AccessKeyID != "" || r.SecretAccessKey != "" {
config.Credentials = credentials.NewStaticCredentials(r.AccessKeyID, r.SecretAccessKey, "")
}
return config
}
func (r *Replica) findBucketRegion(ctx context.Context, bucket string) (string, error) { func (r *Replica) findBucketRegion(ctx context.Context, bucket string) (string, error) {
// Connect to US standard region to fetch info. // Connect to US standard region to fetch info.
sess, err := session.NewSession(&aws.Config{ config := r.config()
Credentials: credentials.NewStaticCredentials(r.AccessKeyID, r.SecretAccessKey, ""), config.Region = aws.String("us-east-1")
Region: aws.String("us-east-1"), sess, err := session.NewSession(config)
})
if err != nil { if err != nil {
return "", err return "", err
} }