carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package hostenv
2
3import (
4 "bytes"
5 "context"
6 "errors"
7 "os"
8 "path/filepath"
9 "reflect"
10 "runtime"
11 "slices"
12 "strings"
13 "testing"
14)
15
16// fakeParams is a parameter store in memory. failPrefix makes ByPath fail
17// for one prefix, as an unreachable or refused SSM call would. staleOnce
18// makes the next ByPath of that prefix answer as it stood before every
19// value now under it was written: a seed that read just before another
20// seed wrote.
21type fakeParams struct {
22 vals map[string]string
23 sets []string
24 failPrefix string
25 staleOnce string
26}
27
28func (f *fakeParams) ByPath(_ context.Context, prefix string) (map[string]string, error) {
29 if prefix == f.failPrefix {
30 return nil, errors.New("AccessDeniedException")
31 }
32 if prefix == f.staleOnce {
33 f.staleOnce = ""
34 return map[string]string{}, nil
35 }
36 out := map[string]string{}
37 for name, v := range f.vals {
38 if rest, ok := strings.CutPrefix(name, prefix); ok && !strings.Contains(rest, "/") {
39 out[rest] = v
40 }
41 }
42 return out, nil
43}
44
45func (f *fakeParams) Set(_ context.Context, name, value string) error {
46 f.vals[name] = value
47 f.sets = append(f.sets, name)
48 return nil
49}
50
51func (f *fakeParams) SetNew(_ context.Context, name, value string) error {
52 if _, ok := f.vals[name]; ok {
53 return ErrExists
54 }
55 f.vals[name] = value
56 f.sets = append(f.sets, name)
57 return nil
58}
59
60// today is a hosting box's host.env as an operator has it today.
61func today() map[string]string {
62 return map[string]string{
63 "CARLOS_ALERT_SNS_TOPIC_ARN": "arn:aws:sns:eu-west-1:123456789012:carlos-alerts",
64 "CARLOS_DEPLOYMENT_BUCKET": "deploy-bucket",
65 "CARLOS_HIBERNATE_BUCKET": "data-ie",
66 "CARLOS_HIBERNATE_REGION": "eu-west-1",
67 "CARLOS_HOST_CLAIM": "1",
68 "CARLOS_HOST_CLAIM_ORIGINS": "1",
69 "CARLOS_REGION": "ie",
70 "CARLOS_REGION_HA": "1",
71 "CARLOS_RELEASE_PUBKEY": "q3J0bXlwdWJsaWNrZXk+/=",
72 "CARLOS_SECRETS_KEY": "c2VjcmV0c2tleQ==",
73 "CARLOS_SYSTEM_CHANNEL": "stable",
74 "CARLOS_HIBERNATE_PREFIX": "fleet",
75 }
76}
77
78func TestSeedThenFetchIsTodaysFile(t *testing.T) {
79 ctx := context.Background()
80 p := &fakeParams{vals: map[string]string{}}
81 s, err := Seed(ctx, p, "flagship", "ie", today())
82 if err != nil {
83 t.Fatal(err)
84 }
85 if s.Shared != len(SharedKeys) || s.Regional != len(today())-len(SharedKeys) || s.SharedPrefix != "/carlos/flagship/shared/" || s.RegionPrefix != "/carlos/flagship/ie/" {
86 t.Fatalf("seeded %+v", s)
87 }
88 if p.vals["/carlos/flagship/shared/CARLOS_RELEASE_PUBKEY"] != today()["CARLOS_RELEASE_PUBKEY"] || p.vals["/carlos/flagship/ie/CARLOS_REGION"] != "ie" {
89 t.Fatalf("layout %v", p.vals)
90 }
91 // Another region's parameter, and a non-variable under shared, are not
92 // this region's host.env.
93 p.vals["/carlos/flagship/uk/CARLOS_REGION"] = "uk"
94 p.vals["/carlos/flagship/shared/bootstrap-sha256"] = strings.Repeat("a", 64)
95 body, err := Fetch(ctx, p, "flagship", "ie")
96 if err != nil {
97 t.Fatal(err)
98 }
99 got, err := Parse(body)
100 if err != nil {
101 t.Fatal(err)
102 }
103 // A box write assembles host.env for is one its group launched: it
104 // takes its group reference (BoxReference), which is never a parameter.
105 want := today()
106 want[BoxReference] = GroupReference
107 if !reflect.DeepEqual(got, want) {
108 t.Fatalf("round trip:\n got %v\nwant %v", got, want)
109 }
110 if !bytes.HasPrefix(body, []byte("# ")) || !bytes.Contains(body, []byte("\nCARLOS_ALERT_SNS_TOPIC_ARN=arn:aws:sns:eu-west-1:123456789012:carlos-alerts\nCARLOS_BOX_REFERENCE=group\nCARLOS_DEPLOYMENT_BUCKET=deploy-bucket\n")) {
111 t.Fatalf("not sorted KEY=value lines:\n%s", body)
112 }
113}
114
115func TestFetchRefusesAMissingRequiredValue(t *testing.T) {
116 ctx := context.Background()
117 for _, k := range Required {
118 p := &fakeParams{vals: map[string]string{}}
119 env := today()
120 if _, err := Seed(ctx, p, "flagship", "ie", env); err != nil {
121 t.Fatal(err)
122 }
123 for name := range p.vals {
124 if strings.HasSuffix(name, "/"+k) {
125 delete(p.vals, name)
126 }
127 }
128 _, err := Fetch(ctx, p, "flagship", "ie")
129 if !errors.Is(err, ErrMissing) || !strings.Contains(err.Error(), k) {
130 t.Fatalf("without %s: %v", k, err)
131 }
132 }
133}
134
135func TestFetchRefusesAFailedReadAndAConflict(t *testing.T) {
136 ctx := context.Background()
137 p := &fakeParams{vals: map[string]string{}}
138 if _, err := Seed(ctx, p, "flagship", "ie", today()); err != nil {
139 t.Fatal(err)
140 }
141 // A refused read is never an empty scope: nothing is assembled.
142 p.failPrefix = "/carlos/flagship/shared/"
143 if _, err := Fetch(ctx, p, "flagship", "ie"); err == nil || errors.Is(err, ErrMissing) {
144 t.Fatalf("failed read: %v", err)
145 }
146 p.failPrefix = ""
147 // A variable in both scopes is one of them wrong.
148 p.vals["/carlos/flagship/ie/CARLOS_RELEASE_PUBKEY"] = "other"
149 if _, err := Fetch(ctx, p, "flagship", "ie"); !errors.Is(err, ErrConflict) || !strings.Contains(err.Error(), "CARLOS_RELEASE_PUBKEY") {
150 t.Fatalf("conflict: %v", err)
151 }
152}
153
154func TestRenderRefusesAnUnsafeValueWithoutPrintingIt(t *testing.T) {
155 env := today()
156 env["CARLOS_SECRETS_KEY"] = "abc$(reboot)"
157 _, err := Render(env)
158 if !errors.Is(err, ErrValue) || !strings.Contains(err.Error(), "CARLOS_SECRETS_KEY") || strings.Contains(err.Error(), "reboot") {
159 t.Fatalf("unsafe value: %v", err)
160 }
161 for _, v := range []string{"a b", "a'b", `a"b`, "a\\b", "a\nb", "a#b", "a;b"} {
162 if _, err := Render(map[string]string{"CARLOS_X": v}); !errors.Is(err, ErrValue) {
163 t.Fatalf("%q rendered: %v", v, err)
164 }
165 }
166}
167
168func TestSeedNeverChangesASharedValue(t *testing.T) {
169 ctx := context.Background()
170 p := &fakeParams{vals: map[string]string{}}
171 if _, err := Seed(ctx, p, "flagship", "ie", today()); err != nil {
172 t.Fatal(err)
173 }
174 // A second region with the same shared values writes only its own.
175 uk := today()
176 uk["CARLOS_REGION"], uk["CARLOS_HIBERNATE_BUCKET"] = "uk", "data-uk"
177 p.sets = nil
178 s, err := Seed(ctx, p, "flagship", "uk", uk)
179 if err != nil {
180 t.Fatal(err)
181 }
182 if s.Shared != 0 {
183 t.Fatalf("rewrote shared values: %v", p.sets)
184 }
185 // A third region whose file disagrees on a shared value writes nothing.
186 au := today()
187 au["CARLOS_REGION"], au["CARLOS_SECRETS_KEY"] = "au", "b3RoZXI="
188 p.sets = nil
189 if _, err := Seed(ctx, p, "flagship", "au", au); !errors.Is(err, ErrConflict) || len(p.sets) != 0 {
190 t.Fatalf("conflicting shared value: %v, wrote %v", err, p.sets)
191 }
192 // A group box's own host.env seeds no BoxReference.
193 grp := today()
194 grp[BoxReference] = GroupReference
195 p.sets = nil
196 if _, err := Seed(ctx, p, "flagship", "ie", grp); err != nil || slices.ContainsFunc(p.sets, func(n string) bool { return strings.HasSuffix(n, BoxReference) }) {
197 t.Fatalf("seeded %v: %v", p.sets, err)
198 }
199 // An incomplete file writes nothing either.
200 partial := today()
201 delete(partial, "CARLOS_REGION_HA")
202 p.sets = nil
203 if _, err := Seed(ctx, p, "flagship", "au", partial); !errors.Is(err, ErrMissing) || len(p.sets) != 0 {
204 t.Fatalf("incomplete file: %v, wrote %v", err, p.sets)
205 }
206}
207
208// R4: two seeds racing for a shared value. The second read before the
209// first wrote, so both try to create it; the second's SetNew is refused,
210// it reads the value back, and it goes on only if the value is its own.
211func TestSeedRacesForASharedValueAndOnlyOneWins(t *testing.T) {
212 ctx := context.Background()
213 p := &fakeParams{vals: map[string]string{}}
214 if _, err := Seed(ctx, p, "flagship", "ie", today()); err != nil {
215 t.Fatal(err)
216 }
217 // The same shared values: nothing shared is written, the region's are.
218 uk := today()
219 uk["CARLOS_REGION"] = "uk"
220 p.staleOnce, p.sets = "/carlos/flagship/shared/", nil
221 s, err := Seed(ctx, p, "flagship", "uk", uk)
222 if err != nil || s.Shared != 0 || s.Regional != len(today())-len(SharedKeys) {
223 t.Fatalf("same values: %+v %v, wrote %v", s, err, p.sets)
224 }
225 // A different secrets key: refused, naming the variable and never
226 // either value, and the first seed's value stands.
227 au := today()
228 au["CARLOS_REGION"], au["CARLOS_SECRETS_KEY"] = "au", "b3RoZXI="
229 p.staleOnce, p.sets = "/carlos/flagship/shared/", nil
230 _, err = Seed(ctx, p, "flagship", "au", au)
231 if !errors.Is(err, ErrConflict) || !strings.Contains(err.Error(), "CARLOS_SECRETS_KEY") || strings.Contains(err.Error(), "b3RoZXI=") || strings.Contains(err.Error(), today()["CARLOS_SECRETS_KEY"]) {
232 t.Fatalf("racing seed: %v", err)
233 }
234 if p.vals["/carlos/flagship/shared/CARLOS_SECRETS_KEY"] != today()["CARLOS_SECRETS_KEY"] || len(p.sets) != 0 {
235 t.Fatalf("the second seed changed or wrote %v", p.sets)
236 }
237}
238
239func TestPrefixAndParse(t *testing.T) {
240 if _, err := Prefix("flagship", "../x"); !errors.Is(err, ErrValue) {
241 t.Fatalf("bad scope: %v", err)
242 }
243 if _, err := Fetch(context.Background(), &fakeParams{}, "flagship", SharedScope); !errors.Is(err, ErrValue) {
244 t.Fatalf("shared as a region: %v", err)
245 }
246 got, err := Parse([]byte("# comment\n\nexport CARLOS_REGION=\"ie\"\nCARLOS_HOST_CLAIM='1'\n"))
247 if err != nil || !reflect.DeepEqual(got, map[string]string{"CARLOS_REGION": "ie", "CARLOS_HOST_CLAIM": "1"}) {
248 t.Fatalf("parse: %v %v", got, err)
249 }
250 for _, body := range []string{"CARLOS_REGION=ie\nCARLOS_REGION=uk\n", "not an assignment\n", "lower=x\n"} {
251 if _, err := Parse([]byte(body)); !errors.Is(err, ErrValue) {
252 t.Fatalf("%q parsed: %v", body, err)
253 }
254 }
255}
256
257// The command: write refuses without root and leaves no file on a failed
258// assembly; with root it writes 0600; seed reads the named file.
259func TestHostEnvCommand(t *testing.T) {
260 ctx := context.Background()
261 dir := withHostEnvRoot(t)
262 out := filepath.Join(dir, "etc", "carlos", "host.env")
263 p := &fakeParams{vals: map[string]string{}}
264 open := func(context.Context, string, string) (Params, error) { return p, nil }
265 var stdout bytes.Buffer
266 args := []string{"write", "--deployment", "flagship", "--region", "ie"}
267 if err := runHostEnv(ctx, args, &stdout, 1000, open); err == nil || !strings.Contains(err.Error(), "root") {
268 t.Fatalf("non-root write: %v", err)
269 }
270 if err := runHostEnv(ctx, args, &stdout, 0, open); !errors.Is(err, ErrMissing) {
271 t.Fatalf("write with no parameters: %v", err)
272 }
273 if _, err := os.Stat(out); !os.IsNotExist(err) {
274 t.Fatalf("a failed write left a file: %v", err)
275 }
276 from := filepath.Join(dir, "today.env")
277 body, _ := Render(today())
278 if err := os.WriteFile(from, body, 0o600); err != nil {
279 t.Fatal(err)
280 }
281 seed := []string{"seed", "--deployment", "flagship", "--region", "ie", "--kms-key", "alias/carlos-flagship-config", "--from", from}
282 if err := runHostEnv(ctx, seed, &stdout, 1000, open); err != nil {
283 t.Fatal(err)
284 }
285 if !strings.Contains(stdout.String(), "wrote 7 parameters under /carlos/flagship/ie/ and 5 under /carlos/flagship/shared/\n") {
286 t.Fatalf("output %q", stdout.String())
287 }
288 err := runHostEnv(ctx, args, &stdout, 0, open)
289 if runtime.GOOS != "linux" {
290 if err == nil || !strings.Contains(err.Error(), "runs only on Linux") {
291 t.Fatalf("write off Linux: %v", err)
292 }
293 } else {
294 if err != nil {
295 t.Fatal(err)
296 }
297 fi, err := os.Stat(out)
298 if err != nil || fi.Mode().Perm() != 0o600 {
299 t.Fatalf("host.env: %v %v", fi, err)
300 }
301 if !strings.Contains(stdout.String(), "wrote "+out+" from the parameters for ie\n") {
302 t.Fatalf("output %q", stdout.String())
303 }
304 }
305 // The path is fixed: there is no flag to move it.
306 for _, bad := range [][]string{nil, {"seed", "--deployment", "flagship", "--region", "ie"}, {"write", "--region", "ie"}, {"read"}, append(args, "--out", filepath.Join(dir, "elsewhere"))} {
307 if err := runHostEnv(ctx, bad, &stdout, 0, open); err == nil || !strings.HasPrefix(err.Error(), "usage: carlos system host-env") {
308 t.Fatalf("%v: %v", bad, err)
309 }
310 }
311}
312
313// runHostEnv's census conditions (platform-1e, region-ha-2b): write takes
314// only allowlisted names, refuses values that could start a second line,
315// end the file or change how systemd reads it, and writes through no
316// symlinked directory. A refusal names the variable, never the value.
317func TestWriteTakesOnlyAllowedNamesAndSafeValues(t *testing.T) {
318 ctx := context.Background()
319 p := &fakeParams{vals: map[string]string{}}
320 if _, err := Seed(ctx, p, "flagship", "ie", today()); err != nil {
321 t.Fatal(err)
322 }
323 // A name off the allowlist, under either scope, is refused by name.
324 for _, name := range []string{"/carlos/flagship/ie/CARLOS_PRIVATE_IP", "/carlos/flagship/shared/CARLOS_FLEET"} {
325 p.vals[name] = "10.0.0.9"
326 _, err := Fetch(ctx, p, "flagship", "ie")
327 short := name[strings.LastIndex(name, "/")+1:]
328 if !errors.Is(err, ErrValue) || !strings.Contains(err.Error(), short) || strings.Contains(err.Error(), "10.0.0.9") {
329 t.Fatalf("%s: %v", name, err)
330 }
331 delete(p.vals, name)
332 }
333 // Seeding a file with such a name writes nothing.
334 odd := today()
335 odd["CARLOS_POOLS"] = "gold"
336 p.sets = nil
337 if _, err := Seed(ctx, p, "flagship", "uk", odd); !errors.Is(err, ErrValue) || !strings.Contains(err.Error(), "CARLOS_POOLS") || len(p.sets) != 0 {
338 t.Fatalf("seed with an unknown name: %v, wrote %v", err, p.sets)
339 }
340 // Values that would break a line, end the file early or change how
341 // systemd reads it.
342 for _, v := range []string{"a\nCARLOS_REGION=uk", "a\x00b", "a\rb", "$(id)", "a b", "\"quoted\""} {
343 p.vals["/carlos/flagship/ie/CARLOS_HIBERNATE_PREFIX"] = v
344 if _, err := Fetch(ctx, p, "flagship", "ie"); !errors.Is(err, ErrValue) || strings.Contains(err.Error(), v) {
345 t.Fatalf("%q: %v", v, err)
346 }
347 }
348}
349
350// withHostEnvRoot points write's fixed path at a directory of the test's
351// own, with an etc in it as a box has one.
352func withHostEnvRoot(t *testing.T) string {
353 t.Helper()
354 root := t.TempDir()
355 if err := os.Mkdir(filepath.Join(root, "etc"), 0o755); err != nil {
356 t.Fatal(err)
357 }
358 if err := os.Chmod(filepath.Join(root, "etc"), 0o755); err != nil {
359 t.Fatal(err)
360 }
361 was := hostEnvRoot
362 hostEnvRoot = root
363 t.Cleanup(func() { hostEnvRoot = was })
364 return root
365}
366
367// A provider token is not on the allowlist: Assemble and Seed refuse it by
368// name, and the refusal never carries its value.
369func TestDNSProviderTokenIsRefusedByNameNotValue(t *testing.T) {
370 const name, secret = "CARLOS_DNSIMPLE_TOKEN", "tok-9f8e7d6c"
371 if Allowed(name) {
372 t.Fatalf("%s is on the allowlist", name)
373 }
374 check := func(what string, err error) {
375 t.Helper()
376 if !errors.Is(err, ErrValue) || !strings.Contains(err.Error(), name) || strings.Contains(err.Error(), secret) {
377 t.Fatalf("%s: %v", what, err)
378 }
379 }
380 env := today()
381 env[name] = secret
382 shared, regional := Split(env)
383 _, err := Assemble(shared, regional)
384 check("Assemble", err)
385 p := &fakeParams{vals: map[string]string{}}
386 _, err = Seed(context.Background(), p, "flagship", "ie", env)
387 check("Seed", err)
388 if len(p.sets) != 0 {
389 t.Fatalf("Seed wrote %v", p.sets)
390 }
391}
392
393// The status destination overrides are off the allowlist: a group box
394// reports to the deployment bucket, unprefixed, where the console reads,
395// while joining and once hosting. So is the alert webhook, whose URL can
396// carry a credential. Assemble and Seed refuse each by name, and the
397// refusal never carries its value.
398func TestStatusOverridesAndTheAlertWebhookAreRefusedByNameNotValue(t *testing.T) {
399 for name, value := range map[string]string{
400 "CARLOS_STATUS_BUCKET": "other-status-bucket-5c1d",
401 "CARLOS_STATUS_PREFIX": "fleets-prefix-7a2b",
402 "CARLOS_STATUS_REGION": "ap-southeast-9z",
403 "CARLOS_ALERT_WEBHOOK": "https://hooks.example.com/services/T0/B0/s3cr3t-4e5f",
404 } {
405 t.Run(name, func(t *testing.T) {
406 if Allowed(name) {
407 t.Fatalf("%s is on the allowlist", name)
408 }
409 check := func(what string, err error) {
410 t.Helper()
411 if !errors.Is(err, ErrValue) || !strings.Contains(err.Error(), name) || strings.Contains(err.Error(), value) {
412 t.Fatalf("%s: %v", what, err)
413 }
414 }
415 env := today()
416 env[name] = value
417 shared, regional := Split(env)
418 _, err := Assemble(shared, regional)
419 check("Assemble", err)
420 p := &fakeParams{vals: map[string]string{}}
421 _, err = Seed(context.Background(), p, "flagship", "ie", env)
422 check("Seed", err)
423 if len(p.sets) != 0 {
424 t.Fatalf("Seed wrote %v", p.sets)
425 }
426 })
427 }
428}
429