carlos / platform Public

ship: a control-plane artifact must match its target and its version

Two refusals read from the bytes that arrived, stdlib only. debug/elf
catches an amd64 binary declared linux-arm64 — which has happened and
briefly broke a box, and which -target only protects the careful caller
from. debug/buildinfo catches a binary whose embedded version.Build is not
the version being shipped, which is the ldflags trap: forget the flag and
the roll succeeds while the probe reports 'dev' forever, so a healthy
deploy reports failure. The forgotten flag gets its own sentence, because
it is the common case and the fix is the flag, not the value.

Control-plane apps only, and binary kinds only (a static release ships a
tarball). A tenant may legitimately ship an artifact we would guess wrong
about, and refusing their ship on our inference is not our call.

The app row is now read ONCE in handleAPIShip, before release.Ship writes
anything, and both the control-plane verdict and the build-probe echo come
from it. That moves task 6's read ahead of the write, so its "a failed
metadata read must not fail a stored ship" ruling no longer applies:
nothing is stored yet, and an unreadable row is a question the guard
cannot answer, so the ship is refused with a 503. Its test now asserts
that (TestAPIShipRefusesAnUnreadableAppRow).

The control-plane tests that shipped a placeholder byte now ship a real
linux-arm64 binary stamped with the version they ship it as; buildTestELF
compiles one per (goos, goarch, build) and caches it, so what is proven is
that debug/buildinfo reads what the real build recipe writes through -s -w.
Paul Campbell pushed by paul@keymail.dev e3f9ca0b089af77deb41e05838dbe1953efa79f0 parent 93a2b4d
4 files changed, +410 −41
  • internal/console/apicli.go +50 −17
  • internal/console/apicli_test.go +27 −21
  • internal/console/controlplane.go +128 −0
  • internal/console/controlplane_test.go +205 −3
diff --git a/internal/console/apicli.go b/internal/console/apicli.go
index 9f28025b4..76996e5ca 100644
--- a/internal/console/apicli.go
+++ b/internal/console/apicli.go
@@ -1534,6 +1534,40 @@ func (s *Server) handleAPIShip(w http.ResponseWriter, r *http.Request) {
return
}
+ // The app's row is read ONCE for the rest of this handler: the
+ // control-plane verdict immediately below and the build-probe echo on
+ // both success paths are two columns of the same row, and a ship has no
+ // reason to ask twice.
+ //
+ // It fails CLOSED — an unreadable row refuses the ship — which is the
+ // same ruling every other reader of this flag follows (controlplane.go):
+ // treating an unreadable row as "not the control plane" would make a
+ // database blip the way past the guard, and the guard here is the one
+ // standing between a wrong-architecture binary and every box that runs
+ // the console.
+ //
+ // That the ECHO now refuses too, where task 6 had it degrade, is the
+ // move rather than a reversal: that ruling rested on "the bytes are
+ // already stored by the time this runs", and this read happens BEFORE
+ // release.Ship writes anything. Refusing costs the caller a retry; there
+ // is no half-done ship to be honest about.
+ cp, probe, err := s.store.AppFlags(acct.ID, app)
+ if err != nil {
+ log.Printf("console: api ship %s/%s: read app flags: %v", acct.Sqid, app, err)
+ jsonError(w, http.StatusServiceUnavailable,
+ "could not check this app's control-plane status — nothing was shipped")
+ return
+ }
+ // Read the bytes that actually arrived, before a single one is written.
+ // A static release ships a tarball, not an ELF, so it is exempt — there
+ // is nothing in it for either check to read.
+ if cp && meta.Kind != "static" {
+ if err := checkControlPlaneArtifacts(files, ver); err != nil {
+ jsonError(w, http.StatusBadRequest, err.Error())
+ return
+ }
+ }
+
m := release.Manifest{
Schema: 1,
App: app,
@@ -1603,7 +1637,7 @@ func (s *Server) handleAPIShip(w http.ResponseWriter, r *http.Request) {
// above, and echoing what is ON RECORD is the honest answer to "what
// did this version end up declaring".
addCapabilitiesEcho(body, existing.Capabilities)
- s.addBuildProbe(body, acct, app)
+ addBuildProbe(body, probe)
writeJSON(w, http.StatusOK, body)
return
}
@@ -1635,7 +1669,7 @@ func (s *Server) handleAPIShip(w http.ResponseWriter, r *http.Request) {
}
body := map[string]any{"result": "shipped", "version": ver, "artifacts": out}
addCapabilitiesEcho(body, m.Capabilities)
- s.addBuildProbe(body, acct, app)
+ addBuildProbe(body, probe)
writeJSON(w, http.StatusCreated, body)
}
@@ -1647,22 +1681,21 @@ func (s *Server) handleAPIShip(w http.ResponseWriter, r *http.Request) {
// It rides the ship response rather than a door of its own because deploy
// always ships before it watches: the value arrives with no extra round trip,
// and the ship response is already scoped to exactly the app whose probe this
-// is. Read from the app ROW (Store.AppFlags) — never from provision.Record,
-// whose handleAPIInstanceUpdate is a read-modify-write that would silently
-// drop a field it does not know: the probe is app-level, not per-instance.
+// is. The value comes from the app ROW (Store.AppFlags) — never from
+// provision.Record, whose handleAPIInstanceUpdate is a read-modify-write that
+// would silently drop a field it does not know: the probe is app-level, not
+// per-instance.
//
-// Additive twice over. A client that does not read the key is unaffected, an
-// app with no probe carries no key at all (so every ship the fleet does today
-// answers byte for byte as before) — and a FAILED read is not a failed ship.
-// The bytes are stored by the time this runs; degrading to the header watch
-// with a logged line is the honest outcome, refusing a successful upload over
-// a metadata read is not.
-func (s *Server) addBuildProbe(body map[string]any, acct *Account, app string) {
- _, probe, err := s.store.AppFlags(acct.ID, app)
- if err != nil {
- log.Printf("console: api ship %s/%s: read build_probe: %v", acct.Sqid, app, err)
- return
- }
+// It takes the probe rather than reading it because handleAPIShip already
+// read that row, for the control-plane check that must happen before any
+// bytes are written — so the read moved UP, ahead of release.Ship, and this
+// function is now only the echo. The read's failure is handled there, and
+// refuses: see the comment at the call.
+//
+// Additive: a client that does not read the key is unaffected, and an app
+// with no probe carries no key at all, so every ship the fleet does today
+// answers byte for byte as before.
+func addBuildProbe(body map[string]any, probe string) {
if probe != "" {
body["build_probe"] = probe
}
diff --git a/internal/console/apicli_test.go b/internal/console/apicli_test.go
index 8d5158098..2b5f2993b 100644
--- a/internal/console/apicli_test.go
+++ b/internal/console/apicli_test.go
@@ -2571,19 +2571,25 @@ func TestAPIShipOmitsTheBuildProbeWhenNoneIsDeclared(t *testing.T) {
}
}
-// TestAPIShipSurvivesAnUnreadableBuildProbe pins the degradation the ruling
-// rests on: the bytes are STORED by the time addBuildProbe runs, so a failed
-// metadata read must log and leave the response alone — never turn a
-// successful upload into an error. The CLI then sees no key, reads "", and
-// keeps the header watch, which is the same outcome as talking to a console
-// too old to echo the field at all.
+// TestAPIShipRefusesAnUnreadableAppRow pins the ruling the ship door's ONE
+// app-row read follows: it fails CLOSED.
//
-// The column is dropped rather than the database closed, the same technique
-// (and for the same reason) as TestControlPlaneLookupFailureRefuses: closing
-// it would fail the token lookup and the app 404 long before the ship handler
-// reached this read. Dropping ONE column leaves every earlier gate working
-// and fails exactly AppFlags.
-func TestAPIShipSurvivesAnUnreadableBuildProbe(t *testing.T) {
+// This test used to assert the opposite (TestAPIShipSurvivesAnUnreadableBuildProbe,
+// task 6): the build-probe echo read the row AFTER release.Ship had stored the
+// bytes, and there the only honest answer was to degrade — refusing a
+// successful upload over a metadata read would have been a lie about what
+// happened. That premise is gone. The control-plane checks need the same row
+// BEFORE a byte is written, so the read moved up and the two columns are read
+// once, together; and an unreadable row is now a question the guard cannot
+// answer, on the one app where the wrong answer ships an amd64 binary to a
+// Graviton box. Refusing costs the caller a retry and leaves nothing
+// half-done.
+//
+// The column is dropped rather than the database closed: closing it would
+// fail the token lookup and the app 404 long before the ship handler reached
+// this read. Dropping ONE column leaves every earlier gate working and fails
+// exactly AppFlags.
+func TestAPIShipRefusesAnUnreadableAppRow(t *testing.T) {
fx := newConsoleFixture(t)
now := time.Now()
if _, err := fx.st.CreateAppIn(OpsAccountID, "console", "paul@keymail.dev", now); err != nil {
@@ -2597,17 +2603,17 @@ func TestAPIShipSurvivesAnUnreadableBuildProbe(t *testing.T) {
t.Fatalf("drop the probe column: %v", err)
}
- payload := randomBytes(t, 256)
status, out := postShip(t, fx.ts.URL+"/api/cli/apps/bab/console/releases", token,
- []shipPart{metaPart(t, "abc1234", "binary"), {name: "linux-arm64", body: payload}})
- if status != http.StatusCreated {
- t.Fatalf("ship with an unreadable build_probe = %d %v, want 201 — the bytes are already stored", status, out)
+ []shipPart{metaPart(t, "abc1234", "binary"), {name: "linux-arm64", body: randomBytes(t, 256)}})
+ if status != http.StatusServiceUnavailable {
+ t.Fatalf("ship with an unreadable app row = %d %v, want 503 — the flag check cannot fall through", status, out)
}
- if _, present := out["build_probe"]; present {
- t.Fatalf("an unreadable probe must leave the key off, not guess: %v", out)
+ if msg := str(t, out, "error"); !strings.Contains(msg, "nothing was shipped") {
+ t.Fatalf("refusal %q does not tell the caller the upload did not land", msg)
}
- // The release really landed — the point is that only the ECHO degraded.
- if _, err := release.GetManifest(context.Background(), fx.rel, release.OpsAccount, "console", "abc1234"); err != nil {
- t.Fatalf("manifest after a degraded echo: %v", err)
+ // And it really did not land: a refusal that stores the release anyway is
+ // the worst of both answers.
+ if _, err := release.GetManifest(context.Background(), fx.rel, release.OpsAccount, "console", "abc1234"); err == nil {
+ t.Fatal("the release was stored despite the 503")
}
}
diff --git a/internal/console/controlplane.go b/internal/console/controlplane.go
index fbb0d8bb8..01f1d7f1e 100644
--- a/internal/console/controlplane.go
+++ b/internal/console/controlplane.go
@@ -27,10 +27,15 @@
package console
import (
+ "bytes"
"crypto/subtle"
+ "debug/buildinfo"
+ "debug/elf"
+ "fmt"
"log"
"net/http"
"net/url"
+ "runtime/debug"
"strings"
)
@@ -289,3 +294,126 @@ func (s *Server) handleAppControlPlaneSet(w http.ResponseWriter, r *http.Request
s.audit(acct, app, addr, "control-plane", "marked "+app+" as the control plane")
back("ok", "Marked "+app+" as the control plane — trashing, deleting, renaming and moving it are refused until the flag is cleared.")
}
+
+// --- Ship-time refusals -----------------------------------------------------
+//
+// The flag gates DESTRUCTION above; it also buys two REFUSALS at the ship
+// door, and they are here rather than in apicli.go for the same reason the
+// clear door is: they are the flag's meaning, and the file that says what the
+// flag means is this one.
+
+// targetMachine maps an artifact part name — which IS the declared target,
+// "linux-arm64" and friends — to the ELF machine a binary for it must carry.
+// Unknown targets are not refused: this guard exists to catch a wrong build of
+// a target we know, not to bound the target vocabulary, which targetRe
+// already does.
+var targetMachine = map[string]elf.Machine{
+ "linux-arm64": elf.EM_AARCH64,
+ "linux-amd64": elf.EM_X86_64,
+}
+
+// buildLDFlag is the assignment the release recipe links in. Matched as a
+// SUFFIX of the symbol path rather than the whole of it, so a vendored or
+// renamed module prefix does not silently turn every artifact into "carries
+// no version.Build".
+const buildLDFlag = "internal/version.Build="
+
+// checkControlPlaneArtifacts refuses two ways a control-plane ship can be
+// wrong in a manner nothing downstream would catch.
+//
+// ARCHITECTURE. An amd64 binary on a Graviton box has happened and briefly
+// broke one. `carlos deploy` defaults -target to linux-arm64, which protects
+// the careful caller and nobody else; this closes the same door against a
+// hand-run `carlos ship`, a CI job, or a script written next year.
+//
+// EMBEDDED BUILD. version.Build is set by ldflags the operator has to
+// remember. Forget it and the artifact says "dev" forever: the roll SUCCEEDS,
+// the deploy watch's probe never matches, and a perfectly healthy deploy
+// reports failure — the exact inverse of the lie the probe exists to kill.
+// Caught here it is one sentence at the door instead of a timeout nobody can
+// explain.
+//
+// Both read the bytes that actually arrived, and both are stdlib. Verified
+// against a real arm64 console build that `-s -w` strips neither the ELF
+// header nor the build info: debug/buildinfo still reports
+// Settings["-ldflags"] verbatim.
+//
+// CONTROL-PLANE APPS ONLY, and that scoping is the ruling, not an oversight:
+// a tenant may legitimately ship an artifact we would guess wrong about, and
+// refusing their ship on our inference is not our call. The console is
+// different because we own the boxes it lands on and know what they are.
+//
+// version is the RESOLVED version of this ship — after minting, never the
+// meta part's field, which is empty on a target-minted ship.
+func checkControlPlaneArtifacts(files map[string][]byte, version string) error {
+ for name, b := range files {
+ want, known := targetMachine[name]
+ if !known {
+ continue
+ }
+ f, err := elf.NewFile(bytes.NewReader(b))
+ if err != nil {
+ return fmt.Errorf("artifact %s is not a readable ELF binary: %v", name, err)
+ }
+ machine := f.Machine
+ if err := f.Close(); err != nil {
+ return fmt.Errorf("artifact %s could not be read: %v", name, err)
+ }
+ if machine != want {
+ return fmt.Errorf("artifact %s is a %s binary, not %s — rebuild it with GOOS/GOARCH matching the target it is shipped under",
+ name, machineName(machine), name)
+ }
+ bi, err := buildinfo.Read(bytes.NewReader(b))
+ if err != nil {
+ return fmt.Errorf("artifact %s carries no Go build info: %v", name, err)
+ }
+ // The empty case gets its own sentence because it is the COMMON one:
+ // a build with the ldflag simply forgotten. "was built with
+ // version.Build=\"\"" would be true and would still leave the
+ // operator working out that a link flag is missing.
+ got := embeddedBuild(bi)
+ if got == "" {
+ return fmt.Errorf("artifact %s was linked without its build stamp, so it would report %q forever and the deploy watch would never go green — rebuild with -ldflags %q",
+ name, "dev", `-s -w -X github.com/carlosframework/platform/internal/version.Build=`+version)
+ }
+ if got != version {
+ return fmt.Errorf("artifact %s was built with version.Build=%q but is being shipped as %q — rebuild with -ldflags %q, or ship it under the version it was built as",
+ name, got, version, `-s -w -X github.com/carlosframework/platform/internal/version.Build=`+version)
+ }
+ }
+ return nil
+}
+
+// embeddedBuild pulls internal/version.Build out of a binary's recorded
+// -ldflags, returning "" when the flag is absent — which is exactly the build
+// that reports "dev" on the wire.
+//
+// Scanned FIELD BY FIELD rather than with one Index over the whole string, so
+// that both spellings the linker accepts are read the same way: `-X` and its
+// assignment as separate arguments, and `-X=path=value` as one.
+func embeddedBuild(bi *debug.BuildInfo) string {
+ for _, s := range bi.Settings {
+ if s.Key != "-ldflags" {
+ continue
+ }
+ for _, f := range strings.Fields(s.Value) {
+ if i := strings.Index(f, buildLDFlag); i >= 0 {
+ return f[i+len(buildLDFlag):]
+ }
+ }
+ }
+ return ""
+}
+
+// machineName says an ELF machine the way the operator's build command says
+// it. Anything outside the two targets the platform ships falls back to the
+// elf constant, which is still more use than a number.
+func machineName(m elf.Machine) string {
+ switch m {
+ case elf.EM_AARCH64:
+ return "arm64"
+ case elf.EM_X86_64:
+ return "amd64"
+ }
+ return m.String()
+}
diff --git a/internal/console/controlplane_test.go b/internal/console/controlplane_test.go
index ff38a7cd9..f1938e034 100644
--- a/internal/console/controlplane_test.go
+++ b/internal/console/controlplane_test.go
@@ -1,9 +1,14 @@
package console
import (
+ "context"
"net/http"
"net/url"
+ "os"
+ "os/exec"
+ "path/filepath"
"strings"
+ "sync"
"testing"
"time"
@@ -64,8 +69,11 @@ func TestControlPlaneAppRefusesTrash(t *testing.T) {
// would break the very command this work exists to create.
func TestControlPlaneAppAllowsAPersonToPromote(t *testing.T) {
fx, token := controlPlaneFixture(t, "code-cp-promote")
+ // A real linux-arm64 binary stamped cp00001: the ship door checks both of
+ // those on a control-plane app (checkControlPlaneArtifacts), so a
+ // placeholder byte would never reach the promote this test is about.
status, out := postShip(t, fx.ts.URL+"/api/cli/apps/bab/console/releases", token,
- []shipPart{metaPart(t, "cp00001", "binary"), {name: "linux-arm64", body: []byte("x")}})
+ []shipPart{metaPart(t, "cp00001", "binary"), {name: "linux-arm64", body: buildTestELF(t, "linux", "arm64", "cp00001")}})
if status != http.StatusCreated {
t.Fatalf("ship = %d %v", status, out)
}
@@ -82,7 +90,7 @@ func TestControlPlaneAppAllowsAPersonToPromote(t *testing.T) {
func TestControlPlaneAppRefusesAServicePromote(t *testing.T) {
fx, token := controlPlaneFixture(t, "code-cp-svc")
status, out := postShip(t, fx.ts.URL+"/api/cli/apps/bab/console/releases", token,
- []shipPart{metaPart(t, "cp00002", "binary"), {name: "linux-arm64", body: []byte("x")}})
+ []shipPart{metaPart(t, "cp00002", "binary"), {name: "linux-arm64", body: buildTestELF(t, "linux", "arm64", "cp00002")}})
if status != http.StatusCreated {
t.Fatalf("ship = %d %v", status, out)
}
@@ -102,7 +110,7 @@ func TestControlPlaneAppRefusesAServiceRollback(t *testing.T) {
liftShipBudget(fx)
for _, v := range []string{"cp00003", "cp00004"} {
status, out := postShip(t, fx.ts.URL+"/api/cli/apps/bab/console/releases", token,
- []shipPart{metaPart(t, v, "binary"), {name: "linux-arm64", body: []byte("x")}})
+ []shipPart{metaPart(t, v, "binary"), {name: "linux-arm64", body: buildTestELF(t, "linux", "arm64", v)}})
if status != http.StatusCreated {
t.Fatalf("ship %s = %d %v", v, status, out)
}
@@ -570,3 +578,197 @@ func TestControlPlaneLookupFailureRefuses(t *testing.T) {
t.Fatalf("purge refusal flash does not say the check failed:\n%s", page)
}
}
+
+// --- Ship-time refusals -----------------------------------------------------
+
+// testELFCache holds one compiled binary per (goos, goarch, build): several
+// tests here ship the same artifact, and a link is cheap but not free.
+var testELFCache sync.Map
+
+// testELFSource is the smallest program that is still a real Go binary — it
+// has to be one, because what is under test is that debug/elf and
+// debug/buildinfo read back what a real build writes.
+const testELFSource = `package main
+
+var Build = "dev"
+
+func main() { println(Build) }
+`
+
+// buildTestELF compiles a real binary for goos/goarch, stamped with the
+// release recipe's own ldflags, and returns its bytes.
+//
+// A REAL build rather than a handcrafted ELF header, deliberately: the claim
+// these checks rest on is that `-s -w` strips neither the machine field nor
+// the recorded -ldflags, and a fixture assembled by hand would only prove
+// that the checks can read the fixture. This is measured against the
+// toolchain that actually links the console.
+//
+// The -X names the PLATFORM's symbol path even though this throwaway module
+// has no such symbol. The linker ignores an unknown -X target silently and
+// records the flag either way, and the flag record is exactly what
+// checkControlPlaneArtifacts parses — spelling it any other way would test a
+// spelling no console build uses.
+func buildTestELF(t *testing.T, goos, goarch, build string) []byte {
+ t.Helper()
+ key := goos + "/" + goarch + "/" + build
+ if b, ok := testELFCache.Load(key); ok {
+ return b.([]byte)
+ }
+ if _, err := exec.LookPath("go"); err != nil {
+ t.Skipf("no go toolchain available to build a %s test binary: %v", key, err)
+ }
+ dir := t.TempDir()
+ for name, body := range map[string]string{
+ "go.mod": "module carlostestelf\n\ngo 1.24\n",
+ "main.go": testELFSource,
+ } {
+ if err := os.WriteFile(filepath.Join(dir, name), []byte(body), 0o644); err != nil {
+ t.Fatal(err)
+ }
+ }
+ out := filepath.Join(dir, "artifact")
+ // An EMPTY build is the forgotten-ldflag case, so the flag is left off
+ // altogether rather than passed with an empty value: `-X path=` would
+ // still record a -X, and the binary under test has to be the one an
+ // operator actually produces by forgetting.
+ ldflags := "-s -w"
+ if build != "" {
+ ldflags += " -X github.com/carlosframework/platform/internal/version.Build=" + build
+ }
+ cmd := exec.Command("go", "build", "-ldflags", ldflags, "-o", out, ".")
+ cmd.Dir = dir
+ // GOWORK=off and CGO_ENABLED=0 so the build does not depend on this
+ // checkout's module graph or on a C toolchain for the host arch.
+ cmd.Env = append(os.Environ(), "GOOS="+goos, "GOARCH="+goarch, "CGO_ENABLED=0", "GOWORK=off")
+ if msg, err := cmd.CombinedOutput(); err != nil {
+ t.Fatalf("build a %s test binary: %v\n%s", key, err, msg)
+ }
+ b, err := os.ReadFile(out)
+ if err != nil {
+ t.Fatal(err)
+ }
+ testELFCache.Store(key, b)
+ return b
+}
+
+// TestControlPlaneShipRefusesTheWrongArchitecture: failure mode #1, an amd64
+// binary shipped to Graviton boxes. -target linux-arm64 is carlos deploy's
+// default, which protects the careful caller; this closes the path a hand-run
+// ship, a CI job or a future script takes into the same channel.
+func TestControlPlaneShipRefusesTheWrongArchitecture(t *testing.T) {
+ fx, token := controlPlaneFixture(t, "code-cp-arch")
+ liftShipBudget(fx)
+ amd64ELF := buildTestELF(t, "linux", "amd64", "cp00003")
+ status, out := postShip(t, fx.ts.URL+"/api/cli/apps/bab/console/releases", token,
+ []shipPart{metaPart(t, "cp00003", "binary"), {name: "linux-arm64", body: amd64ELF}})
+ if status != http.StatusBadRequest {
+ t.Fatalf("amd64 ELF as linux-arm64 = %d %v, want 400", status, out)
+ }
+ msg := str(t, out, "error")
+ if !strings.Contains(msg, "linux-arm64") || !strings.Contains(msg, "amd64") {
+ t.Fatalf("refusal %q must name both the declared target and what was found", msg)
+ }
+ // Refused BEFORE anything was written: a refusal that leaves the release
+ // stored is not a refusal.
+ if _, err := release.GetManifest(context.Background(), fx.rel, release.OpsAccount, "console", "cp00003"); err == nil {
+ t.Fatal("the release landed despite the refusal")
+ }
+}
+
+// TestControlPlaneShipRefusesAMismatchedEmbeddedBuild: closes the ldflags trap
+// at the door. Build without -X …version.Build and the roll succeeds while the
+// probe reports "dev" forever — deploy reports failure on a healthy deploy,
+// the exact inverse of the lie the probe exists to kill.
+func TestControlPlaneShipRefusesAMismatchedEmbeddedBuild(t *testing.T) {
+ fx, token := controlPlaneFixture(t, "code-cp-embed")
+ liftShipBudget(fx)
+ wrong := buildTestELF(t, "linux", "arm64", "notthesha")
+ status, out := postShip(t, fx.ts.URL+"/api/cli/apps/bab/console/releases", token,
+ []shipPart{metaPart(t, "cp00004", "binary"), {name: "linux-arm64", body: wrong}})
+ if status != http.StatusBadRequest {
+ t.Fatalf("embedded build != -version = %d %v, want 400", status, out)
+ }
+ if msg := str(t, out, "error"); !strings.Contains(msg, "notthesha") || !strings.Contains(msg, "cp00004") {
+ t.Fatalf("refusal %q must name both builds", msg)
+ }
+}
+
+// TestControlPlaneShipRefusesAMissingBuildStamp is the COMMON half of the
+// same trap — the ldflag simply forgotten — and it earns its own sentence:
+// "built with version.Build=\"\"" is true and tells the operator nothing,
+// where naming the flag is the whole fix.
+func TestControlPlaneShipRefusesAMissingBuildStamp(t *testing.T) {
+ fx, token := controlPlaneFixture(t, "code-cp-nostamp")
+ liftShipBudget(fx)
+ unstamped := buildTestELF(t, "linux", "arm64", "")
+ status, out := postShip(t, fx.ts.URL+"/api/cli/apps/bab/console/releases", token,
+ []shipPart{metaPart(t, "cp00006", "binary"), {name: "linux-arm64", body: unstamped}})
+ if status != http.StatusBadRequest {
+ t.Fatalf("unstamped binary = %d %v, want 400", status, out)
+ }
+ msg := str(t, out, "error")
+ if !strings.Contains(msg, "internal/version.Build=") {
+ t.Fatalf("refusal %q does not name the link flag that is missing", msg)
+ }
+ if !strings.Contains(msg, "cp00006") {
+ t.Fatalf("refusal %q does not say what to stamp the build as", msg)
+ }
+}
+
+// TestControlPlaneShipAcceptsAMatchingArtifact.
+func TestControlPlaneShipAcceptsAMatchingArtifact(t *testing.T) {
+ fx, token := controlPlaneFixture(t, "code-cp-ok")
+ liftShipBudget(fx)
+ good := buildTestELF(t, "linux", "arm64", "cp00005")
+ status, out := postShip(t, fx.ts.URL+"/api/cli/apps/bab/console/releases", token,
+ []shipPart{metaPart(t, "cp00005", "binary"), {name: "linux-arm64", body: good}})
+ if status != http.StatusCreated {
+ t.Fatalf("matching artifact = %d %v, want 201", status, out)
+ }
+}
+
+// TestControlPlaneStaticShipIsNotELFChecked: a static release ships a
+// tarball, so both checks are exempt by kind — reading it as an ELF would
+// refuse every static ship the control plane could ever make.
+func TestControlPlaneStaticShipIsNotELFChecked(t *testing.T) {
+ fx, token := controlPlaneFixture(t, "code-cp-static")
+ liftShipBudget(fx)
+ status, out := postShip(t, fx.ts.URL+"/api/cli/apps/bab/console/releases", token,
+ []shipPart{metaPart(t, "cp00007", "static"), {name: "site.tar.gz", body: []byte("not an ELF, and never was")}})
+ if status != http.StatusCreated {
+ t.Fatalf("static ship of a control-plane app = %d %v, want 201", status, out)
+ }
+}
+
+// TestControlPlaneShipIgnoresAnUnknownTarget: the guard catches a wrong build
+// of a target we KNOW, and does not bound the target vocabulary — targetRe
+// already does that. A part named for a machine this map has never heard of
+// passes, rather than the platform refusing a target it has not been taught.
+func TestControlPlaneShipIgnoresAnUnknownTarget(t *testing.T) {
+ fx, token := controlPlaneFixture(t, "code-cp-unknown-target")
+ liftShipBudget(fx)
+ status, out := postShip(t, fx.ts.URL+"/api/cli/apps/bab/console/releases", token,
+ []shipPart{metaPart(t, "cp00008", "binary"), {name: "plan9-riscv64", body: []byte("whatever that is")}})
+ if status != http.StatusCreated {
+ t.Fatalf("unknown target = %d %v, want 201", status, out)
+ }
+}
+
+// TestOrdinaryAppShipIsNotArchChecked: a tenant may legitimately ship an
+// artifact we would guess wrong about; refusing on our inference is not our
+// call. Only the app that IS the control plane, on boxes we own and know are
+// arm64, gets the check.
+func TestOrdinaryAppShipIsNotArchChecked(t *testing.T) {
+ fx := newConsoleFixture(t)
+ if _, err := fx.st.CreateAppIn(OpsAccountID, "titogo", "paul@keymail.dev", time.Now()); err != nil {
+ t.Fatal(err)
+ }
+ token := cliToken(t, fx, "paul@keymail.dev", "code-cp-tenant")
+ liftShipBudget(fx)
+ status, out := postShip(t, fx.ts.URL+"/api/cli/apps/bab/titogo/releases", token,
+ []shipPart{metaPart(t, "t0000001", "binary"), {name: "linux-arm64", body: []byte("not an ELF at all")}})
+ if status != http.StatusCreated {
+ t.Fatalf("ordinary app shipping a non-ELF = %d %v, want 201 (unaffected)", status, out)
+ }
+}