Skip to content

Commit 92bc022

Browse files
feat: add rclone config file validation with error message
1 parent 9b8ee22 commit 92bc022

5 files changed

Lines changed: 126 additions & 20 deletions

File tree

.gitignore

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# Binaries for programs and plugins
2+
*.exe
3+
*.exe~
4+
*.dll
5+
*.so
6+
*.dylib
7+
8+
# Test binary, built with `go test -c`
9+
*.test
10+
11+
# Output of the go coverage tool
12+
*.out
13+
14+
# Go workspace file
15+
go.work
16+
17+
# Dependency directories
18+
vendor/
19+
20+
# Build artifacts
21+
/dbstash
22+
/dist/
23+
24+
# Environment files
25+
.env
26+
.env.*
27+
!.env.example
28+
29+
# IDE and editor files
30+
.vscode/
31+
.idea/
32+
*.swp
33+
*.swo
34+
*~
35+
36+
# OS files
37+
.DS_Store
38+
Thumbs.db
39+
40+
# Temporary files
41+
*.tmp
42+
/tmp/
43+
44+
# Rclone config (for testing)
45+
rclone.conf
46+
*.conf
47+
48+
# Secrets
49+
secrets/

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ Post-backup hooks receive `DBSTASH_STATUS` (`success`/`failure`) and `DBSTASH_FI
183183
| Variable | Required | Default | Description |
184184
|---|---|---|---|
185185
| `LOG_LEVEL` | No | `info` | `debug`, `info`, `warn`, `error` |
186-
| `LOG_FORMAT` | No | `json` | `json` or `text` |
186+
| `LOG_FORMAT` | No | `text` | `json` or `text` |
187187

188188
## Backup Modes
189189

internal/config/config.go

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,11 @@ func Load() (*Config, error) {
105105
return nil, fmt.Errorf("RCLONE_REMOTE is required")
106106
}
107107

108-
cfg.RcloneConfigFile = resolveRcloneConfig()
108+
rcloneConfigFile, err := resolveRcloneConfig()
109+
if err != nil {
110+
return nil, err
111+
}
112+
cfg.RcloneConfigFile = rcloneConfigFile
109113
cfg.RcloneExtraArgs = envOrDefault("RCLONE_EXTRA_ARGS", "")
110114

111115
// Schedule
@@ -149,7 +153,7 @@ func Load() (*Config, error) {
149153

150154
// Logging
151155
cfg.LogLevel = envOrDefault("LOG_LEVEL", "info")
152-
cfg.LogFormat = envOrDefault("LOG_FORMAT", "json")
156+
cfg.LogFormat = envOrDefault("LOG_FORMAT", "text")
153157

154158
// Hooks
155159
cfg.HookPreBackup = envOrDefault("HOOK_PRE_BACKUP", "")
@@ -225,8 +229,8 @@ func resolveFileVar(baseVar, fileVar string) string {
225229
}
226230

227231
// resolveRcloneConfig handles RCLONE_CONFIG (base64), RCLONE_CONFIG_FILE,
228-
// and the default path.
229-
func resolveRcloneConfig() string {
232+
// and the default path. It validates that the config file exists.
233+
func resolveRcloneConfig() (string, error) {
230234
// Check _FILE variant first
231235
filePath := resolveFileVar("RCLONE_CONFIG_FILE", "")
232236
if filePath == "" {
@@ -242,15 +246,25 @@ func resolveRcloneConfig() string {
242246
if err == nil {
243247
tmpFile.Write(decoded)
244248
tmpFile.Close()
245-
return tmpFile.Name()
249+
// Temp file was just created, so it exists
250+
return tmpFile.Name(), nil
246251
}
247252
}
248253
}
249254

250-
if filePath != "" {
251-
return filePath
255+
if filePath == "" {
256+
filePath = "/config/rclone.conf"
252257
}
253-
return "/config/rclone.conf"
258+
259+
// Validate that the config file exists
260+
if _, err := os.Stat(filePath); err != nil {
261+
if os.IsNotExist(err) {
262+
return "", fmt.Errorf("rclone config file not found at %q. Please set RCLONE_CONFIG_FILE to a valid path", filePath)
263+
}
264+
return "", fmt.Errorf("cannot access rclone config file at %q: %w", filePath, err)
265+
}
266+
267+
return filePath, nil
254268
}
255269

256270
func envOrDefault(key, fallback string) string {

internal/config/config_test.go

Lines changed: 44 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,22 @@ func clearEnv() {
2222
}
2323
}
2424

25-
func setMinimalEnv() {
25+
func setMinimalEnv(t *testing.T) {
2626
os.Setenv("ENGINE", "pg")
2727
os.Setenv("DB_HOST", "localhost")
2828
os.Setenv("DB_NAME", "testdb")
2929
os.Setenv("RCLONE_REMOTE", "s3:my-bucket/backups")
30+
31+
// Create temporary rclone config file
32+
dir := t.TempDir()
33+
rcloneConfigFile := filepath.Join(dir, "rclone.conf")
34+
os.WriteFile(rcloneConfigFile, []byte("[s3]\ntype = s3\n"), 0o644)
35+
os.Setenv("RCLONE_CONFIG_FILE", rcloneConfigFile)
3036
}
3137

3238
func TestLoad_MinimalConfig(t *testing.T) {
3339
clearEnv()
34-
setMinimalEnv()
40+
setMinimalEnv(t)
3541

3642
cfg, err := Load()
3743
if err != nil {
@@ -115,6 +121,12 @@ func TestLoad_DBURI(t *testing.T) {
115121
os.Setenv("DB_URI", "mongodb://user:pass@host:27017/mydb")
116122
os.Setenv("RCLONE_REMOTE", "s3:bucket")
117123

124+
// Create temporary rclone config file
125+
dir := t.TempDir()
126+
rcloneConfigFile := filepath.Join(dir, "rclone.conf")
127+
os.WriteFile(rcloneConfigFile, []byte("[s3]\ntype = s3\n"), 0o644)
128+
os.Setenv("RCLONE_CONFIG_FILE", rcloneConfigFile)
129+
118130
cfg, err := Load()
119131
if err != nil {
120132
t.Fatalf("unexpected error: %v", err)
@@ -132,9 +144,14 @@ func TestLoad_FileVariant(t *testing.T) {
132144
uriFile := filepath.Join(dir, "db_uri.txt")
133145
os.WriteFile(uriFile, []byte("postgresql://user:secret@dbhost:5432/prod\n"), 0o644)
134146

147+
// Create temporary rclone config file
148+
rcloneConfigFile := filepath.Join(dir, "rclone.conf")
149+
os.WriteFile(rcloneConfigFile, []byte("[s3]\ntype = s3\n"), 0o644)
150+
135151
os.Setenv("ENGINE", "pg")
136152
os.Setenv("DB_URI_FILE", uriFile)
137153
os.Setenv("RCLONE_REMOTE", "s3:bucket")
154+
os.Setenv("RCLONE_CONFIG_FILE", rcloneConfigFile)
138155

139156
cfg, err := Load()
140157
if err != nil {
@@ -152,10 +169,15 @@ func TestLoad_FileVariantPrecedence(t *testing.T) {
152169
uriFile := filepath.Join(dir, "db_uri.txt")
153170
os.WriteFile(uriFile, []byte("from-file"), 0o644)
154171

172+
// Create temporary rclone config file
173+
rcloneConfigFile := filepath.Join(dir, "rclone.conf")
174+
os.WriteFile(rcloneConfigFile, []byte("[s3]\ntype = s3\n"), 0o644)
175+
155176
os.Setenv("ENGINE", "pg")
156177
os.Setenv("DB_URI", "from-env")
157178
os.Setenv("DB_URI_FILE", uriFile)
158179
os.Setenv("RCLONE_REMOTE", "s3:bucket")
180+
os.Setenv("RCLONE_CONFIG_FILE", rcloneConfigFile)
159181

160182
cfg, err := Load()
161183
if err != nil {
@@ -174,11 +196,16 @@ func TestLoad_PasswordFile(t *testing.T) {
174196
pwFile := filepath.Join(dir, "password.txt")
175197
os.WriteFile(pwFile, []byte(" s3cret \n"), 0o644)
176198

199+
// Create temporary rclone config file
200+
rcloneConfigFile := filepath.Join(dir, "rclone.conf")
201+
os.WriteFile(rcloneConfigFile, []byte("[s3]\ntype = s3\n"), 0o644)
202+
177203
os.Setenv("ENGINE", "pg")
178204
os.Setenv("DB_HOST", "localhost")
179205
os.Setenv("DB_NAME", "testdb")
180206
os.Setenv("DB_PASSWORD_FILE", pwFile)
181207
os.Setenv("RCLONE_REMOTE", "s3:bucket")
208+
os.Setenv("RCLONE_CONFIG_FILE", rcloneConfigFile)
182209

183210
cfg, err := Load()
184211
if err != nil {
@@ -191,7 +218,7 @@ func TestLoad_PasswordFile(t *testing.T) {
191218

192219
func TestLoad_ScheduleOnce(t *testing.T) {
193220
clearEnv()
194-
setMinimalEnv()
221+
setMinimalEnv(t)
195222
os.Setenv("BACKUP_SCHEDULE", "once")
196223

197224
cfg, err := Load()
@@ -205,7 +232,7 @@ func TestLoad_ScheduleOnce(t *testing.T) {
205232

206233
func TestLoad_ScheduleOnceCaseInsensitive(t *testing.T) {
207234
clearEnv()
208-
setMinimalEnv()
235+
setMinimalEnv(t)
209236
os.Setenv("BACKUP_SCHEDULE", "Once")
210237

211238
cfg, err := Load()
@@ -219,7 +246,7 @@ func TestLoad_ScheduleOnceCaseInsensitive(t *testing.T) {
219246

220247
func TestLoad_InvalidSchedule(t *testing.T) {
221248
clearEnv()
222-
setMinimalEnv()
249+
setMinimalEnv(t)
223250
os.Setenv("BACKUP_SCHEDULE", "invalid-cron")
224251

225252
_, err := Load()
@@ -230,7 +257,7 @@ func TestLoad_InvalidSchedule(t *testing.T) {
230257

231258
func TestLoad_InvalidMode(t *testing.T) {
232259
clearEnv()
233-
setMinimalEnv()
260+
setMinimalEnv(t)
234261
os.Setenv("BACKUP_MODE", "invalid")
235262

236263
_, err := Load()
@@ -241,7 +268,7 @@ func TestLoad_InvalidMode(t *testing.T) {
241268

242269
func TestLoad_Timeout(t *testing.T) {
243270
clearEnv()
244-
setMinimalEnv()
271+
setMinimalEnv(t)
245272
os.Setenv("BACKUP_TIMEOUT", "30m")
246273

247274
cfg, err := Load()
@@ -255,7 +282,7 @@ func TestLoad_Timeout(t *testing.T) {
255282

256283
func TestLoad_InvalidTimeout(t *testing.T) {
257284
clearEnv()
258-
setMinimalEnv()
285+
setMinimalEnv(t)
259286
os.Setenv("BACKUP_TIMEOUT", "notaduration")
260287

261288
_, err := Load()
@@ -266,7 +293,7 @@ func TestLoad_InvalidTimeout(t *testing.T) {
266293

267294
func TestLoad_InvalidNotifyOn(t *testing.T) {
268295
clearEnv()
269-
setMinimalEnv()
296+
setMinimalEnv(t)
270297
os.Setenv("NOTIFY_ON", "never")
271298

272299
_, err := Load()
@@ -285,6 +312,12 @@ func TestLoad_AllEngines(t *testing.T) {
285312
os.Setenv("DB_NAME", "testdb")
286313
os.Setenv("RCLONE_REMOTE", "s3:bucket")
287314

315+
// Create temporary rclone config file
316+
dir := t.TempDir()
317+
rcloneConfigFile := filepath.Join(dir, "rclone.conf")
318+
os.WriteFile(rcloneConfigFile, []byte("[s3]\ntype = s3\n"), 0o644)
319+
os.Setenv("RCLONE_CONFIG_FILE", rcloneConfigFile)
320+
288321
cfg, err := Load()
289322
if err != nil {
290323
t.Fatalf("unexpected error for engine %s: %v", eng, err)
@@ -298,7 +331,7 @@ func TestLoad_AllEngines(t *testing.T) {
298331

299332
func TestLoad_Defaults(t *testing.T) {
300333
clearEnv()
301-
setMinimalEnv()
334+
setMinimalEnv(t)
302335

303336
cfg, err := Load()
304337
if err != nil {
@@ -314,7 +347,7 @@ func TestLoad_Defaults(t *testing.T) {
314347
{"BackupNameTemplate", cfg.BackupNameTemplate, "{db}-{date}-{time}"},
315348
{"NotifyOn", cfg.NotifyOn, "failure"},
316349
{"LogLevel", cfg.LogLevel, "info"},
317-
{"LogFormat", cfg.LogFormat, "json"},
350+
{"LogFormat", cfg.LogFormat, "text"},
318351
{"Timezone", cfg.Timezone, "UTC"},
319352
{"BackupTempDir", cfg.BackupTempDir, "/tmp/dbstash-work"},
320353
{"DBAuthSource", cfg.DBAuthSource, "admin"},

internal/pipeline/stream.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,23 +47,33 @@ func (p *StreamPipeline) Execute(ctx context.Context, eng engine.Engine, cfg *co
4747
var dumpStderr bytes.Buffer
4848
dumpCmd.Stderr = &dumpStderr
4949

50+
log.Debug().Str("dump_cmd", dumpCmd.String()).Msg("executing dump command")
51+
log.Debug().Strs("rclone_args", rcloneArgs).Msg("executing rclone command")
52+
5053
// Start rclone first, then dump
54+
log.Debug().Msg("starting rclone process")
5155
if err := rcloneCmd.Start(); err != nil {
5256
return "", 0, fmt.Errorf("starting rclone: %w", err)
5357
}
5458

59+
log.Debug().Msg("starting dump process")
5560
if err := dumpCmd.Start(); err != nil {
5661
pw.Close()
5762
rcloneCmd.Wait()
5863
return "", 0, fmt.Errorf("starting dump: %w", err)
5964
}
6065

66+
log.Debug().Msg("waiting for dump to complete")
67+
6168
// Wait for dump to finish, then close the pipe
6269
dumpErr := dumpCmd.Wait()
70+
log.Debug().Err(dumpErr).Msg("dump process finished")
6371
pw.Close()
6472

6573
// Wait for rclone to finish
74+
log.Debug().Msg("waiting for rclone to complete")
6675
rcloneErr := rcloneCmd.Wait()
76+
log.Debug().Err(rcloneErr).Msg("rclone process finished")
6777
pr.Close()
6878

6979
if dumpErr != nil {

0 commit comments

Comments
 (0)