//go:build integration // Multi-arch image index coverage. Buildx, ko, kaniko, and most modern build // tooling push an OCI image index referencing per-platform children — exercising // the manifest-list validation path in pkg/appview/storage/manifest_store.go // (isManifestList check and the per-child s.Exists() loop). Without this test, // any regression in that path goes unnoticed by the rest of the suite, which // only pushes single-arch v1.Image. package integration import ( "fmt" "testing" "github.com/google/go-containerregistry/pkg/authn" "github.com/google/go-containerregistry/pkg/name" v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/empty" "github.com/google/go-containerregistry/pkg/v1/mutate" "github.com/google/go-containerregistry/pkg/v1/random" "github.com/google/go-containerregistry/pkg/v1/remote" "atcr.io/internal/testharness" _ "github.com/distribution/distribution/v3/registry/auth/token" _ "github.com/distribution/distribution/v3/registry/storage/driver/inmemory" ) func TestMultiArchIndexPushPull(t *testing.T) { h := testharness.New(t) alice := h.AddSailor("alice.test") amdImg, err := random.Image(1<<18, 2) if err != nil { t.Fatalf("amd image: %v", err) } armImg, err := random.Image(1<<18, 2) if err != nil { t.Fatalf("arm image: %v", err) } idx := mutate.AppendManifests(empty.Index, mutate.IndexAddendum{Add: amdImg, Descriptor: v1.Descriptor{ Platform: &v1.Platform{Architecture: "amd64", OS: "linux"}, }}, mutate.IndexAddendum{Add: armImg, Descriptor: v1.Descriptor{ Platform: &v1.Platform{Architecture: "arm64", OS: "linux"}, }}, ) pushedDigest, err := idx.Digest() if err != nil { t.Fatalf("idx digest: %v", err) } for _, c := range Clients { t.Run(c.Name(), func(t *testing.T) { // Per-client repo so concurrent / matrixed runs don't collide on // shared blob digests at the registry (same pattern as // TestPushPullHappyPath). ref, err := name.ParseReference( fmt.Sprintf("%s/%s/multi-%s:latest", h.AppViewHostPort(), alice.Handle(), c.Name()), name.Insecure, ) if err != nil { t.Fatalf("parse ref: %v", err) } creds := h.RegistryCreds(alice) if err := c.PushIndex(t.Context(), t, ref.String(), idx, creds); err != nil { t.Fatalf("push index: %v", err) } pulledDigest, err := c.PullIndex(t.Context(), ref.String(), creds) if err != nil { t.Fatalf("pull index: %v", err) } if pushedDigest != pulledDigest { t.Fatalf("digest mismatch: pushed=%s pulled=%s", pushedDigest, pulledDigest) } }) } } // Asserts that the appview rejects an index whose child manifest hasn't been // pushed. manifest_store.go's per-child s.Exists() loop is the only guard // against dangling-reference indexes; this test pins it open. func TestMultiArchIndex_RejectsMissingChild(t *testing.T) { h := testharness.New(t) alice := h.AddSailor("alice.test") orphan, err := random.Image(1<<17, 1) if err != nil { t.Fatalf("orphan image: %v", err) } idx := mutate.AppendManifests(empty.Index, mutate.IndexAddendum{Add: orphan, Descriptor: v1.Descriptor{ Platform: &v1.Platform{Architecture: "amd64", OS: "linux"}, }}, ) // Client libraries upload child manifests + blobs alongside the index, // so a naive PushIndex would succeed. remote.Put writes only the index // manifest body — no children — which is exactly the dangling-reference // scenario manifest_store.go guards against. ref, err := name.ParseReference( fmt.Sprintf("%s/%s/orphan-idx:latest", h.AppViewHostPort(), alice.Handle()), name.Insecure, ) if err != nil { t.Fatalf("parse ref: %v", err) } creds := h.RegistryCreds(alice) err = remote.Put(ref, idx, remote.WithAuth(&authn.Basic{Username: creds.Username, Password: creds.Password}), ) if err == nil { t.Fatal("expected index PUT to be rejected (orphan child manifest)") } }