carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package host
2
3import (
4 "crypto/ed25519"
5 "os"
6 "path/filepath"
7 "testing"
8
9 "github.com/carlosframework/platform/internal/hostfloor"
10 "github.com/carlosframework/platform/internal/hostwrite"
11 "github.com/carlosframework/platform/internal/provision"
12 "github.com/carlosframework/platform/internal/release"
13 "github.com/carlosframework/platform/internal/rootparticipation"
14 "github.com/carlosframework/platform/internal/rootstate"
15)
16
17func TestPlan14Fix2LiveSafety(t *testing.T) {
18 if !plan14RootFixture(t) {
19 return
20 }
21 for _, scenario := range []string{"console update with empty history", "two consecutive updates", "incident recipe", "failed forward fix", "crash rollback", "successful restart crash before settle"} {
22 t.Run(scenario, func(t *testing.T) {
23 e, rt, base, c, _ := plan14AdoptionFixture(t)
24 u := &r2Units{plan14RestartUnits: base}
25 c.legacy, c.legacyReady = legacyRootRestart, c.ready
26 if err := e.store.Delete(t.Context(), provision.RecordKey(rt.Account, rt.App, rt.Host)); err != nil {
27 t.Fatal(err)
28 }
29 rt.Account = ""
30 if err := e.root.reg.Upsert(rt); err != nil {
31 t.Fatal(err)
32 }
33 source := productionProofBindingSource(e.store, u, u.opt)
34 source.namedPath = filepath.Join(t.TempDir(), "named-units.json")
35 source.unitDirs = []string{filepath.Dir(u.props.FragmentPath)}
36 source.healthy = nil
37 makeProofAncestorsPublic(t, source.namedPath)
38 put := func(path string, raw []byte) {
39 t.Helper()
40 if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
41 t.Fatal(err)
42 }
43 if err := os.WriteFile(path, raw, 0644); err != nil {
44 t.Fatal(err)
45 }
46 makeProofAncestorsPublic(t, path)
47 }
48 put(filepath.Join(u.opt, "system/guard/console-ready.json"), []byte(release.LiveLink(u.opt, rt.Host)))
49 drop := filepath.Join(source.unitDirs[0], "console.service.d/90-host-floor.conf")
50 put(drop, []byte(hostfloor.ConsoleGuardDropIn(release.LiveLink(u.opt, rt.Host))))
51 u.props.DropInPaths = []string{drop}
52 u.props.ExecPath = "/opt/carlos/system/guard/carlos"
53 put(u.props.ExecPath, []byte("guard"))
54 makeProofAncestorsPublic(t, u.exe)
55 makeProofAncestorsPublic(t, u.props.FragmentPath)
56 c.resolve, c.host, c.rollback, c.prepareRollback, c.dispatch, c.targetFile = source.resolve, source.forHost, source.rollback, source.prepareRollback, source.needsRestart, rootstate.OpenTrustedFile
57 pub, key, err := ed25519.GenerateKey(nil)
58 if err != nil {
59 t.Fatal(err)
60 }
61 if err := e.store.Delete(t.Context(), release.ChannelKey("bab", "console", rt.Channel)); err != nil {
62 t.Fatal(err)
63 }
64 promote := func(v string) {
65 t.Helper()
66 if err := release.Promote(t.Context(), e.store, key, "bab", "console", rt.Channel, v); err != nil {
67 t.Fatal(err)
68 }
69 }
70 promote("v1")
71 promote("v2")
72 if scenario == "crash rollback" {
73 u.crashNew = true
74 }
75 if scenario == "failed forward fix" {
76 u.props.ActiveState, u.props.SubState, u.props.MainPID = "failed", "failed", 0
77 }
78 if scenario == "incident recipe" {
79 if err := release.StageArtifact(u.opt, "", "v2", "console", []byte("new build")); err != nil {
80 t.Fatal(err)
81 }
82 if err := release.PointLive(u.opt, rt.Host, "", "v2", "console"); err != nil {
83 t.Fatal(err)
84 }
85 if err := e.root.reg.SetVersions(rt.Host, "v2", "v1"); err != nil {
86 t.Fatal(err)
87 }
88 u.exe = release.VersionPath(u.opt, "", "v2", "console")
89 u.props.ActiveState, u.props.SubState, u.props.MainPID = "failed", "failed", 0
90 promote("v1")
91 }
92 restart := rootRestartRouteWith(e.root.reg, u, u.opt, func(string) string { return "5s" }, nil, c)
93 rollback := rollbackUnitRestart(e.root.reg, u, u.opt, func(string) string { return "5s" }, c)
94 if scenario == "successful restart crash before settle" {
95 u.afterEffect = func() { panic("crash") }
96 func() {
97 defer func() {
98 if recover() != "crash" {
99 t.Fatal("missing crash")
100 }
101 }()
102 _ = release.Pass(t.Context(), e.root.reg, e.store, pub, u.opt, restart, release.WithRestartFailurePropagation())
103 }()
104 u.afterEffect = nil
105 before := len(u.effects)
106 if err := recoverRootRestarts(t.Context(), u, u.opt, func(string) string { return "5s" }, c, hostwrite.Config{}); err != nil {
107 t.Fatal(err)
108 }
109 if len(u.effects) != before || u.exe != release.VersionPath(u.opt, "", "v2", "console") {
110 t.Fatal("recovery repeated restart", u.effects)
111 }
112 return
113 }
114 run := func() error {
115 return release.Pass(t.Context(), e.root.reg, e.store, pub, u.opt, restart, release.WithRestartFailurePropagation(), release.WithRollbackRestart(rollback))
116 }
117 err = run()
118 if err != nil {
119 t.Fatal(err)
120 }
121 want := "v2"
122 if scenario == "incident recipe" {
123 want = "v1"
124 }
125 if scenario == "crash rollback" || scenario == "incident recipe" || scenario == "failed forward fix" {
126 // Main cannot settle a guard ExecStart against the live-link
127 // stamp. A stopped unit needs the operator's reset and start.
128 if u.props.MainPID != 0 {
129 t.Fatal("main path woke a stopped guard unit", u.effects)
130 }
131 if live, err := release.LiveVersion(u.opt, rt.Host); err != nil || live != want {
132 t.Fatal("main pointer result", live, err)
133 }
134 if scenario == "crash rollback" {
135 return
136 }
137 if err := u.ResetFailed(rt.Unit); err != nil {
138 t.Fatal(err)
139 }
140 if err := u.Start(rt.Unit); err != nil {
141 t.Fatal(err)
142 }
143 }
144 if u.props.MainPID <= 0 || u.exe != release.VersionPath(u.opt, "", want, "console") {
145 t.Fatalf("not serving %s: %+v", want, u)
146 }
147 if scenario == "two consecutive updates" {
148 m := release.Manifest{App: "console", Version: "v3", Kind: "binary", Artifacts: map[string]release.Artifact{release.AdoptPlatform(): {}}}
149 if err := release.Ship(t.Context(), e.store, "bab", m, map[string][]byte{release.AdoptPlatform(): []byte("third build")}); err != nil {
150 t.Fatal(err)
151 }
152 promote("v3")
153 if err := run(); err != nil {
154 t.Fatal(err)
155 }
156 if u.exe != release.VersionPath(u.opt, "", "v3", "console") || len(u.effects) != 2 {
157 t.Fatal("second update", u.effects, u.exe)
158 }
159 }
160 })
161 }
162}
163
164func TestPlan14Fix2PlatformUpdateClaimsOffNoCensus(t *testing.T) {
165 if !plan14RootFixture(t) {
166 return
167 }
168 for _, flag := range []string{"", "1"} {
169 t.Run("flag="+flag, func(t *testing.T) {
170 t.Setenv("CARLOS_HOST_CLAIM", flag)
171 l, required, err := rootparticipation.OpenForUpdate()
172 if l.State != nil {
173 l.State.Close()
174 }
175 if err != nil || required {
176 t.Fatalf("unactivated census blocked update: %v %v", required, err)
177 }
178 if legacyActivationReady(t.Context()) == nil {
179 t.Fatal("claims activated without census and verifier")
180 }
181 guard := hostfloor.Disk{Dir: t.TempDir()}
182 data := t.TempDir()
183 if err := os.Mkdir(filepath.Join(data, ".hostexec"), 0700); err != nil {
184 t.Fatal(err)
185 }
186 if err := guard.Begin(t.Context(), "update", data); err != nil {
187 t.Fatal("real update path", err)
188 }
189 if _, err := os.Stat(filepath.Join(guard.Dir, "update-begun.json")); err != nil {
190 t.Fatal(err)
191 }
192 if _, err := os.Stat(rootparticipation.Namespace); !os.IsNotExist(err) {
193 t.Fatal("update created census", err)
194 }
195 })
196 }
197}
198