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 "errors"
6 "os"
7 "strings"
8 "testing"
9
10 "github.com/carlosframework/platform/internal/caps"
11 "github.com/carlosframework/platform/internal/hostbox"
12 "github.com/carlosframework/platform/internal/hostclaim"
13 "github.com/carlosframework/platform/internal/hostguard"
14 "github.com/carlosframework/platform/internal/hostwrite"
15 "github.com/carlosframework/platform/internal/registry"
16 "github.com/carlosframework/platform/internal/rootstate"
17)
18
19func TestPlan14UnitShapeAllOperationKinds(t *testing.T) {
20 t.Parallel()
21 for _, unit := range []string{"console.service", "woodstar-socket.service", "carlos-app@shop.example.service"} {
22 t.Run(unit, func(t *testing.T) {
23 b, in, shape := plan14UnitBindingFixture(t, unit, "", "")
24 if strings.HasPrefix(unit, "carlos-app@") {
25 shape.Backing = "unit"
26 }
27 if unit == "woodstar-socket.service" {
28 shape.Addr = ""
29 shape.Socket = "/run/carlos/woodstar.sock"
30 }
31 b.canonical = func(context.Context, string) (claimUnitShape, error) { return shape, nil }
32 if _, err := b.establish(t.Context(), in.Host); err != nil {
33 t.Fatal(err)
34 }
35 candidate := shape.route()
36 read := func(ctx context.Context, rt registry.Route, s hostbox.Selection) error {
37 return checkClaimUnitShape(ctx, b, map[string]claimUnitShape{in.Host: shape}, rt, s)
38 }
39 refuse := claimUnitShapePolicy(func(string) (*registry.Route, error) { return &candidate, nil }, read)
40 for _, kind := range []string{hostclaim.KindServing, hostclaim.KindMoving, hostclaim.KindLanding, hostclaim.KindReverting, hostclaim.KindRestoring, hostclaim.KindRelocating} {
41 for _, action := range []string{"install", "restore", "spawn", "writer", "config", "release", "timers", "held-config", "held-release", "held-timers", "probe"} {
42 s := hostbox.Selection{Host: in.Host, Box: in.Box, Captured: hostguard.Captured{Incarnation: in.Incarnation, Stamp: in.Stamp}, Action: action, Kind: kind}
43 c := &hostclaim.Claim{Host: in.Host, Op: hostclaim.Op{Kind: kind}}
44 candidate = shape.route()
45 if err := refuse(t.Context(), c, s, hostwrite.Identity{}); err != nil {
46 t.Fatal(kind, action, err)
47 }
48 for _, mutate := range []func(*registry.Route){
49 func(r *registry.Route) { r.Unit = ""; r.Backing = "" }, func(r *registry.Route) { r.Unit = "sibling.service" }, func(r *registry.Route) { r.Backing = "exec" },
50 func(r *registry.Route) { r.Account = "other" }, func(r *registry.Route) { r.App = "other" }, func(r *registry.Route) { r.Kind = "sidecar" },
51 func(r *registry.Route) { r.DBPath = "/data/sibling.db" }, func(r *registry.Route) { r.Socket = "/run/carlos/sibling.sock" }, func(r *registry.Route) { r.Addr = "127.0.0.1:9999" },
52 func(r *registry.Route) { r.Tunnel = "other" }, func(r *registry.Route) { r.Replica = true }, func(r *registry.Route) { r.Seed = "seed" },
53 } {
54 candidate = shape.route()
55 mutate(&candidate)
56 if err := refuse(t.Context(), c, s, hostwrite.Identity{}); err == nil {
57 t.Fatal("shape edit reached operation", kind, action, candidate)
58 }
59 }
60 }
61 }
62 s := hostbox.Selection{Host: in.Host, Box: in.Box, Captured: hostguard.Captured{Incarnation: in.Incarnation, Stamp: in.Stamp + 1}}
63 candidate = shape.route()
64 if err := refuse(t.Context(), nil, s, hostwrite.Identity{}); err == nil {
65 t.Fatal("successor used predecessor binding")
66 }
67 sibling := registry.Route{Host: "sibling.example", Kind: "instance"}
68 if err := checkClaimUnitShape(t.Context(), b, map[string]claimUnitShape{in.Host: shape}, sibling, hostbox.Selection{Host: sibling.Host}); err != nil {
69 t.Fatal("ordinary sibling refused", err)
70 }
71 })
72 }
73}
74
75func TestPlan14BoundCapabilitiesRefuseEffects(t *testing.T) {
76 b, in, shape := plan14UnitBindingFixture(t, "console.service", "", "")
77 if _, err := b.establish(t.Context(), in.Host); err != nil {
78 t.Fatal(err)
79 }
80 oldOpen, oldShapes := openClaimUnitBindingsRead, productionClaimUnitShapes
81 defer func() { openClaimUnitBindingsRead, productionClaimUnitShapes = oldOpen, oldShapes }()
82 openClaimUnitBindingsRead = func() (rootstate.Store, error) { return b.state, nil }
83 for _, config := range []string{"good", "removed", "corrupt"} {
84 t.Run(config, func(t *testing.T) {
85 productionClaimUnitShapes = func() (map[string]claimUnitShape, error) {
86 if config == "corrupt" {
87 return nil, os.ErrPermission
88 }
89 if config == "removed" {
90 return map[string]claimUnitShape{}, nil
91 }
92 return map[string]claimUnitShape{in.Host: shape}, nil
93 }
94 dir := t.TempDir()
95 ops := productionClaimCapsOps(caps.Ops{EtcDir: dir, Systemctl: func(...string) error { t.Fatal("bound capability reached systemctl"); return nil }}, func() (bool, error) { return false, nil })
96 if _, err := caps.Converge(ops, caps.Plan{Host: in.Host}); err == nil {
97 t.Fatal("converge accepted bound host")
98 }
99 if err := caps.Restore(ops, caps.Snapshot{Host: in.Host}); err == nil {
100 t.Fatal("restore accepted bound host")
101 }
102 if err := caps.Restart(ops, caps.Plan{Host: in.Host}); err == nil {
103 t.Fatal("restart accepted bound host")
104 }
105 entries, err := os.ReadDir(dir)
106 if err != nil || len(entries) != 0 {
107 t.Fatal("wrote capability dropin", entries, err)
108 }
109 if err := claimBoundCapsRefusal("ordinary.example"); err != nil {
110 t.Fatal("ordinary host refused", err)
111 }
112 })
113 }
114}
115
116func TestPlan14BoundCapabilitiesNoCensus(t *testing.T) {
117 oldOpen, oldShapes := openClaimUnitBindingsRead, productionClaimUnitShapes
118 defer func() { openClaimUnitBindingsRead, productionClaimUnitShapes = oldOpen, oldShapes }()
119 openClaimUnitBindingsRead = func() (rootstate.Store, error) { t.Fatal("no-census binding read"); return nil, os.ErrPermission }
120 productionClaimUnitShapes = func() (map[string]claimUnitShape, error) {
121 t.Fatal("no-census config read")
122 return nil, os.ErrPermission
123 }
124 calls := 0
125 prior := caps.Ops{CheckHost: func(string) error { calls++; return nil }}
126 if err := productionClaimCapsOps(prior, func() (bool, error) { return true, nil }).CheckHost("console.example"); err != nil || calls != 1 {
127 t.Fatal(err, calls)
128 }
129 if err := productionClaimCapsOps(prior, func() (bool, error) { return false, os.ErrPermission }).CheckHost("console.example"); err == nil || calls != 1 {
130 t.Fatal("census error reached effect", err, calls)
131 }
132}
133
134func TestPlan14UnitShapePolicyProductionModes(t *testing.T) {
135 b, in, shape := plan14UnitBindingFixture(t, "console.service", "", "")
136 if _, err := b.establish(t.Context(), in.Host); err != nil {
137 t.Fatal(err)
138 }
139 oldOpen, oldShapes := openClaimUnitBindingsRead, productionClaimUnitShapes
140 defer func() { openClaimUnitBindingsRead, productionClaimUnitShapes = oldOpen, oldShapes }()
141 opens := 0
142 openClaimUnitBindingsRead = func() (rootstate.Store, error) { opens++; return b.state, nil }
143 productionClaimUnitShapes = func() (map[string]claimUnitShape, error) { return map[string]claimUnitShape{in.Host: shape}, nil }
144 for _, mode := range []string{"instance-control", "adopt"} {
145 row := shape.route()
146 p := productionClaimUnitShapePolicy(mode, func(string) (*registry.Route, error) { return &row, nil })
147 s := hostbox.Selection{Host: in.Host, Box: in.Box, Captured: hostguard.Captured{Incarnation: in.Incarnation, Stamp: in.Stamp}}
148 if err := p(t.Context(), nil, s, hostwrite.Identity{}); err != nil {
149 t.Fatal(mode, err)
150 }
151 row.Unit = ""
152 if err := p(t.Context(), nil, s, hostwrite.Identity{}); err == nil {
153 t.Fatal(mode, "cleared binding")
154 }
155 }
156 if opens != 4 {
157 t.Fatal("root did not read independent binding", opens)
158 }
159 openClaimUnitBindingsRead = func() (rootstate.Store, error) { return nil, os.ErrPermission }
160 p := productionClaimUnitShapePolicy("adopt", func(string) (*registry.Route, error) { r := shape.route(); return &r, nil })
161 if p(t.Context(), nil, hostbox.Selection{Host: in.Host}, hostwrite.Identity{}) == nil {
162 t.Fatal("unavailable root read permitted")
163 }
164}
165
166func TestPlan14BoundRestartShapeRefusal(t *testing.T) {
167 for _, op := range []string{"restart", "rollback", "checked"} {
168 for _, shape := range []string{"unchanged", "cleared", "ordinary", "sibling", "backing", "socket", "missing"} {
169 t.Run(op+"/"+shape, func(t *testing.T) {
170 reg := restartTestRegistry(t)
171 b, in, bound := plan14UnitBindingFixture(t, "console.service", "", "")
172 if _, err := b.establish(t.Context(), in.Host); err != nil {
173 t.Fatal(err)
174 }
175 row := bound.route()
176 switch shape {
177 case "cleared":
178 row.Unit = ""
179 case "ordinary":
180 row.Unit = "carlos-app@" + in.Host + ".service"
181 case "sibling":
182 row.Unit = "sibling.service"
183 case "backing":
184 row.Backing = "unit"
185 case "socket":
186 row.Socket = "/run/carlos/other.sock"
187 }
188 if shape != "missing" {
189 if err := reg.Upsert(row); err != nil {
190 t.Fatal(err)
191 }
192 }
193 calls := 0
194 oldOpen, oldShapes := openClaimUnitBindingsRead, productionClaimUnitShapes
195 defer func() { openClaimUnitBindingsRead, productionClaimUnitShapes = oldOpen, oldShapes }()
196 openClaimUnitBindingsRead = func() (rootstate.Store, error) { return b.state, nil }
197 productionClaimUnitShapes = func() (map[string]claimUnitShape, error) { return map[string]claimUnitShape{in.Host: bound}, nil }
198 checks := rootRestartChecks{unitShape: func(ctx context.Context, host string, rt *registry.Route) error {
199 calls++
200 return rootClaimRestartShape(ctx, host, rt)
201 }}
202 sys := &fakeUnits{}
203 var err error
204 switch op {
205 case "restart":
206 err = rootRestartRouteWith(reg, sys, t.TempDir(), func(string) string { return "" }, nil, checks)(in.Host)
207 case "rollback":
208 err = rollbackUnitRestart(reg, sys, t.TempDir(), func(string) string { return "" }, checks)(in.Host)
209 case "checked":
210 if shape == "missing" {
211 row = registry.Route{Host: in.Host}
212 }
213 err = checkedUnitRestart(sys, t.TempDir(), func(string) string { return "" }, nil, checks)(row)
214 }
215 if err == nil || calls != 1 || len(sys.restarts) != 0 {
216 t.Fatal("shape bypass", err, calls, sys.restarts)
217 }
218 })
219 }
220 }
221}
222
223// A fresh install has no registry row until registerRemote's second
224// work.With writes it, and the unit-shape refusal runs on every work.With
225// before that (harness G10, 2026-10-21). Only the installation's own work
226// (hostbox.Selection.Installation) may go on without a row, and then the
227// refusal checks an empty route for the host: it passes only when the host
228// has no configured shape and no saved unit binding. Any other work refuses
229// a missing row (fix round 2).
230func TestClaimUnitShapePolicyFreshInstall(t *testing.T) {
231 oldOpen, oldShapes := openClaimUnitBindingsRead, productionClaimUnitShapes
232 defer func() { openClaimUnitBindingsRead, productionClaimUnitShapes = oldOpen, oldShapes }()
233 for _, tc := range []struct {
234 name string
235 row string // "", "other", "matching", "ordinary"
236 shape bool
237 binding bool
238 storeless bool
239 want error // for the installation's own work
240 }{
241 {name: "no row, no shape, no binding", want: nil},
242 {name: "no row, no binding store", storeless: true, want: nil},
243 {name: "no row, configured shape", shape: true, want: hostbox.ErrUnsupported},
244 {name: "no row, saved binding", shape: true, binding: true, want: hostbox.ErrChanged},
245 {name: "no row, saved binding, shape removed", binding: true, want: hostbox.ErrChanged},
246 {name: "row for another host", row: "other", want: hostbox.ErrChanged},
247 {name: "matching row", row: "matching", shape: true, binding: true, want: nil},
248 {name: "matching ordinary row", row: "ordinary", want: nil},
249 } {
250 t.Run(tc.name, func(t *testing.T) {
251 b, in, shape := plan14UnitBindingFixture(t, "console.service", "", "")
252 if tc.binding {
253 if _, err := b.establish(t.Context(), in.Host); err != nil {
254 t.Fatal(err)
255 }
256 }
257 openClaimUnitBindingsRead = func() (rootstate.Store, error) {
258 if tc.storeless {
259 return nil, os.ErrNotExist
260 }
261 return b.state, nil
262 }
263 productionClaimUnitShapes = func() (map[string]claimUnitShape, error) {
264 if tc.shape {
265 return map[string]claimUnitShape{in.Host: shape}, nil
266 }
267 return map[string]claimUnitShape{}, nil
268 }
269 var row *registry.Route
270 switch tc.row {
271 case "other":
272 r := registry.Route{Host: "sibling.example", Kind: "instance"}
273 row = &r
274 case "matching":
275 r := shape.route()
276 row = &r
277 case "ordinary":
278 r := registry.Route{Host: in.Host, Account: in.Account, App: in.App, Kind: "instance", DBPath: "/data/instances/shop/data.db", Addr: "127.0.0.1:8090"}
279 row = &r
280 }
281 lookups := 0
282 route := func(host string) (*registry.Route, error) {
283 lookups++
284 if host != in.Host {
285 t.Fatal("looked up", host)
286 }
287 return row, nil
288 }
289 s := hostbox.Selection{Host: in.Host, Box: in.Box, Action: "reconcile", Kind: hostclaim.KindServing, Captured: hostguard.Captured{Incarnation: in.Incarnation, Stamp: in.Stamp}}
290 if s.Installation() {
291 t.Fatal("a selection literal is marked as installation work")
292 }
293 check := func(name string, err, want error) {
294 t.Helper()
295 if want == nil && err != nil {
296 t.Fatalf("%s: refused: %v", name, err)
297 }
298 if want != nil && !errors.Is(err, want) {
299 t.Fatalf("%s: got %v, want %v", name, err, want)
300 }
301 }
302 // Ordinary work, through the production policy: the selection is
303 // not an installation's, so a missing row refuses.
304 ordinary := tc.want
305 if row == nil {
306 ordinary = hostbox.ErrChanged
307 }
308 p := productionClaimUnitShapePolicy("instance-control", route)
309 check("ordinary work", p(t.Context(), &hostclaim.Claim{Host: in.Host, Op: hostclaim.Op{Kind: hostclaim.KindServing}}, s, hostwrite.Identity{}), ordinary)
310 // The installation's own work, with instance-control's reader
311 // (productionClaimUnitShapePolicy's).
312 read := func(ctx context.Context, rt registry.Route, s hostbox.Selection) error {
313 return withClaimUnitShape(ctx, rt, s, func(claimUnitBinding) error { return nil })
314 }
315 check("installation work", claimUnitShapeCheck(t.Context(), route, read, s, true), tc.want)
316 if lookups != 2 {
317 t.Fatal("route lookups", lookups)
318 }
319 })
320 }
321}
322
323// The agent's reader is root's boundInputs (claimunit_policy.go's agent
324// branch, over the role-launch socket). Ordinary work never reaches it
325// without a row; the installation's own work sends it the empty route, which
326// it passes only when root has no binding and no configured shape.
327func TestClaimUnitShapePolicyFreshInstallAgentReader(t *testing.T) {
328 var seen []registry.Route
329 route := func(string) (*registry.Route, error) { return nil, nil }
330 read := func(_ context.Context, rt registry.Route, _ hostbox.Selection) error {
331 seen = append(seen, rt)
332 return nil
333 }
334 s := hostbox.Selection{Host: "shop.example"}
335 if err := claimUnitShapePolicy(route, read)(t.Context(), nil, s, hostwrite.Identity{}); !errors.Is(err, hostbox.ErrChanged) || len(seen) != 0 {
336 t.Fatal("ordinary work without a row:", err, seen)
337 }
338 if err := claimUnitShapeCheck(t.Context(), route, read, s, true); err != nil || len(seen) != 1 || seen[0] != (registry.Route{Host: "shop.example"}) {
339 t.Fatal(err, seen)
340 }
341 for _, tc := range []struct {
342 name string
343 binding, shape bool
344 want error
345 }{
346 {"no binding, no shape", false, false, nil},
347 {"configured shape", false, true, os.ErrPermission},
348 {"saved binding", true, true, os.ErrPermission},
349 } {
350 t.Run(tc.name, func(t *testing.T) {
351 b, in, shape := plan14UnitBindingFixture(t, "console.service", "", "")
352 if tc.binding {
353 if _, err := b.establish(t.Context(), in.Host); err != nil {
354 t.Fatal(err)
355 }
356 }
357 if !tc.shape {
358 b.canonical = func(context.Context, string) (claimUnitShape, error) { return claimUnitShape{}, os.ErrNotExist }
359 } else {
360 b.canonical = func(context.Context, string) (claimUnitShape, error) { return shape, nil }
361 }
362 h := &ClaimLaunchRoot{unitBindings: b, unitProgram: func(claimUnitShape) (claimUnitProgram, error) { return claimUnitProgram{}, nil }}
363 inputs, err := h.boundInputs(t.Context(), registry.Route{Host: in.Host})
364 if tc.want == nil && (err != nil || inputs != nil) {
365 t.Fatal(inputs, err)
366 }
367 if tc.want != nil && !errors.Is(err, tc.want) {
368 t.Fatalf("got %v, want %v", err, tc.want)
369 }
370 })
371 }
372}
373