P3 sweep fixes: snap-row CSS, tree expand, --no-ownership drop, target path
Bug fixes from the Playwright sweep against the live smoke server:
1. Snapshot-picker layout. The .snap-row class was used in the wireframe
but never landed in web/styles/input.css; rows rendered as vertical
blocks instead of a 6-column grid. Added the token (mirrors host-row
shape with restore-specific column widths).
2. Tree expansion. hx-target='closest .tree-row + .tree-children' isn't
a valid HTMX selector — modifiers don't chain. Replaced HTMX-driven
expansion with a small window.__rmTreeToggle helper that uses plain
fetch + .tree-pair wrapper structure for trivial sibling lookup.
Caches loaded state per node.
3. --no-ownership flag dropped. Restic 0.17 introduced --no-ownership;
0.16 rejects it ('unknown flag') before doing any work. Since the
agent runs as root in the systemd unit, restored files keep their
original uid/gid either way and the parent dir is root-owned, so
the 'cp without sudo' rationale doesn't hold. Drop the flag entirely.
4. Default target dir moved to /var/lib/restic-manager/restore. The
systemd unit pins ReadWritePaths to /etc/restic-manager +
/var/lib/restic-manager (with ProtectSystem=strict making the rest
of /var read-only); writes to /var/restic-restore failed with
'read-only file system'.
5. Confirm summary HTML escaping. defaultTarget JS literal evaluates
to a string with literal angle brackets; insertion into innerHTML
must escape them. Added an inline HTML-escape pass.
tasks.md ticked for the Restore sub-phase with a sweep summary
covering the live end-to-end test.
This commit is contained in:
@@ -141,9 +141,11 @@ esac
|
||||
}
|
||||
}
|
||||
|
||||
// TestRunRestoreNewDirArgvHasNoOwnership: complement of the above —
|
||||
// non-in-place restore must include --no-ownership.
|
||||
func TestRunRestoreNewDirArgvHasNoOwnership(t *testing.T) {
|
||||
// TestRunRestoreNewDirArgvShape: non-in-place restore passes --target
|
||||
// to the operator-chosen new directory and includes the path filters.
|
||||
// We deliberately do NOT pass --no-ownership (added in restic 0.17;
|
||||
// older versions error out — the comment in restore.go explains why).
|
||||
func TestRunRestoreNewDirArgvShape(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
bin := setupScript(t, `
|
||||
@@ -175,8 +177,8 @@ esac
|
||||
if argv == "" {
|
||||
t.Fatal("no argv echo")
|
||||
}
|
||||
if !strings.Contains(argv, "--no-ownership") {
|
||||
t.Errorf("new-dir restore should pass --no-ownership; got argv=%q", argv)
|
||||
if strings.Contains(argv, "--no-ownership") {
|
||||
t.Errorf("restic 0.16 doesn't accept --no-ownership; got argv=%q", argv)
|
||||
}
|
||||
if !strings.Contains(argv, "--target /tmp/restore-out") {
|
||||
t.Errorf("expected --target /tmp/restore-out; got argv=%q", argv)
|
||||
|
||||
@@ -65,9 +65,15 @@ func (e Env) RunRestore(ctx context.Context, snapshotID string, paths []string,
|
||||
target = "/"
|
||||
}
|
||||
args = append(args, "--target", target)
|
||||
if !inPlace {
|
||||
args = append(args, "--no-ownership")
|
||||
}
|
||||
// NOTE: restic added --no-ownership in 0.17. Older versions reject
|
||||
// the flag with "unknown flag: --no-ownership" before doing any
|
||||
// work. Since the agent runs as root in the systemd unit, files
|
||||
// land under /var/restic-restore with their original uid/gid
|
||||
// either way — the original "cp without sudo" rationale doesn't
|
||||
// hold (operators copying from /var/restic-restore need sudo
|
||||
// regardless because the parent dir is root-owned). Drop the flag
|
||||
// entirely until we drop 0.16 support; revisit if a non-root
|
||||
// agent deployment requirement comes back.
|
||||
for _, p := range paths {
|
||||
args = append(args, "--include", p)
|
||||
}
|
||||
|
||||
@@ -384,11 +384,14 @@ func (s *Server) handleUIRestoreTree(w stdhttp.ResponseWriter, r *stdhttp.Reques
|
||||
}
|
||||
|
||||
// defaultRestoreTargetRoot is the parent of the per-job restore
|
||||
// directory. Chosen on a per-host basis would be nicer but the agent
|
||||
// is the one that actually creates it, and /var/restic-restore is
|
||||
// fine for Linux hosts (the agent's systemd unit runs as root).
|
||||
// directory. The agent's systemd unit pins ReadWritePaths to
|
||||
// /etc/restic-manager + /var/lib/restic-manager (with ProtectSystem=
|
||||
// strict making the rest of /var read-only); restore writes have to
|
||||
// land inside one of those, so we keep them under
|
||||
// /var/lib/restic-manager/restore where the agent is already allowed
|
||||
// to write. The /restore subdir is created by the agent on demand.
|
||||
func defaultRestoreTargetRoot() string {
|
||||
return "/var/restic-restore"
|
||||
return "/var/lib/restic-manager/restore"
|
||||
}
|
||||
|
||||
// defaultRestoreTargetDir surfaces the placeholder path shown on the
|
||||
|
||||
@@ -302,8 +302,8 @@ func TestRestorePostHappyPathDispatches(t *testing.T) {
|
||||
if cp.Restore.InPlace {
|
||||
t.Fatal("expected new-directory mode (in_place=false)")
|
||||
}
|
||||
if !strings.HasPrefix(cp.Restore.TargetDir, "/var/restic-restore/") {
|
||||
t.Fatalf("target_dir: got %q, want prefix /var/restic-restore/", cp.Restore.TargetDir)
|
||||
if !strings.HasPrefix(cp.Restore.TargetDir, "/var/lib/restic-manager/restore/") {
|
||||
t.Fatalf("target_dir: got %q, want prefix /var/lib/restic-manager/restore/", cp.Restore.TargetDir)
|
||||
}
|
||||
if len(cp.Restore.Paths) != 2 {
|
||||
t.Fatalf("paths: got %d, want 2", len(cp.Restore.Paths))
|
||||
|
||||
Reference in New Issue
Block a user