carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1//go:build linux
2
3package console
4
5import (
6 "context"
7 "encoding/json"
8 "errors"
9 "fmt"
10 "os"
11 "os/exec"
12 "strconv"
13 "strings"
14 "testing"
15 "time"
16
17 "github.com/carlosframework/platform/internal/hostactors"
18 "github.com/carlosframework/platform/internal/hostfloor"
19 "github.com/carlosframework/platform/internal/release"
20 "github.com/carlosframework/platform/internal/status"
21)
22
23func TestPlan9Fix1ConsoleRestartReplacesDeadProcess(t *testing.T) {
24 bootRaw, err := os.ReadFile("/proc/sys/kernel/random/boot_id")
25 if err != nil {
26 t.Fatal(err)
27 }
28 boot := strings.TrimSpace(string(bootRaw))
29 child := exec.Command("sleep", "30")
30 if err := child.Start(); err != nil {
31 t.Fatal(err)
32 }
33 t.Cleanup(func() { _ = child.Process.Kill() })
34 stat, err := os.ReadFile(fmt.Sprintf("/proc/%d/stat", child.Process.Pid))
35 if err != nil {
36 t.Fatal(err)
37 }
38 fields := strings.Fields(string(stat[strings.LastIndexByte(string(stat), ')')+1:]))
39 namespace, err := hostReportPIDNamespace()
40 if err != nil {
41 t.Fatal(err)
42 }
43 oldID := fmt.Sprintf("%s:%s:%d:%s", boot, namespace, child.Process.Pid, fields[19])
44 nextID := hostReportProcessID(boot)
45 old := plan9Report(oldID, time.Now().Add(-time.Minute))
46 next := plan9Report(nextID, time.Now())
47 if err := hostReportCollision(old, next, time.Now()); err == nil {
48 t.Fatal("live predecessor allowed replacement")
49 }
50 _ = child.Process.Kill()
51 _ = child.Wait()
52 if err := hostReportCollision(old, next, time.Now()); err != nil {
53 t.Fatalf("restart blocked by dead predecessor: %v", err)
54 }
55 for _, id := range []string{"other:" + strconv.Itoa(child.Process.Pid) + ":1", boot + ":" + strconv.Itoa(child.Process.Pid) + ":bad"} {
56 if err := hostReportCollision(plan9Report(id, time.Now()), next, time.Now()); err == nil {
57 t.Fatalf("unproven predecessor accepted: %s", id)
58 }
59 }
60}
61
62type plan9ReportReleaseStore struct {
63 release.Store
64 rosterReads int
65 failWrite bool
66}
67
68func (s *plan9ReportReleaseStore) Get(ctx context.Context, key string) ([]byte, string, error) {
69 if key == hostactors.Key {
70 s.rosterReads++
71 }
72 return s.Store.Get(ctx, key)
73}
74func (s *plan9ReportReleaseStore) Put(ctx context.Context, key string, b []byte, etag string) (string, error) {
75 if s.failWrite && strings.HasPrefix(key, "status/consoles/") {
76 return "", errors.New("report unavailable")
77 }
78 return s.Store.Put(ctx, key, b, etag)
79}
80func TestPlan9Fix1ReportFailureStillReleasesActivation(t *testing.T) {
81 for _, failure := range []string{"write", "collision"} {
82 t.Run(failure, func(t *testing.T) {
83 s, d, target := activationFixture(t)
84 p, err := s.promoteHostUpdate(t.Context(), target, "router", "stable", release.SystemPromoteOptions{})
85 if err != nil || p.ActivationID == "" {
86 t.Fatal(p, err)
87 }
88 observed, err := d.Read(t.Context())
89 if err != nil {
90 t.Fatal(err)
91 }
92 observed.Roster.Consoles["console-2"] = hostactors.Console{Enrollment: strings.Repeat("c", 32), InstanceID: "console-2", Namespaces: []string{"platform"}, DeploymentBucket: d.Bucket}
93 raw, err := release.SignRootEnvelope(release.RootSigner(s.cfg.Key), observed.Roster)
94 if err != nil {
95 t.Fatal(err)
96 }
97 if _, err = d.Store.Put(t.Context(), hostactors.Key, raw, observed.ETag); err != nil {
98 t.Fatal(err)
99 }
100 s.cfg.HostConsoleID, s.cfg.ReleasePub = "console-2", d.PublicKey
101 previous := hostRunningImage
102 hostRunningImage = func() (hostfloor.Running, error) { return hostfloor.Running{BootID: "boot"}, nil }
103 t.Cleanup(func() { hostRunningImage = previous })
104 if failure == "collision" {
105 b, _ := json.Marshal(hostactors.Report{ProcessID: "foreign:22", GeneratedAt: time.Now()})
106 if _, err := d.Store.Put(t.Context(), "status/consoles/console-2.json", b, ""); err != nil {
107 t.Fatal(err)
108 }
109 }
110 boxRaw, boxETag, err := d.Store.Get(t.Context(), "status/boxes/box.json")
111 if err != nil {
112 t.Fatal(err)
113 }
114 var box status.Box
115 if err := json.Unmarshal(boxRaw, &box); err != nil {
116 t.Fatal(err)
117 }
118 box.Metrics.Release, box.Metrics.ReleaseTag = target.Version, target.ReleaseTag
119 box.Metrics.BinarySHA256 = target.Artifacts["linux-amd64"].SHA256
120 box.Metrics.ActivationID, box.Metrics.ActivationComplete = p.ActivationID, true
121 boxRaw, err = json.Marshal(box)
122 if err != nil {
123 t.Fatal(err)
124 }
125 if _, err := d.Store.Put(t.Context(), "status/boxes/box.json", boxRaw, boxETag); err != nil {
126 t.Fatal(err)
127 }
128 spy := &plan9ReportReleaseStore{Store: d.Store, failWrite: failure == "write"}
129 s.cfg.Releases = spy
130 ctx, cancel := context.WithTimeout(t.Context(), 100*time.Millisecond)
131 defer cancel()
132 s.StartHostReports(ctx)
133 after, err := d.Read(t.Context())
134 if err != nil {
135 t.Fatal(err)
136 }
137 if _, held := after.Roster.Activations[p.ActivationID]; held {
138 t.Fatal("completed activation remains reserved after report failure")
139 }
140 if spy.rosterReads < 3 {
141 t.Fatalf("ReleaseActivation was skipped after %s failure: roster reads=%d", failure, spy.rosterReads)
142 }
143 })
144 }
145}
146