carlos / platform Public

zdd-4 task 3: StagedIdentity, ResolveLive and VerifyStaged

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev acf3bd9fdb4094b7dc86e5ad5b3581f1135aa655 parent 4ec84cd
3 files changed, +205 −9
  • internal/release/staged.go +77 −0
  • internal/release/staged_test.go +114 −0
  • internal/release/versions.go +14 −9
diff --git a/internal/release/staged.go b/internal/release/staged.go
new file mode 100644
index 000000000..d42b12f2d
--- /dev/null
+++ b/internal/release/staged.go
@@ -0,0 +1,77 @@
+// The identity of a staged artifact (zero-downtime deploys spec §4.2, §7.9,
+// §8.5): which bytes a process was exec'd from, kept so that recovery runs
+// exactly those bytes again and can tell when they have changed.
+package release
+
+import (
+ "errors"
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+)
+
+// StagedIdentity is the retained identity of a staged artifact (spec §4.2,
+// §8.5): the version name it was staged under, the immutable path it is
+// exec'd from, the sha256 of its bytes, and the release's own swap demand as
+// verified for those bytes ("hold" or ""). The agent captures it at exec,
+// never from the live link afterwards, because the link moves under a
+// running process.
+type StagedIdentity struct {
+ Version string
+ StagedPath string
+ Digest string
+ Demand string
+}
+
+// ErrStagedCopyChanged is recovery's refusal (spec §4.2): the staged file no
+// longer hashes to the digest retained for it, so it is not the build that
+// passed, and running it would be running something nobody verified.
+var ErrStagedCopyChanged = errors.New("staged copy no longer matches the digest it was verified with")
+
+// ErrVersionReused is root staging's refusal (spec §4.2): the store holds
+// different bytes under a version name this box already staged, and
+// something on the box still refers to the staged copy, so it is not
+// overwritten. The text is the spec's.
+var ErrVersionReused = errors.New("version name reused with different content, ship under a new name")
+
+// ResolveLive reads host's live link ONCE and reports the file it names and
+// that file's sha256. Version is the link's version as LiveVersion reads it,
+// "" for a link outside the version store; the path and digest identify the
+// bytes either way. Demand is left empty: only CanonicalDemand can vouch for
+// it.
+func ResolveLive(optDir, host string) (StagedIdentity, error) {
+ link := LiveLink(optDir, host)
+ raw, err := os.Readlink(link)
+ if err != nil {
+ return StagedIdentity{}, fmt.Errorf("read live link %s: %w", link, err)
+ }
+ path := raw
+ if !filepath.IsAbs(path) {
+ path = filepath.Join(filepath.Dir(link), path)
+ }
+ path = filepath.Clean(path)
+ sum, err := FileSHA256(path)
+ if err != nil {
+ return StagedIdentity{}, fmt.Errorf("hash %s: %w", path, err)
+ }
+ return StagedIdentity{Version: versionOfTarget(optDir, raw), StagedPath: path, Digest: sum}, nil
+}
+
+// VerifyStaged checks the file at id.StagedPath against id.Digest, the digest
+// retained when the build was exec'd (spec §4.2's recovery check). It never
+// consults the canonical manifest: a later re-ship of the same name must not
+// revoke local recovery.
+func VerifyStaged(id StagedIdentity) error {
+ if id.StagedPath == "" || id.Digest == "" {
+ return fmt.Errorf("%w: no retained path and digest for %q", ErrStagedCopyChanged, id.Version)
+ }
+ sum, err := FileSHA256(id.StagedPath)
+ if err != nil {
+ return fmt.Errorf("%w: %s: %v", ErrStagedCopyChanged, id.StagedPath, err)
+ }
+ if !strings.EqualFold(sum, id.Digest) {
+ return fmt.Errorf("%w: %s hashes to %s, retained %s", ErrStagedCopyChanged, id.StagedPath, sum, id.Digest)
+ }
+ return nil
+}
diff --git a/internal/release/staged_test.go b/internal/release/staged_test.go
new file mode 100644
index 000000000..5f9b2cd36
--- /dev/null
+++ b/internal/release/staged_test.go
@@ -0,0 +1,114 @@
+package release
+
+import (
+ "errors"
+ "os"
+ "path/filepath"
+ "testing"
+)
+
+func TestResolveLiveReadsTheLinkOnce(t *testing.T) {
+ opt := t.TempDir()
+ if err := StageArtifact(opt, "bab", "v1", "km", []byte("build one")); err != nil {
+ t.Fatal(err)
+ }
+ if err := PointLive(opt, "h.example.com", "bab", "v1", "km"); err != nil {
+ t.Fatal(err)
+ }
+ id, err := ResolveLive(opt, "h.example.com")
+ if err != nil {
+ t.Fatalf("ResolveLive: %v", err)
+ }
+ want := StagedIdentity{Version: "v1", StagedPath: VersionPath(opt, "bab", "v1", "km"), Digest: etagOf([]byte("build one"))}
+ if id != want {
+ t.Fatalf("ResolveLive = %+v, want %+v", id, want)
+ }
+ if id.StagedPath == LiveLink(opt, "h.example.com") {
+ t.Fatal("the identity names the live link, which moves; it must name the staged file")
+ }
+}
+
+// Review Focus 1: a link an operator made by hand, outside the version store.
+func TestResolveLiveOutsideTheStoreStillIdentifiesTheBytes(t *testing.T) {
+ opt := t.TempDir()
+ elsewhere := filepath.Join(t.TempDir(), "hand-built")
+ if err := os.WriteFile(elsewhere, []byte("operator's build"), 0o755); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.MkdirAll(filepath.Join(opt, "live"), 0o755); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.Symlink(elsewhere, LiveLink(opt, "h.example.com")); err != nil {
+ t.Fatal(err)
+ }
+ id, err := ResolveLive(opt, "h.example.com")
+ if err != nil {
+ t.Fatalf("ResolveLive: %v", err)
+ }
+ want := StagedIdentity{Version: "", StagedPath: elsewhere, Digest: etagOf([]byte("operator's build"))}
+ if id != want {
+ t.Fatalf("ResolveLive = %+v, want %+v (no version, but the bytes still identified)", id, want)
+ }
+}
+
+// Review Focus 1: a relative link resolves against live/, and its version
+// reads exactly as LiveVersion reads it.
+func TestResolveLiveFollowsARelativeLink(t *testing.T) {
+ opt := t.TempDir()
+ if err := StageArtifact(opt, "bab", "v1", "km", []byte("b")); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.MkdirAll(filepath.Join(opt, "live"), 0o755); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.Symlink(filepath.Join("..", "versions", "v1", "km"), LiveLink(opt, "h.example.com")); err != nil {
+ t.Fatal(err)
+ }
+ id, err := ResolveLive(opt, "h.example.com")
+ if err != nil {
+ t.Fatalf("ResolveLive: %v", err)
+ }
+ lv, _ := LiveVersion(opt, "h.example.com")
+ if id.StagedPath != VersionPath(opt, "bab", "v1", "km") || id.Digest != etagOf([]byte("b")) || id.Version != lv {
+ t.Fatalf("ResolveLive = %+v; want path %s, the file's digest, and version %q", id, VersionPath(opt, "bab", "v1", "km"), lv)
+ }
+}
+
+func TestResolveLiveWithNoLinkIsAnError(t *testing.T) {
+ if _, err := ResolveLive(t.TempDir(), "h.example.com"); err == nil {
+ t.Fatal("ResolveLive with no live link returned no error")
+ }
+}
+
+func TestVerifyStaged(t *testing.T) {
+ opt := t.TempDir()
+ if err := StageArtifact(opt, "bab", "v1", "km", []byte("build one")); err != nil {
+ t.Fatal(err)
+ }
+ if err := PointLive(opt, "h.example.com", "bab", "v1", "km"); err != nil {
+ t.Fatal(err)
+ }
+ id, err := ResolveLive(opt, "h.example.com")
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := VerifyStaged(id); err != nil {
+ t.Fatalf("VerifyStaged on untouched bytes: %v", err)
+ }
+ // Damaged in place, to a different length.
+ if err := os.WriteFile(id.StagedPath, []byte("build one, damaged"), 0o755); err != nil {
+ t.Fatal(err)
+ }
+ if err := VerifyStaged(id); !errors.Is(err, ErrStagedCopyChanged) {
+ t.Fatalf("VerifyStaged on changed bytes = %v, want ErrStagedCopyChanged", err)
+ }
+ if err := os.Remove(id.StagedPath); err != nil {
+ t.Fatal(err)
+ }
+ if err := VerifyStaged(id); !errors.Is(err, ErrStagedCopyChanged) {
+ t.Fatalf("VerifyStaged on a missing file = %v, want ErrStagedCopyChanged", err)
+ }
+ if err := VerifyStaged(StagedIdentity{Version: "v1"}); !errors.Is(err, ErrStagedCopyChanged) {
+ t.Fatalf("VerifyStaged with no retained path or digest = %v, want ErrStagedCopyChanged", err)
+ }
+}
diff --git a/internal/release/versions.go b/internal/release/versions.go
index c2952e1f1..47a7cf1fe 100644
--- a/internal/release/versions.go
+++ b/internal/release/versions.go
@@ -295,21 +295,26 @@ func LiveVersion(optDir, host string) (string, error) {
}
return "", err
}
- // target is <opt>/versions/<ver>/<app> (ops) or
- // <opt>/versions/<ver>/<acct>/<app> (everyone else), so the version is the
- // FIRST segment under the versions dir — not simply the parent directory,
- // which would read an account sqid as a version.
+ return versionOfTarget(optDir, target), nil
+}
+
+// versionOfTarget is the version a live link's RAW target names, or "" when
+// the target is not under the version store. target is
+// <opt>/versions/<ver>/<app> (ops) or <opt>/versions/<ver>/<acct>/<app>
+// (everyone else), so the version is the FIRST segment under the versions
+// dir, not simply the parent directory, which would read an account sqid as
+// a version. A target outside the store, or a relative one, is not ours to
+// interpret: "" is the same answer an absent link gets.
+func versionOfTarget(optDir, target string) string {
rel, err := filepath.Rel(versionsDir(optDir), target)
if err != nil || strings.HasPrefix(rel, "..") {
- // A link pointing outside the version store isn't ours to interpret;
- // "" means "not under version management", the same answer as absent.
- return "", nil
+ return ""
}
ver, _, ok := strings.Cut(filepath.ToSlash(rel), "/")
if !ok {
- return "", nil
+ return ""
}
- return ver, nil
+ return ver
}
// WaitHealthyVersion waits until the host behind socket not only answers but