package host import ( "bytes" "errors" "os" "path/filepath" "reflect" "strings" "testing" ) func TestClaimStagingBootstrapIgnoresUnreadableLiveEnvironment(t *testing.T) { t.Setenv("CARLOS_HOST_CLAIM", "") fallback := filepath.Join(t.TempDir(), "host.env") if err := os.WriteFile(fallback, []byte("CARLOS_HOST_CLAIM=1\n"), 0000); err != nil { t.Fatal(err) } original, read := claimBootstrapHostEnv, readClaimUnitFile claimBootstrapHostEnv = fallback reads := 0 readClaimUnitFile = func(path string) ([]byte, error) { if path == fallback { reads++ return nil, &os.PathError{Op: "open", Path: path, Err: os.ErrPermission} } return read(path) } t.Cleanup(func() { claimBootstrapHostEnv, readClaimUnitFile = original, read }) root := t.TempDir() if err := bootstrap(root, nil); err != nil { t.Fatalf("flag-off staging read live environment: %v", err) } if reads != 0 { t.Fatalf("staging read live environment %d times", reads) } got, err := os.ReadFile(filepath.Join(root, "etc/systemd/system", claimControlUnit)) if err != nil { t.Fatal(err) } want, err := unitsFS.ReadFile("units/" + claimControlUnit) if err != nil { t.Fatal(err) } if !bytes.Equal(got, want) { t.Fatal("flag-off staging changed the unit") } } func TestClaimControlUnitInstallers(t *testing.T) { legacy, err := unitsFS.ReadFile("units/" + claimControlUnit) if err != nil { t.Fatal(err) } for _, flag := range []string{"", " ", "1", "0"} { t.Run("flag="+flag, func(t *testing.T) { t.Setenv("CARLOS_HOST_CLAIM", flag) enabled := strings.TrimSpace(flag) != "" check := func(path string) { t.Helper() got, err := os.ReadFile(path) if err != nil { t.Fatal(err) } if !enabled { if !bytes.Equal(got, legacy) { t.Fatal("flag-off unit differs from main") } return } props := map[string][]string{} for _, line := range strings.Split(string(got), "\n") { k, v, ok := strings.Cut(line, "=") if ok { props[k] = append(props[k], v) } } for key, want := range map[string][]string{ "EnvironmentFile": {"-/etc/carlos/host.env"}, "StateDirectory": {"carlos-claims"}, "StateDirectoryMode": {"0700"}, "ReadWritePaths": {"/run/carlos-control /run/carlos /var/lib/carlos-claims /var/lib/carlos-release/private /run/systemd/system /data /opt/carlos /etc/systemd/system /sys/fs/cgroup"}, "RestrictAddressFamilies": {"AF_UNIX AF_INET AF_INET6"}, "ProtectSystem": {"strict"}, "ProtectHome": {"yes"}, } { if !reflect.DeepEqual(props[key], want) { t.Errorf("%s = %q, want %q", key, props[key], want) } } } writeEnv := func(etc string) { t.Helper() if err := os.MkdirAll(filepath.Join(etc, "carlos"), 0755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(etc, "carlos", "host.env"), []byte("CARLOS_HOST_CLAIM=\""+flag+"\"\n"), 0600); err != nil { t.Fatal(err) } } stage := t.TempDir() writeEnv(filepath.Join(stage, "etc")) if enabled { t.Setenv("CARLOS_HOST_CLAIM", "") } else { t.Setenv("CARLOS_HOST_CLAIM", "1") } if err := bootstrap(stage, nil); err != nil { t.Fatal(err) } check(filepath.Join(stage, "etc/systemd/system", claimControlUnit)) f := newApplyFixture(t, "stable", "v1.0.0") writeEnv(f.a.etcDir) if err := os.Remove(filepath.Join(stage, "etc/carlos/host.env")); err != nil { t.Fatal(err) } if err := bootstrap(stage, nil); err != nil { t.Fatal(err) } if err := f.a.installUnits(t.Context(), stage, nil); err != nil { t.Fatal(err) } check(filepath.Join(f.a.etcDir, "systemd/system", claimControlUnit)) if enabled && !strings.Contains(strings.Join(f.calls, "\n"), "systemctl try-restart "+claimControlUnit) { t.Fatal("apply did not restart the control service") } // Repair a running, armed box whose root unit still has the other mode. etc := t.TempDir() writeEnv(etc) units := filepath.Join(etc, "systemd/system") if err := os.MkdirAll(units, 0755); err != nil { t.Fatal(err) } for _, name := range systemUnitFiles { b, e := unitsFS.ReadFile("units/" + name) if e != nil { t.Fatal(e) } if name == claimControlUnit && !enabled { b = append(append([]byte{}, b...), []byte("\n[Service]\nEnvironmentFile=-/etc/carlos/host.env\n")...) } if e = os.WriteFile(filepath.Join(units, name), b, 0644); e != nil { t.Fatal(e) } } var calls []string ctl := func(args ...string) (string, error) { calls = append(calls, strings.Join(args, " ")); return "", nil } probe := func(args ...string) (string, error) { // host.env here does not arm db reads, so the db-read socket // is dark (dbReadsSocketWanted), as ensureSystemUnits keeps it. if args[1] == dbReadSocketUnit { if args[0] == "is-active" { return "inactive\n", systemctlExit(3) } return "disabled\n", systemctlExit(1) } if args[0] == "is-active" { return "active\n", nil } return "enabled\n", nil } lock := filepath.Join(t.TempDir(), "lock") ensureSystemUnits(etc, lock, ctl, probe, t.Logf) check(filepath.Join(units, claimControlUnit)) if !strings.Contains(strings.Join(calls, "\n"), "try-restart "+claimControlUnit) { t.Fatal("stale armed unit was not repaired") } calls = nil ensureSystemUnits(etc, lock, ctl, probe, t.Logf) if len(calls) != 0 { t.Fatalf("unchanged unit was reinstalled: %v", calls) } }) } } func TestClaimControlUnitSettings(t *testing.T) { t.Setenv("CARLOS_HOST_CLAIM", "1") t.Setenv("CARLOS_DATA_DIR", "/wrong") t.Setenv("CARLOS_OPT_DIR", "/wrong") path := filepath.Join(t.TempDir(), "host.env") missing, err := readClaimUnitSettings(path) if err != nil || missing.enabled || missing.data != "/data" || missing.opt != "/opt/carlos" { t.Fatalf("missing env: %+v %v", missing, err) } for _, tc := range []struct { name, body string fail bool enabled bool data, opt string }{ {"absent flag", "OTHER=value\n", false, false, "/data", "/opt/carlos"}, {"quoted", "CARLOS_HOST_CLAIM='0'\nCARLOS_DATA_DIR=\"/srv/my data\"\nCARLOS_OPT_DIR=/srv/opt%box\n", false, true, "/srv/my data", "/srv/opt%box"}, {"last assignment", "CARLOS_HOST_CLAIM=1\nCARLOS_HOST_CLAIM=\" \"\n", false, false, "/data", "/opt/carlos"}, {"multiline", "OTHER='line\nCARLOS_HOST_CLAIM=1\n'\n", true, false, "", ""}, {"root", "CARLOS_HOST_CLAIM=1\nCARLOS_DATA_DIR=/\n", true, false, "", ""}, {"relative", "CARLOS_HOST_CLAIM=1\nCARLOS_OPT_DIR=relative\n", true, false, "", ""}, } { t.Run(tc.name, func(t *testing.T) { if err := os.WriteFile(path, []byte(tc.body), 0600); err != nil { t.Fatal(err) } cfg, err := readClaimUnitSettings(path) if (err != nil) != tc.fail { t.Fatalf("settings: %+v %v", cfg, err) } if tc.fail { return } if cfg.enabled != tc.enabled || cfg.data != tc.data || cfg.opt != tc.opt { t.Fatalf("settings: %+v", cfg) } if tc.name == "quoted" { raw, _ := unitsFS.ReadFile("units/" + claimControlUnit) got := string(claimUnitContent(claimControlUnit, raw, cfg)) if !strings.Contains(got, `"/srv/my data" "/srv/opt%%box"`) { t.Fatal(got) } } }) } } func TestClaimControlRestartFailureRemainsRepairable(t *testing.T) { path := filepath.Join(t.TempDir(), claimControlUnit) before, _ := unitsFS.ReadFile("units/" + claimControlUnit) after := claimUnitContent(claimControlUnit, before, claimUnitSettings{enabled: true, data: "/data", opt: "/opt/carlos"}) if err := os.WriteFile(path, after, 0644); err != nil { t.Fatal(err) } sentinel := errors.New("restart failed") calls := 0 err := restartClaimControl(path, before, after, func(args ...string) (string, error) { calls++ if args[0] == "try-restart" { return "", sentinel } return "", nil }) if !errors.Is(err, sentinel) || calls != 2 { t.Fatalf("restart: %v calls=%d", err, calls) } got, err := os.ReadFile(path) if err != nil || !bytes.Equal(got, before) { t.Fatalf("not restored: %v", err) } if !claimControlChanged(got, after) { t.Fatal("next repair would not retry") } } func TestClaimBadUnitSettingsStillRepairSystemUnits(t *testing.T) { etc := t.TempDir() if err := os.MkdirAll(filepath.Join(etc, "carlos"), 0755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(etc, "carlos", "host.env"), []byte("OTHER='multiline\n"), 0600); err != nil { t.Fatal(err) } units := filepath.Join(etc, "systemd", "system") if err := os.MkdirAll(units, 0755); err != nil { t.Fatal(err) } old := []byte("old control unit") if err := os.WriteFile(filepath.Join(units, claimControlUnit), old, 0644); err != nil { t.Fatal(err) } var calls []string ctl := func(args ...string) (string, error) { calls = append(calls, strings.Join(args, " ")); return "", nil } probe := func(args ...string) (string, error) { return "inactive\n", nil } ensureSystemUnits(etc, filepath.Join(t.TempDir(), "lock"), ctl, probe, t.Logf) got, err := os.ReadFile(filepath.Join(units, claimControlUnit)) want, _ := unitsFS.ReadFile("units/" + claimControlUnit) if err != nil || !bytes.Equal(got, want) { t.Fatal("control unit not restored to main on unknown settings") } for _, name := range systemUnitFiles { if name != claimControlUnit { if _, err := os.Stat(filepath.Join(units, name)); err != nil { t.Fatal(err) } } } if !strings.Contains(strings.Join(calls, "\n"), "daemon-reload") { t.Fatal("system units were not repaired") } if strings.Contains(strings.Join(calls, "\n"), "try-restart") { t.Fatal("unknown settings restarted control service") } } func TestPlan14RootRetryUnitDirectoryScope(t *testing.T) { raw := []byte("[Service]\nReadWritePaths=/run/carlos-control /run/systemd/system\nRestrictAddressFamilies=AF_UNIX\nMemoryMax=64M\n") legacy := claimUnitContent(claimControlUnit, raw, claimUnitSettings{enabled: true, data: "/data", opt: "/opt/carlos"}) if bytes.Contains(legacy, []byte(rootRetryProjection)) { t.Fatal("no-census unit received E3 write access") } census := claimUnitContent(claimControlUnit, raw, claimUnitSettings{enabled: true, rootRetry: true, data: "/data", opt: "/opt/carlos"}) if !bytes.Contains(census, []byte(" "+rootRetryProjection+"\n")) { t.Fatal("root retry projection is read-only in the controller") } if got := claimUnitContent(claimControlUnit, raw, claimUnitSettings{rootRetry: true}); !bytes.Equal(got, raw) { t.Fatal("disabled claims changed the unit") } }