Skip to content

fix(backup): resolve the restic repository against the live DataDir (agent-os-9au) - #33

Merged
thinkbig1979 merged 1 commit into
mainfrom
fix/backup-repo-path-resolution
Jul 31, 2026
Merged

fix(backup): resolve the restic repository against the live DataDir (agent-os-9au)#33
thinkbig1979 merged 1 commit into
mainfrom
fix/backup-repo-path-resolution

Conversation

@thinkbig1979

Copy link
Copy Markdown
Owner

Four handlers resolved their BackupConfig with services.ResolveBackupConfig(db), which called resolveBackupConfig(db, &config.Config{}) — an empty config. With cfg.DataDir == "" the default repository became filepath.Join("", "restic-repo"), i.e. the relative path restic-repo, resolved against the server's working directory (/app).

Every other code path used the service's real config and correctly produced <DataDir>/restic-repo. So init created a repository at /app/restic-repo and reported success, while every backup looked in /app/data/restic-repo and failed.

Observed before the fix

Real container, DATA_DIR=/app/data, no restic_repository setting:

POST /api/v1/backups/repo/init -> {"initialized":true}
log: INFO Initialising restic repository component=restic-manager path=restic-repo
ls /app/restic-repo            -> a valid restic repository   (WRONG location)
ls /app/data/restic-repo       -> did not exist
GET  /api/v1/settings/backup   -> repository=/app/data/restic-repo, repositoryInitialized=false
POST /api/v1/backups/run       -> failed, item durationMs=76

Out of the box — no restic_repository setting, no RESTIC_REPOSITORY env — the UI "Initialise repository" flow could not produce a working backup. /app is also the container's writable layer rather than the data volume, so the repository that was created is destroyed on every container recreation.

Why it was never caught

The four sites — repoInit, cloudTest, listSnapshotsViaRestic, previewSnapshotViaRestic — also constructed their managers with services.NewResticManager / NewRcloneManager directly, bypassing BackupService's factory seam. A test with an injected runner could not reach those paths at all.

The pre-existing TestResolveBackupConfig_ExportedVariantReadsMergedConfig set restic_repository explicitly, so it never exercised the default branch. That is precisely how this survived.

Fix

Both problems have one fix. BackupService gains ResolveConfig, NewResticManager and NewRcloneManager, which resolve against the live config and honour the factory seam. The handlers call those.

The DataDir-less exported ResolveBackupConfig is removed rather than corrected — any resolver that does not take the live config can reintroduce this silently, so the type system now forbids it. A comment at the old site records why.

Tests

Written before the fix and confirmed failing against the old code for the right reason (the injected runner never saw the call at all):

--- FAIL: TestRepoInit_InitialisesRepositoryUnderDataDir
    repoInit must build its ResticManager through the service factory;
    constructing one directly bypasses both the injected runner and the live config
--- FAIL: TestSnapshotListing_ResolvesRepositoryUnderDataDir
    snapshot listing must build its ResticManager through the service factory

Plus two resolver-level guards, including one for the default branch the old test skipped.

Gates

go build and go vet clean; 667 tests pass in 9 packages (663 baseline + 4 added).

Found while running the agent-os-9e1 backup drill, not by reading code. It also clears the way for agent-os-36o's DR drill, which starts from a fresh install and would otherwise hit this immediately.

Closes agent-os-9au.

Four handlers resolved their BackupConfig with services.ResolveBackupConfig(db),
which called resolveBackupConfig(db, &config.Config{}) — an EMPTY config. With
cfg.DataDir == "" the default repository became filepath.Join("", "restic-repo"),
i.e. the RELATIVE path "restic-repo", resolved against the server's working
directory (/app).

Every other code path used the service's real config and correctly produced
<DataDir>/restic-repo. So repo init created a repository at /app/restic-repo and
reported success, while every backup looked in /app/data/restic-repo and failed.

Observed on a real container before the fix (DATA_DIR=/app/data, no
restic_repository setting):

  POST /api/v1/backups/repo/init -> {"initialized":true}
  log: INFO Initialising restic repository component=restic-manager path=restic-repo
  ls /app/restic-repo            -> a valid restic repository (WRONG location)
  ls /app/data/restic-repo       -> did not exist
  GET  /api/v1/settings/backup   -> repository=/app/data/restic-repo,
                                    repositoryInitialized=false
  POST /api/v1/backups/run       -> failed, item durationMs=76

Out of the box — no restic_repository setting, no RESTIC_REPOSITORY env — the UI
"Initialise repository" flow could not produce a working backup. /app is also the
container's writable layer, not the data volume, so the repository that WAS
created is destroyed on every container recreation.

The four sites were handlers/backup.go: repoInit (792), cloudTest (814),
listSnapshotsViaRestic (1036), previewSnapshotViaRestic (1043).

They also constructed their managers with services.NewResticManager /
NewRcloneManager directly, bypassing BackupService's factory seam. That is why
the defect was never caught: those paths could not be reached by a test with an
injected runner at all.

Both problems have one fix. BackupService gains ResolveConfig, NewResticManager
and NewRcloneManager, which resolve against the live config and honour the
factory seam; the handlers call those.

The DataDir-less exported ResolveBackupConfig is REMOVED rather than corrected.
Any resolver that does not take the live config can reintroduce this silently,
so the type system now forbids it: callers go through the service, which cannot
exist without a config.Config. A comment at the old site records why.

Tests, written before the fix and confirmed failing against the old code for the
right reason (the injected runner never saw the call at all):

  TestRepoInit_InitialisesRepositoryUnderDataDir
  TestSnapshotListing_ResolvesRepositoryUnderDataDir

plus two resolver-level guards. Note that the pre-existing
TestResolveBackupConfig_ExportedVariantReadsMergedConfig set restic_repository
explicitly, so it never exercised the default branch — which is exactly how this
survived. TestResolveBackupConfig_DefaultRepositoryIsAbsoluteUnderDataDir now
covers it.

Gates: go build, go vet clean; 667 tests pass in 9 packages (663 baseline + 4).

Found while running the agent-os-9e1 backup drill, not by reading code.

Refs: agent-os-9au

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@thinkbig1979
thinkbig1979 merged commit 238e36c into main Jul 31, 2026
9 checks passed
@thinkbig1979
thinkbig1979 deleted the fix/backup-repo-path-resolution branch July 31, 2026 17:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant