//go:build linux && rootrunner package host import ( "encoding/json" "errors" "os" "os/exec" "strings" "testing" "github.com/carlosframework/platform/internal/controlsock" "github.com/carlosframework/platform/internal/hostguard" "github.com/carlosframework/platform/internal/rootstate" ) const lifecycleCrashPath = "/var/lib/carlos-claims/lifecycle-crash" func lifecycleCrash(t *testing.T, point string) { t.Helper() if os.Getenv("PLAN14_LIFECYCLE_CRASH") != point { return } if _, err := os.Stat(lifecycleCrashPath); err == nil { return } else if !errors.Is(err, os.ErrNotExist) { t.Fatal(err) } state, err := rootstate.Open("/var/lib/carlos-claims", false, false) lifecycleCheck(t, "crash checkpoint store", err) lifecycleCheck(t, "durable crash checkpoint", state.Write("lifecycle-crash", []byte(point))) os.Exit(86) } type lifecycleCrashStore struct { rootstate.Store t *testing.T } func (s lifecycleCrashStore) Write(name string, body []byte) error { if err := s.Store.Write(name, body); err != nil { return err } if name != lifecycleHost+".json" { return nil } var r struct { Installation hostguard.RootInstallation Retired []hostguard.Captured Barrier *hostguard.FenceTarget State struct { ActiveStamp uint64 `json:"active_stamp"` } } if err := json.Unmarshal(body, &r); err != nil { return err } if r.Barrier != nil { lifecycleCrash(s.t, "handover") } if r.State.ActiveStamp == 0 && r.Installation.Stamp != 0 && len(r.Retired) != 0 { lifecycleCrash(s.t, "retirement") } return nil } func lifecycleControlFaults(t *testing.T) { original := openClaimRootAuthority openClaimRootAuthority = func() (rootstate.Store, error) { s, err := original() if err != nil { return nil, err } return lifecycleCrashStore{s, t}, nil } t.Cleanup(func() { openClaimRootAuthority = original }) } func lifecycleStartControl(t *testing.T, endpoint string) func() { t.Helper() raw, err := unitsFS.ReadFile("units/carlos-instance-control.service") lifecycleCheck(t, "controller template", err) var lines []string for _, line := range strings.Split(string(raw), "\n") { key, _, _ := strings.Cut(line, "=") if key == "ExecStart" || key == "Requires" || key == "After" { continue } lines = append(lines, line) } lines = append(lines, "[Service]", "ExecStart=/opt/carlos/carlos -test.run=^TestPlan14LifecycleControlProcessPayload$ -test.v -test.timeout=12m", "Environment=PLAN14_LIFECYCLE_CONTROL=1", "Environment=PLAN14_LIFECYCLE_CRASH="+os.Getenv("PLAN14_LIFECYCLE_CRASH"), "Environment=CARLOS_S3_ENDPOINT="+endpoint, "ReadWritePaths=/data /opt/carlos /var/lib/carlos-claims /var/lib/carlos-release /sys/fs/cgroup "+lifecycleHome, "RestrictAddressFamilies=", "RestrictAddressFamilies=AF_UNIX AF_INET", "MemoryMax=512M", "StandardOutput=append:"+lifecycleHome+"/controller.log", "StandardError=append:"+lifecycleHome+"/controller.log") path := "/run/systemd/system/carlos-instance-control.service" f, err := os.OpenFile(path, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0644) lifecycleCheck(t, "exclusive controller unit", err) _, err = f.WriteString(strings.Join(lines, "\n")) f.Close() lifecycleCheck(t, "controller unit", err) out, err := exec.Command("systemctl", "daemon-reload").CombinedOutput() lifecycleCheck(t, string(out), err) start := func() { exec.Command("systemctl", "stop", "carlos-instance-control.service").Run() os.Remove("/run/carlos-control/lifecycle-ready") os.Remove(controlsock.Path) out, err := exec.Command("systemctl", "start", "carlos-instance-control.service").CombinedOutput() lifecycleCheck(t, string(out), err) } t.Cleanup(func() { exec.Command("systemctl", "stop", "carlos-instance-control.service").Run() os.Remove(path) exec.Command("systemctl", "daemon-reload").Run() body, _ := os.ReadFile(lifecycleHome + "/controller.log") t.Logf("controller log: %s", body) }) start() return start }