package hostenv import ( "bytes" "context" "errors" "os" "path/filepath" "reflect" "runtime" "slices" "strings" "testing" ) // fakeParams is a parameter store in memory. failPrefix makes ByPath fail // for one prefix, as an unreachable or refused SSM call would. staleOnce // makes the next ByPath of that prefix answer as it stood before every // value now under it was written: a seed that read just before another // seed wrote. type fakeParams struct { vals map[string]string sets []string failPrefix string staleOnce string } func (f *fakeParams) ByPath(_ context.Context, prefix string) (map[string]string, error) { if prefix == f.failPrefix { return nil, errors.New("AccessDeniedException") } if prefix == f.staleOnce { f.staleOnce = "" return map[string]string{}, nil } out := map[string]string{} for name, v := range f.vals { if rest, ok := strings.CutPrefix(name, prefix); ok && !strings.Contains(rest, "/") { out[rest] = v } } return out, nil } func (f *fakeParams) Set(_ context.Context, name, value string) error { f.vals[name] = value f.sets = append(f.sets, name) return nil } func (f *fakeParams) SetNew(_ context.Context, name, value string) error { if _, ok := f.vals[name]; ok { return ErrExists } f.vals[name] = value f.sets = append(f.sets, name) return nil } // today is a hosting box's host.env as an operator has it today. func today() map[string]string { return map[string]string{ "CARLOS_ALERT_SNS_TOPIC_ARN": "arn:aws:sns:eu-west-1:123456789012:carlos-alerts", "CARLOS_DEPLOYMENT_BUCKET": "deploy-bucket", "CARLOS_HIBERNATE_BUCKET": "data-ie", "CARLOS_HIBERNATE_REGION": "eu-west-1", "CARLOS_HOST_CLAIM": "1", "CARLOS_HOST_CLAIM_ORIGINS": "1", "CARLOS_REGION": "ie", "CARLOS_REGION_HA": "1", "CARLOS_RELEASE_PUBKEY": "q3J0bXlwdWJsaWNrZXk+/=", "CARLOS_SECRETS_KEY": "c2VjcmV0c2tleQ==", "CARLOS_SYSTEM_CHANNEL": "stable", "CARLOS_HIBERNATE_PREFIX": "fleet", } } func TestSeedThenFetchIsTodaysFile(t *testing.T) { ctx := context.Background() p := &fakeParams{vals: map[string]string{}} s, err := Seed(ctx, p, "flagship", "ie", today()) if err != nil { t.Fatal(err) } if s.Shared != len(SharedKeys) || s.Regional != len(today())-len(SharedKeys) || s.SharedPrefix != "/carlos/flagship/shared/" || s.RegionPrefix != "/carlos/flagship/ie/" { t.Fatalf("seeded %+v", s) } if p.vals["/carlos/flagship/shared/CARLOS_RELEASE_PUBKEY"] != today()["CARLOS_RELEASE_PUBKEY"] || p.vals["/carlos/flagship/ie/CARLOS_REGION"] != "ie" { t.Fatalf("layout %v", p.vals) } // Another region's parameter, and a non-variable under shared, are not // this region's host.env. p.vals["/carlos/flagship/uk/CARLOS_REGION"] = "uk" p.vals["/carlos/flagship/shared/bootstrap-sha256"] = strings.Repeat("a", 64) body, err := Fetch(ctx, p, "flagship", "ie") if err != nil { t.Fatal(err) } got, err := Parse(body) if err != nil { t.Fatal(err) } // A box write assembles host.env for is one its group launched: it // takes its group reference (BoxReference), which is never a parameter. want := today() want[BoxReference] = GroupReference if !reflect.DeepEqual(got, want) { t.Fatalf("round trip:\n got %v\nwant %v", got, want) } 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")) { t.Fatalf("not sorted KEY=value lines:\n%s", body) } } func TestFetchRefusesAMissingRequiredValue(t *testing.T) { ctx := context.Background() for _, k := range Required { p := &fakeParams{vals: map[string]string{}} env := today() if _, err := Seed(ctx, p, "flagship", "ie", env); err != nil { t.Fatal(err) } for name := range p.vals { if strings.HasSuffix(name, "/"+k) { delete(p.vals, name) } } _, err := Fetch(ctx, p, "flagship", "ie") if !errors.Is(err, ErrMissing) || !strings.Contains(err.Error(), k) { t.Fatalf("without %s: %v", k, err) } } } func TestFetchRefusesAFailedReadAndAConflict(t *testing.T) { ctx := context.Background() p := &fakeParams{vals: map[string]string{}} if _, err := Seed(ctx, p, "flagship", "ie", today()); err != nil { t.Fatal(err) } // A refused read is never an empty scope: nothing is assembled. p.failPrefix = "/carlos/flagship/shared/" if _, err := Fetch(ctx, p, "flagship", "ie"); err == nil || errors.Is(err, ErrMissing) { t.Fatalf("failed read: %v", err) } p.failPrefix = "" // A variable in both scopes is one of them wrong. p.vals["/carlos/flagship/ie/CARLOS_RELEASE_PUBKEY"] = "other" if _, err := Fetch(ctx, p, "flagship", "ie"); !errors.Is(err, ErrConflict) || !strings.Contains(err.Error(), "CARLOS_RELEASE_PUBKEY") { t.Fatalf("conflict: %v", err) } } func TestRenderRefusesAnUnsafeValueWithoutPrintingIt(t *testing.T) { env := today() env["CARLOS_SECRETS_KEY"] = "abc$(reboot)" _, err := Render(env) if !errors.Is(err, ErrValue) || !strings.Contains(err.Error(), "CARLOS_SECRETS_KEY") || strings.Contains(err.Error(), "reboot") { t.Fatalf("unsafe value: %v", err) } for _, v := range []string{"a b", "a'b", `a"b`, "a\\b", "a\nb", "a#b", "a;b"} { if _, err := Render(map[string]string{"CARLOS_X": v}); !errors.Is(err, ErrValue) { t.Fatalf("%q rendered: %v", v, err) } } } func TestSeedNeverChangesASharedValue(t *testing.T) { ctx := context.Background() p := &fakeParams{vals: map[string]string{}} if _, err := Seed(ctx, p, "flagship", "ie", today()); err != nil { t.Fatal(err) } // A second region with the same shared values writes only its own. uk := today() uk["CARLOS_REGION"], uk["CARLOS_HIBERNATE_BUCKET"] = "uk", "data-uk" p.sets = nil s, err := Seed(ctx, p, "flagship", "uk", uk) if err != nil { t.Fatal(err) } if s.Shared != 0 { t.Fatalf("rewrote shared values: %v", p.sets) } // A third region whose file disagrees on a shared value writes nothing. au := today() au["CARLOS_REGION"], au["CARLOS_SECRETS_KEY"] = "au", "b3RoZXI=" p.sets = nil if _, err := Seed(ctx, p, "flagship", "au", au); !errors.Is(err, ErrConflict) || len(p.sets) != 0 { t.Fatalf("conflicting shared value: %v, wrote %v", err, p.sets) } // A group box's own host.env seeds no BoxReference. grp := today() grp[BoxReference] = GroupReference p.sets = nil if _, err := Seed(ctx, p, "flagship", "ie", grp); err != nil || slices.ContainsFunc(p.sets, func(n string) bool { return strings.HasSuffix(n, BoxReference) }) { t.Fatalf("seeded %v: %v", p.sets, err) } // An incomplete file writes nothing either. partial := today() delete(partial, "CARLOS_REGION_HA") p.sets = nil if _, err := Seed(ctx, p, "flagship", "au", partial); !errors.Is(err, ErrMissing) || len(p.sets) != 0 { t.Fatalf("incomplete file: %v, wrote %v", err, p.sets) } } // R4: two seeds racing for a shared value. The second read before the // first wrote, so both try to create it; the second's SetNew is refused, // it reads the value back, and it goes on only if the value is its own. func TestSeedRacesForASharedValueAndOnlyOneWins(t *testing.T) { ctx := context.Background() p := &fakeParams{vals: map[string]string{}} if _, err := Seed(ctx, p, "flagship", "ie", today()); err != nil { t.Fatal(err) } // The same shared values: nothing shared is written, the region's are. uk := today() uk["CARLOS_REGION"] = "uk" p.staleOnce, p.sets = "/carlos/flagship/shared/", nil s, err := Seed(ctx, p, "flagship", "uk", uk) if err != nil || s.Shared != 0 || s.Regional != len(today())-len(SharedKeys) { t.Fatalf("same values: %+v %v, wrote %v", s, err, p.sets) } // A different secrets key: refused, naming the variable and never // either value, and the first seed's value stands. au := today() au["CARLOS_REGION"], au["CARLOS_SECRETS_KEY"] = "au", "b3RoZXI=" p.staleOnce, p.sets = "/carlos/flagship/shared/", nil _, err = Seed(ctx, p, "flagship", "au", au) 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"]) { t.Fatalf("racing seed: %v", err) } if p.vals["/carlos/flagship/shared/CARLOS_SECRETS_KEY"] != today()["CARLOS_SECRETS_KEY"] || len(p.sets) != 0 { t.Fatalf("the second seed changed or wrote %v", p.sets) } } func TestPrefixAndParse(t *testing.T) { if _, err := Prefix("flagship", "../x"); !errors.Is(err, ErrValue) { t.Fatalf("bad scope: %v", err) } if _, err := Fetch(context.Background(), &fakeParams{}, "flagship", SharedScope); !errors.Is(err, ErrValue) { t.Fatalf("shared as a region: %v", err) } got, err := Parse([]byte("# comment\n\nexport CARLOS_REGION=\"ie\"\nCARLOS_HOST_CLAIM='1'\n")) if err != nil || !reflect.DeepEqual(got, map[string]string{"CARLOS_REGION": "ie", "CARLOS_HOST_CLAIM": "1"}) { t.Fatalf("parse: %v %v", got, err) } for _, body := range []string{"CARLOS_REGION=ie\nCARLOS_REGION=uk\n", "not an assignment\n", "lower=x\n"} { if _, err := Parse([]byte(body)); !errors.Is(err, ErrValue) { t.Fatalf("%q parsed: %v", body, err) } } } // The command: write refuses without root and leaves no file on a failed // assembly; with root it writes 0600; seed reads the named file. func TestHostEnvCommand(t *testing.T) { ctx := context.Background() dir := withHostEnvRoot(t) out := filepath.Join(dir, "etc", "carlos", "host.env") p := &fakeParams{vals: map[string]string{}} open := func(context.Context, string, string) (Params, error) { return p, nil } var stdout bytes.Buffer args := []string{"write", "--deployment", "flagship", "--region", "ie"} if err := runHostEnv(ctx, args, &stdout, 1000, open); err == nil || !strings.Contains(err.Error(), "root") { t.Fatalf("non-root write: %v", err) } if err := runHostEnv(ctx, args, &stdout, 0, open); !errors.Is(err, ErrMissing) { t.Fatalf("write with no parameters: %v", err) } if _, err := os.Stat(out); !os.IsNotExist(err) { t.Fatalf("a failed write left a file: %v", err) } from := filepath.Join(dir, "today.env") body, _ := Render(today()) if err := os.WriteFile(from, body, 0o600); err != nil { t.Fatal(err) } seed := []string{"seed", "--deployment", "flagship", "--region", "ie", "--kms-key", "alias/carlos-flagship-config", "--from", from} if err := runHostEnv(ctx, seed, &stdout, 1000, open); err != nil { t.Fatal(err) } if !strings.Contains(stdout.String(), "wrote 7 parameters under /carlos/flagship/ie/ and 5 under /carlos/flagship/shared/\n") { t.Fatalf("output %q", stdout.String()) } err := runHostEnv(ctx, args, &stdout, 0, open) if runtime.GOOS != "linux" { if err == nil || !strings.Contains(err.Error(), "runs only on Linux") { t.Fatalf("write off Linux: %v", err) } } else { if err != nil { t.Fatal(err) } fi, err := os.Stat(out) if err != nil || fi.Mode().Perm() != 0o600 { t.Fatalf("host.env: %v %v", fi, err) } if !strings.Contains(stdout.String(), "wrote "+out+" from the parameters for ie\n") { t.Fatalf("output %q", stdout.String()) } } // The path is fixed: there is no flag to move it. for _, bad := range [][]string{nil, {"seed", "--deployment", "flagship", "--region", "ie"}, {"write", "--region", "ie"}, {"read"}, append(args, "--out", filepath.Join(dir, "elsewhere"))} { if err := runHostEnv(ctx, bad, &stdout, 0, open); err == nil || !strings.HasPrefix(err.Error(), "usage: carlos system host-env") { t.Fatalf("%v: %v", bad, err) } } } // runHostEnv's census conditions (platform-1e, region-ha-2b): write takes // only allowlisted names, refuses values that could start a second line, // end the file or change how systemd reads it, and writes through no // symlinked directory. A refusal names the variable, never the value. func TestWriteTakesOnlyAllowedNamesAndSafeValues(t *testing.T) { ctx := context.Background() p := &fakeParams{vals: map[string]string{}} if _, err := Seed(ctx, p, "flagship", "ie", today()); err != nil { t.Fatal(err) } // A name off the allowlist, under either scope, is refused by name. for _, name := range []string{"/carlos/flagship/ie/CARLOS_PRIVATE_IP", "/carlos/flagship/shared/CARLOS_FLEET"} { p.vals[name] = "10.0.0.9" _, err := Fetch(ctx, p, "flagship", "ie") short := name[strings.LastIndex(name, "/")+1:] if !errors.Is(err, ErrValue) || !strings.Contains(err.Error(), short) || strings.Contains(err.Error(), "10.0.0.9") { t.Fatalf("%s: %v", name, err) } delete(p.vals, name) } // Seeding a file with such a name writes nothing. odd := today() odd["CARLOS_POOLS"] = "gold" p.sets = nil if _, err := Seed(ctx, p, "flagship", "uk", odd); !errors.Is(err, ErrValue) || !strings.Contains(err.Error(), "CARLOS_POOLS") || len(p.sets) != 0 { t.Fatalf("seed with an unknown name: %v, wrote %v", err, p.sets) } // Values that would break a line, end the file early or change how // systemd reads it. for _, v := range []string{"a\nCARLOS_REGION=uk", "a\x00b", "a\rb", "$(id)", "a b", "\"quoted\""} { p.vals["/carlos/flagship/ie/CARLOS_HIBERNATE_PREFIX"] = v if _, err := Fetch(ctx, p, "flagship", "ie"); !errors.Is(err, ErrValue) || strings.Contains(err.Error(), v) { t.Fatalf("%q: %v", v, err) } } } // withHostEnvRoot points write's fixed path at a directory of the test's // own, with an etc in it as a box has one. func withHostEnvRoot(t *testing.T) string { t.Helper() root := t.TempDir() if err := os.Mkdir(filepath.Join(root, "etc"), 0o755); err != nil { t.Fatal(err) } if err := os.Chmod(filepath.Join(root, "etc"), 0o755); err != nil { t.Fatal(err) } was := hostEnvRoot hostEnvRoot = root t.Cleanup(func() { hostEnvRoot = was }) return root } // A provider token is not on the allowlist: Assemble and Seed refuse it by // name, and the refusal never carries its value. func TestDNSProviderTokenIsRefusedByNameNotValue(t *testing.T) { const name, secret = "CARLOS_DNSIMPLE_TOKEN", "tok-9f8e7d6c" if Allowed(name) { t.Fatalf("%s is on the allowlist", name) } check := func(what string, err error) { t.Helper() if !errors.Is(err, ErrValue) || !strings.Contains(err.Error(), name) || strings.Contains(err.Error(), secret) { t.Fatalf("%s: %v", what, err) } } env := today() env[name] = secret shared, regional := Split(env) _, err := Assemble(shared, regional) check("Assemble", err) p := &fakeParams{vals: map[string]string{}} _, err = Seed(context.Background(), p, "flagship", "ie", env) check("Seed", err) if len(p.sets) != 0 { t.Fatalf("Seed wrote %v", p.sets) } } // The status destination overrides are off the allowlist: a group box // reports to the deployment bucket, unprefixed, where the console reads, // while joining and once hosting. So is the alert webhook, whose URL can // carry a credential. Assemble and Seed refuse each by name, and the // refusal never carries its value. func TestStatusOverridesAndTheAlertWebhookAreRefusedByNameNotValue(t *testing.T) { for name, value := range map[string]string{ "CARLOS_STATUS_BUCKET": "other-status-bucket-5c1d", "CARLOS_STATUS_PREFIX": "fleets-prefix-7a2b", "CARLOS_STATUS_REGION": "ap-southeast-9z", "CARLOS_ALERT_WEBHOOK": "https://hooks.example.com/services/T0/B0/s3cr3t-4e5f", } { t.Run(name, func(t *testing.T) { if Allowed(name) { t.Fatalf("%s is on the allowlist", name) } check := func(what string, err error) { t.Helper() if !errors.Is(err, ErrValue) || !strings.Contains(err.Error(), name) || strings.Contains(err.Error(), value) { t.Fatalf("%s: %v", what, err) } } env := today() env[name] = value shared, regional := Split(env) _, err := Assemble(shared, regional) check("Assemble", err) p := &fakeParams{vals: map[string]string{}} _, err = Seed(context.Background(), p, "flagship", "ie", env) check("Seed", err) if len(p.sets) != 0 { t.Fatalf("Seed wrote %v", p.sets) } }) } }