| index a1fe4a571..490e05cc0 100644 |
| --- a/internal/host/movespolicy_test.go |
| +++ b/internal/host/movespolicy_test.go |
| @@ -809,14 +809,15 @@ func rpRefused(t *testing.T, err error, reason string) { |
| // phase of the rollback, under the claim phase that phase runs under. The |
| // dirty install's held writes are granted at their phases (timers at local, |
| // config and release at configured, restore and install at restored), and |
| -// the held config render and the spawn at probing; nothing else. A record |
| +// the held config render, the release materialization and the spawn at |
| +// probing (§7.7); nothing else. A record |
| // phase under the wrong claim phase is refused. |
| func TestMovesClaimPolicyRestoringRows(t *testing.T) { |
| granted := map[string]map[string]bool{ |
| provision.RbSourceLocal: {"held-timers": true}, |
| provision.RbSourceConfigured: {"held-config": true, "held-release": true}, |
| provision.RbSourceRestored: {"restore": true, "install": true}, |
| - provision.RbSourceProbing: {"held-config": true, "spawn": true}, |
| + provision.RbSourceProbing: {"held-config": true, "held-release": true, "spawn": true}, |
| } |
| grants := 0 |
| for _, phase := range rpSourcePhases { |
| @@ -843,8 +844,8 @@ func TestMovesClaimPolicyRestoringRows(t *testing.T) { |
| t.Errorf("%s: the rows are keyed by the record: %d reads %v", phase, reader.reads, reader.args) |
| } |
| } |
| - if grants != 7 { |
| - t.Fatalf("%d cells granted, want 7", grants) |
| + if grants != 8 { |
| + t.Fatalf("%d cells granted, want 8", grants) |
| } |
| t.Run("the wrong claim phase", func(t *testing.T) { |
| for _, tc := range []struct{ record, claim, action string }{ |
| @@ -862,8 +863,8 @@ func TestMovesClaimPolicyRestoringRows(t *testing.T) { |
| } |
| |
| // reverting, the source's pre-flip recovery (J12): the rows are keyed by the |
| -// claim's rev_phase. Only probing grants: the held config render before each |
| -// probe and the spawn. The same cells through hostbox.Policy.Check with the |
| +// claim's rev_phase. Only probing grants: the held config render and the |
| +// release materialization before each probe (§7.7), and the spawn. The same cells through hostbox.Policy.Check with the |
| // revert's hold, the move id. |
| func TestMovesClaimPolicyRevertingRows(t *testing.T) { |
| reader := &mpReader{rec: rpRevertingRecord()} |
| @@ -875,7 +876,7 @@ func TestMovesClaimPolicyRevertingRows(t *testing.T) { |
| c := rpRevertingClaim(rev) |
| s := rpSelection(c, action) |
| err := moves(t.Context(), c, s, rpIdentity(action)) |
| - want := rev == hostclaim.PhaseProbing && (action == "held-config" || action == "spawn") |
| + want := rev == hostclaim.PhaseProbing && (action == "held-config" || action == "held-release" || action == "spawn") |
| if (err == nil) != want { |
| t.Errorf("%s/%s: err %v, want granted %v", rev, action, err, want) |
| continue |
| @@ -891,8 +892,8 @@ func TestMovesClaimPolicyRevertingRows(t *testing.T) { |
| } |
| } |
| } |
| - if grants != 2 { |
| - t.Fatalf("%d cells granted, want 2", grants) |
| + if grants != 3 { |
| + t.Fatalf("%d cells granted, want 3", grants) |
| } |
| t.Run("rev_phase and op.phase disagree", func(t *testing.T) { |
| c := rpRevertingClaim(hostclaim.PhaseProbing) |
| @@ -1178,7 +1179,12 @@ func TestMovesClaimPolicyBindsRecoveryInputs(t *testing.T) { |
| rpRefused(t, moves(t.Context(), c, rpSelection(c, "spawn"), id), "the identity does not fit the action") |
| } |
| rpRefused(t, moves(t.Context(), c, rpSelection(c, "held-config"), rpLaunch(recovered)), "the identity does not fit the action") |
| - rpRefused(t, moves(t.Context(), c, rpSelection(c, "held-release"), hostwrite.Identity{Release: "v1.3.0"}), "the phase does not permit the action") |
| + for _, in := range []*provision.MoveInputs{recovered, pinned} { |
| + if err := moves(t.Context(), c, rpSelection(c, "held-release"), hostwrite.Identity{Release: in.Release}); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + rpRefused(t, moves(t.Context(), c, rpSelection(c, "held-release"), rpLaunch(recovered)), "the identity does not fit the action") |
| }) |
| t.Run("a landing: the same difference is refused", func(t *testing.T) { |
| moves, _ := mpPolicy() |
| @@ -1232,3 +1238,91 @@ func TestMovesClaimPolicyRestoringThroughPolicyCheck(t *testing.T) { |
| t.Fatalf("held-timers at configured: %v", err) |
| } |
| } |
| + |
| +// The release is materialized before every recovery probe (§7.7; Task 3b |
| +// fix round 2), through hostbox.Policy.Check with each recovery's own hold: |
| +// held-release at probing is granted to the executing source, bound to the |
| +// recorded recovery inputs on a return and by shape on a revert, and |
| +// refused at another phase, to another box, and for a release that is not |
| +// the recorded one. |
| +func TestMovesClaimPolicyRecoveryReleaseAtProbing(t *testing.T) { |
| + through := func(hold string, rec *provision.Record, c *hostclaim.Claim, s hostbox.Selection, id hostwrite.Identity) error { |
| + p := hostbox.Policy{Moves: MovesClaimPolicy((&mpReader{rec: rec}).read), Held: func(context.Context, string) (string, error) { return hold, nil }} |
| + return p.Check(t.Context(), c, s, id) |
| + } |
| + recovered, pinned := rpRecovery(), mpPins() |
| + t.Run("restoring", func(t *testing.T) { |
| + c := rpRestoringClaim(hostclaim.PhaseProbing) |
| + s := rpSelection(c, "held-release") |
| + rec := rpRestoringRecord(provision.RbSourceProbing) |
| + if err := through(rpRB, rec, c, s, hostwrite.Identity{Release: recovered.Release}); err != nil { |
| + t.Fatalf("the executor at probing: %v", err) |
| + } |
| + for _, v := range []string{pinned.Release, "v1.3.1", ""} { |
| + if err := through(rpRB, rec, c, s, hostwrite.Identity{Release: v}); err == nil { |
| + t.Fatalf("release %q granted against the recorded %q", v, recovered.Release) |
| + } |
| + } |
| + rpRefused(t, through(rpRB, rec, c, s, hostwrite.Identity{Release: pinned.Release}), "the release is not the recovery's") |
| + // The wrong phase: the record past probing, or the claim behind it. |
| + for _, tc := range []struct{ record, claim string }{ |
| + {provision.RbSourceProbed, hostclaim.PhaseProbed}, |
| + {provision.RbSourceProbing, hostclaim.PhaseClaimed}, |
| + {provision.RbSourceRestored, hostclaim.PhaseClaimed}, |
| + } { |
| + c := rpRestoringClaim(tc.claim) |
| + if err := through(rpRB, rpRestoringRecord(tc.record), c, rpSelection(c, "held-release"), hostwrite.Identity{Release: recovered.Release}); err == nil { |
| + t.Fatalf("record %s, claim %s: granted", tc.record, tc.claim) |
| + } |
| + } |
| + // The wrong box: the target asking, or a third box executing. |
| + t2 := rpSelection(c, "held-release") |
| + t2.Box, t2.Region = mpTarget, "uk" |
| + if err := through(rpRB, rec, c, t2, hostwrite.Identity{Release: recovered.Release}); err == nil { |
| + t.Fatal("the target granted") |
| + } |
| + c3 := rpRestoringClaim(hostclaim.PhaseProbing) |
| + c3.Op.Exec = "de-box" |
| + s3 := rpSelection(c3, "held-release") |
| + s3.Box = "de-box" |
| + if err := through(rpRB, rec, c3, s3, hostwrite.Identity{Release: recovered.Release}); err == nil { |
| + t.Fatal("a third box granted") |
| + } |
| + // A clean return materializes before its inputs are recorded; a dirty |
| + // one needs them. |
| + clean := rpRestoringRecord(provision.RbSourceProbing) |
| + clean.Move.Rollback.Dirty, clean.Move.Rollback.RecoveryInputs = false, nil |
| + if err := through(rpRB, clean, c, s, hostwrite.Identity{Release: "v9"}); err != nil { |
| + t.Fatalf("a clean return before the record: %v", err) |
| + } |
| + dirty := rpRestoringRecord(provision.RbSourceProbing) |
| + dirty.Move.Rollback.RecoveryInputs = nil |
| + rpRefused(t, through(rpRB, dirty, c, s, hostwrite.Identity{Release: recovered.Release}), "no recorded recovery inputs") |
| + }) |
| + t.Run("reverting", func(t *testing.T) { |
| + c := rpRevertingClaim(hostclaim.PhaseProbing) |
| + s := rpSelection(c, "held-release") |
| + rec := rpRevertingRecord() |
| + for _, v := range []string{recovered.Release, pinned.Release} { |
| + if err := through(mpOp, rec, c, s, hostwrite.Identity{Release: v}); err != nil { |
| + t.Fatalf("the executor at probing, release %s: %v", v, err) |
| + } |
| + } |
| + rpRefused(t, through(mpOp, rec, c, s, hostwrite.Identity{Release: recovered.Release, Config: "x"}), "the identity does not fit the action") |
| + for _, rev := range []string{hostclaim.RevPhaseEvidence, hostclaim.RevPhaseVerify, hostclaim.PhaseProbed, hostclaim.PhaseOpened} { |
| + c := rpRevertingClaim(rev) |
| + if err := through(mpOp, rec, c, rpSelection(c, "held-release"), hostwrite.Identity{Release: recovered.Release}); err == nil { |
| + t.Fatalf("rev_phase %s: granted", rev) |
| + } |
| + } |
| + t2 := rpSelection(c, "held-release") |
| + t2.Box, t2.Region = mpTarget, "uk" |
| + if err := through(mpOp, rec, c, t2, hostwrite.Identity{Release: recovered.Release}); err == nil { |
| + t.Fatal("the target granted") |
| + } |
| + // A revert's row is held by the move id, never a rollback's. |
| + if err := through(rpRB, rec, c, s, hostwrite.Identity{Release: recovered.Release}); !errors.Is(err, hostbox.ErrChanged) { |
| + t.Fatalf("a rollback hold on a revert: %v", err) |
| + } |
| + }) |
| +} |