carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1// fleets.go: customer fleets (2026-08-05-customer-fleets-design.md) — an
2// account-owned group of remote boxes that dial this host over a channel
3// identified by "<fleet>/<label>". Two tables: fleets (the account-owned
4// record: which bucket it belongs to) and fleet_boxes (the boxes registered
5// to it, each carrying a hashed bearer token). This package never sees a
6// plaintext token — minting happens console-side, in a later task; here it
7// is always TokenHash, the sha256 hex.
8package registry
9
10import (
11 "database/sql"
12 "errors"
13 "fmt"
14 "regexp"
15 "strings"
16 "time"
17)
18
19// ErrFleetBoxActive is AddFleetBox's signal that the RowsAffected guard
20// fired — the (fleet, label) pair already names an ACTIVE box — as opposed
21// to any other failure (a bad shape, a missing fleet, a tx/busy/disk error)
22// that reaches AddFleetBox's caller. errors.Is against this is how a caller
23// tells "this is the ordinary duplicate-box conflict" apart from
24// "something is actually broken", which the plain fmt.Errorf this used to
25// return could not do without brittle string matching.
26var ErrFleetBoxActive = errors.New("registry: fleet already has an active box under that label")
27
28// Fleet is one account-owned collection of remote boxes.
29type Fleet struct {
30 Name string // ValidTunnelName
31 Account string // owning account identity, same domain as Route.Account
32 Bucket string // customer data bucket name
33 CreatedAt int64
34}
35
36// FleetBox is one box registered to a Fleet. (Fleet, Label) is the channel
37// identity a remote-host-agent dials in as ("<fleet>/<label>" — see
38// FleetChannelAuth); TokenHash is the sha256 hex of its bearer token, never
39// the plaintext.
40type FleetBox struct {
41 Fleet string
42 Label string // ValidTunnelName; (Fleet, Label) is the channel identity
43 TokenHash string // sha256 hex of the bearer token; never plaintext
44 Status string // "active" | "detached"
45 AddedAt int64
46 Tags string // comma-separated, ValidBoxTags grammar; "" = untagged
47}
48
49// tokenHashRe: exactly 64 lowercase hex characters — a sha256 digest,
50// structurally enforcing the "token hashes only, never plaintext" rule at
51// every writer that accepts one (AddFleetBox, SetFleetBoxToken). A bearer
52// token or an empty string both fail this shape.
53var tokenHashRe = regexp.MustCompile(`^[0-9a-f]{64}$`)
54
55func validTokenHash(hash string) bool {
56 return tokenHashRe.MatchString(hash)
57}
58
59// boxTagRe: a single tag segment — lowercase alphanumerics and interior
60// hyphens, 1-24 characters, never leading or trailing with a hyphen (a
61// leading '-' is how a value gets mistaken for a flag by anything that ever
62// builds an argv from a tag, the same reasoning unitNameRe's first-character
63// carve-out documents). Same shape family as ValidTunnelName, but its own
64// regexp since tags are a comma-separated LIST rather than a single name.
65var boxTagRe = regexp.MustCompile(`^[a-z0-9]([a-z0-9-]{0,22}[a-z0-9])?$`)
66
67// ValidBoxTags reports whether tags is a well-formed comma-separated tag
68// list: each tag matches boxTagRe, at most 8 tags, no empty segments. ""
69// is valid and means untagged.
70func ValidBoxTags(tags string) bool {
71 if tags == "" {
72 return true
73 }
74 parts := strings.Split(tags, ",")
75 if len(parts) > 8 {
76 return false
77 }
78 for _, p := range parts {
79 if !boxTagRe.MatchString(p) {
80 return false
81 }
82 }
83 return true
84}
85
86// validFleetBoxStatus reports whether status is one of the two values the
87// FleetBox.Status doc comment promises. Rejecting anything else at the
88// writer matters because AddFleetBox's uniqueness guard keys off status =
89// 'active': a typo'd status would make a live box invisible to it.
90func validFleetBoxStatus(status string) bool {
91 return status == "active" || status == "detached"
92}
93
94// migrateFleets creates the fleets and fleet_boxes tables — additive
95// (CREATE TABLE IF NOT EXISTS), called from migrate() beside the routes DDL,
96// so deploying this code over an existing registry is always safe.
97//
98// Both tables carry `rev`, bumped by every writer — see Fingerprint's doc
99// comment (registry.go) for why a plain length/count/rowid aggregate is
100// blind to a fixed-length rewrite (a sha256 token_hash never changes
101// length) and rev is what fixes that.
102func (r *Registry) migrateFleets() error {
103 if _, err := r.sql.Exec(`
104CREATE TABLE IF NOT EXISTS fleets (
105 name TEXT PRIMARY KEY,
106 account TEXT NOT NULL,
107 bucket TEXT NOT NULL,
108 created_at INTEGER NOT NULL,
109 rev INTEGER NOT NULL DEFAULT 0
110);`); err != nil {
111 return err
112 }
113 if _, err := r.sql.Exec(`
114CREATE TABLE IF NOT EXISTS fleet_boxes (
115 fleet TEXT NOT NULL,
116 label TEXT NOT NULL,
117 token_hash TEXT NOT NULL,
118 status TEXT NOT NULL,
119 added_at INTEGER NOT NULL,
120 rev INTEGER NOT NULL DEFAULT 0,
121 PRIMARY KEY (fleet, label)
122);`); err != nil {
123 return err
124 }
125 // tags: additive (2026-08-10-fleets-console-design.md D3). Pre-change rows
126 // read as '' — untagged.
127 if _, err := r.sql.Exec(`ALTER TABLE fleet_boxes ADD COLUMN tags TEXT NOT NULL DEFAULT ''`); err != nil && !strings.Contains(err.Error(), "duplicate column") {
128 return err
129 }
130 return nil
131}
132
133// freshFleetsConn opens a NEW connection over the same file — the List /
134// Fingerprint pattern (registry.go:455): modernc/sqlite's long-lived pool can
135// serve a stale WAL snapshot for another process's commit. Every read below
136// that a later task polls cross-process (FleetChannelAuth, read by the edge
137// after Fingerprint moves) or that a short-lived CLI process may run moments
138// after a different process wrote (ListFleets, ListFleetBoxes) goes through
139// this, not r.sql.
140func (r *Registry) freshFleetsConn() (*sql.DB, error) {
141 db, err := sql.Open("sqlite", r.path)
142 if err != nil {
143 return nil, err
144 }
145 if _, err := db.Exec("PRAGMA busy_timeout=5000"); err != nil {
146 db.Close()
147 return nil, err
148 }
149 return db, nil
150}
151
152// UpsertFleet inserts or updates a fleet's account/bucket linkage by name.
153func (r *Registry) UpsertFleet(f Fleet) error {
154 if r.remote != nil {
155 _, err := r.remote.call(nil, remoteRequest{Method: "UpsertFleet", F: f}, nil)
156 return err
157 }
158
159 if !ValidTunnelName(f.Name) {
160 return fmt.Errorf("invalid fleet name %q", f.Name)
161 }
162 if f.CreatedAt == 0 {
163 f.CreatedAt = time.Now().Unix()
164 }
165 _, err := r.sql.Exec(`
166INSERT INTO fleets (name, account, bucket, created_at, rev)
167VALUES (?, ?, ?, ?, 1)
168ON CONFLICT(name) DO UPDATE SET
169 account = excluded.account, bucket = excluded.bucket, rev = fleets.rev + 1`,
170 f.Name, f.Account, f.Bucket, f.CreatedAt)
171 return err
172}
173
174// CreateFleet inserts a brand-new fleet, INSERT-owning the "does this name
175// already exist" decision the same way AddFleetBox's uniqueness guard owns
176// box uniqueness (below): the INSERT's own `ON CONFLICT(name) DO NOTHING`
177// and RowsAffected are the atomic check, not a caller's read-then-write —
178// so two callers racing the SAME never-before-seen name can never both
179// believe they created it, and the loser can never silently steal
180// ownership of the winner's row the way a call through UpsertFleet's
181// ON CONFLICT DO UPDATE would. created reports which caller won; a losing
182// call makes no change to the existing row at all — not even a rev bump.
183func (r *Registry) CreateFleet(f Fleet) (created bool, err error) {
184 if r.remote != nil {
185 out, err := r.remote.call(nil, remoteRequest{Method: "CreateFleet", F: f}, nil)
186 return out.Changed, err
187 }
188
189 if !ValidTunnelName(f.Name) {
190 return false, fmt.Errorf("invalid fleet name %q", f.Name)
191 }
192 if f.CreatedAt == 0 {
193 f.CreatedAt = time.Now().Unix()
194 }
195 res, err := r.sql.Exec(`
196INSERT INTO fleets (name, account, bucket, created_at, rev)
197VALUES (?, ?, ?, ?, 1)
198ON CONFLICT(name) DO NOTHING`,
199 f.Name, f.Account, f.Bucket, f.CreatedAt)
200 if err != nil {
201 return false, err
202 }
203 n, err := res.RowsAffected()
204 if err != nil {
205 return false, err
206 }
207 return n == 1, nil
208}
209
210// GetFleet returns the fleet named name, or (nil, nil) if there is none.
211func (r *Registry) GetFleet(name string) (*Fleet, error) {
212 if r.remote != nil {
213 out, err := r.remote.call(nil, remoteRequest{Method: "GetFleet", Name: name}, nil)
214 return out.Fleet, err
215 }
216
217 row := r.sql.QueryRow(`SELECT name, account, bucket, created_at FROM fleets WHERE name = ?`, name)
218 var f Fleet
219 err := row.Scan(&f.Name, &f.Account, &f.Bucket, &f.CreatedAt)
220 if err == sql.ErrNoRows {
221 return nil, nil
222 }
223 if err != nil {
224 return nil, err
225 }
226 return &f, nil
227}
228
229// ListFleets returns every fleet, ordered by name, over a fresh connection.
230func (r *Registry) ListFleets() ([]Fleet, error) {
231 if r.remote != nil {
232 out, err := r.remote.call(nil, remoteRequest{Method: "ListFleets"}, nil)
233 return out.Fleets, err
234 }
235
236 db, err := r.freshFleetsConn()
237 if err != nil {
238 return nil, err
239 }
240 defer db.Close()
241 rows, err := db.Query(`SELECT name, account, bucket, created_at FROM fleets ORDER BY name`)
242 if err != nil {
243 return nil, err
244 }
245 defer rows.Close()
246 var out []Fleet
247 for rows.Next() {
248 var f Fleet
249 if err := rows.Scan(&f.Name, &f.Account, &f.Bucket, &f.CreatedAt); err != nil {
250 return nil, err
251 }
252 out = append(out, f)
253 }
254 return out, rows.Err()
255}
256
257// AddFleetBox registers a box to a fleet. It errors if the fleet does not
258// exist, if TokenHash or Status fail their shape checks, and if
259// (fleet, label) already names an ACTIVE box.
260//
261// The fleet-existence check is a plain SELECT rather than a SQLite FOREIGN
262// KEY, because FK enforcement is a per-connection PRAGMA
263// (`foreign_keys=ON`) that Open() never sets and that a fresh short-lived
264// connection (freshFleetsConn, another process entirely) could disagree
265// with — an FK that only some connections enforce is worse than none, since
266// it would look enforced in tests that happen to run through the same
267// handle. Both statements run inside one transaction so the check and the
268// insert see the same snapshot.
269//
270// The active-box check is NOT a read-then-write in Go: it is the INSERT's
271// own ON CONFLICT ... WHERE clause, so a second process racing this call on
272// the same (fleet, label) can never observe a stale "not active yet" read
273// and steal a live channel's token out from under it — SQLite evaluates the
274// WHERE clause and applies (or skips) the DO UPDATE atomically. A label that
275// exists but is DETACHED clears the WHERE guard and is reactivated in place
276// with the incoming token/status/added_at (rev bumped) — the path a
277// re-provisioned box takes back onto its old name. RowsAffected()==0 means
278// the guard fired (an active row was left untouched), which is the signal
279// this returns wrapping ErrFleetBoxActive — errors.Is against it, not
280// string matching, is how a caller tells this ordinary conflict apart from
281// an actual failure below.
282func (r *Registry) AddFleetBox(b FleetBox) error {
283 if r.remote != nil {
284 _, err := r.remote.call(nil, remoteRequest{Method: "AddFleetBox", B: b}, nil)
285 return err
286 }
287
288 if !ValidTunnelName(b.Fleet) {
289 return fmt.Errorf("invalid fleet name %q", b.Fleet)
290 }
291 if !ValidTunnelName(b.Label) {
292 return fmt.Errorf("invalid box label %q", b.Label)
293 }
294 if b.Status == "" {
295 b.Status = "active"
296 }
297 if !validFleetBoxStatus(b.Status) {
298 return fmt.Errorf("invalid box status %q", b.Status)
299 }
300 if !validTokenHash(b.TokenHash) {
301 return fmt.Errorf("invalid token hash %q (want 64 lowercase hex characters)", b.TokenHash)
302 }
303 if b.AddedAt == 0 {
304 b.AddedAt = time.Now().Unix()
305 }
306 tx, err := r.sql.Begin()
307 if err != nil {
308 return err
309 }
310 var exists int
311 err = tx.QueryRow(`SELECT 1 FROM fleets WHERE name = ?`, b.Fleet).Scan(&exists)
312 if err == sql.ErrNoRows {
313 tx.Rollback()
314 return fmt.Errorf("no fleet %q", b.Fleet)
315 }
316 if err != nil {
317 tx.Rollback()
318 return err
319 }
320 res, err := tx.Exec(`
321INSERT INTO fleet_boxes (fleet, label, token_hash, status, added_at, rev)
322VALUES (?, ?, ?, ?, ?, 1)
323ON CONFLICT(fleet, label) DO UPDATE SET
324 token_hash = excluded.token_hash, status = excluded.status, added_at = excluded.added_at,
325 rev = fleet_boxes.rev + 1
326WHERE fleet_boxes.status <> 'active'`,
327 b.Fleet, b.Label, b.TokenHash, b.Status, b.AddedAt)
328 if err != nil {
329 tx.Rollback()
330 return err
331 }
332 n, err := res.RowsAffected()
333 if err != nil {
334 tx.Rollback()
335 return err
336 }
337 if n == 0 {
338 tx.Rollback()
339 return fmt.Errorf("fleet %q already has an active box %q: %w", b.Fleet, b.Label, ErrFleetBoxActive)
340 }
341 return tx.Commit()
342}
343
344// SetFleetBoxToken rotates a box's stored token hash. Errors when no
345// (fleet, label) row exists — a caller minting a fresh token against a box
346// that no longer exists is a race worth reporting, not swallowing. Bumps
347// rev: a sha256 token_hash is always 64 characters, so the rotation alone
348// changes no length Fingerprint could otherwise key off.
349func (r *Registry) SetFleetBoxToken(fleet, label, tokenHash string) error {
350 if r.remote != nil {
351 _, err := r.remote.call(nil, remoteRequest{Method: "SetFleetBoxToken", Fleet: fleet, Label: label, TokenHash: tokenHash}, nil)
352 return err
353 }
354
355 if !ValidTunnelName(fleet) {
356 return fmt.Errorf("invalid fleet name %q", fleet)
357 }
358 if !ValidTunnelName(label) {
359 return fmt.Errorf("invalid box label %q", label)
360 }
361 if !validTokenHash(tokenHash) {
362 return fmt.Errorf("invalid token hash %q (want 64 lowercase hex characters)", tokenHash)
363 }
364 res, err := r.sql.Exec(`UPDATE fleet_boxes SET token_hash = ?, rev = rev + 1 WHERE fleet = ? AND label = ?`, tokenHash, fleet, label)
365 if err != nil {
366 return err
367 }
368 n, err := res.RowsAffected()
369 if err != nil {
370 return err
371 }
372 if n == 0 {
373 return fmt.Errorf("no box %q/%q", fleet, label)
374 }
375 return nil
376}
377
378// SetFleetBoxTags mirrors SetFleetBoxToken: same rev bump, same
379// RowsAffected guard for an unknown (fleet, label).
380func (r *Registry) SetFleetBoxTags(fleet, label, tags string) error {
381 if r.remote != nil {
382 _, err := r.remote.call(nil, remoteRequest{Method: "SetFleetBoxTags", Fleet: fleet, Label: label, Tags: tags}, nil)
383 return err
384 }
385
386 if !ValidTunnelName(fleet) {
387 return fmt.Errorf("invalid fleet name %q", fleet)
388 }
389 if !ValidTunnelName(label) {
390 return fmt.Errorf("invalid box label %q", label)
391 }
392 if !ValidBoxTags(tags) {
393 return fmt.Errorf("invalid box tags %q", tags)
394 }
395 res, err := r.sql.Exec(`UPDATE fleet_boxes SET tags = ?, rev = rev + 1 WHERE fleet = ? AND label = ?`, tags, fleet, label)
396 if err != nil {
397 return err
398 }
399 n, err := res.RowsAffected()
400 if err != nil {
401 return err
402 }
403 if n == 0 {
404 return fmt.Errorf("no box %q/%q", fleet, label)
405 }
406 return nil
407}
408
409// SetFleetBoxStatus flips a box between "active" and "detached". Errors when
410// no (fleet, label) row exists, same contract as SetFleetBoxToken. Rejects
411// any status outside those two values — AddFleetBox's uniqueness guard keys
412// off status = 'active', so a typo'd status here would make a live box
413// invisible to it.
414func (r *Registry) SetFleetBoxStatus(fleet, label, status string) error {
415 if r.remote != nil {
416 _, err := r.remote.call(nil, remoteRequest{Method: "SetFleetBoxStatus", Fleet: fleet, Label: label, Status: status}, nil)
417 return err
418 }
419
420 if !ValidTunnelName(fleet) {
421 return fmt.Errorf("invalid fleet name %q", fleet)
422 }
423 if !ValidTunnelName(label) {
424 return fmt.Errorf("invalid box label %q", label)
425 }
426 if !validFleetBoxStatus(status) {
427 return fmt.Errorf("invalid box status %q", status)
428 }
429 res, err := r.sql.Exec(`UPDATE fleet_boxes SET status = ?, rev = rev + 1 WHERE fleet = ? AND label = ?`, status, fleet, label)
430 if err != nil {
431 return err
432 }
433 n, err := res.RowsAffected()
434 if err != nil {
435 return err
436 }
437 if n == 0 {
438 return fmt.Errorf("no box %q/%q", fleet, label)
439 }
440 return nil
441}
442
443// ListFleetBoxes returns every box registered to fleet, ordered by label,
444// over a fresh connection (same rationale as ListFleets).
445func (r *Registry) ListFleetBoxes(fleet string) ([]FleetBox, error) {
446 if r.remote != nil {
447 out, err := r.remote.call(nil, remoteRequest{Method: "ListFleetBoxes", Fleet: fleet}, nil)
448 return out.FleetBoxes, err
449 }
450
451 db, err := r.freshFleetsConn()
452 if err != nil {
453 return nil, err
454 }
455 defer db.Close()
456 rows, err := db.Query(`SELECT fleet, label, token_hash, status, added_at, tags FROM fleet_boxes WHERE fleet = ? ORDER BY label`, fleet)
457 if err != nil {
458 return nil, err
459 }
460 defer rows.Close()
461 var out []FleetBox
462 for rows.Next() {
463 var b FleetBox
464 if err := rows.Scan(&b.Fleet, &b.Label, &b.TokenHash, &b.Status, &b.AddedAt, &b.Tags); err != nil {
465 return nil, err
466 }
467 out = append(out, b)
468 }
469 return out, rows.Err()
470}
471
472// fleetPlacementPrefix is the Region-column prefix that marks a route as
473// bound to a customer fleet rather than a residency code — see
474// FleetPlacement/PlacementTarget below.
475const fleetPlacementPrefix = "fleet/"
476
477// FleetPlacement parses a Route.Region value as a fleet placement target:
478// "fleet/pi" -> ("pi", true). Anything else answers ("", false) — "" (no
479// placement), a bare region code like "eu" (a FUTURE placement form this v1
480// does not parse — SetPlacement refuses it too), and "fleet/<name>" where
481// name fails ValidTunnelName (a syntactically-right prefix is not enough;
482// the name half must be one Registry would itself accept as a fleet name).
483func FleetPlacement(region string) (fleet string, ok bool) {
484 name, hasPrefix := strings.CutPrefix(region, fleetPlacementPrefix)
485 if !hasPrefix || !ValidTunnelName(name) {
486 return "", false
487 }
488 return name, true
489}
490
491// PlacementTarget renders a fleet name as the Region-column value that binds
492// a route to it — the inverse of FleetPlacement. It does not validate name;
493// callers that need "is this fleet name legal" should ask ValidTunnelName
494// (or round-trip through FleetPlacement) themselves.
495func PlacementTarget(fleet string) string {
496 return fleetPlacementPrefix + fleet
497}
498
499// FleetChannelAuth returns every ACTIVE box's token hash, keyed by its
500// channel name "<fleet>/<label>" — the edge's auth table for an incoming
501// remote-host-agent dial-in. Fresh connection: this is exactly the
502// cross-process poll List/Fingerprint exist for (the edge holds a long-lived
503// Registry handle and reloads this whenever Fingerprint moves).
504func (r *Registry) FleetChannelAuth() (map[string]string, error) {
505 if r.remote != nil {
506 out, err := r.remote.call(nil, remoteRequest{Method: "FleetChannelAuth"}, nil)
507 return out.StringMap, err
508 }
509
510 db, err := r.freshFleetsConn()
511 if err != nil {
512 return nil, err
513 }
514 defer db.Close()
515 rows, err := db.Query(`SELECT fleet, label, token_hash FROM fleet_boxes WHERE status = 'active'`)
516 if err != nil {
517 return nil, err
518 }
519 defer rows.Close()
520 out := make(map[string]string)
521 for rows.Next() {
522 var fleet, label, hash string
523 if err := rows.Scan(&fleet, &label, &hash); err != nil {
524 return nil, err
525 }
526 out[fleet+"/"+label] = hash
527 }
528 return out, rows.Err()
529}
530