proxy: Add support for manifest lists

We need to support manifest lists. I'm not sure how I missed this
originally.  At least now we have integration tests that cover this.

The issue here is fairly subtle - the way c/image works right now,
`image.FromUnparsedImage` does pick a matching image from a list
by default.  But it also overrides `GetManifest()` to return the
original manifest list, which defeats our goal here.

Handle this by adding explicit manifest list support code.  We'll
want this anyways for future support for `GetRawManifest` or so
which exposes OCI manifest lists to the client.

Signed-off-by: Colin Walters <walters@verbum.org>
This commit is contained in:
Colin Walters 2021-11-08 09:24:43 -05:00
parent 83416068d3
commit 644074cbb4
2 changed files with 118 additions and 39 deletions

View File

@ -143,9 +143,9 @@ type activePipe struct {
// openImage is an opened image reference
type openImage struct {
// id is an opaque integer handle
id uint32
src types.ImageSource
img types.Image
id uint32
src types.ImageSource
cachedimg types.Image
}
// proxyHandler is the state associated with our socket.
@ -219,16 +219,11 @@ func (h *proxyHandler) OpenImage(args []interface{}) (replyBuf, error) {
if err != nil {
return ret, err
}
img, err := image.FromUnparsedImage(context.Background(), h.sysctx, image.UnparsedInstance(imgsrc, nil))
if err != nil {
return ret, fmt.Errorf("failed to load image: %w", err)
}
h.imageSerial++
openimg := &openImage{
id: h.imageSerial,
src: imgsrc,
img: img,
}
h.images[openimg.id] = openimg
ret.value = openimg.id
@ -326,7 +321,46 @@ func (h *proxyHandler) returnBytes(retval interface{}, buf []byte) (replyBuf, er
return ret, nil
}
// cacheTargetManifest is invoked when GetManifest or GetConfig is invoked
// the first time for a given image. If the requested image is a manifest
// list, this function resolves it to the image matching the calling process'
// operating system and architecture.
//
// TODO: Add GetRawManifest or so that exposes manifest lists
func (h *proxyHandler) cacheTargetManifest(img *openImage) error {
ctx := context.Background()
if img.cachedimg != nil {
return nil
}
unparsedToplevel := image.UnparsedInstance(img.src, nil)
mfest, manifestType, err := unparsedToplevel.Manifest(ctx)
if err != nil {
return err
}
var target *image.UnparsedImage
if manifest.MIMETypeIsMultiImage(manifestType) {
manifestList, err := manifest.ListFromBlob(mfest, manifestType)
if err != nil {
return err
}
instanceDigest, err := manifestList.ChooseInstance(h.sysctx)
if err != nil {
return err
}
target = image.UnparsedInstance(img.src, &instanceDigest)
} else {
target = unparsedToplevel
}
cachedimg, err := image.FromUnparsedImage(ctx, h.sysctx, target)
if err != nil {
return err
}
img.cachedimg = cachedimg
return nil
}
// GetManifest returns a copy of the manifest, converted to OCI format, along with the original digest.
// Manifest lists are resolved to the current operating system and architecture.
func (h *proxyHandler) GetManifest(args []interface{}) (replyBuf, error) {
h.lock.Lock()
defer h.lock.Unlock()
@ -344,11 +378,18 @@ func (h *proxyHandler) GetManifest(args []interface{}) (replyBuf, error) {
return ret, err
}
ctx := context.TODO()
rawManifest, manifestType, err := imgref.img.Manifest(ctx)
err = h.cacheTargetManifest(imgref)
if err != nil {
return ret, err
}
img := imgref.cachedimg
ctx := context.Background()
rawManifest, manifestType, err := img.Manifest(ctx)
if err != nil {
return ret, err
}
// We only support OCI and docker2schema2. We know docker2schema2 can be easily+cheaply
// converted into OCI, so consumers only need to see OCI.
switch manifestType {
@ -373,7 +414,7 @@ func (h *proxyHandler) GetManifest(args []interface{}) (replyBuf, error) {
// docker schema and MIME types.
if manifestType != imgspecv1.MediaTypeImageManifest {
manifestUpdates := types.ManifestUpdateOptions{ManifestMIMEType: imgspecv1.MediaTypeImageManifest}
ociImage, err := imgref.img.UpdatedImage(ctx, manifestUpdates)
ociImage, err := img.UpdatedImage(ctx, manifestUpdates)
if err != nil {
return ret, err
}
@ -406,9 +447,14 @@ func (h *proxyHandler) GetConfig(args []interface{}) (replyBuf, error) {
if err != nil {
return ret, err
}
err = h.cacheTargetManifest(imgref)
if err != nil {
return ret, err
}
img := imgref.cachedimg
ctx := context.TODO()
config, err := imgref.img.OCIConfig(ctx)
config, err := img.OCIConfig(ctx)
if err != nil {
return ret, err
}

View File

@ -145,9 +145,25 @@ func newProxy() (*proxy, error) {
return nil, err
}
return &proxy{
p := &proxy{
c: mysock.(*net.UnixConn),
}, nil
}
v, fd, err := p.call("Initialize", nil)
if err != nil {
return nil, err
}
if fd != nil {
return nil, fmt.Errorf("proxy Initialize: Unexpected fd")
}
semver, ok := v.(string)
if !ok {
return nil, fmt.Errorf("proxy Initialize: Unexpected value %T", v)
}
if !strings.HasPrefix(semver, expectedProxySemverMajor) {
return nil, fmt.Errorf("Unexpected semver %s", semver)
}
return p, nil
}
func init() {
@ -182,32 +198,28 @@ type byteFetch struct {
err error
}
func (s *ProxySuite) TestProxy(c *check.C) {
p, err := newProxy()
c.Assert(err, check.IsNil)
v, fd, err := p.call("Initialize", nil)
c.Assert(err, check.IsNil)
semver, ok := v.(string)
if !ok {
c.Fatalf("Unexpected value %T", v)
func runTestGetManifest(p *proxy, img string) error {
v, fd, err := p.call("OpenImage", []interface{}{knownNotManifestListedImage_x8664})
if err != nil {
return err
}
if !strings.HasPrefix(semver, expectedProxySemverMajor) {
c.Fatalf("Unexpected semver %s", semver)
if fd != nil {
return fmt.Errorf("Unexpected fd")
}
c.Assert(fd, check.IsNil)
v, fd, err = p.call("OpenImage", []interface{}{knownNotManifestListedImage_x8664})
c.Assert(err, check.IsNil)
c.Assert(fd, check.IsNil)
imgidv, ok := v.(float64)
c.Assert(ok, check.Equals, true)
if !ok {
return fmt.Errorf("OpenImage return value is %T", v)
}
imgid := uint32(imgidv)
v, fd, err = p.call("GetManifest", []interface{}{imgid})
c.Assert(err, check.IsNil)
c.Assert(fd, check.NotNil)
if err != nil {
return err
}
if fd == nil {
return fmt.Errorf("expected GetManifest fd")
}
fetchchan := make(chan byteFetch)
go func() {
manifestBytes, err := ioutil.ReadAll(fd.fd)
@ -217,15 +229,36 @@ func (s *ProxySuite) TestProxy(c *check.C) {
}
}()
_, _, err = p.call("FinishPipe", []interface{}{fd.id})
c.Assert(err, check.IsNil)
if err != nil {
return err
}
fetchRes := <-fetchchan
c.Assert(fetchRes.err, check.IsNil)
if fetchRes.err != nil {
return err
}
_, err = manifest.OCI1FromManifest(fetchRes.content)
if err != nil {
return err
}
_, _, err = p.call("CloseImage", []interface{}{imgid})
return nil
}
func (s *ProxySuite) TestProxy(c *check.C) {
p, err := newProxy()
c.Assert(err, check.IsNil)
td, err := ioutil.TempDir("", "skopeo-proxy")
defer os.RemoveAll(td)
err = runTestGetManifest(p, knownNotManifestListedImage_x8664)
if err != nil {
err = fmt.Errorf("Testing image %s: %v", knownNotManifestListedImage_x8664, err)
}
c.Assert(err, check.IsNil)
c.Assert(initOci(td), check.IsNil)
err = runTestGetManifest(p, knownListImage)
if err != nil {
err = fmt.Errorf("Testing image %s: %v", knownListImage, err)
}
c.Assert(err, check.IsNil)
}