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
10 "github.com/carlosframework/platform/internal/hostguard"
11 "github.com/carlosframework/platform/internal/registry"
12 "github.com/carlosframework/platform/internal/rootstate"
13)
14
15func plan14UnitBindingFixture(t *testing.T, unit, backing, socket string) (*claimUnitBindings, hostguard.RootInstallation, claimUnitShape) {
16 t.Helper()
17 in := hostguard.RootInstallation{Host: "shop.example", Account: "bab", App: "shop", Record: "apps/bab/shop/instances/shop.example.json", Incarnation: "inc", Stamp: 1, Migration: 1, InventoryRevision: 1, Box: "box", Enrollment: "enrollment"}
18 shape := claimUnitShape{Host: in.Host, Account: in.Account, App: in.App, Kind: "instance", Unit: unit, Backing: backing, Socket: socket, DBPath: "/data/instances/shop/data.db"}
19 if socket == "" {
20 shape.Addr = "127.0.0.1:8090"
21 }
22 b := &claimUnitBindings{state: &rollbackMemory{files: map[string][]byte{}}, installation: func(context.Context, string) (hostguard.RootInstallation, error) { return in, nil }, canonical: func(context.Context, string) (claimUnitShape, error) { return shape, nil }}
23 return b, in, shape
24}
25
26func TestPlan14CanonicalUnitBinding(t *testing.T) {
27 for _, tc := range []struct{ name, unit, backing, socket string }{
28 {"named", "console.service", "", ""},
29 {"socket", "woodstar-atbridge.service", "", "/run/carlos/woodstar.sock"},
30 {"whole-app", "carlos-app@shop.example.service", "unit", ""},
31 } {
32 t.Run(tc.name, func(t *testing.T) {
33 b, in, shape := plan14UnitBindingFixture(t, tc.unit, tc.backing, tc.socket)
34 saved, err := b.establish(t.Context(), in.Host)
35 if err != nil {
36 t.Fatal(err)
37 }
38 row := shape.route()
39 if _, err := b.check(t.Context(), in.Host, &row); err != nil {
40 t.Fatal(err)
41 }
42 raw, _ := b.state.Read(in.Host + ".json")
43 for _, mutate := range []func(*registry.Route){
44 func(r *registry.Route) { r.Unit = "other.service" }, func(r *registry.Route) { r.Backing = "exec" },
45 func(r *registry.Route) { r.Socket = "/run/other.sock" }, func(r *registry.Route) { r.Addr = "127.0.0.1:9000" },
46 func(r *registry.Route) { r.DBPath = "/data/sibling.db" }, func(r *registry.Route) { r.Account = "other" },
47 func(r *registry.Route) { r.Tunnel = "other" }, func(r *registry.Route) { r.Kind = "sidecar" },
48 } {
49 forged := row
50 mutate(&forged)
51 if _, err := b.check(t.Context(), in.Host, &forged); err == nil {
52 t.Fatal("accepted changed shape", forged)
53 }
54 }
55 b.canonical = func(context.Context, string) (claimUnitShape, error) {
56 changed := shape
57 changed.Unit = "other.service"
58 return changed, nil
59 }
60 if _, err := b.establish(t.Context(), in.Host); err == nil {
61 t.Fatal("rebound a live installation")
62 }
63 if _, err := b.check(t.Context(), in.Host, &row); err == nil {
64 t.Fatal("accepted changed canonical configuration")
65 }
66 now, _ := b.state.Read(in.Host + ".json")
67 if string(now) != string(raw) {
68 t.Fatal("replaced binding")
69 }
70 b.canonical = func(context.Context, string) (claimUnitShape, error) { return shape, nil }
71 successor := in
72 successor.Stamp++
73 b.installation = func(context.Context, string) (hostguard.RootInstallation, error) { return successor, nil }
74 if _, err := b.check(t.Context(), in.Host, &row); err == nil {
75 t.Fatal("adopted successor")
76 }
77 if err := b.retire(t.Context(), saved, func(claimUnitBinding) error { return os.ErrPermission }); err == nil {
78 t.Fatal("retired without stop")
79 }
80 pending, _ := b.read(in.Host)
81 if !pending.Retiring || pending.Retired {
82 t.Fatal("missing durable retirement intent")
83 }
84 if _, err := b.establish(t.Context(), in.Host); err == nil {
85 t.Fatal("overwrote predecessor retirement")
86 }
87 called := false
88 if err := b.retire(t.Context(), saved, func(got claimUnitBinding) error {
89 called = true
90 if got.Owner != saved.Owner || got.Shape != shape {
91 t.Fatal("retirement retargeted")
92 }
93 return nil
94 }); err != nil {
95 t.Fatal(err)
96 }
97 if !called {
98 t.Fatal("no stop verification")
99 }
100 if _, err := b.check(t.Context(), in.Host, &row); err == nil {
101 t.Fatal("retired binding granted work")
102 }
103 if _, err := b.establish(t.Context(), in.Host); err == nil {
104 t.Fatal("implicit successor grant")
105 }
106 })
107 }
108}
109
110func TestPlan14CanonicalUnitBindingRefusals(t *testing.T) {
111 b, in, _ := plan14UnitBindingFixture(t, "console.service", "", "")
112 for _, raw := range []string{"", "{}", `{"Schema":1,"unknown":1}`} {
113 b.state.Write(in.Host+".json", []byte(raw))
114 if _, err := b.establish(t.Context(), in.Host); err == nil {
115 t.Fatal("corrupt history became permission")
116 }
117 }
118 b.state.Remove(in.Host + ".json")
119 b.canonical = func(context.Context, string) (claimUnitShape, error) { return claimUnitShape{}, os.ErrNotExist }
120 if _, err := b.establish(t.Context(), in.Host); !errors.Is(err, os.ErrNotExist) {
121 t.Fatal(err)
122 }
123 if _, err := b.state.Read(in.Host + ".json"); !errors.Is(err, os.ErrNotExist) {
124 t.Fatal("created binding without root config", err)
125 }
126}
127
128func TestPlan14UnitBindingRetirementCrash(t *testing.T) {
129 b, in, _ := plan14UnitBindingFixture(t, "console.service", "", "")
130 saved, err := b.establish(t.Context(), in.Host)
131 if err != nil {
132 t.Fatal(err)
133 }
134 if err := b.retire(t.Context(), saved, func(claimUnitBinding) error { return os.ErrPermission }); err == nil {
135 t.Fatal("lost stop accepted")
136 }
137 restarted := *b
138 if _, err := restarted.check(t.Context(), in.Host, nil); err == nil {
139 t.Fatal("crash reopened admission")
140 }
141 if err := restarted.retire(t.Context(), saved, func(claimUnitBinding) error { return nil }); err != nil {
142 t.Fatal(err)
143 }
144 raw, _ := b.state.Read(in.Host + ".json")
145 var record claimUnitBinding
146 if err := json.Unmarshal(raw, &record); err != nil || !record.Retired {
147 t.Fatal(record, err)
148 }
149}
150
151func TestPlan14UnitBindingConfig(t *testing.T) {
152 _, _, s := plan14UnitBindingFixture(t, "console.service", "", "")
153 raw, _ := json.Marshal(struct {
154 Schema int
155 Units []claimUnitShape
156 }{1, []claimUnitShape{s}})
157 if got, err := decodeClaimUnitShapes(raw); err != nil || got[s.Host] != s {
158 t.Fatal(got, err)
159 }
160 for _, kind := range []string{"duplicate host", "duplicate unit", "tunnel", "unknown field", "trailing", "unsafe addr", "template", "backing"} {
161 t.Run(kind, func(t *testing.T) {
162 other := s
163 other.Host = "sibling.example"
164 config := struct {
165 Schema int
166 Units []claimUnitShape
167 }{1, []claimUnitShape{s}}
168 switch kind {
169 case "duplicate host":
170 config.Units = append(config.Units, s)
171 case "duplicate unit":
172 config.Units = append(config.Units, other)
173 case "unsafe addr":
174 config.Units[0].Addr = "0.0.0.0:8090"
175 case "template":
176 config.Units[0].Unit = "x@.service"
177 case "backing":
178 config.Units[0].Backing = "unit"
179 }
180 body, _ := json.Marshal(config)
181 switch kind {
182 case "tunnel":
183 body = []byte(`{"Schema":1,"Units":[{"Tunnel":"x"}]}`)
184 case "unknown field":
185 body = []byte(`{"Schema":1,"Units":[],"Authority":true}`)
186 case "trailing":
187 body = append(body, []byte(`{}`)...)
188 }
189 if _, err := decodeClaimUnitShapes(body); err == nil {
190 t.Fatal("accepted invalid config")
191 }
192 })
193 }
194}
195
196type plan14UnitBindingFault struct {
197 rootstate.Store
198 fail bool
199 writes int
200}
201
202func (s *plan14UnitBindingFault) Write(name string, body []byte) error {
203 s.writes++
204 if err := s.Store.Write(name, body); err != nil {
205 return err
206 }
207 if s.fail {
208 return os.ErrInvalid
209 }
210 return nil
211}
212
213func TestPlan14UnitBindingDurability(t *testing.T) {
214 b, in, _ := plan14UnitBindingFixture(t, "console.service", "", "")
215 fault := &plan14UnitBindingFault{Store: b.state, fail: true}
216 b.state = fault
217 if _, err := b.establish(t.Context(), in.Host); err == nil {
218 t.Fatal("reported unsynced creation")
219 }
220 if _, err := b.establish(t.Context(), in.Host); err == nil {
221 t.Fatal("duplicate creation skipped durability")
222 }
223 fault.fail = false
224 saved, err := b.establish(t.Context(), in.Host)
225 if err != nil {
226 t.Fatal(err)
227 }
228 fault.fail = true
229 effects := 0
230 if err := b.retire(t.Context(), saved, func(claimUnitBinding) error { effects++; return nil }); err == nil {
231 t.Fatal("reported unsynced retirement intent")
232 }
233 if effects != 0 {
234 t.Fatal("effects before durable intent")
235 }
236 if _, err := b.check(t.Context(), in.Host, nil); err == nil {
237 t.Fatal("visible retirement intent reopened")
238 }
239 fault.fail = false
240 if err := b.retire(t.Context(), saved, func(claimUnitBinding) error { effects++; return nil }); err != nil {
241 t.Fatal(err)
242 }
243 if effects != 1 {
244 t.Fatal(effects)
245 }
246 fault.fail = true
247 if err := b.retire(t.Context(), saved, func(claimUnitBinding) error { effects++; return nil }); err == nil {
248 t.Fatal("duplicate completion skipped durability")
249 }
250 if effects != 1 {
251 t.Fatal("replayed completed effect")
252 }
253}
254