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 "errors"
5 "os"
6 "path/filepath"
7 "slices"
8 "strings"
9 "testing"
10
11 "github.com/carlosframework/platform/internal/hostbox"
12 "github.com/carlosframework/platform/internal/moves"
13 "github.com/carlosframework/platform/internal/provision"
14)
15
16// The move's env pin (home part 2, Task 2b). The source pins the report
17// digest of the environment its ExecBackend would launch (sourceReport,
18// copied into the target's report as Inputs.EnvReport); the probe and the
19// committed wake on the target check the digest of the target's own
20// composed launch (claimMoveLaunchSource). Each box owns some variables of
21// that environment (agentOwnedVars: its region, its control socket, its
22// purge channel, the host row's name), so the two compositions differ in
23// those whatever the instance carries. Both sides hash the move-portable
24// env: the composition without the box-owned variables (moves.BoxOwned).
25
26// TestProbeEnvPinIsMovePortable: a landing whose source and target compose
27// their environments through the production composer, each with its own
28// region, passes the production probe and commits, named to named and
29// named to home.
30func TestProbeEnvPinIsMovePortable(t *testing.T) {
31 for name, opts := range map[string][]p1Option{
32 "named to named": {realLaunchEnv},
33 "named to home": {intoHome, realLaunchEnv},
34 } {
35 t.Run(name, func(t *testing.T) {
36 h := newCloseHarness(t, opts...)
37 p := h.productionProbe(t)
38 h.land(t)
39 if m := h.read(t, p1Host).Move; m.Reason != "" {
40 t.Fatalf("the landing stopped for a rollback: %q", m.Reason)
41 }
42 if len(p.admissions) != 1 || p.admissions[0].Box != h.dstBox {
43 t.Fatalf("the probe's admissions: %+v", p.admissions)
44 }
45 h.committedAndOpen(t)
46 // The probe starts through root's launch check: the reserved
47 // LaunchInputs is the hash of the launch root is asked to start,
48 // an instance whose files set app variables included.
49 if !slices.Contains(p.selected.Env, "APP_MODE=prod") {
50 t.Fatalf("the launch carries no instance app variable: %v", p.selected.Env)
51 }
52 root, err := rootLaunchSpec(p.selected).Hash()
53 if err != nil || p.attempt.LaunchInputs == "" || root != p.attempt.LaunchInputs {
54 t.Fatalf("root's spec hash %s %v, the reserved LaunchInputs %s", root, err, p.attempt.LaunchInputs)
55 }
56 })
57 }
58}
59
60// TestProbeEnvPinRefusesAnInstanceChange: the pin still binds the
61// instance's own variables. An app variable that differs on the target
62// (its installed .env changed after the pin) refuses the probe, and the
63// landing stops for the configuration-changed rollback.
64func TestProbeEnvPinRefusesAnInstanceChange(t *testing.T) {
65 for name, opts := range map[string][]p1Option{
66 "named to named": {realLaunchEnv},
67 "named to home": {intoHome, realLaunchEnv},
68 } {
69 t.Run(name, func(t *testing.T) {
70 h := newCloseHarness(t, opts...)
71 p := h.productionProbe(t)
72 if err := os.WriteFile(filepath.Join(h.tdir, p1Host+".env"), []byte("APP_MODE=staging\n"), 0o600); err != nil {
73 t.Fatal(err)
74 }
75 h.land(t)
76 if len(p.admissions) != 0 {
77 t.Fatalf("a changed instance env admitted: %+v", p.admissions)
78 }
79 if m := h.read(t, p1Host).Move; m.Reason != provision.RollbackConfigChanged || m.State == provision.MoveCommitted {
80 t.Fatalf("the landing after a changed instance env: state %s reason %q", m.State, m.Reason)
81 }
82 })
83 }
84}
85
86// TestProbeEnvPinCoversAnInstanceBoxVariable (fix round 2): a box
87// variable such as HOSTNAME is the box's only while the box supplies it.
88// The agent never overwrites it, and childEnv lays the instance's files
89// over the inherited value, so one the instance's own .env sets is what
90// runs and is pinned like an app variable. Through the production
91// composition on both boxes:
92// - the target's .env gains HOSTNAME=b after the pin (the source's came
93// from its box): refused;
94// - the source's .env sets HOSTNAME=a and the target's still does after
95// the landing: the pin holds and the landing commits;
96// - the target's .env changes it to HOSTNAME=b after the pin: refused.
97func TestProbeEnvPinCoversAnInstanceBoxVariable(t *testing.T) {
98 for name, opts := range map[string][]p1Option{
99 "named to named": {realLaunchEnv},
100 "named to home": {intoHome, realLaunchEnv},
101 } {
102 t.Run(name, func(t *testing.T) {
103 refused := func(t *testing.T, h *finalHarness, p *prodProbe) {
104 t.Helper()
105 h.land(t)
106 if len(p.admissions) != 0 {
107 t.Fatalf("a changed instance HOSTNAME admitted: %+v", p.admissions)
108 }
109 if m := h.read(t, p1Host).Move; m.Reason != provision.RollbackConfigChanged || m.State == provision.MoveCommitted {
110 t.Fatalf("the landing after a changed instance HOSTNAME: state %s reason %q", m.State, m.Reason)
111 }
112 }
113 t.Run("the target's env file adds HOSTNAME", func(t *testing.T) {
114 h := newCloseHarness(t, opts...)
115 p := h.productionProbe(t)
116 if err := os.WriteFile(filepath.Join(h.tdir, p1Host+".env"), []byte("APP_MODE=prod\nHOSTNAME=b\n"), 0o600); err != nil {
117 t.Fatal(err)
118 }
119 refused(t, h, p)
120 })
121 withHostname := append([]p1Option{sourceEnvFile("APP_MODE=prod\nHOSTNAME=a\n")}, opts...)
122 t.Run("the instance's HOSTNAME is carried and pinned", func(t *testing.T) {
123 h := newCloseHarness(t, withHostname...)
124 src := h.sourceReport(t).Env
125 if len(src.Instance) != 1 || src.Instance[0] != "HOSTNAME" {
126 t.Fatalf("the source's report does not name the instance's HOSTNAME: %v", src.Instance)
127 }
128 p := h.productionProbe(t)
129 h.land(t)
130 if len(p.admissions) != 1 {
131 t.Fatalf("the probe's admissions: %+v (move %+v)", p.admissions, h.read(t, p1Host).Move)
132 }
133 h.committedAndOpen(t)
134 root, err := rootLaunchSpec(p.selected).Hash()
135 if err != nil || root != p.attempt.LaunchInputs || !slices.Contains(p.selected.Env, "HOSTNAME=a") {
136 t.Fatalf("root's spec hash %s %v, the reserved LaunchInputs %s", root, err, p.attempt.LaunchInputs)
137 }
138 })
139 t.Run("a box-only HOSTNAME differs and is left out", func(t *testing.T) {
140 t.Setenv("HOSTNAME", "source-box")
141 h := newCloseHarness(t, opts...)
142 if inst := h.sourceReport(t).Env.Instance; len(inst) != 0 {
143 t.Fatalf("an inherited HOSTNAME is named the instance's: %v", inst)
144 }
145 p := h.productionProbe(t)
146 t.Setenv("HOSTNAME", "target-box")
147 h.land(t)
148 if len(p.admissions) != 1 {
149 t.Fatalf("a box-only HOSTNAME refused the probe: %+v (move %+v)", p.admissions, h.read(t, p1Host).Move)
150 }
151 h.committedAndOpen(t)
152 root, err := rootLaunchSpec(p.selected).Hash()
153 if err != nil || root != p.attempt.LaunchInputs || !slices.Contains(p.selected.Env, "HOSTNAME=target-box") {
154 t.Fatalf("root's spec hash %s %v, the reserved LaunchInputs %s, env %v", root, err, p.attempt.LaunchInputs, p.selected.Env)
155 }
156 })
157 t.Run("the target's env file changes HOSTNAME", func(t *testing.T) {
158 h := newCloseHarness(t, withHostname...)
159 p := h.productionProbe(t)
160 if err := os.WriteFile(filepath.Join(h.tdir, p1Host+".env"), []byte("APP_MODE=prod\nHOSTNAME=b\n"), 0o600); err != nil {
161 t.Fatal(err)
162 }
163 refused(t, h, p)
164 })
165 })
166 }
167}
168
169// TestRecoveryEnvCheckIsTheWholeLocalEnv (fix round 2): the recovery's
170// inputs are recorded on the recovering box just before the wake
171// (movesrecoveryprobe.go, recoveryInputs) and checked against that box's
172// launch, so the check keeps the whole composed env (localEnvDigest, every
173// variable but CARLOS_MOVE_PROBE). A box-owned variable that changed
174// between the record and the wake is refused, as is an instance variable.
175func TestRecoveryEnvCheckIsTheWholeLocalEnv(t *testing.T) {
176 recorded := []string{"A=1", "CARLOS_REGION=ie", "CARLOS_CONTROL_SOCKET=/run/carlos/ie.sock.control", "CARLOS_HOST=" + poHost}
177 for _, kind := range []string{rcClean, rcDirty, rcRevert} {
178 t.Run(kind, func(t *testing.T) {
179 f := newRCFixture(t, kind)
180 f.inputs.EnvReport = localEnvDigest(poEnvKey, recorded)
181 f.launch.Env = append(append([]string(nil), recorded...), "CARLOS_MOVE_PROBE=0")
182 if _, err := f.owners.Select(t.Context(), f.route, f.read(), f.policy()); err != nil {
183 t.Fatalf("the recorded env, launched: %v", err)
184 }
185 for name, launched := range map[string][]string{
186 "a box-owned variable appears": append(append([]string(nil), recorded...), "CARLOS_PURGE=header"),
187 "a box-owned variable changes": {"A=1", "CARLOS_REGION=uk", "CARLOS_CONTROL_SOCKET=/run/carlos/ie.sock.control", "CARLOS_HOST=" + poHost},
188 "a box variable appears": append(append([]string(nil), recorded...), "HOSTNAME=box"),
189 "an instance variable changes": {"A=2", "CARLOS_REGION=ie", "CARLOS_CONTROL_SOCKET=/run/carlos/ie.sock.control", "CARLOS_HOST=" + poHost},
190 } {
191 f.launch.Env = launched
192 if _, err := f.owners.Select(t.Context(), f.route, f.read(), f.policy()); !errors.Is(err, hostbox.ErrChanged) || !strings.Contains(err.Error(), "launch environment") {
193 t.Fatalf("%s: %v", name, err)
194 }
195 }
196 })
197 }
198}
199
200// TestRecoveryAndCutInputsDigestTheirOwnEnv (fix round 2): on one box, over
201// the production composition, the cut's currentInputs pins the
202// move-portable digest the target's probe checks (envDigest with the
203// instance's layer names), and the recovery's recoveryInputs records the
204// whole local digest its own check uses (localEnvDigest). The two differ:
205// the box's own variables are in one and not the other.
206func TestRecoveryAndCutInputsDigestTheirOwnEnv(t *testing.T) {
207 h := newCutHarness(t, realLaunchEnv)
208 rec := h.start(t, "manual")
209 ctx := t.Context()
210 w := h.w()
211 cur, err := w.currentInputs(ctx, h.s, rec)
212 if err != nil {
213 t.Fatal(err)
214 }
215 rin, err := w.recoveryInputs(ctx, h.s, rec)
216 if err != nil {
217 t.Fatal(err)
218 }
219 env, instance, err := h.cfg.LaunchEnv(p1Host)
220 if err != nil {
221 t.Fatal(err)
222 }
223 if env[moves.RegionVar] != "ie" || env["CARLOS_HOST"] != p1Host {
224 t.Fatalf("the composition carries no box-owned variables: %v", env[moves.RegionVar])
225 }
226 list := make([]string, 0, len(env))
227 for k, v := range env {
228 list = append(list, k+"="+v)
229 }
230 var names []string
231 for k := range instance {
232 names = append(names, k)
233 }
234 key, err := moves.ReportKeyFromSecrets(h.cfg.SecretsKey)
235 if err != nil {
236 t.Fatal(err)
237 }
238 if cur.EnvReport != envDigest(key, list, names...) {
239 t.Fatal("currentInputs is not the probe's move-portable digest")
240 }
241 if rin.EnvReport != localEnvDigest(key, list) {
242 t.Fatal("recoveryInputs is not the recovery check's local digest")
243 }
244 if cur.EnvReport == rin.EnvReport {
245 t.Fatal("the recovery's digest left out the box's own variables")
246 }
247 if rin.Release != cur.Release || rin.OverlayETag != cur.OverlayETag || len(rin.BundleETags) != len(cur.BundleETags) {
248 t.Fatalf("the recovery's other inputs differ: %+v %+v", rin, cur)
249 }
250}
251
252// envDigest is a test's move-portable pin of a launch's K=V list
253// (CARLOS_MOVE_PROBE dropped), instance naming what the instance's own
254// layers supplied: what the production launch source's report carries
255// (claimMoveLaunchSource, moves.ReportLaunch).
256func envDigest(key []byte, env []string, instance ...string) string {
257 supplied := map[string]bool{}
258 for _, k := range instance {
259 supplied[k] = true
260 }
261 return moves.PortableDigest(key, launchVars(env), supplied)
262}
263