Files
at-container-registry/test/integration/multiarch_test.go
2026-05-11 19:53:13 -05:00

122 lines
3.8 KiB
Go

//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)")
}
}