carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package host
2
3import (
4 "bytes"
5 "context"
6 "encoding/json"
7 "errors"
8 "os"
9 "testing"
10
11 "github.com/carlosframework/platform/internal/hostwrite"
12 "github.com/carlosframework/platform/internal/provision"
13 "github.com/carlosframework/platform/internal/release"
14 "github.com/carlosframework/platform/internal/rootstate"
15)
16
17func TestPlan14HistoryRollbackLink(t *testing.T) {
18 t.Parallel()
19 for _, scenario := range []string{"success", "rollback", "failed rollback", "intent crash", "link crash", "restart crash"} {
20 t.Run(scenario, func(t *testing.T) {
21 e, rt, u, c, pub := plan14AdoptionFixture(t)
22 h := seedHostRootHistory(t, rt)
23 deps := boundRootHistory(h, c.resolve, e.root.obs)
24 c.history = func() (*release.RootHistoryPass, error) { return deps, nil }
25 linkRootHistory(deps, c, u.opt, func(string) string { return "5s" })
26 u.crashNew = scenario != "success"
27 u.badRollback = scenario == "failed rollback"
28 stopped := errors.New("crash")
29 prepare := deps.Prepare
30 deps.Prepare = func(ctx context.Context, b release.HistoryBinding, a release.HistoryAttempt, id hostwrite.Identity) error {
31 if scenario == "intent crash" {
32 panic(stopped)
33 }
34 if err := prepare(ctx, b, a, id); err != nil {
35 return err
36 }
37 live, _ := release.LiveVersion(u.opt, rt.Host)
38 if live != "v1" {
39 t.Fatal("rollback captured after live effect")
40 }
41 s, _ := c.attempts()
42 journal, err := readRollbackAttempt(s, rt.Host)
43 if err != nil || journal.History == nil || journal.History.ID != a.ID {
44 t.Fatalf("unlinked intent: %+v %v", journal, err)
45 }
46 if scenario == "link crash" {
47 panic(stopped)
48 }
49 return nil
50 }
51 if scenario == "restart crash" {
52 u.afterEffect = func() { panic(stopped) }
53 }
54 forward := rootRestartRouteWith(e.root.reg, u, u.opt, func(string) string { return "5s" }, nil, c)
55 var passErr error
56 func() {
57 defer func() {
58 if p := recover(); p != nil && p != stopped {
59 panic(p)
60 }
61 }()
62 passErr = release.Pass(t.Context(), e.root.reg, e.store, pub, u.opt, forward,
63 release.WithRootHistory(deps), release.WithRestartFailurePropagation(),
64 release.WithRollbackRestart(rollbackUnitRestart(e.root.reg, u, u.opt, func(string) string { return "5s" }, c)))
65 }()
66 u.afterEffect = nil
67 if scenario == "success" && passErr != nil {
68 t.Fatal(passErr)
69 }
70 b := release.HistoryBinding{Host: rt.Host, Account: rt.Account, App: rt.App}
71 if scenario == "intent crash" || scenario == "failed rollback" {
72 if _, err := h.Refusals(b); err == nil {
73 t.Fatal("unresolved attempt granted permission")
74 }
75 return
76 }
77 if err := recoverRootRestarts(t.Context(), u, u.opt, func(string) string { return "5s" }, c, hostwrite.Config{}); err != nil {
78 t.Fatal(err)
79 }
80 refs, err := h.Refusals(b)
81 if err != nil {
82 t.Fatal(err)
83 }
84 if scenario == "success" {
85 if len(refs) != 0 {
86 t.Fatalf("successful target refused: %+v", refs)
87 }
88 } else if len(refs) == 0 || refs[0].Version != "v2" || !refs[0].Recovered || refs[0].RestoredVersion != "v1" {
89 t.Fatalf("rollback lost exact deny: %+v", refs)
90 }
91 before := len(u.effects)
92 if err := recoverRootRestarts(t.Context(), u, u.opt, func(string) string { return "5s" }, c, hostwrite.Config{}); err != nil || len(u.effects) != before {
93 t.Fatalf("recovery replay: %v", err)
94 }
95 })
96 }
97}
98
99type historyVisibleFailure struct {
100 rootstate.Store
101 armed bool
102 visible []byte
103}
104
105func (s *historyVisibleFailure) Write(k string, raw []byte) error {
106 if err := s.Store.Write(k, raw); err != nil {
107 return err
108 }
109 var record struct {
110 Binding release.HistoryBinding `json:"binding"`
111 Attempt *release.HistoryAttempt `json:"attempt"`
112 }
113 if s.armed && json.Unmarshal(raw, &record) == nil && record.Binding.Host != "" && record.Attempt == nil {
114 s.visible = append([]byte(nil), raw...)
115 return os.ErrPermission
116 }
117 return nil
118}
119
120func TestPlan14HistorySettlementRetriesVisibleSync(t *testing.T) {
121 t.Parallel()
122 e, rt, u, c, pub := plan14AdoptionFixture(t)
123 h := seedHostRootHistory(t, rt)
124 fault := &historyVisibleFailure{Store: h.Store}
125 h.Store = fault
126 deps := boundRootHistory(h, c.resolve, e.root.obs)
127 c.history = func() (*release.RootHistoryPass, error) { return deps, nil }
128 linkRootHistory(deps, c, u.opt, func(string) string { return "5s" })
129 u.afterEffect = func() { fault.armed = true }
130 forward := rootRestartRouteWith(e.root.reg, u, u.opt, func(string) string { return "5s" }, nil, c)
131 if err := release.Pass(t.Context(), e.root.reg, e.store, pub, u.opt, forward, release.WithRootHistory(deps)); err == nil {
132 t.Fatal("failed settlement sync reported success")
133 }
134 if len(fault.visible) == 0 {
135 t.Fatal("settlement did not become visible")
136 }
137 binding := release.HistoryBinding{Host: rt.Host, Account: rt.Account, App: rt.App}
138 before := len(u.effects)
139 if _, err := h.Refusals(binding); err == nil {
140 t.Fatal("unsynced settlement granted mint")
141 }
142 if err := recoverRootRestarts(t.Context(), u, u.opt, func(string) string { return "5s" }, c, hostwrite.Config{}); err == nil {
143 t.Fatal("recovery ignored failed sync")
144 }
145 if len(u.effects) != before {
146 t.Fatal("sync recovery repeated restart")
147 }
148 want := append([]byte(nil), fault.visible...)
149 fault.armed = false
150 if err := recoverRootRestarts(t.Context(), u, u.opt, func(string) string { return "5s" }, c, hostwrite.Config{}); err != nil {
151 t.Fatal(err)
152 }
153 if refs, err := h.Refusals(binding); err != nil || len(refs) != 0 {
154 t.Fatalf("durable settlement: %+v %v", refs, err)
155 }
156 if len(u.effects) != before {
157 t.Fatal("durable recovery repeated restart")
158 }
159 found := false
160 for _, raw := range fault.Store.(*rollbackMemory).files {
161 if bytes.Equal(raw, want) {
162 found = true
163 }
164 }
165 if !found {
166 t.Fatal("sync retry refreshed settlement evidence")
167 }
168}
169
170func TestPlan14HistoryRecoveryRefusesChanges(t *testing.T) {
171 t.Parallel()
172 for _, attack := range []string{"record", "unit", "attempt", "missing capability", "stopped", "target"} {
173 t.Run(attack, func(t *testing.T) {
174 e, rt, u, c, pub := plan14AdoptionFixture(t)
175 h := seedHostRootHistory(t, rt)
176 deps := boundRootHistory(h, c.resolve, e.root.obs)
177 c.history = func() (*release.RootHistoryPass, error) { return deps, nil }
178 linkRootHistory(deps, c, u.opt, func(string) string { return "5s" })
179 stopped := errors.New("crash")
180 u.crashNew = true
181 u.afterEffect = func() { panic(stopped) }
182 forward := rootRestartRouteWith(e.root.reg, u, u.opt, func(string) string { return "5s" }, nil, c)
183 func() {
184 defer func() {
185 if p := recover(); p != stopped {
186 t.Fatalf("crash: %v", p)
187 }
188 }()
189 _ = release.Pass(t.Context(), e.root.reg, e.store, pub, u.opt, forward, release.WithRootHistory(deps))
190 }()
191 u.afterEffect = nil
192 s, _ := c.attempts()
193 a, err := readRollbackAttempt(s, rt.Host)
194 if err != nil {
195 t.Fatal(err)
196 }
197 switch attack {
198 case "record":
199 rec, _, err := provision.FindRecordByHost(t.Context(), e.store, rt.Host)
200 if err != nil {
201 t.Fatal(err)
202 }
203 rec.RecordSeq++
204 plan14PutRecord(t, e.store, *rec)
205 case "unit":
206 if err := os.Rename(u.props.FragmentPath, u.props.FragmentPath+".old"); err != nil {
207 t.Fatal(err)
208 }
209 if err := os.WriteFile(u.props.FragmentPath, []byte("changed"), 0644); err != nil {
210 t.Fatal(err)
211 }
212 case "attempt":
213 a.History.ID = "another-attempt"
214 if err := writeRollbackState(s, rollbackJournalKey(rt.Host), a); err != nil {
215 t.Fatal(err)
216 }
217 case "missing capability":
218 if err := s.Remove(rollbackCapabilityKey(rt.Host, a.ID)); err != nil {
219 t.Fatal(err)
220 }
221 case "stopped":
222 u.props.ActiveState, u.props.SubState = "inactive", "dead"
223 case "target":
224 if err := os.Remove(release.LiveLink(u.opt, rt.Host)); err != nil {
225 t.Fatal(err)
226 }
227 }
228 before := len(u.effects)
229 if err := recoverRootRestarts(t.Context(), u, u.opt, func(string) string { return "5s" }, c, hostwrite.Config{}); err == nil || len(u.effects) != before {
230 t.Fatalf("unsafe recovery: %v %v", err, u.effects)
231 }
232 h.Store.(*rollbackMemory).failWrite = false
233 if _, err := h.Refusals(a.HistoryBinding); err == nil {
234 t.Fatal("failed recovery cleared pending history")
235 }
236 })
237 }
238}
239