diff --git a/lib/images/oci.go b/lib/images/oci.go index 0051c7eb..7cc6eff0 100644 --- a/lib/images/oci.go +++ b/lib/images/oci.go @@ -80,14 +80,21 @@ func (c *ociClient) inspectManifest(ctx context.Context, imageRef string) (strin // Use system authentication (reads from ~/.docker/config.json, etc.) // Default retry: only on network errors, max ~1.3s total - descriptor, err := remote.Head(ref, + // WithPlatform ensures we resolve multi-arch tags/digests to the + // platform-specific manifest digest that pullToOCILayout will actually pull. + img, err := remote.Image(ref, remote.WithContext(ctx), - remote.WithAuthFromKeychain(authn.DefaultKeychain)) + remote.WithAuthFromKeychain(authn.DefaultKeychain), + remote.WithPlatform(currentPlatform())) if err != nil { - return "", fmt.Errorf("fetch manifest: %w", wrapRegistryError(err)) + return "", fmt.Errorf("fetch image manifest: %w", wrapRegistryError(err)) } - return descriptor.Digest.String(), nil + d, err := img.Digest() + if err != nil { + return "", fmt.Errorf("compute image digest: %w", err) + } + return d.String(), nil } // pullResult contains the metadata and digest from pulling an image diff --git a/lib/images/oci_inspect_test.go b/lib/images/oci_inspect_test.go new file mode 100644 index 00000000..d6e47094 --- /dev/null +++ b/lib/images/oci_inspect_test.go @@ -0,0 +1,43 @@ +package images + +import ( + "context" + "testing" + "time" + + "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/stretchr/testify/require" +) + +func TestInspectManifestReturnsPlatformSpecificDigestForMultiArch(t *testing.T) { + // alpine:latest is a multi-arch image on Docker Hub; we should resolve the + // digest of the platform-specific manifest that we will actually pull. + const imageRef = "docker.io/library/alpine:latest" + + client, err := newOCIClient(t.TempDir()) + require.NoError(t, err) + + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) + defer cancel() + + gotDigest, err := client.inspectManifest(ctx, imageRef) + require.NoError(t, err) + require.NotEmpty(t, gotDigest) + + ref, err := name.ParseReference(imageRef) + require.NoError(t, err) + + img, err := remote.Image(ref, + remote.WithContext(ctx), + remote.WithAuthFromKeychain(authn.DefaultKeychain), + remote.WithPlatform(currentPlatform())) + require.NoError(t, err) + + want, err := img.Digest() + require.NoError(t, err) + + require.Equal(t, want.String(), gotDigest) +} +