//go:build linux package console import ( "context" "encoding/json" "errors" "fmt" "os" "os/exec" "strconv" "strings" "testing" "time" "github.com/carlosframework/platform/internal/hostactors" "github.com/carlosframework/platform/internal/hostfloor" "github.com/carlosframework/platform/internal/release" "github.com/carlosframework/platform/internal/status" ) func TestPlan9Fix1ConsoleRestartReplacesDeadProcess(t *testing.T) { bootRaw, err := os.ReadFile("/proc/sys/kernel/random/boot_id") if err != nil { t.Fatal(err) } boot := strings.TrimSpace(string(bootRaw)) child := exec.Command("sleep", "30") if err := child.Start(); err != nil { t.Fatal(err) } t.Cleanup(func() { _ = child.Process.Kill() }) stat, err := os.ReadFile(fmt.Sprintf("/proc/%d/stat", child.Process.Pid)) if err != nil { t.Fatal(err) } fields := strings.Fields(string(stat[strings.LastIndexByte(string(stat), ')')+1:])) namespace, err := hostReportPIDNamespace() if err != nil { t.Fatal(err) } oldID := fmt.Sprintf("%s:%s:%d:%s", boot, namespace, child.Process.Pid, fields[19]) nextID := hostReportProcessID(boot) old := plan9Report(oldID, time.Now().Add(-time.Minute)) next := plan9Report(nextID, time.Now()) if err := hostReportCollision(old, next, time.Now()); err == nil { t.Fatal("live predecessor allowed replacement") } _ = child.Process.Kill() _ = child.Wait() if err := hostReportCollision(old, next, time.Now()); err != nil { t.Fatalf("restart blocked by dead predecessor: %v", err) } for _, id := range []string{"other:" + strconv.Itoa(child.Process.Pid) + ":1", boot + ":" + strconv.Itoa(child.Process.Pid) + ":bad"} { if err := hostReportCollision(plan9Report(id, time.Now()), next, time.Now()); err == nil { t.Fatalf("unproven predecessor accepted: %s", id) } } } type plan9ReportReleaseStore struct { release.Store rosterReads int failWrite bool } func (s *plan9ReportReleaseStore) Get(ctx context.Context, key string) ([]byte, string, error) { if key == hostactors.Key { s.rosterReads++ } return s.Store.Get(ctx, key) } func (s *plan9ReportReleaseStore) Put(ctx context.Context, key string, b []byte, etag string) (string, error) { if s.failWrite && strings.HasPrefix(key, "status/consoles/") { return "", errors.New("report unavailable") } return s.Store.Put(ctx, key, b, etag) } func TestPlan9Fix1ReportFailureStillReleasesActivation(t *testing.T) { for _, failure := range []string{"write", "collision"} { t.Run(failure, func(t *testing.T) { s, d, target := activationFixture(t) p, err := s.promoteHostUpdate(t.Context(), target, "router", "stable", release.SystemPromoteOptions{}) if err != nil || p.ActivationID == "" { t.Fatal(p, err) } observed, err := d.Read(t.Context()) if err != nil { t.Fatal(err) } observed.Roster.Consoles["console-2"] = hostactors.Console{Enrollment: strings.Repeat("c", 32), InstanceID: "console-2", Namespaces: []string{"platform"}, DeploymentBucket: d.Bucket} raw, err := release.SignRootEnvelope(release.RootSigner(s.cfg.Key), observed.Roster) if err != nil { t.Fatal(err) } if _, err = d.Store.Put(t.Context(), hostactors.Key, raw, observed.ETag); err != nil { t.Fatal(err) } s.cfg.HostConsoleID, s.cfg.ReleasePub = "console-2", d.PublicKey previous := hostRunningImage hostRunningImage = func() (hostfloor.Running, error) { return hostfloor.Running{BootID: "boot"}, nil } t.Cleanup(func() { hostRunningImage = previous }) if failure == "collision" { b, _ := json.Marshal(hostactors.Report{ProcessID: "foreign:22", GeneratedAt: time.Now()}) if _, err := d.Store.Put(t.Context(), "status/consoles/console-2.json", b, ""); err != nil { t.Fatal(err) } } boxRaw, boxETag, err := d.Store.Get(t.Context(), "status/boxes/box.json") if err != nil { t.Fatal(err) } var box status.Box if err := json.Unmarshal(boxRaw, &box); err != nil { t.Fatal(err) } box.Metrics.Release, box.Metrics.ReleaseTag = target.Version, target.ReleaseTag box.Metrics.BinarySHA256 = target.Artifacts["linux-amd64"].SHA256 box.Metrics.ActivationID, box.Metrics.ActivationComplete = p.ActivationID, true boxRaw, err = json.Marshal(box) if err != nil { t.Fatal(err) } if _, err := d.Store.Put(t.Context(), "status/boxes/box.json", boxRaw, boxETag); err != nil { t.Fatal(err) } spy := &plan9ReportReleaseStore{Store: d.Store, failWrite: failure == "write"} s.cfg.Releases = spy ctx, cancel := context.WithTimeout(t.Context(), 100*time.Millisecond) defer cancel() s.StartHostReports(ctx) after, err := d.Read(t.Context()) if err != nil { t.Fatal(err) } if _, held := after.Roster.Activations[p.ActivationID]; held { t.Fatal("completed activation remains reserved after report failure") } if spy.rosterReads < 3 { t.Fatalf("ReleaseActivation was skipped after %s failure: roster reads=%d", failure, spy.rosterReads) } }) } }