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 "context"
5 "errors"
6 "os"
7 "strings"
8 "testing"
9
10 "github.com/carlosframework/platform/internal/release"
11)
12
13// Settings plan 4, Astra round 1 findings 1, 4 and 5: the held wake's answer
14// (heldTarget, wakeTarget) and the pre-exec write's revalidation.
15
16const astraHost = "a.example.com"
17
18// decided writes root's consumed file as a decision made for the route's
19// current update_settings column (Consumed.Settings, ruling R-HELD-PURE).
20func decided(t *testing.T, b *buildStates, c release.Consumed) {
21 t.Helper()
22 rt, err := b.reg.Get(astraHost)
23 if err != nil || rt == nil {
24 t.Fatalf("route: %v", err)
25 }
26 c.Settings = release.SettingsDigest(rt.UpdateSettings)
27 writeConsumed(t, b, c)
28}
29
30// installLive stages ver of km and points the host's live link at it, as
31// root's install leaves a box.
32func installLive(t *testing.T, b *buildStates, ver string) {
33 t.Helper()
34 if err := release.StageArtifact(b.optDir, "bab", ver, "km", []byte(ver)); err != nil {
35 t.Fatal(err)
36 }
37 if err := release.PointLive(b.optDir, astraHost, "bab", ver, "km"); err != nil {
38 t.Fatal(err)
39 }
40 if v, _ := release.LiveVersion(b.optDir, astraHost); v != ver {
41 t.Fatalf("live = %q, want %s", v, ver)
42 }
43}
44
45// Finding 4 (§4.3 rule 3; ruling R-HELD-PURE): a sleeper last ran B (major
46// 1); under auto, root cold-installed A (major 2). The member sets major 1.
47// Before root decides for the new settings, its consumed file still says
48// adopt, for the old column. The wake must not read that as "not held" and
49// run A against B's data: it holds on B and asks root. Root's decision for
50// the new column then decides: a major hold holds on B; an adoption (the head
51// is in major 1) is not held. No consumed file at all holds and asks too.
52func TestAMajorHoldIsNotBypassedBeforeRootDecidesForTheSettings(t *testing.T) {
53 t.Parallel()
54 b, _, reg := testBuildStates(t)
55 installLive(t, b, "A")
56 setCopy(t, reg, release.BuildStateCopy{Known: true, Replica: true, State: &release.BuildState{LastExecuted: "B"}})
57 setSettings(t, reg, release.UpdateSettings{Updates: release.UpdatesAuto, Channel: "stable"})
58 decided(t, b, release.Consumed{Reason: string(release.ReasonAdopt), Desired: "A", Detail: "A"})
59 if h := b.heldTarget(astraHost); h.Held {
60 t.Fatalf("auto, root decided adoption for these settings: %+v; want today's wake", h)
61 }
62 one := 1
63 setSettings(t, reg, release.UpdateSettings{Updates: release.UpdatesAuto, Channel: "stable", Major: &one})
64 if h := b.heldTarget(astraHost); !h.Held || !h.AskRoot || h.Target != "B" || h.Staged != nil {
65 t.Fatalf("major 1, root's decision is for the old settings: %+v; want held on B, asking root, never A", h)
66 }
67 decided(t, b, release.Consumed{Reason: string(release.ReasonMajor), Desired: "B", Detail: "B"})
68 if h := b.heldTarget(astraHost); !h.Held || h.AskRoot || h.Target != "B" {
69 t.Fatalf("root's major hold for these settings: %+v; want held on B", h)
70 }
71 decided(t, b, release.Consumed{Reason: string(release.ReasonAdopt), Desired: "C", Detail: "C"})
72 if h := b.heldTarget(astraHost); h.Held {
73 t.Fatalf("root's adoption for these settings: %+v; want today's wake", h)
74 }
75 if err := os.Remove(release.ConsumedPath(b.optDir, astraHost)); err != nil {
76 t.Fatal(err)
77 }
78 if h := b.heldTarget(astraHost); !h.Held || !h.AskRoot || h.Target != "B" {
79 t.Fatalf("no decision from root at all: %+v; want held on B, asking root", h)
80 }
81}
82
83// Finding 5 (§4.3 failure precedence, §6): a legacy manual instance with its
84// build state read and absent, T installed, and root's decision that T
85// failed. The installed-build fallback is failure-checked like
86// last_executed: held on T and failed, never "not held" (which ran T).
87//
88// It runs twice: with root's file tagged for the route's tenant, as root
89// writes it now, and with the file left untagged, as a settings-4 build
90// wrote it (ruling R28: an untagged file is read as before task 8).
91func TestAFailedInstalledBuildWithNoBuildStateIsHeldFailed(t *testing.T) {
92 t.Parallel()
93 for _, untagged := range []bool{false, true} {
94 b, _, reg := testBuildStates(t)
95 installLive(t, b, "T")
96 setSettings(t, reg, release.UpdateSettings{Updates: release.UpdatesManual, Channel: "stable"})
97 setCopy(t, reg, release.BuildStateCopy{Known: true, Replica: true})
98 write := func(c release.Consumed) {
99 if !untagged {
100 decided(t, b, c)
101 return
102 }
103 rt, _ := reg.Get(astraHost)
104 c.Settings = release.SettingsDigest(rt.UpdateSettings)
105 writeConsumedAsIs(t, b, c)
106 }
107 write(release.Consumed{Reason: string(release.ReasonFailed), Detail: "T"})
108 if h := b.heldTarget(astraHost); !h.Held || !h.Failed || h.Target != "T" || h.Staged != nil {
109 t.Fatalf("untagged %v: heldTarget = %+v; want T held and failed", untagged, h)
110 }
111 // Not failed: held on the installed build, as root's Target holds it.
112 write(release.Consumed{Reason: string(release.ReasonManual), Desired: "T", Detail: "T"})
113 if h := b.heldTarget(astraHost); !h.Held || h.Failed || h.Target != "T" {
114 t.Fatalf("untagged %v: heldTarget = %+v; want held on the installed T", untagged, h)
115 }
116 }
117}
118
119// stateGetFails fails every Get of a build state, as a bucket blip.
120type stateGetFails struct {
121 release.Store
122 err error
123}
124
125func (f stateGetFails) Get(ctx context.Context, key string) ([]byte, string, error) {
126 if strings.HasSuffix(key, "/build-state.json") {
127 return nil, "", f.err
128 }
129 return f.Store.Get(ctx, key)
130}
131
132// Finding 1, the wake's half (§4.3 stale-link refusal): a recovered
133// instance's copy says last_executed A, and A is live, while its durable
134// build state names B. The wake's refresh fails: the wake is refused, never
135// decided from the old copy (which runs A).
136func TestAHeldWakeWhoseRefreshFailedIsRefused(t *testing.T) {
137 b, s, reg := testBuildStates(t)
138 ctx := context.Background()
139 installLive(t, b, "A")
140 if err := put(t, b, ctx, astraHost, release.BuildState{LastExecuted: "B"}); err != nil {
141 t.Fatal(err)
142 }
143 setCopy(t, reg, release.BuildStateCopy{Known: true, Replica: true, State: &release.BuildState{LastExecuted: "A"}})
144 setSettings(t, reg, release.UpdateSettings{Updates: release.UpdatesManual, Channel: "stable"})
145 decided(t, b, release.Consumed{Reason: string(release.ReasonManual), Desired: "A", Detail: "A"})
146 b.storeFor = storesOf(map[string]release.Store{"hib": stateGetFails{Store: s, err: errors.New("s3: 503")}})
147 if h := b.wakeTarget(ctx, astraHost); !h.Held || !h.Unread || h.Target == "A" {
148 t.Fatalf("wakeTarget after a failed refresh = %+v; want refused as unread, never A", h)
149 }
150 // The registry keeps the failure, so the answer read again says the same.
151 if h := b.heldTarget(astraHost); !h.Unread {
152 t.Fatalf("heldTarget after the failed refresh = %+v; want unread", h)
153 }
154 // With the bucket back the wake targets the durable B.
155 b.storeFor = storesOf(map[string]release.Store{"hib": s})
156 if h := b.wakeTarget(ctx, astraHost); !h.Held || h.Unread || h.Target != "B" {
157 t.Fatalf("wakeTarget with the bucket back = %+v; want held on B", h)
158 }
159}
160
161// Finding 1, the write's half: the wake decided on a stale copy (A), and the
162// pre-exec write reads the durable state under the lease (B). It names a
163// held target other than the build about to run, so the record is refused
164// as stale and the object is not touched; nothing runs. B itself records,
165// and a route with default settings records whatever runs, as today.
166func TestThePreExecWriteRevalidatesTheHeldTarget(t *testing.T) {
167 b, s, reg := testBuildStates(t)
168 ctx := context.Background()
169 installLive(t, b, "A")
170 if err := put(t, b, ctx, astraHost, release.BuildState{LastExecuted: "B"}); err != nil {
171 t.Fatal(err)
172 }
173 setCopy(t, reg, release.BuildStateCopy{Known: true, Replica: true, State: &release.BuildState{LastExecuted: "A"}})
174 setSettings(t, reg, release.UpdateSettings{Updates: release.UpdatesManual, Channel: "stable"})
175 decided(t, b, release.Consumed{Reason: string(release.ReasonManual), Desired: "A", Detail: "A"})
176 err := b.beforeExec(ctx, astraHost, "A")
177 if err == nil || !strings.Contains(err.Error(), "B") {
178 t.Fatalf("beforeExec(A) over a durable B = %v; want a stale refusal naming B", err)
179 }
180 st, _, _ := getState(ctx, s, release.BuildStateKey("inst/"+astraHost), astraHost)
181 if st == nil || st.LastExecuted != "B" {
182 t.Fatalf("state after the refusal = %+v; want B untouched", st)
183 }
184 if err := b.beforeExec(ctx, astraHost, "B"); err != nil {
185 t.Fatalf("beforeExec(B) = %v; the held target records", err)
186 }
187 setSettings(t, reg, release.UpdateSettings{})
188 if err := b.beforeExec(ctx, astraHost, "A"); err != nil {
189 t.Fatalf("default settings: beforeExec(A) = %v; want today's record", err)
190 }
191}
192