carlos / platform Public

zdd-4 task 1: Manifest.Swap, the release's own demand (additive)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 62c15feb9df48da48e735414cbc5b8ecc0abb264 parent 4f26f15
3 files changed, +137 −0
  • internal/release/demand.go +18 −0
  • internal/release/demand_test.go +102 −0
  • internal/release/release.go +17 −0
diff --git a/internal/release/demand.go b/internal/release/demand.go
new file mode 100644
index 000000000..b452c6fb9
--- /dev/null
+++ b/internal/release/demand.go
@@ -0,0 +1,18 @@
+// The release's own swap demand (zero-downtime deploys spec §4.2): how an
+// awake instance may move onto a build, as the build's shipper said.
+package release
+
+// DemandHold is the one swap demand a release can make (`carlos deploy
+// --swap hold`, spec §9.1).
+const DemandHold = "hold"
+
+// NormalizeDemand is the demand a box acts on for a manifest's swap field.
+// "" is no demand. Anything else is DemandHold: "hold" itself, and any value
+// a newer shipper wrote that this binary does not know, because missing or
+// unverifiable policy data never permits an overlap (spec §4.2).
+func NormalizeDemand(swap string) string {
+ if swap == "" {
+ return ""
+ }
+ return DemandHold
+}
diff --git a/internal/release/demand_test.go b/internal/release/demand_test.go
new file mode 100644
index 000000000..75510154b
--- /dev/null
+++ b/internal/release/demand_test.go
@@ -0,0 +1,102 @@
+package release
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "fmt"
+ "strings"
+ "testing"
+)
+
+// putRawRelease writes a release the way a NEWER shipper would: the artifact,
+// then a manifest whose JSON this binary did not marshal. extra is spliced in
+// before "artifacts" and must end with a comma when non-empty, for example
+// `"swap":"hold",`. Returns the manifest bytes exactly as stored.
+func putRawRelease(t *testing.T, s Store, acct, app, ver string, body []byte, extra string) []byte {
+ t.Helper()
+ ctx := context.Background()
+ if _, err := s.Put(ctx, ArtifactKey(acct, app, ver, adoptPlatform), body, ""); err != nil {
+ t.Fatalf("put artifact: %v", err)
+ }
+ raw := []byte(fmt.Sprintf(`{"schema":1,"app":%q,"version":%q,"kind":"binary",%s"artifacts":{%q:{"file":%q,"sha256":%q,"size":%d}}}`,
+ app, ver, extra, adoptPlatform, adoptPlatform, etagOf(body), len(body)))
+ if _, err := s.Put(ctx, ManifestKey(acct, app, ver), raw, ""); err != nil {
+ t.Fatalf("put manifest: %v", err)
+ }
+ return raw
+}
+
+func TestManifestSwapIsAdditive(t *testing.T) {
+ old := []byte(`{"schema":1,"app":"km","version":"de2766f8","kind":"binary","artifacts":{"linux-arm64":{"file":"linux-arm64","sha256":"` +
+ strings.Repeat("a", 64) + `","size":1}}}`)
+ var m Manifest
+ if err := json.Unmarshal(old, &m); err != nil {
+ t.Fatalf("a manifest written before swap existed no longer parses: %v", err)
+ }
+ if m.Swap != "" {
+ t.Fatalf("absent swap decoded as %q", m.Swap)
+ }
+ out, err := json.MarshalIndent(m, "", " ")
+ if err != nil {
+ t.Fatal(err)
+ }
+ // Byte-identical marshal is what keeps Ship's idempotent re-ship of an
+ // old release working.
+ if bytes.Contains(out, []byte(`"swap"`)) {
+ t.Fatalf("an empty swap leaked into the marshal:\n%s", out)
+ }
+}
+
+func TestShipRecordsAHoldDemand(t *testing.T) {
+ s := NewDirStore(t.TempDir())
+ ctx := context.Background()
+ m := Manifest{App: "km", Version: "v1", Kind: "binary", Swap: DemandHold,
+ Artifacts: map[string]Artifact{adoptPlatform: {}}}
+ if err := Ship(ctx, s, "bab", m, map[string][]byte{adoptPlatform: []byte("bin")}); err != nil {
+ t.Fatalf("Ship: %v", err)
+ }
+ got, err := GetManifest(ctx, s, "bab", "km", "v1")
+ if err != nil || got.Swap != DemandHold {
+ t.Fatalf("GetManifest = %+v, %v; want swap %q", got, err, DemandHold)
+ }
+}
+
+func TestShipRefusesAnUnknownSwapDemand(t *testing.T) {
+ s := NewDirStore(t.TempDir())
+ ctx := context.Background()
+ m := Manifest{App: "km", Version: "v1", Kind: "binary", Swap: "drain",
+ Artifacts: map[string]Artifact{adoptPlatform: {}}}
+ err := Ship(ctx, s, "bab", m, map[string][]byte{adoptPlatform: []byte("bin")})
+ if err == nil || !strings.Contains(err.Error(), `"drain"`) {
+ t.Fatalf("Ship with swap drain = %v; want a refusal naming the value", err)
+ }
+ if _, _, gerr := s.Get(ctx, ManifestKey("bab", "km", "v1")); gerr == nil {
+ t.Fatal("a refused ship still wrote its manifest")
+ }
+ if _, _, gerr := s.Get(ctx, ArtifactKey("bab", "km", "v1", adoptPlatform)); gerr == nil {
+ t.Fatal("a refused ship still wrote its artifact")
+ }
+}
+
+// Review Focus 2: a newer shipper's value must not stop a box reading the
+// manifest, and must act as hold.
+func TestReadsAcceptAnUnknownSwapDemand(t *testing.T) {
+ s := NewDirStore(t.TempDir())
+ putRawRelease(t, s, "bab", "km", "v1", []byte("bin"), `"swap":"drain",`)
+ m, err := GetManifest(context.Background(), s, "bab", "km", "v1")
+ if err != nil {
+ t.Fatalf("a manifest from a newer shipper must still read: %v", err)
+ }
+ if got := NormalizeDemand(m.Swap); got != DemandHold {
+ t.Fatalf("NormalizeDemand(%q) = %q; an unknown demand must act as hold", m.Swap, got)
+ }
+}
+
+func TestNormalizeDemand(t *testing.T) {
+ for in, want := range map[string]string{"": "", "hold": "hold", "overlap": "hold", "HOLD": "hold", "drain": "hold"} {
+ if got := NormalizeDemand(in); got != want {
+ t.Errorf("NormalizeDemand(%q) = %q, want %q", in, got, want)
+ }
+ }
+}
diff --git a/internal/release/release.go b/internal/release/release.go
index 4f73d15e6..6d7182466 100644
--- a/internal/release/release.go
+++ b/internal/release/release.go
@@ -67,6 +67,16 @@ type Manifest struct {
// D2 (program.go).
Program string `json:"program,omitempty"`
ProgramChange bool `json:"program_change,omitempty"`
+ // Swap is this release's own swap demand (zero-downtime deploys spec
+ // §4.2). DemandHold says the build's boot must not share the instance's
+ // database with the build before it, so an awake instance moves onto it
+ // stop-then-start. Empty means the release demands nothing and the app's
+ // policy decides. ADDITIVE to schema 1 like Label: absent from every
+ // manifest written before it, omitted from marshal when empty, ignored by
+ // old binaries. A box reads it from the CANONICAL manifest only
+ // (CanonicalDemand), never from its staged copy, because a root older
+ // than this field re-marshalled its copies without it.
+ Swap string `json:"swap,omitempty"`
// Artifacts is keyed "linux-arm64" etc for binaries, or "site" for a
// static tree tar.
Artifacts map[string]Artifact `json:"artifacts"`
@@ -597,6 +607,13 @@ func claimRawShip(ctx context.Context, s Store, acct string, m Manifest, files m
return fmt.Errorf("ship: invalid account %q", acct)
}
m.Schema = 1
+ // Ship is where a person is present to read a refusal, so the one value
+ // a release may demand is enforced here. Reads stay permissive
+ // (NormalizeDemand): a value from a newer shipper must not stop a box
+ // staging the release, only stop an overlap.
+ if m.Swap != "" && m.Swap != DemandHold {
+ return fmt.Errorf("ship: swap %q: a release can demand %q or nothing", m.Swap, DemandHold)
+ }
for k, a := range m.Artifacts {
b, ok := files[k]
if !ok {