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 linux
2
3package hostbox
4
5import (
6 "bytes"
7 "context"
8 "database/sql"
9 "encoding/json"
10 "errors"
11 "os"
12 "path/filepath"
13 "strings"
14 "testing"
15)
16
17// legacyBoundJournals writes one valid legacy launch and its inventory, then
18// lets the case rewrite the raw rows the way an agent could.
19func legacyBoundJournals(t *testing.T, in LaunchInventory, mutate func(launches, inventory *sql.DB)) string {
20 t.Helper()
21 legacy := t.TempDir()
22 old, err := OpenLaunchJournal(filepath.Join(legacy, "launches.db"))
23 if err != nil {
24 t.Fatal(err)
25 }
26 if err = old.Create(t.Context(), in); err != nil {
27 t.Fatal(err)
28 }
29 inventory, err := OpenStopInventoryDB(filepath.Join(legacy, "inventory.db"))
30 if err != nil {
31 t.Fatal(err)
32 }
33 if err = inventory.WriteStopInventory(t.Context(), StopInventory{Complete: true, Host: in.Target.Host, Incarnation: "inc", InstallationStamp: 1, Box: "box", Enrollment: "enrollment", DirectoryRevision: "1"}); err != nil {
34 t.Fatal(err)
35 }
36 if mutate != nil {
37 mutate(old.db, inventory.db)
38 }
39 if err = old.Close(); err != nil {
40 t.Fatal(err)
41 }
42 inventory.Close()
43 return legacy
44}
45
46func padJSON(t *testing.T, v any, size int) []byte {
47 t.Helper()
48 raw, err := json.Marshal(v)
49 if err != nil {
50 t.Fatal(err)
51 }
52 return append(raw, bytes.Repeat([]byte(" "), size-len(raw))...)
53}
54
55func boundExec(t *testing.T, db *sql.DB, q string, args ...any) {
56 t.Helper()
57 if _, err := db.Exec(q, args...); err != nil {
58 t.Fatal(err)
59 }
60}
61
62// R2: a single legacy cell is bounded by SQL before it is scanned into
63// memory, the legacy files are capped near the import budget, and a schema
64// that computes columns on read refuses. Every refusal imports nothing,
65// publishes no marker and leaves no snapshot behind.
66func TestPlan9Fix1MigrationRefusesUnboundedCells(t *testing.T) {
67 _, _, base := journalFixture(t)
68 inventory := StopInventory{Complete: true, Host: base.Target.Host, Incarnation: "inc", InstallationStamp: 1, Box: "box", Enrollment: "enrollment", DirectoryRevision: "1"}
69 longHost := base
70 longHost.Target.Host = strings.Repeat("a", 5000)
71 cases := map[string]func(*testing.T) string{
72 "launch body over the row cap": func(t *testing.T) string {
73 return legacyBoundJournals(t, base, func(launches, _ *sql.DB) {
74 boundExec(t, launches, "UPDATE launches SET body=?", padJSON(t, base, 4<<20))
75 })
76 },
77 "launch identity over the key cap": func(t *testing.T) string {
78 return legacyBoundJournals(t, base, func(launches, inv *sql.DB) {
79 body, _ := json.Marshal(longHost)
80 boundExec(t, launches, "UPDATE launches SET identity=?, body=?", launchKey(longHost.Target), body)
81 long := inventory
82 long.Host = longHost.Target.Host
83 invBody, _ := json.Marshal(long)
84 boundExec(t, inv, "UPDATE stop_inventory SET host=?, body=?", long.Host, invBody)
85 })
86 },
87 "inventory body over the row cap": func(t *testing.T) string {
88 return legacyBoundJournals(t, base, func(_, inv *sql.DB) {
89 boundExec(t, inv, "UPDATE stop_inventory SET body=?", padJSON(t, inventory, 4<<20))
90 })
91 },
92 "inventory host over the key cap": func(t *testing.T) string {
93 return legacyBoundJournals(t, base, func(_, inv *sql.DB) {
94 long := inventory
95 long.Host = longHost.Target.Host
96 body, _ := json.Marshal(long)
97 boundExec(t, inv, "INSERT INTO stop_inventory(host,incarnation,stamp,body) VALUES(?,?,?,?)", long.Host, long.Incarnation, long.InstallationStamp, body)
98 })
99 },
100 "generated body column": func(t *testing.T) string {
101 return legacyBoundJournals(t, base, func(launches, _ *sql.DB) {
102 boundExec(t, launches, "ALTER TABLE launches RENAME TO l0")
103 boundExec(t, launches, "CREATE TABLE launches(identity TEXT PRIMARY KEY, raw BLOB NOT NULL, body BLOB GENERATED ALWAYS AS (raw) VIRTUAL)")
104 boundExec(t, launches, "INSERT INTO launches(identity,raw) SELECT identity,body FROM l0")
105 boundExec(t, launches, "DROP TABLE l0")
106 })
107 },
108 "legacy file over the size cap": func(t *testing.T) string {
109 legacy := legacyBoundJournals(t, base, nil)
110 if err := os.Truncate(filepath.Join(legacy, "launches.db"), 64<<20+1); err != nil {
111 t.Fatal(err)
112 }
113 return legacy
114 },
115 }
116 for name, build := range cases {
117 t.Run(name, func(t *testing.T) {
118 s, _, _ := journalFixture(t)
119 legacy := build(t)
120 freeze := func(context.Context) (func(), error) { return func() {}, nil }
121 err := s.MigrateLegacy(t.Context(), legacy, "box", "enrollment", freeze, func(context.Context, LaunchInventory, StopInventory, StopKernelJournal) error { return nil })
122 if !errors.Is(err, ErrTypedJournal) {
123 t.Fatalf("unbounded legacy journal: %v", err)
124 }
125 var count int
126 if err = s.Launches.db.QueryRow("SELECT (SELECT count(*) FROM launches)+(SELECT count(*) FROM stop_inventory)+(SELECT count(*) FROM journal_migration)").Scan(&count); err != nil || count != 0 {
127 t.Fatalf("refused migration imported or published: %d %v", count, err)
128 }
129 entries, err := os.ReadDir(filepath.Dir(s.Launches.path))
130 if err != nil {
131 t.Fatal(err)
132 }
133 for _, entry := range entries {
134 if strings.HasPrefix(entry.Name(), "migration-") {
135 t.Fatalf("snapshot left behind: %s", entry.Name())
136 }
137 }
138 })
139 }
140}
141
142// The bounds leave room for every row root's own journal methods write.
143func TestPlan9Fix1MigrationBoundsAdmitOrdinaryRows(t *testing.T) {
144 s, _, in := journalFixture(t)
145 legacy := legacyBoundJournals(t, in, nil)
146 freeze := func(context.Context) (func(), error) { return func() {}, nil }
147 if err := s.MigrateLegacy(t.Context(), legacy, "box", "enrollment", freeze, func(context.Context, LaunchInventory, StopInventory, StopKernelJournal) error { return nil }); err != nil {
148 t.Fatal(err)
149 }
150 if _, err := s.Launches.Read(t.Context(), in.Target); err != nil {
151 t.Fatal(err)
152 }
153}
154