carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1//go:build unix
2
3package hostbox
4
5import (
6 "context"
7 "database/sql"
8 "encoding/json"
9 "errors"
10 "os"
11 "path/filepath"
12 "testing"
13 "time"
14
15 "github.com/carlosframework/platform/internal/domains"
16 "github.com/carlosframework/platform/internal/hostclaim"
17 "github.com/carlosframework/platform/internal/hostguard"
18 "github.com/carlosframework/platform/internal/provision"
19 "github.com/carlosframework/platform/internal/registry"
20 "github.com/carlosframework/platform/internal/release"
21)
22
23// admissionFixture is a bucket holding one record (instance or sidecar) and
24// the adopted hostclaim for its host, plus the identity a claim hook would
25// stamp on the route.
26type admissionFixture struct {
27 store release.Store
28 data string
29 account, host, key string
30 family, name string
31 holder hostclaim.Holder
32 owner registry.ClaimOwnership
33 pass func(context.Context, release.Store, *registry.Registry, provision.ReconcileConfig) error
34}
35
36func newAdmissionFixture(t *testing.T, sidecar bool) admissionFixture {
37 t.Helper()
38 ctx := t.Context()
39 store := release.NewDirStore(t.TempDir())
40 account := domains.Sqid(2)
41 raw, err := json.Marshal(map[string]any{"id": 2, "sqid": account, "name": "fixture", "created_at": "2026-08-02T09:00:00Z"})
42 if err != nil {
43 t.Fatal(err)
44 }
45 if _, err := store.Put(ctx, "accounts/"+account+"/account.json", raw, ""); err != nil {
46 t.Fatal(err)
47 }
48 data := t.TempDir()
49 if err := os.Mkdir(filepath.Join(data, "hostguard"), 0770); err != nil {
50 t.Fatal(err)
51 }
52 created := time.Date(2026, 10, 7, 0, 0, 0, 0, time.UTC)
53 f := admissionFixture{store: store, data: data, account: account, host: "shop.example", family: "instance"}
54 f.key = provision.RecordKey(account, "app", f.host)
55 f.pass = provision.Reconcile
56 if sidecar {
57 f.host = provision.SidecarHost("worker", "app", account)
58 f.family, f.name = "sidecar", "worker"
59 f.key = provision.SidecarRecordKey(account, "app", "worker")
60 f.pass = provision.ReconcileSidecars
61 if err := provision.PutSidecarManifest(ctx, store, account, "app", provision.SidecarManifest{Schema: 1, App: "app", Args: []string{"run"}, Channel: "stable"}); err != nil {
62 t.Fatal(err)
63 }
64 if err := provision.CreateSidecarRecord(ctx, store, provision.SidecarRecord{Schema: 1, Name: "worker", App: "app", Account: account, Host: f.host, Region: "ie", Channel: "stable", CreatedAt: created}); err != nil {
65 t.Fatal(err)
66 }
67 } else {
68 if err := provision.PutManifest(ctx, store, account, "app", provision.Manifest{Schema: 1, App: "app", Domain: "example"}); err != nil {
69 t.Fatal(err)
70 }
71 if err := provision.CreateRecord(ctx, store, provision.Record{Schema: 1, Host: f.host, Slug: "shop", App: "app", Account: account, Region: "ie", Channel: "stable", CreatedAt: created}); err != nil {
72 t.Fatal(err)
73 }
74 }
75 o, err := hostclaim.Read(ctx, store, f.host)
76 if err != nil {
77 t.Fatal(err)
78 }
79 o, err = hostclaim.Update(ctx, store, o, hostclaim.Adopt(hostclaim.NewHolder{Family: f.family, Name: f.name, Key: f.key, Account: account, App: "app", Host: f.host, Region: "ie", CreatedAt: created}, "box", nil, nil, "adopt", func() string { return "id" }))
80 if err != nil {
81 t.Fatal(err)
82 }
83 f.holder = *o.Claim.Holder
84 f.owner = registry.ClaimOwnership{ClaimIncarnation: f.holder.Incarnation, ClaimStamp: f.holder.Stamp, ClaimOwner: "owner", ClaimRowID: "row"}
85 return f
86}
87
88func TestClaimRefusalPreservesDurableGuardAndInventory(t *testing.T) {
89 type env struct {
90 cfg *provision.ReconcileConfig
91 reg *registry.Registry
92 f admissionFixture
93 }
94 allow := func(gates *int) func(context.Context, registry.Route) error {
95 return func(context.Context, registry.Route) error { *gates++; return nil }
96 }
97 refuse := func(gates *int) func(context.Context, registry.Route) error {
98 return func(context.Context, registry.Route) error { *gates++; return errors.New("config refused") }
99 }
100 upsert := func(t *testing.T, e env, rt registry.Route) {
101 t.Helper()
102 if err := e.reg.Upsert(rt); err != nil {
103 t.Fatal(err)
104 }
105 }
106 cases := []struct {
107 name string
108 sidecar bool
109 reached bool // the claim hook ran: guard and inventory written
110 gates int // ConfigGate calls this pass makes
111 apply func(*testing.T, env, *int)
112 }{
113 {name: "inbound", apply: func(_ *testing.T, e env, _ *int) { e.cfg.Inbound = provision.InboundNone }},
114 {name: "instance config", gates: 1, apply: func(_ *testing.T, e env, gates *int) { e.cfg.ConfigGate = refuse(gates) }},
115 {name: "sidecar config", sidecar: true, gates: 1, apply: func(_ *testing.T, e env, gates *int) { e.cfg.ConfigGate = refuse(gates) }},
116 {name: "co-spawn", sidecar: true, apply: func(t *testing.T, e env, _ *int) {
117 if err := os.WriteFile(filepath.Join(e.f.data, e.f.host+"-sidecar.env"), []byte("A=1\n"), 0600); err != nil {
118 t.Fatal(err)
119 }
120 }},
121 {name: "instance shape mismatch", apply: func(t *testing.T, e env, gates *int) {
122 e.cfg.ConfigGate = allow(gates)
123 upsert(t, e, registry.Route{Host: e.f.host, Kind: "instance", App: "other", ClaimOwnership: e.f.owner})
124 }},
125 {name: "instance row without claim owner", apply: func(t *testing.T, e env, gates *int) {
126 e.cfg.ConfigGate = allow(gates)
127 upsert(t, e, registry.Route{Host: e.f.host, Kind: "instance", App: "app", Account: e.f.account, Provisioned: true})
128 }},
129 {name: "sidecar row not owned by this pass", sidecar: true, apply: func(t *testing.T, e env, gates *int) {
130 e.cfg.ConfigGate = allow(gates)
131 upsert(t, e, registry.Route{Host: e.f.host, Kind: provision.SidecarKind, Slug: "worker", App: "app", Account: e.f.account, ClaimOwnership: e.f.owner})
132 }},
133 {name: "sidecar row without claim owner", sidecar: true, apply: func(t *testing.T, e env, gates *int) {
134 e.cfg.ConfigGate = allow(gates)
135 upsert(t, e, registry.Route{Host: e.f.host, Kind: provision.SidecarKind, Slug: "worker", App: "app", Account: e.f.account, Provisioned: true})
136 }},
137 // Positive controls: the same fixtures reach the hook when nothing refuses.
138 {name: "instance accepted", reached: true, gates: 1, apply: func(_ *testing.T, e env, gates *int) { e.cfg.ConfigGate = allow(gates) }},
139 {name: "sidecar accepted", sidecar: true, reached: true, gates: 1, apply: func(_ *testing.T, e env, gates *int) { e.cfg.ConfigGate = allow(gates) }},
140 {name: "instance owned row kept", reached: true, apply: func(t *testing.T, e env, gates *int) {
141 e.cfg.ConfigGate = allow(gates)
142 upsert(t, e, registry.Route{Host: e.f.host, Kind: "instance", App: "app", Account: e.f.account, Provisioned: true, ClaimOwnership: e.f.owner})
143 }},
144 {name: "sidecar owned row kept", sidecar: true, reached: true, apply: func(t *testing.T, e env, gates *int) {
145 e.cfg.ConfigGate = allow(gates)
146 upsert(t, e, registry.Route{Host: e.f.host, Kind: provision.SidecarKind, Slug: "worker", App: "app", Account: e.f.account, Provisioned: true, ClaimOwnership: e.f.owner})
147 }},
148 }
149 for _, tc := range cases {
150 t.Run(tc.name, func(t *testing.T) {
151 ctx := t.Context()
152 f := newAdmissionFixture(t, tc.sidecar)
153 reg, err := registry.Open(filepath.Join(t.TempDir(), "registry.db"))
154 if err != nil {
155 t.Fatal(err)
156 }
157 defer reg.Close()
158 inventory, err := OpenStopInventoryDB(filepath.Join(t.TempDir(), "inventory.db"))
159 if err != nil {
160 t.Fatal(err)
161 }
162 defer inventory.Close()
163 guard := hostguard.Guard{DataDir: f.data, LockDir: t.TempDir(), Claims: hostguard.StoreReader{Store: f.store}}
164 h := f.holder
165 in := StopInventory{Host: f.host, Incarnation: h.Incarnation, InstallationStamp: h.Stamp, Box: "box", Enrollment: "en", DirectoryRevision: "1", Complete: true}
166 calls, gates := 0, 0
167 cfg := provision.ReconcileConfig{DataDir: f.data, ClaimEnabled: true, Region: "ie"}
168 cfg.ClaimInstall = func(ctx context.Context, selected provision.ClaimRecord, write func(registry.ClaimOwnership) error) error {
169 calls++
170 if err := guard.Activate(ctx, selected.Host, "box", nil); err != nil {
171 return err
172 }
173 if err := inventory.WriteStopInventory(ctx, in); err != nil {
174 return err
175 }
176 return write(f.owner)
177 }
178 tc.apply(t, env{cfg: &cfg, reg: reg, f: f}, &gates)
179 before, err := reg.Get(f.host)
180 if err != nil {
181 t.Fatal(err)
182 }
183 if err := f.pass(ctx, f.store, reg, cfg); err != nil {
184 t.Fatal(err)
185 }
186 if gates != tc.gates {
187 t.Fatalf("config gate asked %d times, want %d", gates, tc.gates)
188 }
189 _, guardErr := os.ReadFile(filepath.Join(f.data, "hostguard", f.host+".json"))
190 _, inventoryErr := inventory.ReadInstallation(ctx, f.host, h.Incarnation, h.Stamp)
191 after, err := reg.Get(f.host)
192 if err != nil {
193 t.Fatal(err)
194 }
195 if tc.reached {
196 if calls != 1 || guardErr != nil || inventoryErr != nil {
197 t.Fatalf("positive control: calls=%d guard=%v inventory=%v", calls, guardErr, inventoryErr)
198 }
199 if after == nil || after.ClaimOwnership != f.owner {
200 t.Fatalf("positive control lost ownership: %+v", after)
201 }
202 return
203 }
204 if calls != 0 || !errors.Is(guardErr, os.ErrNotExist) || !errors.Is(inventoryErr, sql.ErrNoRows) {
205 t.Fatalf("refusal changed durable state: calls=%d guard=%v inventory=%v", calls, guardErr, inventoryErr)
206 }
207 if (before == nil) != (after == nil) || (before != nil && *before != *after) {
208 t.Fatalf("refusal changed the registry: before=%+v after=%+v", before, after)
209 }
210 })
211 }
212}
213