restic: ask as root whether a directory is there

The first run on the control node called postgres's dumps missing: the holder looked as the
runtime's account, which cannot see inside a store's 0700 data directory. Every other act already
runs as root; the existence checks do too now.
This commit is contained in:
2026-10-05 12:31:07 +02:00
parent d9120c6848
commit 08c7a79f79
2 changed files with 46 additions and 4 deletions
+10 -2
View File
@@ -184,6 +184,14 @@ type Backups struct {
mu sync.Mutex
}
// exists is whether a path is there, asked as root: a store's directory is often its own user's
// alone (postgres's 0700 data directory), and the runtime's account asking for itself would see
// nothing — every such directory "missing", and its module never backed up.
func (b *Backups) exists(ctx context.Context, path string) bool {
_, err := b.Run(ctx, "test", "-e", path)
return err == nil
}
func (b *Backups) restic(ctx context.Context, args ...string) (string, error) {
return b.Run(ctx, "restic", append([]string{"--repo", b.Where.Repository, "--password-file", b.Where.PasswordFile, "--no-cache"}, args...)...)
}
@@ -253,7 +261,7 @@ func (b *Backups) one(ctx context.Context, d Declared) Night {
}
var missing []string
for _, p := range d.Paths {
if _, err := os.Stat(p); err != nil {
if !b.exists(ctx, p) {
missing = append(missing, p)
}
}
@@ -429,7 +437,7 @@ func (b *Backups) Restore(ctx context.Context, module, snapshot, path string) (*
r := &Restored{Module: module, From: chosen, Live: "untouched — swapping it in is a person's act"}
for _, p := range paths {
target := p + ".restored-" + stamp
if _, err := os.Stat(target); err == nil {
if b.exists(ctx, target) {
return nil, fmt.Errorf("%s already exists; nothing is restored over anything", target)
}
if _, err := b.restic(ctx, "restore", chosen.ID+":"+p, "--target", target); err != nil {
@@ -75,6 +75,9 @@ func TestANightDumpsBeforeEachSnapshotAndOneFailingModuleFailsOnlyItself(t *test
if name == "restic" {
line = "restic " + strings.Join(args[5:], " ")
}
if name == "test" {
return "", nil
}
calls = append(calls, line)
switch {
case line == "sh -c fail-it":
@@ -138,8 +141,13 @@ func TestARepositoryThatWillNotOpenIsNeverReplaced(t *testing.T) {
}
func TestADeclaredDirectoryThatDoesNotExistFailsThatModulesNight(t *testing.T) {
b := &Backups{Where: placed(t, "# pg\npath /nowhere/at/all\n"), Run: func(context.Context, string, ...string) (string, error) { return "", nil },
Now: time.Now, Say: quiet}
run := func(_ context.Context, name string, args ...string) (string, error) {
if name == "test" && args[1] == "/nowhere/at/all" {
return "", errors.New("exit status 1")
}
return "", nil
}
b := &Backups{Where: placed(t, "# pg\npath /nowhere/at/all\n"), Run: run, Now: time.Now, Say: quiet}
outcome, err := b.BackUp(context.Background(), "")
must(t, err)
if outcome["pg"].OK || !strings.Contains(outcome["pg"].Error, "/nowhere/at/all does not exist") {
@@ -226,3 +234,29 @@ func TestWithTheRealResticAMistakeIsUndoneBesideTheLiveData(t *testing.T) {
t.Fatalf("a second restore: %v", err)
}
}
// A store's directory is often its own user's alone; whether it is there is asked as root, never by
// the runtime's account looking for itself — which saw nothing in postgres's 0700 data directory and
// called the dumps missing on the first run (2026-10-05).
func TestWhetherADirectoryIsThereIsAskedAsRoot(t *testing.T) {
var asked []string
run := func(_ context.Context, name string, args ...string) (string, error) {
if name == "test" {
asked = append(asked, strings.Join(args, " "))
}
if name == "restic" && args[5] == "backup" {
return "{\"message_type\":\"summary\",\"snapshot_id\":\"0123456789abcdef\"}\n", nil
}
return "", nil
}
// A path the account cannot see, which root can.
b := &Backups{Where: placed(t, "# pg\npath /root/only/dumps\n"), Run: run, Now: time.Now, Say: quiet}
outcome, err := b.BackUp(context.Background(), "")
must(t, err)
if !outcome["pg"].OK {
t.Fatalf("a directory only root sees was called missing: %+v", outcome["pg"])
}
if !reflect.DeepEqual(asked, []string{"-e /root/only/dumps"}) {
t.Errorf("asked %v", asked)
}
}