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 activator
4
5import (
6 "context"
7 "crypto/sha256"
8 "encoding/hex"
9 "os"
10 "path/filepath"
11 "strings"
12 "testing"
13
14 "github.com/carlosframework/platform/internal/instancectl"
15 "github.com/carlosframework/platform/internal/instancekey"
16 "github.com/carlosframework/platform/internal/provision"
17 "github.com/carlosframework/platform/internal/registry"
18 "github.com/carlosframework/platform/internal/seeds"
19)
20
21// importedRoute is nestedBackend's row as an import's swap leaves it: a
22// fresh seed id, swapped, its snapshot in the box's seed reader.
23func importedRoute(t *testing.T, b *ExecBackend, rt registry.Route) registry.Route {
24 t.Helper()
25 id, _ := seeds.NewID()
26 snap := filepath.Join(t.TempDir(), "snap.db")
27 if err := writeMinimalSQLite(snap); err != nil {
28 t.Fatal(err)
29 }
30 body, _ := os.ReadFile(snap)
31 sum := sha256.Sum256(body)
32 rt.SeedSource = seeds.RowSeed{ID: id, Kind: seeds.KindImport, SHA256: hex.EncodeToString(sum[:]), Bytes: int64(len(body)), State: seeds.StateSwapped}.Encode()
33 b.Seeds = &fakeSeedObjects{body: map[string][]byte{id: body}}
34 return rt
35}
36
37// launchKey is one launch of rt as doWake makes it (the restore, then the
38// run role's preparation): the key the app is handed, and the key the
39// instance's own env file holds ("" for none).
40func launchKey(t *testing.T, b *ExecBackend, rt registry.Route) (handed, own string) {
41 t.Helper()
42 if fresh, err := b.Restore(context.Background(), rt); err != nil || fresh {
43 t.Fatalf("Restore = fresh %v, %v", fresh, err)
44 }
45 _, ctl, logs, env, err := b.prepare(rt, instancectl.RoleRun)
46 if err != nil {
47 t.Fatalf("prepare: %v", err)
48 }
49 logs.Close()
50 if ctl != nil {
51 ctl.Close()
52 }
53 raw, err := os.ReadFile(filepath.Join(provision.InstanceDir(b.DataDir, rt.Host), rt.Host+".env"))
54 if err != nil && !os.IsNotExist(err) {
55 t.Fatal(err)
56 }
57 return env[InstanceKeyVar], parseEnvBytes(raw)[InstanceKeyVar]
58}
59
60// Merge review S1, the reported sequence: an import is verified (its first
61// launch mints key A), aborted and rolled back; the owner then configures
62// the old process's key B and imports the host again. The second import is
63// handed B. Before the fix it was handed A, which the first import's
64// rollback left in <host>.env, where it outranks configuration.
65func TestAnAbortedImportsKeyDoesNotOutliveItsRollback(t *testing.T) {
66 noOwnReplica(t)
67 b, rt := nestedBackend(t, &fakeControl{inv: "inv1"}, &fakeUnit{})
68 dir := provision.InstanceDir(b.DataDir, rt.Host)
69
70 // The swap, before the import's row exists: this host has no env file.
71 before, err := SeedEnvBefore(rt.DBPath, rt.Host)
72 if err != nil || before != registry.EnvBeforeAbsent {
73 t.Fatalf("SeedEnvBefore = %q, %v; want absent", before, err)
74 }
75 // Verify: the import's first launch mints key A.
76 first := importedRoute(t, b, rt)
77 keyA, ownA := launchKey(t, b, first)
78 if !wellFormedInstanceKey(keyA) || ownA != keyA {
79 t.Fatal("the import's first launch did not mint a key into its own env file")
80 }
81 // The adoption record a helper would have written for this writer.
82 if err := os.WriteFile(filepath.Join(dir, rt.Host+instancekey.AdoptionSuffix), []byte("{}\n"), 0o600); err != nil {
83 t.Fatal(err)
84 }
85
86 // Abort: the fenced rollback's step 3, as seedwiring wires it.
87 if err := RetireSeedKey(rt.DBPath, rt.Host, before); err != nil {
88 t.Fatalf("RetireSeedKey: %v", err)
89 }
90 if err := PurgeSeedLocal(rt.DBPath); err != nil {
91 t.Fatal(err)
92 }
93 for _, n := range []string{rt.Host + ".env", rt.Host + instancekey.AdoptionSuffix} {
94 if _, err := os.Lstat(filepath.Join(dir, n)); !os.IsNotExist(err) {
95 t.Fatalf("%s survived the rollback (%v): it was the import's", n, err)
96 }
97 }
98 if err := RetireSeedKey(rt.DBPath, rt.Host, before); err != nil {
99 t.Fatalf("a second retirement: %v", err)
100 }
101
102 // The owner configures the old process's key B; the config sync
103 // delivers it.
104 keyB := strings.Repeat("B", 43) + "="
105 if err := os.WriteFile(filepath.Join(dir, rt.Host+".config.env"), []byte(InstanceKeyVar+"="+keyB+"\n"), 0o600); err != nil {
106 t.Fatal(err)
107 }
108
109 // Reimport, same host: its swap finds no env file again, and its first
110 // launch is handed B. Nothing is minted while configuration sets it.
111 if before, err := SeedEnvBefore(rt.DBPath, rt.Host); err != nil || before != registry.EnvBeforeAbsent {
112 t.Fatalf("SeedEnvBefore at the reimport = %q, %v; want absent", before, err)
113 }
114 second := importedRoute(t, b, rt)
115 handed, own := launchKey(t, b, second)
116 if handed != keyB {
117 if handed == keyA {
118 t.Fatal("the reimport was handed the aborted import's key, not the configured one")
119 }
120 t.Fatalf("the reimport was handed a key that is not the configured one")
121 }
122 if own != "" {
123 t.Fatal("a key was minted into the own env file while configuration sets one")
124 }
125}
126
127// Merge review S1: a key, and any env content, that predated the import
128// survives its rollback. Only what the import created is retired.
129func TestRetireSeedKeyLeavesWhatPredatesTheImport(t *testing.T) {
130 token := "CARLOS_ADMIN_TOKEN=" + strings.Repeat("e", 64)
131 t.Run("a key that was there before", func(t *testing.T) {
132 noOwnReplica(t)
133 b, rt := nestedBackend(t, &fakeControl{inv: "inv1"}, &fakeUnit{})
134 dir := provision.InstanceDir(b.DataDir, rt.Host)
135 keyP := strings.Repeat("P", 43) + "="
136 predating := []byte(token + "\n" + InstanceKeyVar + "=" + keyP + "\n")
137 if err := os.MkdirAll(dir, 0o700); err != nil {
138 t.Fatal(err)
139 }
140 envPath := filepath.Join(dir, rt.Host+".env")
141 if err := os.WriteFile(envPath, predating, 0o600); err != nil {
142 t.Fatal(err)
143 }
144 adoption := filepath.Join(dir, rt.Host+instancekey.AdoptionSuffix)
145 if err := os.WriteFile(adoption, []byte("{}\n"), 0o600); err != nil {
146 t.Fatal(err)
147 }
148 before, err := SeedEnvBefore(rt.DBPath, rt.Host)
149 if err != nil || before != registry.EnvBeforeKey {
150 t.Fatalf("SeedEnvBefore = %q, %v; want key", before, err)
151 }
152 // The import launches with the key that was there: nothing minted.
153 if handed, _ := launchKey(t, b, importedRoute(t, b, rt)); handed != keyP {
154 t.Fatal("the import was not handed the key that predated it")
155 }
156 if err := RetireSeedKey(rt.DBPath, rt.Host, before); err != nil {
157 t.Fatal(err)
158 }
159 if err := PurgeSeedLocal(rt.DBPath); err != nil {
160 t.Fatal(err)
161 }
162 if got, err := os.ReadFile(envPath); err != nil || string(got) != string(predating) {
163 t.Fatalf("the env file that predated the import changed: %v", err)
164 }
165 if _, err := os.Stat(adoption); err != nil {
166 t.Fatalf("the adoption record that predated the import went: %v", err)
167 }
168 })
169 t.Run("an env file with no key", func(t *testing.T) {
170 noOwnReplica(t)
171 b, rt := nestedBackend(t, &fakeControl{inv: "inv1"}, &fakeUnit{})
172 dir := provision.InstanceDir(b.DataDir, rt.Host)
173 if err := os.MkdirAll(dir, 0o700); err != nil {
174 t.Fatal(err)
175 }
176 envPath := filepath.Join(dir, rt.Host+".env")
177 // No trailing newline, a comment, and a line the platform does not own.
178 predating := "# set by hand\n" + token + "\nOTHER=1"
179 if err := os.WriteFile(envPath, []byte(predating), 0o600); err != nil {
180 t.Fatal(err)
181 }
182 adoption := filepath.Join(dir, rt.Host+instancekey.AdoptionSuffix)
183 if err := os.WriteFile(adoption, []byte("{}\n"), 0o600); err != nil {
184 t.Fatal(err)
185 }
186 before, err := SeedEnvBefore(rt.DBPath, rt.Host)
187 if err != nil || before != registry.EnvBeforeNoKey {
188 t.Fatalf("SeedEnvBefore = %q, %v; want no-key", before, err)
189 }
190 handed, own := launchKey(t, b, importedRoute(t, b, rt))
191 if !wellFormedInstanceKey(handed) || own != handed {
192 t.Fatal("the import's launch did not mint a key into the file")
193 }
194 if err := RetireSeedKey(rt.DBPath, rt.Host, before); err != nil {
195 t.Fatal(err)
196 }
197 got, err := os.ReadFile(envPath)
198 if err != nil {
199 t.Fatal(err)
200 }
201 // Every line that was there is there, in order; the key is not.
202 if string(got) != predating+"\n" {
203 t.Fatalf("after the retirement the file is not what predated the import plus its newline (%d bytes, want %d)", len(got), len(predating)+1)
204 }
205 if st, _ := os.Stat(envPath); st.Mode().Perm() != 0o600 {
206 t.Fatalf("mode = %o, want 600", st.Mode().Perm())
207 }
208 if _, err := os.Stat(adoption); err != nil {
209 t.Fatalf("the adoption record went with a file that predated the import: %v", err)
210 }
211 if err := RetireSeedKey(rt.DBPath, rt.Host, before); err != nil {
212 t.Fatal(err)
213 }
214 if again, _ := os.ReadFile(envPath); string(again) != string(got) {
215 t.Fatal("a second retirement changed the file")
216 }
217 })
218 t.Run("a journal row that does not say", func(t *testing.T) {
219 noOwnReplica(t)
220 b, rt := nestedBackend(t, &fakeControl{inv: "inv1"}, &fakeUnit{})
221 handed, own := launchKey(t, b, importedRoute(t, b, rt))
222 if err := RetireSeedKey(rt.DBPath, rt.Host, ""); err != nil {
223 t.Fatal(err)
224 }
225 if _, now := launchKeyFile(t, b, rt); now != own || own != handed {
226 t.Fatal("a key was retired on no record of what predated the import")
227 }
228 })
229 t.Run("nothing there", func(t *testing.T) {
230 b, rt := nestedBackend(t, &fakeControl{}, &fakeUnit{})
231 for _, before := range []string{registry.EnvBeforeAbsent, registry.EnvBeforeNoKey, registry.EnvBeforeKey, ""} {
232 if err := RetireSeedKey(rt.DBPath, rt.Host, before); err != nil {
233 t.Fatalf("%q with no instance directory: %v", before, err)
234 }
235 }
236 if err := RetireSeedKey(rt.DBPath, rt.Host, "whatever"); err == nil {
237 t.Fatal("an unknown env state was accepted")
238 }
239 _ = b
240 })
241}
242
243// launchKeyFile reads the own env file's key without launching.
244func launchKeyFile(t *testing.T, b *ExecBackend, rt registry.Route) (exists bool, key string) {
245 t.Helper()
246 raw, err := os.ReadFile(filepath.Join(provision.InstanceDir(b.DataDir, rt.Host), rt.Host+".env"))
247 if os.IsNotExist(err) {
248 return false, ""
249 }
250 if err != nil {
251 t.Fatal(err)
252 }
253 return true, parseEnvBytes(raw)[InstanceKeyVar]
254}
255
256// The swap does not go ahead on an env file it cannot read: a link there is
257// refused, never followed, and never read as "no file".
258func TestSeedEnvBeforeRefusesALink(t *testing.T) {
259 b, rt := nestedBackend(t, &fakeControl{}, &fakeUnit{})
260 dir := provision.InstanceDir(b.DataDir, rt.Host)
261 if err := os.MkdirAll(dir, 0o700); err != nil {
262 t.Fatal(err)
263 }
264 target := filepath.Join(t.TempDir(), "elsewhere.env")
265 if err := os.WriteFile(target, []byte(InstanceKeyVar+"=x\n"), 0o600); err != nil {
266 t.Fatal(err)
267 }
268 if err := os.Symlink(target, filepath.Join(dir, rt.Host+".env")); err != nil {
269 t.Fatal(err)
270 }
271 if before, err := SeedEnvBefore(rt.DBPath, rt.Host); err == nil {
272 t.Fatalf("SeedEnvBefore = %q over a symlink, want an error", before)
273 }
274}
275