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 "context"
5 "github.com/carlosframework/platform/internal/provision"
6 "os"
7 "strings"
8 "testing"
9 "time"
10
11 "github.com/carlosframework/platform/internal/hostwrite"
12 "github.com/carlosframework/platform/internal/registry"
13 "github.com/carlosframework/platform/internal/release"
14)
15
16func TestPlan14SignedActiveFixIgnoresHistory(t *testing.T) {
17 for _, fault := range []string{"unavailable", "missing", "malformed", "unwritable", "refused", "pending", "unknown bake", "claims on"} {
18 t.Run(fault, func(t *testing.T) {
19 e, u, c, source, pub := signedRecoveryFixture(t)
20 rt, err := e.root.reg.Get(observedHost)
21 if err != nil {
22 t.Fatal(err)
23 }
24 h := seedHostRootHistory(t, *rt)
25 b := release.HistoryBinding{Host: rt.Host, Account: rt.Account, App: rt.App}
26 switch fault {
27 case "missing":
28 h.Store.(*rollbackMemory).files = map[string][]byte{}
29 case "malformed":
30 for k := range h.Store.(*rollbackMemory).files {
31 h.Store.(*rollbackMemory).files[k] = []byte("{")
32 }
33 case "unwritable":
34 h.Store.(*rollbackMemory).failWrite = true
35 case "refused":
36 if err := h.AddRefusal(b, release.Refusal{Host: rt.Host, App: rt.App, Version: "v2", Kind: release.RefusalWrongProgram, RefusedAt: time.Now()}); err != nil {
37 t.Fatal(err)
38 }
39 case "pending":
40 if err := h.Begin(b, release.HistoryAttempt{ID: "unsettled", Version: "v0", SHA256: "abcd", Source: "old-source", Platform: release.AdoptPlatform(), Started: time.Now(), PromotedAt: time.Now()}); err != nil {
41 t.Fatal(err)
42 }
43 }
44 deps := boundRootHistory(h, source.resolve, e.root.obs)
45 if fault == "unavailable" {
46 deps = nil
47 }
48 if fault == "unknown bake" {
49 if err := h.Store.Remove("bake.json"); err != nil {
50 t.Fatal(err)
51 }
52 if _, err := h.LoadBake([]release.HistoryBinding{b}, time.Now()); err == nil {
53 t.Fatal("fixture unexpectedly has bake completeness")
54 }
55 }
56 c.history = func() (*release.RootHistoryPass, error) {
57 if deps == nil {
58 return nil, os.ErrPermission
59 }
60 return deps, nil
61 }
62 n := namedUnitRecovery{source: source, pub: pub, readLive: os.Readlink}
63 if fault == "claims on" {
64 n.writes = hostwrite.Config{Enabled: true, Selector: namedRecoveryWork{}}
65 }
66 err = release.Pass(t.Context(), e.root.reg, e.store, pub, u.opt, nil,
67 release.WithRootHistory(deps), release.WithRecovery(func(ctx context.Context, rt registry.Route) (bool, error) {
68 return n.recover(ctx, rt.Host, c, func(string) string { return "5s" }, e.root.reg)
69 }))
70 if fault == "refused" {
71 live, liveErr := release.LiveVersion(u.opt, rt.Host)
72 if err == nil || liveErr != nil || live != "v1" || len(u.effects) != 0 {
73 t.Fatalf("known refusal ignored: live=%s effects=%v error=%v", live, u.effects, err)
74 }
75 return
76 }
77 if err != nil || len(u.effects) != 1 || u.exe != release.VersionPath(u.opt, rt.Account, "v2", rt.App) {
78 t.Fatalf("history prevented active repair: effects=%v error=%v", u.effects, err)
79 }
80 got, err := e.root.reg.Get(rt.Host)
81 if err != nil || got.Version != "v2" {
82 t.Fatalf("missing verified version: %+v %v", got, err)
83 }
84 if err := release.Pass(t.Context(), e.root.reg, e.store, pub, u.opt, nil,
85 release.WithRootHistory(deps), release.WithRecovery(func(ctx context.Context, rt registry.Route) (bool, error) {
86 return n.recover(ctx, rt.Host, c, func(string) string { return "5s" }, e.root.reg)
87 })); err != nil || len(u.effects) != 1 {
88 t.Fatalf("repair replay: %v %v", err, u.effects)
89 }
90 })
91 }
92}
93
94func TestPlan14SignedActiveFixAuthority(t *testing.T) {
95 for _, attack := range []string{"unsigned target", "unsigned predecessor", "changed artifact", "changed record", "changed unit", "changed pointer", "changed invocation", "operator stop", "held claim", "untrusted live", "capture failure", "rollback", "rollback fails"} {
96 t.Run(attack, func(t *testing.T) {
97 e, u, c, source, pub := signedRecoveryFixture(t)
98 rt, err := e.root.reg.Get(observedHost)
99 if err != nil {
100 t.Fatal(err)
101 }
102 n := namedUnitRecovery{source: source, pub: pub, readLive: os.Readlink}
103 mutate := func() {}
104 switch attack {
105 case "unsigned target":
106 n.pub = make([]byte, 32)
107 case "unsigned predecessor":
108 if err := e.store.Delete(t.Context(), release.PromotionRecordKey(rt.Account, rt.App, "v1")); err != nil {
109 t.Fatal(err)
110 }
111 case "changed artifact":
112 mutate = func() {
113 if err := os.WriteFile(release.VersionPath(u.opt, rt.Account, "v2", rt.App), []byte("wrong"), 0755); err != nil {
114 t.Fatal(err)
115 }
116 }
117 case "changed record":
118 mutate = func() {
119 rec, _, err := provision.FindRecordByHost(t.Context(), e.store, rt.Host)
120 if err != nil {
121 t.Fatal(err)
122 }
123 rec.RecordSeq++
124 plan14PutRecord(t, e.store, *rec)
125 }
126 case "changed unit":
127 mutate = func() {
128 if err := os.WriteFile(u.props.FragmentPath, []byte("wrong"), 0644); err != nil {
129 t.Fatal(err)
130 }
131 }
132 case "changed pointer":
133 mutate = func() {
134 if err := e.store.Delete(t.Context(), release.ChannelKey(rt.Account, rt.App, rt.Channel)); err != nil {
135 t.Fatal(err)
136 }
137 }
138 case "changed invocation":
139 mutate = func() { u.props.InvocationID += "other" }
140 case "operator stop":
141 mutate = func() { u.props.ActiveState, u.props.SubState, u.props.MainPID = "inactive", "dead", 0 }
142 case "held claim":
143 n.writes = hostwrite.Config{Enabled: true, Selector: namedRecoveryWork{deny: true}}
144 case "untrusted live":
145 n.readLive = func(string) (string, error) { return "", os.ErrPermission }
146 case "capture failure":
147 disk, _ := c.attempts()
148 disk.(*rollbackMemory).failWrite = true
149 case "rollback", "rollback fails":
150 u.crashNew = true
151 u.badRollback = attack == "rollback fails"
152 }
153 if strings.HasPrefix(attack, "changed ") || attack == "operator stop" {
154 n.writes = hostwrite.Config{Enabled: true, Selector: namedRecoveryWork{before: mutate}}
155 }
156 handled, err := n.recover(t.Context(), rt.Host, c, func(string) string { return "5s" }, e.root.reg)
157 if !handled || err == nil {
158 t.Fatalf("attack accepted: %v %v", handled, err)
159 }
160 if attack == "rollback" || attack == "rollback fails" {
161 if len(u.effects) != 3 {
162 t.Fatalf("rollback absent: %v %v", u.effects, err)
163 }
164 if attack == "rollback" && u.exe != release.VersionPath(u.opt, rt.Account, "v1", rt.App) {
165 t.Fatal("wrong recovered executable")
166 }
167 count := len(u.effects)
168 _, _ = n.recover(t.Context(), rt.Host, c, func(string) string { return "5s" }, e.root.reg)
169 if len(u.effects) != count {
170 t.Fatalf("repeated failed attempt: %v", u.effects)
171 }
172 } else {
173 live, liveErr := release.LiveVersion(u.opt, rt.Host)
174 if len(u.effects) != 0 || liveErr != nil || live != "v1" {
175 t.Fatalf("unauthorized effect: %v live=%s error=%v", u.effects, live, liveErr)
176 }
177 }
178 })
179 }
180}
181
182func TestPlan14SignedFixSupersedesOlderAttempt(t *testing.T) {
183 e, u, c, source, pub := signedRecoveryFixture(t)
184 b, err := c.host(t.Context(), observedHost)
185 if err != nil {
186 t.Fatal(err)
187 }
188 h := seedHostRootHistory(t, b.Route)
189 c.history = func() (*release.RootHistoryPass, error) { return &release.RootHistoryPass{History: h}, nil }
190 disk, err := c.attempts()
191 if err != nil {
192 t.Fatal(err)
193 }
194 _, err = captureRollback(disk, b, c.now(), unitSettleTimeout(func(string) string { return "5s" }), release.VersionPath(u.opt, b.Route.Account, "obsolete", b.Route.App), c.identity, historyRollbackLink{Binding: release.HistoryBinding{Host: b.Route.Host, Account: b.Route.Account, App: b.Route.App}, Attempt: release.HistoryAttempt{ID: "older-attempt", Version: "obsolete", Source: b.Source, SHA256: "old-digest", Platform: release.AdoptPlatform(), Started: c.now(), PromotedAt: c.now()}, Independent: true})
195 if err != nil {
196 t.Fatal(err)
197 }
198 n := namedUnitRecovery{source: source, pub: pub, readLive: os.Readlink}
199 handled, err := n.recover(t.Context(), observedHost, c, func(string) string { return "5s" }, e.root.reg)
200 if !handled || err != nil || len(u.effects) != 1 || u.exe != release.VersionPath(u.opt, b.Route.Account, "v2", b.Route.App) {
201 t.Fatalf("old attempt blocked new signed fix: %v %v %v", handled, err, u.effects)
202 }
203 if _, err := h.Refusals(release.HistoryBinding{Host: b.Route.Host, Account: b.Route.Account, App: b.Route.App}); err == nil {
204 t.Fatal("superseded ambiguous outcome erased by success")
205 }
206}
207
208func TestPlan14ERecoveryBindingRefusalKeepsPromotedFix(t *testing.T) {
209 e, u, c, source, pub := signedRecoveryFixture(t)
210 c.legacy = func() (bool, error) { return true, nil }
211 c.legacyReady = c.ready
212 // Refuse the final readiness binding after the process effect.
213 original := c.host
214 c.host = func(ctx context.Context, host string) (proofBinding, error) {
215 if len(u.effects) > 0 {
216 return proofBinding{}, os.ErrPermission
217 }
218 return original(ctx, host)
219 }
220 n := namedUnitRecovery{source: source, pub: pub, readLive: os.Readlink}
221 handled, err := n.recover(t.Context(), observedHost, c, func(string) string { return "5s" }, e.root.reg)
222 live, liveErr := release.LiveVersion(u.opt, observedHost)
223 if handled || err != nil || liveErr != nil || live != "v1" || len(u.effects) != 0 {
224 t.Fatalf("no-census recovery selected effects: handled=%v err=%v live=%s/%v effects=%v", handled, err, live, liveErr, u.effects)
225 }
226 rt, err := e.root.reg.Get(observedHost)
227 if err != nil {
228 t.Fatal(err)
229 }
230 if _, _, err := release.StageSignedHead(t.Context(), e.store, pub, nil, u.opt, *rt); err != nil {
231 t.Fatal(err)
232 }
233 if err := release.PointLive(u.opt, observedHost, rt.Account, "v2", rt.App); err != nil {
234 t.Fatal(err)
235 }
236 if err := legacyCheckedUnitRestart(u, u.opt, func(string) string { return "5s" }, *rt, nil, c); err != nil {
237 t.Fatal(err)
238 }
239 live, liveErr = release.LiveVersion(u.opt, observedHost)
240 if liveErr != nil || live != "v2" || len(u.effects) != 1 {
241 t.Fatalf("main outcome changed: %s/%v %v", live, liveErr, u.effects)
242 }
243}
244