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 "encoding/json"
6 "errors"
7 "os"
8 "testing"
9 "time"
10
11 "github.com/carlosframework/platform/internal/provision"
12 "github.com/carlosframework/platform/internal/release"
13 "github.com/carlosframework/platform/internal/rootstate"
14)
15
16type rollbackMemory struct {
17 rootstate.Store
18 files map[string][]byte
19 failWrite bool
20}
21
22func (m *rollbackMemory) Read(k string) ([]byte, error) {
23 b, ok := m.files[k]
24 if !ok {
25 return nil, os.ErrNotExist
26 }
27 return append([]byte(nil), b...), nil
28}
29func (m *rollbackMemory) Write(k string, b []byte) error {
30 if m.failWrite {
31 return os.ErrPermission
32 }
33 m.files[k] = append([]byte(nil), b...)
34 return nil
35}
36func (m *rollbackMemory) Remove(k string) error { delete(m.files, k); return nil }
37func (m *rollbackMemory) Lock(context.Context, string) (func(), error) { return func() {}, nil }
38func (m *rollbackMemory) Close() error { return nil }
39
40func TestPlan14RollbackCapability(t *testing.T) {
41 t.Parallel()
42 for _, attack := range []string{"crash", "unit identity", "etag", "missing", "stale", "later process", "settled", "operator stopped", "wrong link", "bad executable", "failed restart result", "missing journal", "different attempt", "capability attempt", "spent", "capture write"} {
43 t.Run(attack, func(t *testing.T) {
44 e, rt, u, c, pub := plan14AdoptionFixture(t)
45 u.crashNew = true
46 mem := &rollbackMemory{files: map[string][]byte{}}
47 c.attempts = func() (rootstate.Store, error) { return mem, nil }
48 if attack == "capture write" {
49 mem.failWrite = true
50 }
51 forward := rootRestartRouteWith(e.root.reg, u, u.opt, func(string) string { return "5s" }, e.root.obs.recordSettled(e.bake), c)
52 restart := func(host string) error {
53 err := forward(host)
54 key := rollbackJournalKey(host)
55 var a rollbackAttempt
56 b, _ := mem.Read(key)
57 _ = json.Unmarshal(b, &a)
58 switch attack {
59 case "unit identity":
60 b, err := os.ReadFile(u.props.FragmentPath)
61 if err != nil {
62 t.Fatal(err)
63 }
64 if err = os.Rename(u.props.FragmentPath, u.props.FragmentPath+".old"); err != nil {
65 t.Fatal(err)
66 }
67 if err = os.WriteFile(u.props.FragmentPath, b, 0644); err != nil {
68 t.Fatal(err)
69 }
70 case "etag":
71 rec, _, err := provision.FindRecordByHost(context.Background(), e.store, host)
72 if err != nil {
73 t.Fatal(err)
74 }
75 rec.RecordSeq++
76 plan14PutRecord(t, e.store, *rec)
77 case "missing":
78 delete(mem.files, rollbackCapabilityKey(host, a.ID))
79 case "stale":
80 c.now = func() time.Time { return a.Deadline.Add(rollbackMargin + time.Second) }
81 case "settled":
82 a.Settled = true
83 case "operator stopped":
84 u.props.ActiveState, u.props.SubState = "inactive", "dead"
85 case "bad executable":
86 u.badRollback = true
87 case "failed restart result":
88 u.props.ActiveState, u.props.SubState = "active", "running"
89 u.failNew = true
90 case "missing journal":
91 delete(mem.files, key)
92 case "different attempt":
93 a.ID = "different"
94 case "capability attempt":
95 var capability rollbackCapability
96 raw, _ := mem.Read(rollbackCapabilityKey(host, a.ID))
97 if e := json.Unmarshal(raw, &capability); e != nil {
98 t.Fatal(e)
99 }
100 capability.Attempt.ID = "another-attempt"
101 raw, _ = json.Marshal(capability)
102 if e := mem.Write(rollbackCapabilityKey(host, a.ID), raw); e != nil {
103 t.Fatal(e)
104 }
105 case "spent":
106 a.Recovering = true
107 }
108 if attack == "settled" || attack == "different attempt" || attack == "spent" {
109 b, _ := json.Marshal(a)
110 _ = mem.Write(key, b)
111 }
112 return err
113 }
114 if attack == "failed restart result" {
115 u.failNew = true
116 u.crashNew = false
117 }
118 var rollbackErr error
119 err := release.Pass(context.Background(), e.root.reg, e.store, pub, u.opt, restart, release.BakeDir(e.bake), release.WithRestartFailurePropagation(), release.WithRollbackRestart(func(host string) error {
120 if attack == "wrong link" {
121 if err := release.PointLive(u.opt, host, "bab", "v2", "console"); err != nil {
122 t.Fatal(err)
123 }
124 }
125 // Reconstruct the callback after the forward operation, as another root process does.
126 rollbackErr = rollbackUnitRestart(e.root.reg, u, u.opt, func(string) string { return "5s" }, c)(host)
127 return rollbackErr
128 }))
129 positive := attack == "missing" || attack == "stale" || attack == "settled" || attack == "missing journal" || attack == "crash" || attack == "later process" || attack == "failed restart result" || attack == "spent"
130 if err == nil {
131 t.Fatal("forward failure disappeared")
132 }
133 if positive {
134 if rollbackErr != nil || len(u.effects) != 3 || u.props.MainPID <= 0 {
135 t.Fatalf("recovery: %v, %+v", rollbackErr, u)
136 }
137 } else if rollbackErr == nil && attack != "capture write" {
138 t.Fatalf("%s accepted", attack)
139 }
140 if !positive && attack != "bad executable" && len(u.effects) > 1 {
141 t.Fatalf("unauthorized restart: %v", u.effects)
142 }
143 if attack == "capture write" && len(u.effects) != 0 {
144 t.Fatal("restart without durable capture")
145 }
146 if len(release.ReadHealthy(e.bake, rt.Account, rt.App)) != 1 {
147 t.Fatal("recorded failed build")
148 }
149 if errors.Is(rollbackErr, os.ErrNotExist) {
150 t.Log("absent capability refused")
151 }
152 for k := range mem.files {
153 if k != rollbackJournalKey(rt.Host) && k != rollbackHostKey(rt.Host)+".previous" {
154 t.Fatalf("capability survived settlement: %s", k)
155 }
156 }
157 })
158 }
159}
160
161func (m *rollbackMemory) Names() ([]string, error) {
162 var names []string
163 for k := range m.files {
164 names = append(names, k)
165 }
166 return names, nil
167}
168
169func TestPlan14RollbackAttemptSettles(t *testing.T) {
170 t.Parallel()
171 for _, failure := range []bool{false, true} {
172 t.Run(map[bool]string{false: "success", true: "terminal"}[failure], func(t *testing.T) {
173 e, rt, u, c, pub := plan14AdoptionFixture(t)
174 mem := &rollbackMemory{files: map[string][]byte{}}
175 c.attempts = func() (rootstate.Store, error) { return mem, nil }
176 forward := rootRestartRouteWith(e.root.reg, u, u.opt, func(string) string { return "5s" }, e.root.obs.recordSettled(e.bake), c)
177 if failure {
178 u.crashNew = true
179 }
180 err := release.Pass(context.Background(), e.root.reg, e.store, pub, u.opt, forward, release.BakeDir(e.bake), release.WithRestartFailurePropagation(), release.WithRestartAttemptSettled(c.finish))
181 if (err != nil) != failure {
182 t.Fatalf("pass: %v", err)
183 }
184 a, err := readRollbackAttempt(mem, rt.Host)
185 if err != nil || !a.Settled {
186 t.Fatalf("unsettled: %+v %v", a, err)
187 }
188 if _, err := mem.Read(rollbackCapabilityKey(rt.Host, a.ID)); !errors.Is(err, os.ErrNotExist) {
189 t.Fatal("capability retained")
190 }
191 })
192 }
193}
194