| 1 | package hostclaim |
| 2 | |
| 3 | import ( |
| 4 | "crypto/ed25519" |
| 5 | "errors" |
| 6 | "reflect" |
| 7 | "testing" |
| 8 | |
| 9 | "github.com/carlosframework/platform/internal/release" |
| 10 | ) |
| 11 | |
| 12 | func TestPlan12ParticipantLossKeepsExecutorAdmission(t *testing.T) { |
| 13 | for _, target := range []bool{false, true} { |
| 14 | t.Run(map[bool]string{false: "participant", true: "target"}[target], func(t *testing.T) { |
| 15 | s := release.NewDirStore(t.TempDir()) |
| 16 | pub, priv, err := ed25519.GenerateKey(nil) |
| 17 | if err != nil { |
| 18 | t.Fatal(err) |
| 19 | } |
| 20 | o := servingClaim(t, t.Context(), s) |
| 21 | o, err = Update(t.Context(), s, o, func(c *Claim) error { |
| 22 | c.Op = Op{ID: "move", Kind: KindLanding, Phase: PhaseProbing, Exec: "box-a", Participants: []string{"box-a", "box-b"}, Intent: map[string]string{"target": "box-a"}} |
| 23 | if target { |
| 24 | c.Op.Intent["target"] = "box-b" |
| 25 | } |
| 26 | return nil |
| 27 | }) |
| 28 | if err != nil { |
| 29 | t.Fatal(err) |
| 30 | } |
| 31 | r := lossForObservation(t, o, "participant", 1, "box-b", LossConsequences{RemoveParticipation: true, RecoveryIntent: []byte(`{"kind":"participant-lost"}`)}) |
| 32 | if _, err = CreateLossRecord(t.Context(), s, release.RootSigner(priv), r); err != nil { |
| 33 | t.Fatal(err) |
| 34 | } |
| 35 | o, err = DeclareLost(t.Context(), s, pub, o, r.ID, testLossOwner{}) |
| 36 | if err != nil { |
| 37 | t.Fatal(err) |
| 38 | } |
| 39 | h := o.Claim.Holder |
| 40 | _, err = Update(t.Context(), s, o, AdmitWriter(Admission{ObservedOpID: o.Claim.Op.ID, ObservedPhase: o.Claim.Op.Phase, ObservedSession: h.Session, ObservedGeneration: h.Generation, Box: "box-a", Mode: "cold", NewGeneration: "healthy-next", Region: "ie"})) |
| 41 | if target && !errors.Is(err, ErrLossRecord) || !target && err != nil { |
| 42 | t.Fatalf("admission: %v", err) |
| 43 | } |
| 44 | }) |
| 45 | } |
| 46 | } |
| 47 | |
| 48 | func TestPlan12DeclareLostRefusesSecondSameOperation(t *testing.T) { |
| 49 | for _, which := range []string{"different-box", "new-loss-id", "revoke-execution", "local-only-different-box"} { |
| 50 | t.Run(which, func(t *testing.T) { |
| 51 | s := release.NewDirStore(t.TempDir()) |
| 52 | pub, priv, err := ed25519.GenerateKey(nil) |
| 53 | if err != nil { |
| 54 | t.Fatal(err) |
| 55 | } |
| 56 | o := servingClaim(t, t.Context(), s) |
| 57 | o, err = Update(t.Context(), s, o, func(c *Claim) error { |
| 58 | c.Op = Op{ID: "move", Kind: KindLanding, Phase: PhaseProbing, Exec: "box-a", Participants: []string{"box-a", "box-b", "box-c"}} |
| 59 | c.Obligations = []Obligation{{ID: "local", Op: "move", Kind: "park-local", Box: "box-c", Retirement: "ret", Owner: "box:box-c"}} |
| 60 | return nil |
| 61 | }) |
| 62 | if err != nil { |
| 63 | t.Fatal(err) |
| 64 | } |
| 65 | first := lossForObservation(t, o, "first", 1, "box-b", LossConsequences{RemoveParticipation: true, RecoveryIntent: []byte(`{}`)}) |
| 66 | if _, err = CreateLossRecord(t.Context(), s, release.RootSigner(priv), first); err != nil { |
| 67 | t.Fatal(err) |
| 68 | } |
| 69 | o, err = DeclareLost(t.Context(), s, pub, o, first.ID, testLossOwner{}) |
| 70 | if err != nil { |
| 71 | t.Fatal(err) |
| 72 | } |
| 73 | box := "box-c" |
| 74 | effects := LossConsequences{RemoveParticipation: true, RecoveryIntent: []byte(`{}`)} |
| 75 | switch which { |
| 76 | case "new-loss-id": |
| 77 | box, effects.RemoveParticipation = "box-b", false |
| 78 | case "revoke-execution": |
| 79 | box, effects.RevokeExecution = "box-a", true |
| 80 | case "local-only-different-box": |
| 81 | effects = LossConsequences{ |
| 82 | DischargeLocal: []ObligationRef{{ID: "local", Op: "move", Retirement: "ret"}}, |
| 83 | DestroyedStorage: []string{"move/local/ret"}, RecoveryIntent: []byte(`{"kind":"destroyed-local-only"}`), |
| 84 | } |
| 85 | } |
| 86 | second := lossForObservation(t, o, "second", 2, box, effects) |
| 87 | if _, err = CreateLossRecord(t.Context(), s, release.RootSigner(priv), second); err != nil { |
| 88 | t.Fatal(err) |
| 89 | } |
| 90 | before := make(map[string]ObservedLossRecord) |
| 91 | for _, id := range []string{first.ID, second.ID} { |
| 92 | r, err := ReadLossRecord(t.Context(), s, pub, id) |
| 93 | if err != nil { |
| 94 | t.Fatal(err) |
| 95 | } |
| 96 | before[id] = r |
| 97 | } |
| 98 | if _, err = DeclareLost(t.Context(), s, pub, o, second.ID, testLossOwner{}); !errors.Is(err, ErrWrongState) { |
| 99 | t.Fatalf("second declaration: %v", err) |
| 100 | } |
| 101 | fresh, err := Read(t.Context(), s, o.Host) |
| 102 | if err != nil || fresh.ETag != o.ETag || !reflect.DeepEqual(fresh.Claim, o.Claim) { |
| 103 | t.Fatal("refusal mutated claim or obligations", err) |
| 104 | } |
| 105 | for id, prior := range before { |
| 106 | after, err := ReadLossRecord(t.Context(), s, pub, id) |
| 107 | if err != nil || !reflect.DeepEqual(prior, after) { |
| 108 | t.Fatal("refusal mutated loss record", id, err) |
| 109 | } |
| 110 | } |
| 111 | }) |
| 112 | } |
| 113 | } |
| 114 | |
| 115 | func TestPlan12OperationLossGateIgnoresOtherParticipants(t *testing.T) { |
| 116 | c := &Claim{Op: Op{ID: "move", Intent: map[string]string{"target": "target"}, Journal: map[string]string{"loss.target": "{}", "loss.participant": "{}", "loss_gate.target": `{"box":"target"}`, "loss_gate.participant": `{"box":"participant"}`}}, LossRefs: []string{"target", "participant"}} |
| 117 | for i := 0; i < 100; i++ { |
| 118 | id, err := OperationLossID(c) |
| 119 | if err != nil || id != "target" { |
| 120 | t.Fatalf("loss selection %q %v", id, err) |
| 121 | } |
| 122 | } |
| 123 | } |
| 124 | |
| 125 | func TestPlan12OperationLossGateIncludesRevokedExecutor(t *testing.T) { |
| 126 | c := &Claim{Op: Op{ID: "move", Intent: map[string]string{"target": "target"}, Journal: map[string]string{ |
| 127 | "loss.executor": "{}", "loss_gate.executor": `{"box":"source","revoke_execution":true}`, |
| 128 | }}, LossRefs: []string{"executor"}} |
| 129 | if id, err := OperationLossID(c); err != nil || id != "executor" { |
| 130 | t.Fatalf("revoked executor loss selection %q %v", id, err) |
| 131 | } |
| 132 | } |
| 133 | |