From 08c7a79f79a5445a2484582ab9d004032d4f7467 Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 5 Oct 2026 12:31:07 +0200 Subject: [PATCH] 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. --- modules/restic/cmd/restic-backups/backups.go | 12 +++++- .../restic/cmd/restic-backups/backups_test.go | 38 ++++++++++++++++++- 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/modules/restic/cmd/restic-backups/backups.go b/modules/restic/cmd/restic-backups/backups.go index a65869e..08a8339 100644 --- a/modules/restic/cmd/restic-backups/backups.go +++ b/modules/restic/cmd/restic-backups/backups.go @@ -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 { diff --git a/modules/restic/cmd/restic-backups/backups_test.go b/modules/restic/cmd/restic-backups/backups_test.go index cf85a03..684ba3f 100644 --- a/modules/restic/cmd/restic-backups/backups_test.go +++ b/modules/restic/cmd/restic-backups/backups_test.go @@ -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) + } +}