Skip to content

Commit

Permalink
Merge pull request #4311 from cpuguy83/one_platform_resolve
Browse files Browse the repository at this point in the history
ResolveImageConfig: Only fetch best matching config
  • Loading branch information
tonistiigi authored Oct 13, 2023
2 parents 0b13071 + 575cb10 commit 4e6e35e
Show file tree
Hide file tree
Showing 2 changed files with 208 additions and 0 deletions.
1 change: 1 addition & 0 deletions util/imageutil/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ func Config(ctx context.Context, str string, resolver remotes.Resolver, cache Co
}

children := childrenConfigHandler(cache, platform)
children = images.LimitManifests(children, platform, 1)

dslHandler, err := docker.AppendDistributionSourceLabel(cache, ref.String())
if err != nil {
Expand Down
207 changes: 207 additions & 0 deletions util/imageutil/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
package imageutil

import (
"bytes"
"context"
"encoding/json"
"io"
"math/rand"
"testing"

"github.com/containerd/containerd/content"
"github.com/containerd/containerd/errdefs"
"github.com/containerd/containerd/platforms"
"github.com/containerd/containerd/remotes"
digest "github.com/opencontainers/go-digest"
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/stretchr/testify/require"
)

func TestConfigMultiplatform(t *testing.T) {
ctx := context.Background()

cc := &testCache{}

pAmd64 := platforms.MustParse("linux/amd64")
cfgDescAmd64 := cc.Add(t, ocispecs.Image{Platform: pAmd64}, ocispecs.MediaTypeImageConfig, nil)
mfstAmd64 := ocispecs.Manifest{MediaType: ocispecs.MediaTypeImageManifest, Config: cfgDescAmd64}

// Add linux/amd64 to the cache
descAmd64 := cc.Add(t, mfstAmd64, mfstAmd64.MediaType, &pAmd64)

// Make a compatible manifest but do not add to the cache
p386 := platforms.MustParse("linux/386")
cfgDesc386 := cc.Add(t, ocispecs.Image{Platform: p386}, ocispecs.MediaTypeImageConfig, nil)
mfst386 := ocispecs.Manifest{MediaType: ocispecs.MediaTypeImageManifest, Config: cfgDesc386}
_, desc386 := makeDesc(t, mfst386, mfst386.MediaType, &p386)

// And add an extra non-compataible platform
pArm64 := platforms.MustParse("linux/arm64")
cfgDescArm64 := cc.Add(t, ocispecs.Image{Platform: pArm64}, ocispecs.MediaTypeImageConfig, nil)
mfstArm64 := ocispecs.Manifest{MediaType: ocispecs.MediaTypeImageManifest, Config: cfgDescArm64}
_, descArm64 := makeDesc(t, mfst386, mfstArm64.MediaType, &pArm64)

idx := ocispecs.Index{
MediaType: ocispecs.MediaTypeImageIndex,
Manifests: []ocispecs.Descriptor{desc386, descAmd64, descArm64},
}

check := func(t *testing.T) {
t.Helper()

idxDesc := cc.Add(t, idx, idx.MediaType, nil)
r := &testResolver{cc: cc, resolve: func(ctx context.Context, ref string) (string, ocispecs.Descriptor, error) {
return ref, idxDesc, nil
}}

// Now we should be able to get the amd64 config without fetching anything from the remote
// If it tries to fetch from the remote this will error out.
const ref = "example.com/test:latest"
_, _, dt, err := Config(ctx, ref, r, cc, nil, &pAmd64, nil)
require.NoError(t, err)

var cfg ocispecs.Image
require.NoError(t, json.Unmarshal(dt, &cfg))
// Validate this is the amd64 config
require.True(t, platforms.OnlyStrict(cfg.Platform).Match(pAmd64))

// Make sure it doesn't select a non-matching platform
pArmv7 := platforms.MustParse("linux/arm/v7")
_, _, _, err = Config(ctx, ref, r, cc, nil, &pArmv7, nil)
require.ErrorIs(t, err, errdefs.ErrNotFound)
}

check(t)

// Shuffle the manifests around and make sure it still works
rand.Shuffle(len(idx.Manifests), func(i, j int) {
idx.Manifests[i], idx.Manifests[j] = idx.Manifests[j], idx.Manifests[i]
})
check(t)
}

type testCache struct {
content.Manager
content map[digest.Digest]content.ReaderAt
}

func (testCache) Info(ctx context.Context, dgst digest.Digest) (content.Info, error) {
return content.Info{}, nil
}

func (testCache) Update(context.Context, content.Info, ...string) (content.Info, error) {
return content.Info{}, nil
}

func (*testCache) Writer(ctx context.Context, opts ...content.WriterOpt) (content.Writer, error) {
// This needs to be implemented because the content helpers will open a writer to use as a lock
return nopWriter{}, nil
}

type nopWriter struct{}

func (nopWriter) Write(p []byte) (int, error) {
return len(p), nil
}

func (nopWriter) Close() error {
return nil
}

func (nopWriter) Commit(ctx context.Context, size int64, expected digest.Digest, opts ...content.Opt) error {
return nil
}

func (nopWriter) Status() (content.Status, error) {
return content.Status{}, nil
}

func (nopWriter) Truncate(size int64) error {
return nil
}

func (nopWriter) Digest() digest.Digest {
panic("not implemented")
}

func makeDesc(t *testing.T, item any, mt string, p *ocispecs.Platform) ([]byte, ocispecs.Descriptor) {
t.Helper()

dt, err := json.Marshal(item)
require.NoError(t, err)

return dt, ocispecs.Descriptor{
MediaType: mt,
Digest: digest.FromBytes(dt),
Size: int64(len(dt)),
Platform: p,
}
}

func (c *testCache) Add(t *testing.T, item any, mt string, p *ocispecs.Platform) ocispecs.Descriptor {
if c.content == nil {
c.content = make(map[digest.Digest]content.ReaderAt)
}

dt, desc := makeDesc(t, item, mt, p)
c.content[desc.Digest] = &sectionNopCloser{io.NewSectionReader(bytes.NewReader(dt), 0, int64(len(dt)))}
return desc
}

type sectionNopCloser struct {
*io.SectionReader
}

func (*sectionNopCloser) Close() error {
return nil
}

func (c *testCache) ReaderAt(ctx context.Context, desc ocispecs.Descriptor) (content.ReaderAt, error) {
ra, ok := c.content[desc.Digest]
if !ok {
return nil, errdefs.ErrNotFound
}
return ra, nil
}

type testResolver struct {
resolve func(ctx context.Context, ref string) (string, ocispecs.Descriptor, error)
cc ContentCache
}

type fetcherFunc func(context.Context, ocispecs.Descriptor) (io.ReadCloser, error)

func (f fetcherFunc) Fetch(ctx context.Context, desc ocispecs.Descriptor) (io.ReadCloser, error) {
return f(ctx, desc)
}

func (r *testResolver) Fetcher(ctx context.Context, ref string) (remotes.Fetcher, error) {
return fetcherFunc(func(ctx context.Context, desc ocispecs.Descriptor) (io.ReadCloser, error) {
ra, err := r.cc.ReaderAt(ctx, desc)
if err != nil {
return nil, err
}
return &raReader{ReaderAt: ra, Closer: ra}, nil
}), nil
}

type raReader struct {
io.ReaderAt
io.Closer

pos int
}

func (r *raReader) Read(p []byte) (int, error) {
n, err := r.ReaderAt.ReadAt(p, int64(r.pos))
r.pos += n
return n, err
}

func (r *testResolver) Resolve(ctx context.Context, ref string) (string, ocispecs.Descriptor, error) {
return r.resolve(ctx, ref)
}

func (r *testResolver) Pusher(context.Context, string) (remotes.Pusher, error) {
panic("not implemented")
}

0 comments on commit 4e6e35e

Please sign in to comment.