Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 52 additions & 9 deletions lib/images/mirror.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (

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

Expand Down Expand Up @@ -47,20 +48,24 @@ func MirrorBaseImage(ctx context.Context, registryURL string, req MirrorRequest,
return nil, err
}

// Pull the image from source
img, err := remote.Image(srcRef,
// Fetch the source descriptor first: what gets pushed depends on what the
// reference names. A digest reference can name a multi-arch *index*, and
// the platform-resolved image inside it has a different digest than the
// index — pushing that image under the index digest fails content-address
// verification at the destination ("digest mismatch"), which is exactly
// what any Dockerfile with a digest-pinned FROM produces. An index named
// by digest is therefore mirrored as the index itself, byte for byte.
// Tag references keep the existing behavior of mirroring only the
// requested platform, which is what saves storage.
desc, err := remote.Get(srcRef,
remote.WithContext(ctx),
remote.WithAuthFromKeychain(authn.DefaultKeychain),
remote.WithPlatform(platform.ToGCR()))
if err != nil {
return nil, fmt.Errorf("pull source image: %w", ClassifyRegistryError(err))
}

// Get the digest
digest, err := img.Digest()
if err != nil {
return nil, fmt.Errorf("get image digest: %w", err)
}
_, isDigestRef := srcRef.(name.Digest)

// Build the local reference under bases/ namespace
// Normalize the source to strip docker.io/ prefix for cleaner local refs
Expand All @@ -87,8 +92,9 @@ func MirrorBaseImage(ctx context.Context, registryURL string, req MirrorRequest,
opts = append(opts, remote.WithAuth(authn.FromConfig(*authConfig)))
}

if err := remote.Write(dstRef, img, opts...); err != nil {
return nil, fmt.Errorf("push to local registry: %w", ClassifyRegistryError(err))
digest, err := pushMirrored(desc, isDigestRef, dstRef, opts...)
if err != nil {
return nil, err
}

return &MirrorResult{
Expand All @@ -98,6 +104,43 @@ func MirrorBaseImage(ctx context.Context, registryURL string, req MirrorRequest,
}, nil
}

// pushMirrored writes the fetched content to dstRef and returns the digest it
// is retrievable under.
//
// An index named by digest is written as the index itself — children first,
// then the index manifest — so the destination stores content whose digest is
// exactly the one the caller pinned. Everything else keeps the original
// behavior: the (platform-resolved) image is written, under its own digest.
func pushMirrored(desc *remote.Descriptor, isDigestRef bool, dstRef name.Reference, opts ...remote.Option) (v1.Hash, error) {
if isDigestRef && desc.MediaType.IsIndex() {
idx, err := desc.ImageIndex()
if err != nil {
return v1.Hash{}, fmt.Errorf("load source index: %w", err)
}
digest, err := idx.Digest()
if err != nil {
return v1.Hash{}, fmt.Errorf("get image digest: %w", err)
}
if err := remote.WriteIndex(dstRef, idx, opts...); err != nil {
return v1.Hash{}, fmt.Errorf("push to local registry: %w", ClassifyRegistryError(err))
}
return digest, nil
}

img, err := desc.Image()
if err != nil {
return v1.Hash{}, fmt.Errorf("pull source image: %w", ClassifyRegistryError(err))
}
digest, err := img.Digest()
if err != nil {
return v1.Hash{}, fmt.Errorf("get image digest: %w", err)
}
if err := remote.Write(dstRef, img, opts...); err != nil {
return v1.Hash{}, fmt.Errorf("push to local registry: %w", ClassifyRegistryError(err))
}
return digest, nil
}

// normalizeToLocalRef converts a source image reference to a normalized local reference.
// It strips the docker.io/ prefix but preserves the library/ prefix for official images.
// The library/ prefix is kept because BuildKit's mirror protocol requests official images
Expand Down
84 changes: 84 additions & 0 deletions lib/images/mirror_test.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
package images

import (
"net/http/httptest"
"strings"
"testing"

"github.com/google/go-containerregistry/pkg/name"
"github.com/google/go-containerregistry/pkg/registry"
"github.com/google/go-containerregistry/pkg/v1/random"
"github.com/google/go-containerregistry/pkg/v1/remote"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
Expand Down Expand Up @@ -89,3 +94,82 @@ func TestStripScheme(t *testing.T) {
})
}
}

// A digest-pinned FROM usually pins the multi-arch *index* digest (it is the
// digest `docker pull` prints), while the mirror pulls the platform-resolved
// image whose digest differs. Pushing that image under the index digest is
// refused by any registry that verifies content addresses — so an index named
// by digest must arrive at the local registry as the index itself, retrievable
// under exactly the digest the caller pinned.
//
// Tested at the pushMirrored seam: httptest registries live on 127.0.0.1:port,
// and a host:port cannot ride through normalizeToLocalRef as a path prefix the
// way docker.io/gcr.io sources do in production.
func TestPushMirroredPreservesIndexDigest(t *testing.T) {
src := httptest.NewServer(registry.New())
defer src.Close()
dst := httptest.NewServer(registry.New())
defer dst.Close()
srcHost := strings.TrimPrefix(src.URL, "http://")
dstHost := strings.TrimPrefix(dst.URL, "http://")

// A two-manifest index, pushed to the source registry and then referenced
// by its digest — the exact shape of a digest-pinned Docker Hub image.
idx, err := random.Index(1024, 1, 2)
require.NoError(t, err)
indexDigest, err := idx.Digest()
require.NoError(t, err)
seed, err := name.ParseReference(srcHost + "/library/python:3.13-alpine")
require.NoError(t, err)
require.NoError(t, remote.WriteIndex(seed, idx))

srcRef, err := name.ParseReference(srcHost + "/library/python@" + indexDigest.String())
require.NoError(t, err)
desc, err := remote.Get(srcRef)
require.NoError(t, err)

dstRef, err := name.ParseReference(dstHost + "/library/python@" + indexDigest.String())
require.NoError(t, err)
digest, err := pushMirrored(desc, true, dstRef)
require.NoError(t, err)
assert.Equal(t, indexDigest, digest,
"the mirrored digest must be the digest the caller pinned")

// The proof the old code could not give: the content is retrievable from
// the destination under the pinned digest, i.e. it verified on push.
mirrored, err := remote.Get(dstRef)
require.NoError(t, err, "the pinned digest must resolve at the local registry")
assert.Equal(t, indexDigest, mirrored.Digest)
assert.True(t, mirrored.MediaType.IsIndex(), "an index must arrive as an index")
}

// The storage-saving path is unchanged: a tag reference mirrors only the
// (platform-resolved) image, not the whole index.
func TestPushMirroredByTagStillMirrorsImage(t *testing.T) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the tag test uses a single-platform image, so it doesn’t check that tagged multi-platform indexes resolve to just the requested platform image

src := httptest.NewServer(registry.New())
defer src.Close()
dst := httptest.NewServer(registry.New())
defer dst.Close()
srcHost := strings.TrimPrefix(src.URL, "http://")
dstHost := strings.TrimPrefix(dst.URL, "http://")

img, err := random.Image(1024, 1)
require.NoError(t, err)
imgDigest, err := img.Digest()
require.NoError(t, err)
srcRef, err := name.ParseReference(srcHost + "/library/alpine:3.21")
require.NoError(t, err)
require.NoError(t, remote.Write(srcRef, img))

desc, err := remote.Get(srcRef)
require.NoError(t, err)
dstRef, err := name.ParseReference(dstHost + "/library/alpine:3.21")
require.NoError(t, err)
digest, err := pushMirrored(desc, false, dstRef)
require.NoError(t, err)
assert.Equal(t, imgDigest, digest)

mirrored, err := remote.Get(dstRef)
require.NoError(t, err)
assert.False(t, mirrored.MediaType.IsIndex())
}