diff --git a/cmd/litestream/main.go b/cmd/litestream/main.go index 2ef87a71e..2b004326a 100644 --- a/cmd/litestream/main.go +++ b/cmd/litestream/main.go @@ -2062,10 +2062,20 @@ func (c *ReplicaConfig) ReplicaType() string { // DefaultConfigPath returns the default config path. func DefaultConfigPath() string { + path, _ := resolveConfigPath("") + return path +} + +// resolveConfigPath returns the config path to read for flagPath and reports +// whether it was explicitly selected by the flag or by LITESTREAM_CONFIG. +func resolveConfigPath(flagPath string) (path string, explicit bool) { + if flagPath != "" { + return flagPath, true + } if v := os.Getenv("LITESTREAM_CONFIG"); v != "" { - return v + return v, true } - return defaultConfigPath + return defaultConfigPath, false } func registerConfigFlag(fs *flag.FlagSet) (configPath *string, noExpandEnv *bool) { diff --git a/cmd/litestream/reset.go b/cmd/litestream/reset.go index 44a4ed311..49a16e660 100644 --- a/cmd/litestream/reset.go +++ b/cmd/litestream/reset.go @@ -2,6 +2,7 @@ package main import ( "context" + "errors" "flag" "fmt" "io/fs" @@ -43,24 +44,24 @@ func (c *ResetCommand) Run(ctx context.Context, args []string) (err error) { } } - // Load configuration to find the database (if config exists) + // A missing config file is tolerated only when the path is the implicit default. + resolvedConfigPath, explicitConfig := resolveConfigPath(*configPath) + var dbConfig *DBConfig - if *configPath != "" { - config, configErr := ReadConfigFile(*configPath, !*noExpandEnv) - if configErr != nil { - return fmt.Errorf("cannot read config: %w", configErr) - } + config, configErr := ReadConfigFile(resolvedConfigPath, !*noExpandEnv) + if configErr != nil && (explicitConfig || !errors.Is(configErr, ErrConfigFileNotFound)) { + return fmt.Errorf("cannot read config: %w", configErr) + } - // Find database config - for _, dbc := range config.DBs { - expandedPath := dbc.Path - if !filepath.IsAbs(expandedPath) { - expandedPath, _ = filepath.Abs(expandedPath) - } - if expandedPath == dbPath { - dbConfig = dbc - break - } + // Find database config + for _, dbc := range config.DBs { + expandedPath := dbc.Path + if !filepath.IsAbs(expandedPath) { + expandedPath, _ = filepath.Abs(expandedPath) + } + if expandedPath == dbPath { + dbConfig = dbc + break } } diff --git a/cmd/litestream/reset_test.go b/cmd/litestream/reset_test.go index 1c6242669..160aefb3b 100644 --- a/cmd/litestream/reset_test.go +++ b/cmd/litestream/reset_test.go @@ -2,6 +2,7 @@ package main import ( "context" + "errors" "os" "path/filepath" "strings" @@ -53,6 +54,100 @@ func TestResetCommand_Run(t *testing.T) { } } +func TestResetCommand_RunWithConfigEnv(t *testing.T) { + dbPath := filepath.Join(t.TempDir(), "db.sqlite") + if err := os.WriteFile(dbPath, []byte(""), 0600); err != nil { + t.Fatal(err) + } + + // Put the Litestream state at a non-default meta path that is only + // discoverable through the config, so the test fails if reset ignores + // LITESTREAM_CONFIG and falls back to the default path. + metaPath := filepath.Join(t.TempDir(), "meta") + db := litestream.NewDB(dbPath) + db.SetMetaPath(metaPath) + ltxDir := filepath.Join(db.LTXDir(), "0") + if err := os.MkdirAll(ltxDir, 0700); err != nil { + t.Fatal(err) + } + ltxPath := filepath.Join(ltxDir, "0000000000000001-0000000000000001.ltx") + if err := os.WriteFile(ltxPath, []byte("ltx"), 0600); err != nil { + t.Fatal(err) + } + + replicaPath := filepath.Join(t.TempDir(), "replica") + configPath := filepath.Join(t.TempDir(), "litestream.yml") + config := "dbs:\n" + + " - path: " + dbPath + "\n" + + " meta-path: " + metaPath + "\n" + + " replicas:\n" + + " - url: file://" + replicaPath + "\n" + if err := os.WriteFile(configPath, []byte(config), 0600); err != nil { + t.Fatal(err) + } + t.Setenv("LITESTREAM_CONFIG", configPath) + + output := captureLTXCommandStdout(t, func() { + cmd := &ResetCommand{} + if err := cmd.Run(context.Background(), []string{dbPath}); err != nil { + t.Fatalf("unexpected error: %v", err) + } + }) + + if !strings.Contains(output, "Reset complete.") { + t.Fatalf("expected reset completion output:\n%s", output) + } + if _, err := os.Stat(ltxPath); !os.IsNotExist(err) { + t.Fatalf("expected LTX file at the config meta path to be removed, stat err=%v", err) + } +} + +func TestResetCommand_RunWithMissingConfigFlag(t *testing.T) { + dbPath, ltxPath := createResetCommandTestData(t) + t.Setenv("LITESTREAM_CONFIG", "") + configPath := filepath.Join(t.TempDir(), "missing.yml") + + cmd := &ResetCommand{} + if err := cmd.Run(context.Background(), []string{"-config", configPath, dbPath}); !errors.Is(err, ErrConfigFileNotFound) { + t.Fatalf("expected ErrConfigFileNotFound, got %v", err) + } + if _, err := os.Stat(ltxPath); err != nil { + t.Fatalf("expected LTX file to be kept: %v", err) + } +} + +func TestResetCommand_RunWithMissingConfigEnv(t *testing.T) { + dbPath, ltxPath := createResetCommandTestData(t) + t.Setenv("LITESTREAM_CONFIG", filepath.Join(t.TempDir(), "missing.yml")) + + cmd := &ResetCommand{} + if err := cmd.Run(context.Background(), []string{dbPath}); !errors.Is(err, ErrConfigFileNotFound) { + t.Fatalf("expected ErrConfigFileNotFound, got %v", err) + } + if _, err := os.Stat(ltxPath); err != nil { + t.Fatalf("expected LTX file to be kept: %v", err) + } +} + +func TestResetCommand_RunWithMissingDefaultConfig(t *testing.T) { + dbPath, ltxPath := createResetCommandTestData(t) + t.Setenv("LITESTREAM_CONFIG", "") + + output := captureLTXCommandStdout(t, func() { + cmd := &ResetCommand{} + if err := cmd.Run(context.Background(), []string{dbPath}); err != nil { + t.Fatalf("unexpected error: %v", err) + } + }) + + if !strings.Contains(output, "Reset complete.") { + t.Fatalf("expected reset completion output:\n%s", output) + } + if _, err := os.Stat(ltxPath); !os.IsNotExist(err) { + t.Fatalf("expected LTX file to be removed, stat err=%v", err) + } +} + func createResetCommandTestData(t *testing.T) (string, string) { t.Helper()