Skip to content

Commit 015a410

Browse files
feat: refactor backup configuration path handling and add integration test coverage
- Move ConfigFile from /etc/eos/backup/config.yaml to /etc/eos/backup.yaml for operator consistency - Add LegacyConfigFile (/etc/eos/backup/config.yaml) with backward-compatible read fallback - Change PasswordDirPerm from 0500 to 0700 to support secure file creation/rotation - Extract ResolveRepositoryName() helper to deduplicate default repository logic across list/restore/verify/update commands - Add ResolveRepositoryName
1 parent 16187ee commit 015a410

22 files changed

Lines changed: 658 additions & 173 deletions

.github/workflows/ci.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,8 @@ jobs:
134134
POSTGRES_URL: postgres://postgres:testpass@localhost:5432/testdb?sslmode=disable
135135
run: |
136136
go test -v -timeout=15m ./test/integration_test.go ./test/integration_scenarios_test.go
137+
# Backup integration layer (20% test pyramid allocation for backup workflow)
138+
go test -v -timeout=15m -run Integration ./pkg/backup/...
137139
go test -v -timeout=15m -tags=integration ./pkg/vault/...
138140
139141
ci-e2e-smoke:
@@ -156,6 +158,11 @@ jobs:
156158
- name: Run smoke e2e tests
157159
run: go test -v -tags=e2e_smoke -timeout=10m ./test/e2e/smoke/...
158160

161+
- name: Run backup e2e smoke tests
162+
run: |
163+
# Backup e2e layer (10% test pyramid allocation for backup workflow)
164+
go test -v -tags=e2e_smoke -timeout=10m -run Backup ./test/e2e/smoke/...
165+
159166
ci-e2e-full:
160167
name: ci-e2e-full
161168
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'

cmd/backup/list.go

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -166,17 +166,11 @@ Examples:
166166
filterHost, _ := cmd.Flags().GetString("host")
167167
filterPath, _ := cmd.Flags().GetString("path")
168168

169-
// Use default repository if not specified
170-
if repoName == "" {
171-
config, err := backup.LoadConfig(rc)
172-
if err != nil {
173-
return fmt.Errorf("loading configuration: %w", err)
174-
}
175-
repoName = config.DefaultRepository
176-
if repoName == "" {
177-
return fmt.Errorf("no repository specified and no default configured")
178-
}
169+
resolvedRepoName, err := backup.ResolveRepositoryName(rc, repoName)
170+
if err != nil {
171+
return err
179172
}
173+
repoName = resolvedRepoName
180174

181175
logger.Info("Listing snapshots",
182176
zap.String("repository", repoName),

cmd/backup/quick.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,10 @@ import (
2525
var quickBackupCmd = &cobra.Command{
2626
Use: ". [directory]",
2727
Short: "Quick backup of current (or specified) directory",
28-
Long: `Instantly backup the current directory or specified path with timestamp.
28+
Long: fmt.Sprintf(`Instantly backup the current directory or specified path with timestamp.
2929
3030
This command reuses your existing backup configuration:
31-
- Uses the default repository defined in /etc/eos/backup.yaml
31+
- Uses the default repository defined in %s
3232
- Honors repository credentials and password files you already manage
3333
- Timestamps each backup automatically
3434
- Recursive by default
@@ -41,7 +41,7 @@ Examples:
4141
4242
Restore:
4343
eos restore . [snapshot-id] # Restore latest or specific snapshot
44-
eos restore . --target /tmp/restored # Restore to different location`,
44+
eos restore . --target /tmp/restored # Restore to different location`, backup.ConfigFile),
4545

4646
Args: cobra.MaximumNArgs(1),
4747
RunE: eos.Wrap(func(rc *eos_io.RuntimeContext, cmd *cobra.Command, args []string) error {
@@ -173,7 +173,7 @@ func resolveQuickBackupRepository(rc *eos_io.RuntimeContext) (string, backup.Rep
173173
}
174174

175175
if len(config.Repositories) == 0 {
176-
return "", backup.Repository{}, fmt.Errorf("no repositories configured; add at least one in /etc/eos/backup.yaml")
176+
return "", backup.Repository{}, fmt.Errorf("no repositories configured; add at least one in %s", backup.ConfigFile)
177177
}
178178

179179
if len(config.Repositories) == 1 {
@@ -192,8 +192,8 @@ func resolveQuickBackupRepository(rc *eos_io.RuntimeContext) (string, backup.Rep
192192
sort.Strings(repoNames)
193193

194194
return "", backup.Repository{}, eos_err.NewExpectedError(rc.Ctx, fmt.Errorf(
195-
"multiple repositories configured (%s) but no default_repository set; update /etc/eos/backup.yaml to select one",
196-
strings.Join(repoNames, ", ")))
195+
"multiple repositories configured (%s) but no default_repository set; update %s to select one",
196+
strings.Join(repoNames, ", "), backup.ConfigFile))
197197
}
198198

199199
func init() {

cmd/backup/restore.go

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
package backup
44

55
import (
6-
"github.com/CodeMonkeyCybersecurity/eos/pkg/shared"
76
"encoding/json"
87
"fmt"
8+
"github.com/CodeMonkeyCybersecurity/eos/pkg/shared"
99
"os"
1010
"path/filepath"
1111

@@ -55,17 +55,11 @@ Examples:
5555
verify, _ := cmd.Flags().GetBool("verify")
5656
force, _ := cmd.Flags().GetBool("force")
5757

58-
// Use default repository if not specified
59-
if repoName == "" {
60-
config, err := backup.LoadConfig(rc)
61-
if err != nil {
62-
return fmt.Errorf("loading configuration: %w", err)
63-
}
64-
repoName = config.DefaultRepository
65-
if repoName == "" {
66-
return fmt.Errorf("no repository specified and no default configured")
67-
}
58+
resolvedRepoName, err := backup.ResolveRepositoryName(rc, repoName)
59+
if err != nil {
60+
return err
6861
}
62+
repoName = resolvedRepoName
6963

7064
logger.Info("Starting restore operation",
7165
zap.String("snapshot", snapshotID),

cmd/backup/update.go

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ import (
88
"github.com/CodeMonkeyCybersecurity/eos/pkg/backup"
99
eos "github.com/CodeMonkeyCybersecurity/eos/pkg/eos_cli"
1010
"github.com/CodeMonkeyCybersecurity/eos/pkg/eos_io"
11-
"github.com/CodeMonkeyCybersecurity/eos/pkg/verify"
1211
"github.com/CodeMonkeyCybersecurity/eos/pkg/patterns"
12+
"github.com/CodeMonkeyCybersecurity/eos/pkg/verify"
1313
"github.com/spf13/cobra"
1414
"github.com/uptrace/opentelemetry-go-extra/otelzap"
1515
"go.uber.org/zap"
@@ -73,12 +73,9 @@ Examples:
7373
}
7474

7575
// Determine repository
76-
repoName := profile.Repository
77-
if repoName == "" {
78-
repoName = config.DefaultRepository
79-
if repoName == "" {
80-
return fmt.Errorf("no repository specified and no default configured")
81-
}
76+
repoName, err := backup.ResolveRepositoryNameFromConfig(config, profile.Repository)
77+
if err != nil {
78+
return err
8279
}
8380

8481
logger.Info("Using repository",

cmd/backup/verify.go

Lines changed: 8 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -45,17 +45,11 @@ var verifyRepoCmd = &cobra.Command{
4545
readData, _ := cmd.Flags().GetBool("read-data")
4646
readDataSubset, _ := cmd.Flags().GetString("read-data-subset")
4747

48-
// Use default repository if not specified
49-
if repoName == "" {
50-
config, err := backup.LoadConfig(rc)
51-
if err != nil {
52-
return fmt.Errorf("loading configuration: %w", err)
53-
}
54-
repoName = config.DefaultRepository
55-
if repoName == "" {
56-
return fmt.Errorf("no repository specified and no default configured")
57-
}
48+
resolvedRepoName, err := backup.ResolveRepositoryName(rc, repoName)
49+
if err != nil {
50+
return err
5851
}
52+
repoName = resolvedRepoName
5953

6054
logger.Info("Verifying repository integrity",
6155
zap.String("repository", repoName),
@@ -110,17 +104,11 @@ var verifySnapshotCmd = &cobra.Command{
110104
repoName, _ := cmd.Flags().GetString("repo")
111105
readData, _ := cmd.Flags().GetBool("read-data")
112106

113-
// Use default repository if not specified
114-
if repoName == "" {
115-
config, err := backup.LoadConfig(rc)
116-
if err != nil {
117-
return fmt.Errorf("loading configuration: %w", err)
118-
}
119-
repoName = config.DefaultRepository
120-
if repoName == "" {
121-
return fmt.Errorf("no repository specified and no default configured")
122-
}
107+
resolvedRepoName, err := backup.ResolveRepositoryName(rc, repoName)
108+
if err != nil {
109+
return err
123110
}
111+
repoName = resolvedRepoName
124112

125113
logger.Info("Verifying snapshot integrity",
126114
zap.String("snapshot", snapshotID),

cmd/list/backups.go

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,6 @@ func listBackups(rc *eos_io.RuntimeContext, cmd *cobra.Command, args []string) e
100100
return err
101101
}
102102

103-
104103
// Get flags
105104
repoName, _ := cmd.Flags().GetString("repo")
106105
filterTags, _ := cmd.Flags().GetStringSlice("tags")
@@ -123,17 +122,11 @@ func listBackups(rc *eos_io.RuntimeContext, cmd *cobra.Command, args []string) e
123122
}
124123
}
125124

126-
// Use default repository if not specified
127-
if repoName == "" {
128-
config, err := backup.LoadConfig(rc)
129-
if err != nil {
130-
return fmt.Errorf("loading configuration: %w", err)
131-
}
132-
repoName = config.DefaultRepository
133-
if repoName == "" {
134-
return fmt.Errorf("no repository specified and no default configured")
135-
}
125+
resolvedRepoName, err := backup.ResolveRepositoryName(rc, repoName)
126+
if err != nil {
127+
return err
136128
}
129+
repoName = resolvedRepoName
137130

138131
logger.Info("Listing backup snapshots",
139132
zap.String("repository", repoName),

pkg/backup/client.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,7 @@ func (c *Client) getRepositoryPassword() (string, error) {
283283
}
284284

285285
// 2. Global secrets directory fallback (used by managed repositories)
286-
secretsPasswordPath := filepath.Join(SecretsDir, fmt.Sprintf("%s.password", c.repository.Name))
286+
secretsPasswordPath := filepath.Join(secretsDirPath, fmt.Sprintf("%s.password", c.repository.Name))
287287
if password, err := readPasswordFile(secretsPasswordPath); err == nil {
288288
logger.Debug("Using secrets directory password file",
289289
zap.String("path", secretsPasswordPath))
@@ -307,7 +307,7 @@ func (c *Client) getRepositoryPassword() (string, error) {
307307
}
308308

309309
// 4a. Secrets directory .env file (fallback for non-local repositories)
310-
secretsEnvPath := filepath.Join(SecretsDir, fmt.Sprintf("%s.env", c.repository.Name))
310+
secretsEnvPath := filepath.Join(secretsDirPath, fmt.Sprintf("%s.env", c.repository.Name))
311311
if password, err := readPasswordFromEnvFile(secretsEnvPath); err == nil {
312312
logger.Debug("Using secrets .env file for restic password",
313313
zap.String("path", secretsEnvPath))

pkg/backup/client_integration_test.go

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -201,12 +201,17 @@ func TestPasswordRetrievalIntegration(t *testing.T) {
201201
}
202202

203203
t.Run("vault unavailable fallback", func(t *testing.T) {
204+
originalSecretsDir := secretsDirPath
205+
secretsDirPath = filepath.Join(repo.TempDir, "secrets", "backup")
206+
t.Cleanup(func() {
207+
secretsDirPath = originalSecretsDir
208+
})
209+
204210
// Create local password file
205-
secretsDir := filepath.Join(repo.TempDir, "secrets", "backup")
206-
err := os.MkdirAll(secretsDir, 0700)
211+
err := os.MkdirAll(secretsDirPath, 0700)
207212
require.NoError(t, err)
208213

209-
passwordFile := filepath.Join(secretsDir, fmt.Sprintf("%s.password", repo.Name))
214+
passwordFile := filepath.Join(secretsDirPath, fmt.Sprintf("%s.password", repo.Name))
210215
err = os.WriteFile(passwordFile, []byte(repo.Password), 0600)
211216
require.NoError(t, err)
212217

@@ -223,13 +228,18 @@ func TestPasswordRetrievalIntegration(t *testing.T) {
223228
})
224229

225230
t.Run("no password available", func(t *testing.T) {
231+
originalSecretsDir := secretsDirPath
232+
secretsDirPath = t.TempDir()
233+
t.Cleanup(func() {
234+
secretsDirPath = originalSecretsDir
235+
})
236+
226237
// Test case where neither Vault nor local file is available
227238
client.repository.Name = "nonexistent-repo"
228239

229240
// This should result in an error when getRepositoryPassword is called
230241
// Since we can't easily mock Vault here, we test the error conditions
231-
secretsDir := "/var/lib/eos/secrets/backup"
232-
passwordFile := filepath.Join(secretsDir, "nonexistent-repo.password")
242+
passwordFile := filepath.Join(secretsDirPath, "nonexistent-repo.password")
233243

234244
// Verify file doesn't exist
235245
_, err := os.Stat(passwordFile)

0 commit comments

Comments
 (0)