sandbox: resolve the image the publisher actually wrote, and let a digest pin it
CI/CD / gate (push) Canceled after 0s
CI/CD / containment (push) Canceled after 0s
CI/CD / image (push) Canceled after 0s
CI/CD / rollout (push) Canceled after 0s
CI/CD / reach (push) Canceled after 0s
CI/CD / fanout (push) Canceled after 0s
CI/CD / receipt (push) Canceled after 0s
CI/CD / gate (push) Canceled after 0s
CI/CD / containment (push) Canceled after 0s
CI/CD / image (push) Canceled after 0s
CI/CD / rollout (push) Canceled after 0s
CI/CD / reach (push) Canceled after 0s
CI/CD / fanout (push) Canceled after 0s
CI/CD / receipt (push) Canceled after 0s
TWO defects, both silent, both found by trying to start a real sandbox. THE TAG ORDER WAS BACKWARDS. imageFor composed <class>-<version> and asked for `dev-2026.6.7`; the registry holds `2026.6.7-dev`. So the default path 404'd on an image sitting right there, and the only reason anything ever ran was a test that overrode the image entirely. The publisher wins this argument: hanzoai/ci appends a per-image tag-suffix to the version, so the whole fleet is <version>-<suffix> and a consumer spelling it the other way is simply wrong. A VERSION TAG IS NOT A PIN HERE. The sandbox image ships from hanzoai/bot under BOT's package.json version, last bumped 2026-06-07. A rebuild today — commit d1904514f4, "box: uv ships at the root" — republished `2026.6.7-dev` from source two months newer. A tag that gets rewritten is not a pin, and one that LOOKS pinned is worse than `latest`, which at least admits what it is. So SANDBOX_IMAGE_DIGEST_<CLASS> is honoured ahead of any tag. `repo@sha256:…` names bytes, and bytes do not change under a running fleet. PER CLASS, not one variable. A single SANDBOX_IMAGE_DIGEST would have handed every class the exec image while every log line still read correctly — the same shape as the tag bug above. image_test.go pins that case specifically, along with the order, because both of these are invisible until a pod fails to pull.
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
package sandbox
|
||||
|
||||
// What image a class resolves to, pinned — because getting this wrong is silent.
|
||||
//
|
||||
// Two real defects motivate every case here. The tag ORDER was reversed: cloud
|
||||
// asked for `dev-2026.6.7` while the registry held `2026.6.7-dev`, so the
|
||||
// default path 404'd on an image sitting right there. And a single
|
||||
// SANDBOX_IMAGE_DIGEST would have handed every class the same image, which
|
||||
// looks correct in every log line it produces.
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestImageForResolvesTheTagThePublisherWrote(t *testing.T) {
|
||||
for _, c := range []struct {
|
||||
name, repo, tag, class, want string
|
||||
digest map[string]string
|
||||
}{{
|
||||
name: "version first, class second — the order CI publishes",
|
||||
repo: "oci.hanzo.ai/hanzoai/sandbox", tag: "2026.6.7", class: "dev",
|
||||
want: "oci.hanzo.ai/hanzoai/sandbox:2026.6.7-dev",
|
||||
}, {
|
||||
name: "no tag is the bare class, unchanged",
|
||||
repo: "oci.hanzo.ai/hanzoai/sandbox", class: "exec",
|
||||
want: "oci.hanzo.ai/hanzoai/sandbox:exec",
|
||||
}, {
|
||||
// A digest names BYTES. A version tag can be republished — hanzoai/bot
|
||||
// ships this image under bot's own package.json version, so a rebuild
|
||||
// today re-wrote 2026.6.7-dev from a commit two months newer.
|
||||
name: "a digest wins over any tag",
|
||||
repo: "oci.hanzo.ai/hanzoai/sandbox", tag: "2026.6.7", class: "dev",
|
||||
digest: map[string]string{"DEV": "sha256:2baf7ede"},
|
||||
want: "oci.hanzo.ai/hanzoai/sandbox@sha256:2baf7ede",
|
||||
}, {
|
||||
// The bug this file caught before it shipped.
|
||||
name: "each class takes ITS OWN digest, never a neighbour's",
|
||||
repo: "oci.hanzo.ai/hanzoai/sandbox", tag: "2026.6.7", class: "exec",
|
||||
digest: map[string]string{"DEV": "sha256:2baf7ede"},
|
||||
want: "oci.hanzo.ai/hanzoai/sandbox:2026.6.7-exec",
|
||||
}} {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
for k, v := range c.digest {
|
||||
t.Setenv("SANDBOX_IMAGE_DIGEST_"+k, v)
|
||||
}
|
||||
r := &runtime{image: c.repo, tag: c.tag}
|
||||
if got := r.imageFor(c.class); got != c.want {
|
||||
t.Fatalf("imageFor(%q) = %q, want %q", c.class, got, c.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
+30
-1
@@ -175,6 +175,9 @@ func (r *runtime) ready() error {
|
||||
// version; nothing here resolves `latest`, because an image decided by WHEN the
|
||||
// pod started rather than by what was shipped is not a deployment.
|
||||
func (r *runtime) imageFor(class string) string {
|
||||
if d := r.digestFor(class); d != "" {
|
||||
return r.image + "@" + d
|
||||
}
|
||||
tag := r.tag
|
||||
if tag == "" {
|
||||
tag = envOr("SANDBOX_IMAGE_TAG_"+strings.ToUpper(class), "")
|
||||
@@ -182,7 +185,33 @@ func (r *runtime) imageFor(class string) string {
|
||||
if tag == "" {
|
||||
return r.image + ":" + class
|
||||
}
|
||||
return r.image + ":" + class + "-" + tag
|
||||
// <version>-<class>, which is the order CI PUBLISHES. This read
|
||||
// `class + "-" + tag` and asked for `dev-2026.6.7` while the registry held
|
||||
// `2026.6.7-dev`, so the default path 404'd on an image that was sitting
|
||||
// right there. The publisher wins that argument: hanzoai/ci appends its
|
||||
// per-image `tag-suffix` to the version, so every image in the fleet is
|
||||
// <version>-<suffix> and a consumer that spells it the other way is simply
|
||||
// wrong.
|
||||
return r.image + ":" + tag + "-" + class
|
||||
}
|
||||
|
||||
// DigestFor pins by CONTENT when the deployment names a digest, and it is the
|
||||
// only pin that cannot move.
|
||||
//
|
||||
// A version tag looks immutable and is not: hanzoai/bot ships the sandbox image
|
||||
// under bot's own package.json version, which was last bumped 2026-06-07, so a
|
||||
// rebuild TODAY republished `2026.6.7-dev` from a commit two months newer. A tag
|
||||
// that gets rewritten is not a pin, and one that LOOKS like a pin is worse than
|
||||
// `latest`, which at least admits what it is.
|
||||
//
|
||||
// SANDBOX_IMAGE_DIGEST is therefore honoured ahead of any tag: `repo@sha256:…`
|
||||
// names bytes, and bytes do not change under a running fleet.
|
||||
// It is PER CLASS, because the three classes are three different images and one
|
||||
// digest names one of them. A single SANDBOX_IMAGE_DIGEST would have quietly
|
||||
// given every class the exec image — the same shape of bug as a tag that looks
|
||||
// pinned and is not, which is what this function exists to end.
|
||||
func (r *runtime) digestFor(class string) string {
|
||||
return strings.TrimSpace(os.Getenv("SANDBOX_IMAGE_DIGEST_" + strings.ToUpper(class)))
|
||||
}
|
||||
|
||||
func (r *runtime) pods() dynamic.ResourceInterface {
|
||||
|
||||
Reference in New Issue
Block a user