carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package release
2
3import (
4 "context"
5 "encoding/json"
6 "errors"
7 "os"
8 "os/exec"
9 "path/filepath"
10 "strings"
11 "testing"
12 "time"
13)
14
15// tinyProgram builds a stripped linux/arm64 Go binary whose main package is
16// modPath, exactly as the console is built (-s -w), at test time. Nothing is
17// checked in.
18func tinyProgram(t *testing.T, modPath string) []byte {
19 t.Helper()
20 if testing.Short() {
21 t.Skip("builds a binary")
22 }
23 dir := t.TempDir()
24 must := func(err error) {
25 t.Helper()
26 if err != nil {
27 t.Fatal(err)
28 }
29 }
30 must(os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module "+modPath+"\n\ngo 1.22\n"), 0o644))
31 must(os.WriteFile(filepath.Join(dir, "main.go"), []byte("package main\nfunc main() {}\n"), 0o644))
32 out := filepath.Join(dir, "bin")
33 cmd := exec.Command("go", "build", "-trimpath", "-ldflags=-s -w", "-o", out, ".")
34 cmd.Dir = dir
35 cmd.Env = append(os.Environ(), "GOOS=linux", "GOARCH=arm64", "CGO_ENABLED=0", "GOFLAGS=-buildvcs=false")
36 if b, err := cmd.CombinedOutput(); err != nil {
37 t.Fatalf("build %s: %v\n%s", modPath, err, b)
38 }
39 b, err := os.ReadFile(out)
40 must(err)
41 return b
42}
43
44func TestArtifactProgram(t *testing.T) {
45 if got := ArtifactProgram(tinyProgram(t, "example.com/console")); got != "example.com/console" {
46 t.Fatalf("program = %q", got)
47 }
48 if got := ArtifactProgram([]byte("#!/bin/sh\necho hi\n")); got != "" {
49 t.Fatalf("non-Go program = %q, want empty", got)
50 }
51}
52
53func TestReleaseProgramDisagreement(t *testing.T) {
54 _, err := ReleaseProgram(map[string][]byte{
55 "linux-arm64": tinyProgram(t, "example.com/console"),
56 "linux-amd64": tinyProgram(t, "example.com/carlos"),
57 })
58 if err == nil || !strings.Contains(err.Error(), "example.com/console") || !strings.Contains(err.Error(), "example.com/carlos") {
59 t.Fatalf("err = %v, want both programs named", err)
60 }
61}
62
63func shipWithProgram(t *testing.T, s Store, ver string, art []byte, allow bool) error {
64 t.Helper()
65 files := map[string][]byte{adoptPlatform: art}
66 m := Manifest{Schema: 1, App: "console", Version: ver, Kind: "binary",
67 Artifacts: map[string]Artifact{adoptPlatform: {}}} // Ship fills file, sha256, size
68 if err := CheckShipProgram(context.Background(), s, "bab", &m, files, allow); err != nil {
69 return err
70 }
71 return Ship(context.Background(), s, "bab", m, files)
72}
73
74func TestCheckShipProgram(t *testing.T) {
75 s := NewDirStore(t.TempDir())
76 console := tinyProgram(t, "example.com/console")
77 cli := tinyProgram(t, "example.com/carlos")
78 if err := shipWithProgram(t, s, "v1", console, false); err != nil {
79 t.Fatalf("first release: %v", err)
80 }
81 err := shipWithProgram(t, s, "v2", cli, false)
82 var pm *ProgramMismatchError
83 if !errors.As(err, &pm) || pm.Got != "example.com/carlos" || pm.Want != "example.com/console" {
84 t.Fatalf("wrong program ship = %v, want ProgramMismatchError", err)
85 }
86 if !strings.Contains(err.Error(), "carlos ship --program-change") {
87 t.Fatalf("refusal %q does not name the override", err)
88 }
89 if err := shipWithProgram(t, s, "v3", cli, true); err != nil {
90 t.Fatalf("--program-change ship: %v", err)
91 }
92 m, err := GetManifest(context.Background(), s, "bab", "console", "v3")
93 if err != nil || m.Program != "example.com/carlos" || !m.ProgramChange {
94 t.Fatalf("v3 manifest = %+v, %v; want program recorded and program_change true", m, err)
95 }
96 if err := shipWithProgram(t, s, "v4", []byte("#!/bin/sh\n"), false); err != nil {
97 t.Fatalf("non-Go artifact refused: %v", err)
98 }
99}
100
101// sameProgram is b with one byte appended: different bytes (a different
102// sha256, so a different release) whose build info still names the same
103// program.
104func sameProgram(b []byte) []byte {
105 return append(b[:len(b):len(b)], 0)
106}
107
108// Ruling G1: --program-change is recorded whenever it is given, even when the
109// newest release already has the same program. Ship A, then B1 with the flag,
110// then B2 with the flag: B2 must carry the consent, because a box that missed
111// B1 is still on A and decides by its own history.
112func TestCheckShipProgramRecordsTheFlagWhenThePredecessorAgrees(t *testing.T) {
113 s := NewDirStore(t.TempDir())
114 a, b1 := tinyProgram(t, "example.com/console"), tinyProgram(t, "example.com/carlos")
115 b2 := sameProgram(b1)
116 if err := shipWithProgram(t, s, "va", a, false); err != nil {
117 t.Fatal(err)
118 }
119 if err := shipWithProgram(t, s, "vb1", b1, true); err != nil {
120 t.Fatal(err)
121 }
122 if err := shipWithProgram(t, s, "vb2", b2, true); err != nil {
123 t.Fatal(err)
124 }
125 m, err := GetManifest(context.Background(), s, "bab", "console", "vb2")
126 if err != nil || m.Program != "example.com/carlos" || !m.ProgramChange {
127 t.Fatalf("vb2 manifest = %+v, %v; want program_change true though vb1 already names the program", m, err)
128 }
129 // And without the flag a same-program ship records no change.
130 if err := shipWithProgram(t, s, "vb3", sameProgram(b2), false); err != nil {
131 t.Fatal(err)
132 }
133 if m, err := GetManifest(context.Background(), s, "bab", "console", "vb3"); err != nil || m.ProgramChange {
134 t.Fatalf("vb3 manifest = %+v, %v; want no program_change", m, err)
135 }
136}
137
138// Every Go artifact of a release agreeing on one program passes, and the
139// manifest names it.
140func TestCheckShipProgramAgreeingArtifactsPass(t *testing.T) {
141 s := NewDirStore(t.TempDir())
142 arm := tinyProgram(t, "example.com/console")
143 files := map[string][]byte{"linux-arm64": arm, "linux-amd64": sameProgram(arm)}
144 m := Manifest{Schema: 1, App: "console", Version: "v1", Kind: "binary",
145 Artifacts: map[string]Artifact{"linux-arm64": {}, "linux-amd64": {}}}
146 if err := CheckShipProgram(context.Background(), s, "bab", &m, files, false); err != nil {
147 t.Fatalf("agreeing artifacts: %v", err)
148 }
149 if m.Program != "example.com/console" {
150 t.Fatalf("program = %q, want example.com/console", m.Program)
151 }
152}
153
154// A static release is never judged or stamped, whatever its bytes are.
155func TestCheckShipProgramSkipsStatic(t *testing.T) {
156 s := NewDirStore(t.TempDir())
157 if err := shipWithProgram(t, s, "v1", tinyProgram(t, "example.com/console"), false); err != nil {
158 t.Fatal(err)
159 }
160 files := map[string][]byte{"site.tar.gz": tinyProgram(t, "example.com/carlos")}
161 m := Manifest{Schema: 1, App: "console", Version: "v2", Kind: "static",
162 Artifacts: map[string]Artifact{"site.tar.gz": {}}}
163 if err := CheckShipProgram(context.Background(), s, "bab", &m, files, false); err != nil {
164 t.Fatalf("static release judged: %v", err)
165 }
166 if m.Program != "" || m.ProgramChange {
167 t.Fatalf("static manifest stamped: program %q change %v", m.Program, m.ProgramChange)
168 }
169}
170
171// putManifest writes a raw manifest for app console under account bab.
172func putManifest(t *testing.T, s Store, ver string, m Manifest) {
173 t.Helper()
174 raw, err := json.Marshal(m)
175 if err != nil {
176 t.Fatal(err)
177 }
178 if _, err := s.Put(context.Background(), ManifestKey("bab", "console", ver), raw, ""); err != nil {
179 t.Fatal(err)
180 }
181}
182
183// PreviousProgram picks by built_at, not by the order the store lists
184// versions in. "z-new" was built last, but its manifest object is the older
185// one (a re-upload, a copy between buckets), so the store's own ranking —
186// newest object first, ties by version — lists "a-old" first.
187func TestPreviousProgramOrdersByBuiltAt(t *testing.T) {
188 root := t.TempDir()
189 s := NewDirStore(root)
190 art := map[string]Artifact{adoptPlatform: {File: adoptPlatform, SHA256: strings.Repeat("a", 64), Size: 1}}
191 putManifest(t, s, "z-new", Manifest{Schema: 1, App: "console", Version: "z-new", Kind: "binary",
192 BuiltAt: "2026-06-01T00:00:00Z", Artifacts: art, Program: "example.com/new"})
193 putManifest(t, s, "a-old", Manifest{Schema: 1, App: "console", Version: "a-old", Kind: "binary",
194 BuiltAt: "2026-01-01T00:00:00Z", Artifacts: art, Program: "example.com/old"})
195 older, newer := time.Now().Add(-time.Hour), time.Now()
196 for ver, mod := range map[string]time.Time{"z-new": older, "a-old": newer} {
197 p := filepath.Join(root, filepath.FromSlash(ManifestKey("bab", "console", ver)))
198 if err := os.Chtimes(p, mod, mod); err != nil {
199 t.Fatal(err)
200 }
201 }
202 if vers, _, err := VersionCandidates(context.Background(), s, "bab", "console", 20); err != nil || len(vers) != 2 || vers[0] != "a-old" {
203 t.Fatalf("listing = %v, %v; the test needs a-old listed first", vers, err)
204 }
205 got, ver, err := PreviousProgram(context.Background(), s, "bab", "console")
206 if err != nil || got != "example.com/new" || ver != "z-new" {
207 t.Fatalf("PreviousProgram = %q %q %v, want example.com/new from z-new", got, ver, err)
208 }
209}
210
211// A manifest that parses but fails validation, and one that names another
212// app or version than its key, are skipped; the older valid one answers.
213func TestPreviousProgramSkipsInvalidAndMisnamedManifests(t *testing.T) {
214 s := NewDirStore(t.TempDir())
215 if err := shipWithProgram(t, s, "v1", tinyProgram(t, "example.com/console"), false); err != nil {
216 t.Fatal(err)
217 }
218 art := map[string]Artifact{adoptPlatform: {File: adoptPlatform, SHA256: strings.Repeat("a", 64), Size: 1}}
219 late := "2999-01-01T00:00:00Z"
220 // Fails validateManifest: schema 2.
221 putManifest(t, s, "v2", Manifest{Schema: 2, App: "console", Version: "v2", Kind: "binary",
222 BuiltAt: late, Artifacts: art, Program: "example.com/invalid"})
223 // Valid, but declares another app.
224 putManifest(t, s, "v3", Manifest{Schema: 1, App: "other", Version: "v3", Kind: "binary",
225 BuiltAt: late, Artifacts: art, Program: "example.com/other-app"})
226 // Valid, but declares another version.
227 putManifest(t, s, "v4", Manifest{Schema: 1, App: "console", Version: "v9", Kind: "binary",
228 BuiltAt: late, Artifacts: art, Program: "example.com/other-version"})
229 got, ver, err := PreviousProgram(context.Background(), s, "bab", "console")
230 if err != nil || got != "example.com/console" || ver != "v1" {
231 t.Fatalf("PreviousProgram = %q %q %v, want example.com/console from v1", got, ver, err)
232 }
233}
234
235// A release from an old client has no program and must not switch the check
236// off for the next ship.
237func TestPreviousProgramSkipsReleasesWithoutOne(t *testing.T) {
238 s := NewDirStore(t.TempDir())
239 if err := shipWithProgram(t, s, "v1", tinyProgram(t, "example.com/console"), false); err != nil {
240 t.Fatal(err)
241 }
242 shipRelease(t, s, "bab", "console", "v2", adoptPlatform, []byte("old client, no program field"))
243 got, ver, err := PreviousProgram(context.Background(), s, "bab", "console")
244 if err != nil || got != "example.com/console" || ver != "v1" {
245 t.Fatalf("PreviousProgram = %q %q %v, want example.com/console from v1", got, ver, err)
246 }
247}
248
249// failingStore wraps a Store and fails Get for a specific key with a non-NotExist error.
250type failingStore struct {
251 Store
252 failKey string
253}
254
255func (fs *failingStore) Get(ctx context.Context, key string) ([]byte, string, error) {
256 if key == fs.failKey {
257 return nil, "", errors.New("simulated store failure")
258 }
259 return fs.Store.Get(ctx, key)
260}
261
262// A store error (not just missing manifest) must fail the check closed.
263func TestPreviousProgramFailsClosedOnStoreError(t *testing.T) {
264 base := NewDirStore(t.TempDir())
265 console := tinyProgram(t, "example.com/console")
266
267 // Ship v1 with console program, then v2 normally.
268 if err := shipWithProgram(t, base, "v1", console, false); err != nil {
269 t.Fatalf("ship v1: %v", err)
270 }
271 shipRelease(t, base, "bab", "console", "v2", adoptPlatform, []byte("v2 content"))
272
273 // Wrap the store to fail on v1's manifest key.
274 manifestKey := ManifestKey("bab", "console", "v1")
275 s := &failingStore{Store: base, failKey: manifestKey}
276
277 // PreviousProgram must return an error (not silently skip the failure).
278 prog, ver, err := PreviousProgram(context.Background(), s, "bab", "console")
279 if err == nil || prog != "" || ver != "" {
280 t.Fatalf("PreviousProgram = %q %q %v, want non-nil error", prog, ver, err)
281 }
282
283 // CheckShipProgram must propagate the error.
284 files := map[string][]byte{adoptPlatform: console}
285 m := Manifest{Schema: 1, App: "console", Version: "v3", Kind: "binary",
286 Artifacts: map[string]Artifact{adoptPlatform: {}}}
287 checkErr := CheckShipProgram(context.Background(), s, "bab", &m, files, false)
288 if checkErr == nil {
289 t.Fatalf("CheckShipProgram returned nil, want error")
290 }
291 // The error should not be a ProgramMismatchError (which would indicate the check
292 // wrongly disabled itself and allowed the program change).
293 var pm *ProgramMismatchError
294 if errors.As(checkErr, &pm) {
295 t.Fatalf("CheckShipProgram = ProgramMismatchError, want other error: %v", checkErr)
296 }
297}
298
299// An in-flight release with no manifest yet is skipped gracefully.
300func TestPreviousProgramSkipsAnInFlightRelease(t *testing.T) {
301 s := NewDirStore(t.TempDir())
302 console := tinyProgram(t, "example.com/console")
303
304 // Ship v1 with console program.
305 if err := shipWithProgram(t, s, "v1", console, false); err != nil {
306 t.Fatal(err)
307 }
308
309 // Ship an artifact for v2 without a manifest (in-flight, manifest still being written).
310 // Write the artifact file directly.
311 artKey := ArtifactKey("bab", "console", "v2", adoptPlatform)
312 if _, err := s.Put(context.Background(), artKey, console, ""); err != nil {
313 t.Fatalf("put artifact: %v", err)
314 }
315
316 // PreviousProgram should skip v2 (no manifest) and return v1.
317 prog, ver, err := PreviousProgram(context.Background(), s, "bab", "console")
318 if err != nil || prog != "example.com/console" || ver != "v1" {
319 t.Fatalf("PreviousProgram = %q %q %v, want example.com/console from v1", prog, ver, err)
320 }
321}
322
323// A corrupt (unparseable) manifest is skipped, and an older valid one is found.
324func TestPreviousProgramSkipsACorruptManifest(t *testing.T) {
325 s := NewDirStore(t.TempDir())
326 console := tinyProgram(t, "example.com/console")
327
328 // Ship v1 with console program.
329 if err := shipWithProgram(t, s, "v1", console, false); err != nil {
330 t.Fatal(err)
331 }
332
333 // Write a corrupt manifest for v2 (not valid JSON).
334 manifestKey := ManifestKey("bab", "console", "v2")
335 if _, err := s.Put(context.Background(), manifestKey, []byte("not json at all"), ""); err != nil {
336 t.Fatalf("put corrupt manifest: %v", err)
337 }
338
339 // PreviousProgram should skip v2 (corrupt manifest) and return v1.
340 prog, ver, err := PreviousProgram(context.Background(), s, "bab", "console")
341 if err != nil || prog != "example.com/console" || ver != "v1" {
342 t.Fatalf("PreviousProgram = %q %q %v, want example.com/console from v1", prog, ver, err)
343 }
344}
345