-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #4311 from cpuguy83/one_platform_resolve
ResolveImageConfig: Only fetch best matching config
- Loading branch information
Showing
2 changed files
with
208 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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] = §ionNopCloser{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") | ||
} |