| index f4b28e030..39982b4e9 100644 |
| --- a/internal/host/movespolicy_test.go |
| +++ b/internal/host/movespolicy_test.go |
| @@ -684,3 +684,512 @@ func TestArmPhase1WiresTheHeldTimersImport(t *testing.T) { |
| t.Fatalf("a refused import wrote: %+v", got) |
| } |
| } |
| + |
| +// The recovery fixtures (Plan 4 Task 3b, ruling J12): the source executing |
| +// again under the move's op, its row held by the rollback id (restoring) or |
| +// the move id (reverting). |
| +const ( |
| + rpRB = "rb_abcdefghijklmnopqrstuvwxyz" |
| + rpStamp = 7 // the source installation's stamp S the flip journalled |
| +) |
| + |
| +// rpRecovery are the inputs the return rendered at configured: another |
| +// bundle etag and another release than the move pinned. |
| +func rpRecovery() *provision.MoveInputs { |
| + return &provision.MoveInputs{BundleETags: map[string]string{"default": "e-default-2", "environment": "e-env"}, OverlayETag: "e-overlay", Release: "v1.3.0"} |
| +} |
| + |
| +// rpRestoringClaim is the claim of a rollback after the return: restoring at |
| +// phase, the source executing at S, the rollback id in the intent. |
| +func rpRestoringClaim(phase string) *hostclaim.Claim { |
| + c := mpClaim(hostclaim.KindRestoring, phase) |
| + c.Op.Exec = mpSource |
| + c.Op.Intent["rollback"] = rpRB |
| + c.Op.Journal = map[string]string{hostclaim.JournalSourceStamp: "7", hostclaim.JournalSrcGen: "g-src"} |
| + c.Holder.Stamp = rpStamp |
| + return c |
| +} |
| + |
| +// rpRevertingClaim is the claim of a revert: reverting, the source |
| +// executing, rev_phase in the journal and op.phase mirrored from probing on |
| +// (J1); before it, the sealed moving phase the revert began from. |
| +func rpRevertingClaim(revPhase string) *hostclaim.Claim { |
| + phase := hostclaim.PhaseSealed |
| + switch revPhase { |
| + case hostclaim.PhaseProbing, hostclaim.PhaseProbed, hostclaim.PhaseOpened: |
| + phase = revPhase |
| + } |
| + c := mpClaim(hostclaim.KindReverting, phase) |
| + c.Op.Exec = mpSource |
| + c.Op.Journal = map[string]string{hostclaim.JournalRevPhase: revPhase, hostclaim.JournalEvidenceSource: "moves/ev"} |
| + return c |
| +} |
| + |
| +// rpSelection is the selection the source captures for that claim. |
| +func rpSelection(c *hostclaim.Claim, action string) hostbox.Selection { |
| + s := mpSelection(c, action) |
| + s.Box, s.Region = mpSource, "ie" |
| + return s |
| +} |
| + |
| +// rpRestoringRecord is the dirty rollback with the source at phase; the |
| +// recovery inputs are recorded from the CAS that ends configured on. |
| +func rpRestoringRecord(phase string) *provision.Record { |
| + rec := mpRecord() |
| + rec.Move.State, rec.Move.LandingPhase = provision.MoveRollingBack, provision.LandRestored |
| + rec.Move.Rollback = &provision.MoveRollback{ID: rpRB, Dirty: true, TargetPhase: provision.RbTargetReturned, SourcePhase: phase} |
| + switch phase { |
| + case provision.RbSourceRestored, provision.RbSourceProbing, provision.RbSourceProbed, provision.RbSourceRecovered, provision.RbSourceOpened: |
| + rec.Move.Rollback.RecoveryInputs = rpRecovery() |
| + } |
| + return rec |
| +} |
| + |
| +// rpRevertingRecord is the revert's record: reverting, the region still the |
| +// source's. |
| +func rpRevertingRecord() *provision.Record { |
| + rec := mpRecord() |
| + rec.Region, rec.Move.State, rec.Move.LandingPhase = "ie", provision.MoveReverting, "" |
| + return rec |
| +} |
| + |
| +// rpLaunch is the launch identity a probe's spawn carries (Plan 3 H4). |
| +func rpLaunch(in *provision.MoveInputs) hostwrite.Identity { |
| + return hostwrite.Identity{Config: HeldConfigIdentity(in.BundleETags, in.OverlayETag).Config, Release: in.Release} |
| +} |
| + |
| +// rpIdentity fits the action against the recovery inputs. |
| +func rpIdentity(action string) hostwrite.Identity { |
| + in := rpRecovery() |
| + switch action { |
| + case "held-config": |
| + return HeldConfigIdentity(in.BundleETags, in.OverlayETag) |
| + case "held-release": |
| + return hostwrite.Identity{Release: in.Release} |
| + case "spawn": |
| + return rpLaunch(in) |
| + } |
| + return hostwrite.Identity{} |
| +} |
| + |
| +// rpClaimPhase is the claim phase a rollback's source phase runs under: the |
| +// claim stays at claimed through the source's install (Task 3) and mirrors |
| +// probing, probed and opened (J1). |
| +func rpClaimPhase(sourcePhase string) string { |
| + switch sourcePhase { |
| + case provision.RbSourceWaiting: |
| + return hostclaim.PhaseReleased |
| + case provision.RbSourceProbing, provision.RbSourceProbed, provision.RbSourceOpened: |
| + return sourcePhase |
| + case provision.RbSourceRecovered: |
| + return hostclaim.PhaseProbed |
| + } |
| + return hostclaim.PhaseClaimed |
| +} |
| + |
| +var rpSourcePhases = []string{ |
| + provision.RbSourceWaiting, provision.RbSourceClaimed, provision.RbSourceRestoring, provision.RbSourceVerified, |
| + provision.RbSourceParking, provision.RbSourceBuilding, provision.RbSourcePublished, provision.RbSourceLocal, |
| + provision.RbSourceConfigured, provision.RbSourceRestored, provision.RbSourceProbing, provision.RbSourceProbed, |
| + provision.RbSourceRecovered, provision.RbSourceOpened, |
| +} |
| + |
| +func rpRefused(t *testing.T, err error, reason string) { |
| + t.Helper() |
| + var r *MovesPolicyRefusal |
| + if err == nil || !errors.As(err, &r) || !errors.Is(err, ErrMovesRefused) { |
| + t.Fatalf("not the typed refusal: %v", err) |
| + } |
| + if reason != "" && r.Reason != reason { |
| + t.Fatalf("reason %q, want %q", r.Reason, reason) |
| + } |
| +} |
| + |
| +// restoring, the source's return (J12): every action against every source |
| +// 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 |
| +// 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}, |
| + } |
| + grants := 0 |
| + for _, phase := range rpSourcePhases { |
| + reader := &mpReader{rec: rpRestoringRecord(phase)} |
| + moves := MovesClaimPolicy(reader.read) |
| + for _, action := range mpActions { |
| + c := rpRestoringClaim(rpClaimPhase(phase)) |
| + err := moves(t.Context(), c, rpSelection(c, action), rpIdentity(action)) |
| + want := granted[phase][action] |
| + if (err == nil) != want { |
| + t.Errorf("%s/%s: err %v, want granted %v", phase, action, err, want) |
| + continue |
| + } |
| + if err != nil { |
| + var r *MovesPolicyRefusal |
| + if !errors.As(err, &r) || r.Kind != hostclaim.KindRestoring || r.Box != mpSource || r.Reason == "" { |
| + t.Errorf("%s/%s: not the typed refusal: %#v", phase, action, err) |
| + } |
| + } else { |
| + grants++ |
| + } |
| + } |
| + if reader.reads == 0 || !reflect.DeepEqual(reader.args, []string{"bab", "titogo", mpHost}) { |
| + 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) |
| + } |
| + t.Run("the wrong claim phase", func(t *testing.T) { |
| + for _, tc := range []struct{ record, claim, action string }{ |
| + {provision.RbSourceLocal, hostclaim.PhaseProbing, "held-timers"}, |
| + {provision.RbSourceConfigured, hostclaim.PhaseReleased, "held-config"}, |
| + {provision.RbSourceRestored, hostclaim.PhaseProbed, "install"}, |
| + {provision.RbSourceProbing, hostclaim.PhaseClaimed, "spawn"}, |
| + {provision.RbSourceProbing, hostclaim.PhaseProbed, "held-config"}, |
| + } { |
| + moves := MovesClaimPolicy((&mpReader{rec: rpRestoringRecord(tc.record)}).read) |
| + c := rpRestoringClaim(tc.claim) |
| + rpRefused(t, moves(t.Context(), c, rpSelection(c, tc.action), rpIdentity(tc.action)), "") |
| + } |
| + }) |
| +} |
| + |
| +// 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 |
| +// revert's hold, the move id. |
| +func TestMovesClaimPolicyRevertingRows(t *testing.T) { |
| + reader := &mpReader{rec: rpRevertingRecord()} |
| + moves := MovesClaimPolicy(reader.read) |
| + policy := hostbox.Policy{Moves: moves, Held: func(context.Context, string) (string, error) { return mpOp, nil }} |
| + grants := 0 |
| + for _, rev := range []string{hostclaim.RevPhaseEvidence, hostclaim.RevPhaseVerify, hostclaim.PhaseProbing, hostclaim.PhaseProbed, hostclaim.PhaseOpened} { |
| + for _, action := range mpActions { |
| + c := rpRevertingClaim(rev) |
| + s := rpSelection(c, action) |
| + err := moves(t.Context(), c, s, rpIdentity(action)) |
| + want := rev == hostclaim.PhaseProbing && (action == "held-config" || action == "spawn") |
| + if (err == nil) != want { |
| + t.Errorf("%s/%s: err %v, want granted %v", rev, action, err, want) |
| + continue |
| + } |
| + if err != nil && !errors.Is(err, ErrMovesRefused) { |
| + t.Errorf("%s/%s: not the typed refusal: %v", rev, action, err) |
| + } |
| + if chk := policy.Check(t.Context(), c, s, rpIdentity(action)); (chk == nil) != want { |
| + t.Errorf("%s/%s through Policy.Check: %v, want granted %v", rev, action, chk, want) |
| + } |
| + if want { |
| + grants++ |
| + } |
| + } |
| + } |
| + if grants != 2 { |
| + t.Fatalf("%d cells granted, want 2", grants) |
| + } |
| + t.Run("rev_phase and op.phase disagree", func(t *testing.T) { |
| + c := rpRevertingClaim(hostclaim.PhaseProbing) |
| + c.Op.Phase = hostclaim.PhaseSealed |
| + rpRefused(t, moves(t.Context(), c, rpSelection(c, "spawn"), rpIdentity("spawn")), "") |
| + c = rpRevertingClaim(hostclaim.RevPhaseVerify) |
| + c.Op.Phase = hostclaim.PhaseProbing |
| + rpRefused(t, moves(t.Context(), c, rpSelection(c, "spawn"), rpIdentity("spawn")), "") |
| + }) |
| +} |
| + |
| +// Only the claim op's executor, the move's source, in the source's region, |
| +// for this rollback's record: the target (the cleaning's ReplaceTimers |
| +// included, which is a plain registry call and never a policy row), a third |
| +// box, a lost source and every record that is not this recovery are refused. |
| +func TestMovesClaimPolicyRecoveryRefusesOtherRoles(t *testing.T) { |
| + type cell struct { |
| + claim func() *hostclaim.Claim |
| + record func() *provision.Record |
| + action string |
| + } |
| + restoring := func(sourcePhase, action string) cell { |
| + return cell{func() *hostclaim.Claim { return rpRestoringClaim(rpClaimPhase(sourcePhase)) }, |
| + func() *provision.Record { return rpRestoringRecord(sourcePhase) }, action} |
| + } |
| + reverting := cell{func() *hostclaim.Claim { return rpRevertingClaim(hostclaim.PhaseProbing) }, rpRevertingRecord, "spawn"} |
| + bases := map[string]cell{ |
| + "restoring local": restoring(provision.RbSourceLocal, "held-timers"), |
| + "restoring configured": restoring(provision.RbSourceConfigured, "held-config"), |
| + "restoring restored": restoring(provision.RbSourceRestored, "install"), |
| + "restoring probing": restoring(provision.RbSourceProbing, "spawn"), |
| + "reverting probing": reverting, |
| + } |
| + type mutation func(c *hostclaim.Claim, s *hostbox.Selection, rec *provision.Record) |
| + cases := map[string]mutation{ |
| + "the target asking, the source executing": func(_ *hostclaim.Claim, s *hostbox.Selection, _ *provision.Record) { |
| + s.Box, s.Region = mpTarget, "uk" |
| + }, |
| + "the target executing and asking": func(c *hostclaim.Claim, s *hostbox.Selection, _ *provision.Record) { |
| + c.Op.Exec = mpTarget |
| + s.Box, s.Region = mpTarget, "uk" |
| + }, |
| + "the target executing, the source asking": func(c *hostclaim.Claim, _ *hostbox.Selection, _ *provision.Record) { |
| + c.Op.Exec = mpTarget |
| + }, |
| + "exec none": func(c *hostclaim.Claim, _ *hostbox.Selection, _ *provision.Record) { c.Op.Exec = "" }, |
| + "a third box executing and asking": func(c *hostclaim.Claim, s *hostbox.Selection, _ *provision.Record) { |
| + c.Op.Exec, s.Box = "de-box", "de-box" |
| + }, |
| + "the source not the first participant": func(c *hostclaim.Claim, _ *hostbox.Selection, _ *provision.Record) { |
| + c.Op.Participants = []string{mpTarget, mpSource} |
| + }, |
| + "no participants": func(c *hostclaim.Claim, _ *hostbox.Selection, _ *provision.Record) { c.Op.Participants = nil }, |
| + "the intent names another source": func(c *hostclaim.Claim, _ *hostbox.Selection, _ *provision.Record) { |
| + c.Op.Intent["source"] = "de-box" |
| + }, |
| + "the intent names this box as target too": func(c *hostclaim.Claim, _ *hostbox.Selection, _ *provision.Record) { |
| + c.Op.Intent["target"] = mpSource |
| + }, |
| + "the source is lost": func(c *hostclaim.Claim, _ *hostbox.Selection, _ *provision.Record) { c.Lost = []string{mpSource} }, |
| + "the target's region": func(_ *hostclaim.Claim, s *hostbox.Selection, _ *provision.Record) { s.Region = "uk" }, |
| + "a third region": func(_ *hostclaim.Claim, s *hostbox.Selection, _ *provision.Record) { s.Region = "de" }, |
| + "the record names another source region": func(_ *hostclaim.Claim, _ *hostbox.Selection, rec *provision.Record) { |
| + rec.Move.From = "de" |
| + }, |
| + "a stale capture": func(_ *hostclaim.Claim, s *hostbox.Selection, _ *provision.Record) { s.Captured.Stamp = 6 }, |
| + "another move's record": func(_ *hostclaim.Claim, _ *hostbox.Selection, rec *provision.Record) { |
| + rec.Move.ID = "mv_other" |
| + }, |
| + "another incarnation's record": func(_ *hostclaim.Claim, _ *hostbox.Selection, rec *provision.Record) { |
| + rec.Move.Incarnation = "0" |
| + }, |
| + "another host's record": func(_ *hostclaim.Claim, _ *hostbox.Selection, rec *provision.Record) { rec.Host = "other.example" }, |
| + "no move": func(_ *hostclaim.Claim, _ *hostbox.Selection, rec *provision.Record) { rec.Move = nil }, |
| + "a stuck record": func(_ *hostclaim.Claim, _ *hostbox.Selection, rec *provision.Record) { |
| + rec.Move.State = provision.MoveStuck |
| + }, |
| + "a landing record": func(_ *hostclaim.Claim, _ *hostbox.Selection, rec *provision.Record) { |
| + rec.Move.State = provision.MoveLanding |
| + }, |
| + } |
| + for baseName, base := range bases { |
| + for name, mutate := range cases { |
| + t.Run(baseName+"/"+name, func(t *testing.T) { |
| + c, rec := base.claim(), base.record() |
| + s := rpSelection(c, base.action) |
| + mutate(c, &s, rec) |
| + moves := MovesClaimPolicy((&mpReader{rec: rec}).read) |
| + rpRefused(t, moves(t.Context(), c, s, rpIdentity(base.action)), "") |
| + }) |
| + } |
| + t.Run(baseName+"/the base cell is granted", func(t *testing.T) { |
| + c := base.claim() |
| + moves := MovesClaimPolicy((&mpReader{rec: base.record()}).read) |
| + if err := moves(t.Context(), c, rpSelection(c, base.action), rpIdentity(base.action)); err != nil { |
| + t.Fatal(err) |
| + } |
| + }) |
| + } |
| + restoringOnly := map[string]mutation{ |
| + "another rollback's record": func(_ *hostclaim.Claim, _ *hostbox.Selection, rec *provision.Record) { |
| + rec.Move.Rollback.ID = "rb_zzzzzzzzzzzzzzzzzzzzzzzzzz" |
| + }, |
| + "the claim names no rollback": func(c *hostclaim.Claim, _ *hostbox.Selection, _ *provision.Record) { |
| + delete(c.Op.Intent, "rollback") |
| + }, |
| + "no rollback journal": func(_ *hostclaim.Claim, _ *hostbox.Selection, rec *provision.Record) { rec.Move.Rollback = nil }, |
| + "a reverting record": func(_ *hostclaim.Claim, _ *hostbox.Selection, rec *provision.Record) { |
| + rec.Move.State = provision.MoveReverting |
| + }, |
| + } |
| + for name, mutate := range restoringOnly { |
| + for _, phase := range []string{provision.RbSourceLocal, provision.RbSourceProbing} { |
| + t.Run("restoring "+phase+"/"+name, func(t *testing.T) { |
| + base := restoring(phase, map[string]string{provision.RbSourceLocal: "held-timers", provision.RbSourceProbing: "spawn"}[phase]) |
| + c, rec := base.claim(), base.record() |
| + s := rpSelection(c, base.action) |
| + mutate(c, &s, rec) |
| + moves := MovesClaimPolicy((&mpReader{rec: rec}).read) |
| + rpRefused(t, moves(t.Context(), c, s, rpIdentity(base.action)), "") |
| + }) |
| + } |
| + } |
| + t.Run("a reverting claim over a rolling_back record", func(t *testing.T) { |
| + c := rpRevertingClaim(hostclaim.PhaseProbing) |
| + moves := MovesClaimPolicy((&mpReader{rec: rpRestoringRecord(provision.RbSourceProbing)}).read) |
| + rpRefused(t, moves(t.Context(), c, rpSelection(c, "spawn"), rpIdentity("spawn")), "") |
| + }) |
| + t.Run("the dirty install rows need a dirty rollback", func(t *testing.T) { |
| + for phase, action := range map[string]string{provision.RbSourceLocal: "held-timers", provision.RbSourceConfigured: "held-config", provision.RbSourceRestored: "restore"} { |
| + rec := rpRestoringRecord(phase) |
| + rec.Move.Rollback.Dirty = false |
| + c := rpRestoringClaim(hostclaim.PhaseClaimed) |
| + moves := MovesClaimPolicy((&mpReader{rec: rec}).read) |
| + rpRefused(t, moves(t.Context(), c, rpSelection(c, action), rpIdentity(action)), "") |
| + } |
| + }) |
| + t.Run("the install runs at the source's pre-move stamp", func(t *testing.T) { |
| + for _, mutate := range []func(*hostclaim.Claim){ |
| + func(c *hostclaim.Claim) { c.Op.Journal[hostclaim.JournalSourceStamp] = "8" }, |
| + func(c *hostclaim.Claim) { delete(c.Op.Journal, hostclaim.JournalSourceStamp) }, |
| + } { |
| + c := rpRestoringClaim(hostclaim.PhaseClaimed) |
| + mutate(c) |
| + moves := MovesClaimPolicy((&mpReader{rec: rpRestoringRecord(provision.RbSourceLocal)}).read) |
| + rpRefused(t, moves(t.Context(), c, rpSelection(c, "held-timers"), hostwrite.Identity{}), "") |
| + } |
| + // The probe's cold admission issues a new stamp (J14): probing is not |
| + // bound to S. |
| + c := rpRestoringClaim(hostclaim.PhaseProbing) |
| + c.Holder.Stamp = 12 |
| + s := rpSelection(c, "spawn") |
| + moves := MovesClaimPolicy((&mpReader{rec: rpRestoringRecord(provision.RbSourceProbing)}).read) |
| + if err := moves(t.Context(), c, s, rpIdentity("spawn")); err != nil { |
| + t.Fatal(err) |
| + } |
| + }) |
| + t.Run("the target's cleaning timers are no policy row", func(t *testing.T) { |
| + for _, phase := range []string{hostclaim.PhaseReleasing, hostclaim.PhaseReleased, hostclaim.PhaseClaimed, hostclaim.PhaseProbing, hostclaim.PhaseOpened} { |
| + for _, exec := range []string{mpTarget, "", mpSource} { |
| + c := rpRestoringClaim(phase) |
| + c.Op.Exec = exec |
| + s := rpSelection(c, "held-timers") |
| + s.Box, s.Region = mpTarget, "uk" |
| + moves := MovesClaimPolicy((&mpReader{rec: rpRestoringRecord(provision.RbSourceLocal)}).read) |
| + rpRefused(t, moves(t.Context(), c, s, hostwrite.Identity{}), "") |
| + } |
| + } |
| + }) |
| + t.Run("no reader, a read error, no account", func(t *testing.T) { |
| + for _, b := range []cell{restoring(provision.RbSourceLocal, "held-timers"), reverting} { |
| + c := b.claim() |
| + rpRefused(t, MovesClaimPolicy(nil)(t.Context(), c, rpSelection(c, b.action), rpIdentity(b.action)), "no record reader") |
| + cause := errors.New("the store is down") |
| + err := MovesClaimPolicy((&mpReader{err: cause}).read)(t.Context(), c, rpSelection(c, b.action), rpIdentity(b.action)) |
| + rpRefused(t, err, "the record cannot be read") |
| + if !errors.Is(err, cause) { |
| + t.Fatalf("the cause is not carried: %v", err) |
| + } |
| + c.Holder.Account = "" |
| + rpRefused(t, MovesClaimPolicy((&mpReader{rec: b.record()}).read)(t.Context(), c, rpSelection(c, b.action), rpIdentity(b.action)), "the holder names no account or app") |
| + } |
| + }) |
| + t.Run("a role refusal reads no record", func(t *testing.T) { |
| + reader := &mpReader{rec: rpRestoringRecord(provision.RbSourceLocal)} |
| + c := rpRestoringClaim(hostclaim.PhaseClaimed) |
| + s := rpSelection(c, "held-timers") |
| + s.Box, s.Region = mpTarget, "uk" |
| + rpRefused(t, MovesClaimPolicy(reader.read)(t.Context(), c, s, hostwrite.Identity{}), "") |
| + s = rpSelection(c, "held-timers") |
| + s.Region = "uk" |
| + rpRefused(t, MovesClaimPolicy(reader.read)(t.Context(), c, s, hostwrite.Identity{}), "this region is the move's target region") |
| + c.Lost = []string{mpSource} |
| + rpRefused(t, MovesClaimPolicy(reader.read)(t.Context(), c, rpSelection(c, "held-timers"), hostwrite.Identity{}), "this box is declared lost") |
| + if reader.reads != 0 { |
| + t.Fatalf("%d reads", reader.reads) |
| + } |
| + }) |
| +} |
| + |
| +// A recovery renders the current inputs and records them as |
| +// Rollback.RecoveryInputs; it is never bound to Move.Inputs (spec §7.7, |
| +// J2-R2-5). The held config and release at configured are that render |
| +// (nothing is recorded until the CAS that ends configured); from then on |
| +// every held render and the spawn name the recorded recovery inputs. A |
| +// revert has no recorded recovery inputs: its render and spawn are bound by |
| +// shape and never to Move.Inputs. The same difference on a landing is |
| +// refused. |
| +func TestMovesClaimPolicyBindsRecoveryInputs(t *testing.T) { |
| + pinned, recovered := mpPins(), rpRecovery() |
| + try := func(phase, action string, rec *provision.Record, id hostwrite.Identity) error { |
| + c := rpRestoringClaim(rpClaimPhase(phase)) |
| + return MovesClaimPolicy((&mpReader{rec: rec}).read)(t.Context(), c, rpSelection(c, action), id) |
| + } |
| + cfgOf := func(in *provision.MoveInputs) hostwrite.Identity { |
| + return HeldConfigIdentity(in.BundleETags, in.OverlayETag) |
| + } |
| + t.Run("configured: the render that records them", func(t *testing.T) { |
| + rec := rpRestoringRecord(provision.RbSourceConfigured) |
| + for _, in := range []*provision.MoveInputs{recovered, pinned} { |
| + if err := try(provision.RbSourceConfigured, "held-config", rec, cfgOf(in)); err != nil { |
| + t.Fatalf("%+v: %v", in, err) |
| + } |
| + if err := try(provision.RbSourceConfigured, "held-release", rec, hostwrite.Identity{Release: in.Release}); err != nil { |
| + t.Fatalf("%+v: %v", in, err) |
| + } |
| + } |
| + rpRefused(t, try(provision.RbSourceConfigured, "held-config", rec, hostwrite.Identity{Config: cfgOf(recovered).Config, Release: "v1.3.0"}), "the identity does not fit the action") |
| + rpRefused(t, try(provision.RbSourceConfigured, "held-release", rec, hostwrite.Identity{}), "the identity does not fit the action") |
| + // A replay of configured that finds them recorded is bound to them. |
| + rec.Move.Rollback.RecoveryInputs = rpRecovery() |
| + if err := try(provision.RbSourceConfigured, "held-config", rec, cfgOf(recovered)); err != nil { |
| + t.Fatal(err) |
| + } |
| + rpRefused(t, try(provision.RbSourceConfigured, "held-config", rec, cfgOf(pinned)), "the config is not the recovery's") |
| + rpRefused(t, try(provision.RbSourceConfigured, "held-release", rec, hostwrite.Identity{Release: pinned.Release}), "the release is not the recovery's") |
| + }) |
| + t.Run("probing, dirty: the recorded recovery inputs, not the pins", func(t *testing.T) { |
| + rec := rpRestoringRecord(provision.RbSourceProbing) |
| + if err := try(provision.RbSourceProbing, "held-config", rec, cfgOf(recovered)); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := try(provision.RbSourceProbing, "spawn", rec, rpLaunch(recovered)); err != nil { |
| + t.Fatal(err) |
| + } |
| + rpRefused(t, try(provision.RbSourceProbing, "held-config", rec, cfgOf(pinned)), "the config is not the recovery's") |
| + rpRefused(t, try(provision.RbSourceProbing, "spawn", rec, rpLaunch(pinned)), "the launch is not the recovery's") |
| + rpRefused(t, try(provision.RbSourceProbing, "spawn", rec, hostwrite.Identity{Config: cfgOf(recovered).Config, Release: pinned.Release}), "the launch is not the recovery's") |
| + for _, id := range []hostwrite.Identity{{}, {Config: cfgOf(recovered).Config}, {Release: recovered.Release}} { |
| + rpRefused(t, try(provision.RbSourceProbing, "spawn", rec, id), "the identity does not fit the action") |
| + } |
| + rec.Move.Rollback.RecoveryInputs = nil |
| + rpRefused(t, try(provision.RbSourceProbing, "held-config", rec, cfgOf(recovered)), "no recorded recovery inputs") |
| + rpRefused(t, try(provision.RbSourceProbing, "spawn", rec, rpLaunch(recovered)), "no recorded recovery inputs") |
| + rec.Move.Rollback.RecoveryInputs = &provision.MoveInputs{BundleETags: recovered.BundleETags} |
| + rpRefused(t, try(provision.RbSourceProbing, "spawn", rec, rpLaunch(recovered)), "no recorded recovery release") |
| + }) |
| + t.Run("probing, clean: the render before the record, the spawn after it", func(t *testing.T) { |
| + rec := rpRestoringRecord(provision.RbSourceProbing) |
| + rec.Move.Rollback.Dirty, rec.Move.Rollback.RecoveryInputs = false, nil |
| + if err := try(provision.RbSourceProbing, "held-config", rec, cfgOf(recovered)); err != nil { |
| + t.Fatal(err) |
| + } |
| + rpRefused(t, try(provision.RbSourceProbing, "spawn", rec, rpLaunch(recovered)), "no recorded recovery inputs") |
| + rec.Move.Rollback.RecoveryInputs = rpRecovery() |
| + if err := try(provision.RbSourceProbing, "spawn", rec, rpLaunch(recovered)); err != nil { |
| + t.Fatal(err) |
| + } |
| + rpRefused(t, try(provision.RbSourceProbing, "held-config", rec, cfgOf(pinned)), "the config is not the recovery's") |
| + }) |
| + t.Run("reverting: never the pins", func(t *testing.T) { |
| + moves := MovesClaimPolicy((&mpReader{rec: rpRevertingRecord()}).read) |
| + c := rpRevertingClaim(hostclaim.PhaseProbing) |
| + for _, in := range []*provision.MoveInputs{recovered, pinned} { |
| + if err := moves(t.Context(), c, rpSelection(c, "held-config"), cfgOf(in)); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := moves(t.Context(), c, rpSelection(c, "spawn"), rpLaunch(in)); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + for _, id := range []hostwrite.Identity{{}, {Config: cfgOf(recovered).Config}, {Release: "v1.3.0"}} { |
| + 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") |
| + }) |
| + t.Run("a landing: the same difference is refused", func(t *testing.T) { |
| + moves, _ := mpPolicy() |
| + c := mpClaim(hostclaim.KindLanding, provision.LandConfigured) |
| + rpRefused(t, moves(t.Context(), c, mpSelection(c, "held-config"), cfgOf(recovered)), "the config is not the pinned one") |
| + rpRefused(t, moves(t.Context(), c, mpSelection(c, "held-release"), hostwrite.Identity{Release: recovered.Release}), "the release is not the pinned one") |
| + // A landing record carrying a rollback's recovery inputs still binds |
| + // to the pins. |
| + rec := mpRecord() |
| + rec.Move.Rollback = &provision.MoveRollback{ID: rpRB, RecoveryInputs: rpRecovery()} |
| + moves = MovesClaimPolicy((&mpReader{rec: rec}).read) |
| + rpRefused(t, moves(t.Context(), c, mpSelection(c, "held-config"), cfgOf(recovered)), "the config is not the pinned one") |
| + }) |
| +} |