add optional check that snapshotter supports the image platform when unpacking

Signed-off-by: Kathryn Baldauf <kabaldau@microsoft.com>
This commit is contained in:
Kathryn Baldauf 2019-08-29 15:57:12 -07:00
parent 1e624fa3de
commit f8992f451c
7 changed files with 150 additions and 6 deletions

View File

@ -39,6 +39,7 @@ import (
snapshotsapi "github.com/containerd/containerd/api/services/snapshots/v1" snapshotsapi "github.com/containerd/containerd/api/services/snapshots/v1"
"github.com/containerd/containerd/api/services/tasks/v1" "github.com/containerd/containerd/api/services/tasks/v1"
versionservice "github.com/containerd/containerd/api/services/version/v1" versionservice "github.com/containerd/containerd/api/services/version/v1"
apitypes "github.com/containerd/containerd/api/types"
"github.com/containerd/containerd/containers" "github.com/containerd/containerd/containers"
"github.com/containerd/containerd/content" "github.com/containerd/containerd/content"
contentproxy "github.com/containerd/containerd/content/proxy" contentproxy "github.com/containerd/containerd/content/proxy"
@ -782,3 +783,33 @@ func CheckRuntime(current, expected string) bool {
} }
return true return true
} }
func (c *Client) GetSnapshotterSupportedPlatforms(ctx context.Context, snapshotterName string) (platforms.MatchComparer, error) {
filters := []string{fmt.Sprintf("type==%s, id==%s", plugin.SnapshotPlugin, snapshotterName)}
in := c.IntrospectionService()
resp, err := in.Plugins(ctx, filters)
if err != nil {
return nil, err
}
if len(resp.Plugins) <= 0 {
return nil, fmt.Errorf("inspection service could not find snapshotter %s plugin", snapshotterName)
}
sn := resp.Plugins[0]
snPlatforms := toPlatforms(sn.Platforms)
return platforms.Any(snPlatforms...), nil
}
func toPlatforms(pt []apitypes.Platform) []ocispec.Platform {
platforms := make([]ocispec.Platform, len(pt))
for i, p := range pt {
platforms[i] = ocispec.Platform{
Architecture: p.Architecture,
OS: p.OS,
Variant: p.Variant,
}
}
return platforms
}

View File

@ -137,7 +137,7 @@ func TestMain(m *testing.M) {
}).Info("running tests against containerd") }).Info("running tests against containerd")
// pull a seed image // pull a seed image
log.G(ctx).Info("start to pull seed image") log.G(ctx).WithField("image", testImage).Info("start to pull seed image")
if _, err = client.Pull(ctx, testImage, WithPullUnpack); err != nil { if _, err = client.Pull(ctx, testImage, WithPullUnpack); err != nil {
fmt.Fprintf(os.Stderr, "%s: %s\n", err, buf.String()) fmt.Fprintf(os.Stderr, "%s: %s\n", err, buf.String())
ctrd.Kill() ctrd.Kill()

View File

@ -18,6 +18,7 @@ package containerd
import ( import (
"context" "context"
"encoding/json"
"fmt" "fmt"
"strings" "strings"
"sync/atomic" "sync/atomic"
@ -281,11 +282,22 @@ type UnpackConfig struct {
ApplyOpts []diff.ApplyOpt ApplyOpts []diff.ApplyOpt
// SnapshotOpts for configuring a snapshotter // SnapshotOpts for configuring a snapshotter
SnapshotOpts []snapshots.Opt SnapshotOpts []snapshots.Opt
// CheckPlatformSupported is whether to validate that a snapshotter
// supports an image's platform before unpacking
CheckPlatformSupported bool
} }
// UnpackOpt provides configuration for unpack // UnpackOpt provides configuration for unpack
type UnpackOpt func(context.Context, *UnpackConfig) error type UnpackOpt func(context.Context, *UnpackConfig) error
// WithSnapshotterPlatformCheck sets `CheckPlatformSupported` on the UnpackConfig
func WithSnapshotterPlatformCheck() UnpackOpt {
return func(ctx context.Context, uc *UnpackConfig) error {
uc.CheckPlatformSupported = true
return nil
}
}
func (i *image) Unpack(ctx context.Context, snapshotterName string, opts ...UnpackOpt) error { func (i *image) Unpack(ctx context.Context, snapshotterName string, opts ...UnpackOpt) error {
ctx, done, err := i.client.WithLease(ctx) ctx, done, err := i.client.WithLease(ctx)
if err != nil { if err != nil {
@ -300,7 +312,12 @@ func (i *image) Unpack(ctx context.Context, snapshotterName string, opts ...Unpa
} }
} }
layers, err := i.getLayers(ctx, i.platform) manifest, err := i.getManifest(ctx, i.platform)
if err != nil {
return err
}
layers, err := i.getLayers(ctx, i.platform, manifest)
if err != nil { if err != nil {
return err return err
} }
@ -320,6 +337,12 @@ func (i *image) Unpack(ctx context.Context, snapshotterName string, opts ...Unpa
if err != nil { if err != nil {
return err return err
} }
if config.CheckPlatformSupported {
if err := i.checkSnapshotterSupport(ctx, snapshotterName, manifest); err != nil {
return err
}
}
for _, layer := range layers { for _, layer := range layers {
unpacked, err = rootfs.ApplyLayerWithOpts(ctx, layer, chain, sn, a, config.SnapshotOpts, config.ApplyOpts) unpacked, err = rootfs.ApplyLayerWithOpts(ctx, layer, chain, sn, a, config.SnapshotOpts, config.ApplyOpts)
if err != nil { if err != nil {
@ -361,14 +384,17 @@ func (i *image) Unpack(ctx context.Context, snapshotterName string, opts ...Unpa
return err return err
} }
func (i *image) getLayers(ctx context.Context, platform platforms.MatchComparer) ([]rootfs.Layer, error) { func (i *image) getManifest(ctx context.Context, platform platforms.MatchComparer) (ocispec.Manifest, error) {
cs := i.client.ContentStore() cs := i.ContentStore()
manifest, err := images.Manifest(ctx, cs, i.i.Target, platform) manifest, err := images.Manifest(ctx, cs, i.i.Target, platform)
if err != nil { if err != nil {
return nil, err return ocispec.Manifest{}, err
} }
return manifest, nil
}
func (i *image) getLayers(ctx context.Context, platform platforms.MatchComparer, manifest ocispec.Manifest) ([]rootfs.Layer, error) {
cs := i.ContentStore()
diffIDs, err := i.i.RootFS(ctx, cs, platform) diffIDs, err := i.i.RootFS(ctx, cs, platform)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "failed to resolve rootfs") return nil, errors.Wrap(err, "failed to resolve rootfs")
@ -388,6 +414,37 @@ func (i *image) getLayers(ctx context.Context, platform platforms.MatchComparer)
return layers, nil return layers, nil
} }
func (i *image) getManifestPlatform(ctx context.Context, manifest ocispec.Manifest) (ocispec.Platform, error) {
cs := i.ContentStore()
p, err := content.ReadBlob(ctx, cs, manifest.Config)
if err != nil {
return ocispec.Platform{}, err
}
var image ocispec.Image
if err := json.Unmarshal(p, &image); err != nil {
return ocispec.Platform{}, err
}
return platforms.Normalize(ocispec.Platform{OS: image.OS, Architecture: image.Architecture}), nil
}
func (i *image) checkSnapshotterSupport(ctx context.Context, snapshotterName string, manifest ocispec.Manifest) error {
snapshotterPlatformMatcher, err := i.client.GetSnapshotterSupportedPlatforms(ctx, snapshotterName)
if err != nil {
return err
}
manifestPlatform, err := i.getManifestPlatform(ctx, manifest)
if err != nil {
return err
}
if snapshotterPlatformMatcher.Match(manifestPlatform) {
return nil
}
return fmt.Errorf("snapshotter %s does not support platform %s for image %s", snapshotterName, manifestPlatform, manifest.Config.Digest)
}
func (i *image) ContentStore() content.Store { func (i *image) ContentStore() content.Store {
return i.client.ContentStore() return i.client.ContentStore()
} }

View File

@ -81,6 +81,9 @@ func TestImageIsUnpacked(t *testing.T) {
} }
func TestImagePullWithDistSourceLabel(t *testing.T) { func TestImagePullWithDistSourceLabel(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip()
}
var ( var (
source = "docker.io" source = "docker.io"
repoName = "library/busybox" repoName = "library/busybox"
@ -233,3 +236,39 @@ func TestImageUsage(t *testing.T) {
t.Fatalf("Expected actual usage with snapshots to be greater: %d <= %d", s, s3) t.Fatalf("Expected actual usage with snapshots to be greater: %d <= %d", s, s3)
} }
} }
func TestImageSupportedBySnapshotter_Error(t *testing.T) {
var unsupportedImage string
if runtime.GOOS == "windows" {
unsupportedImage = "docker.io/library/busybox:latest"
} else {
unsupportedImage = "mcr.microsoft.com/windows/nanoserver:1809"
}
ctx, cancel := testContext(t)
defer cancel()
client, err := newClient(t, address)
if err != nil {
t.Fatal(err)
}
defer client.Close()
// Cleanup
err = client.ImageService().Delete(ctx, unsupportedImage)
if err != nil && !errdefs.IsNotFound(err) {
t.Fatal(err)
}
_, err = client.Pull(ctx, unsupportedImage,
WithSchema1Conversion,
WithPlatform(platforms.DefaultString()),
WithPullSnapshotter(DefaultSnapshotter),
WithPullUnpack,
WithUnpackOpts([]UnpackOpt{WithSnapshotterPlatformCheck()}),
)
if err == nil {
t.Fatalf("expected unpacking %s for snapshotter %s to fail", unsupportedImage, DefaultSnapshotter)
}
}

View File

@ -21,6 +21,7 @@ import (
api "github.com/containerd/containerd/api/services/introspection/v1" api "github.com/containerd/containerd/api/services/introspection/v1"
"github.com/containerd/containerd/errdefs" "github.com/containerd/containerd/errdefs"
"github.com/containerd/containerd/log"
ptypes "github.com/gogo/protobuf/types" ptypes "github.com/gogo/protobuf/types"
) )
@ -40,6 +41,7 @@ func NewIntrospectionServiceFromClient(c api.IntrospectionClient) Service {
} }
func (i *introspectionRemote) Plugins(ctx context.Context, filters []string) (*api.PluginsResponse, error) { func (i *introspectionRemote) Plugins(ctx context.Context, filters []string) (*api.PluginsResponse, error) {
log.G(ctx).WithField("filters", filters).Info("remote introspection plugin filters")
resp, err := i.client.Plugins(ctx, &api.PluginsRequest{ resp, err := i.client.Plugins(ctx, &api.PluginsRequest{
Filters: filters, Filters: filters,
}) })

View File

@ -32,10 +32,12 @@ import (
"github.com/containerd/containerd/errdefs" "github.com/containerd/containerd/errdefs"
"github.com/containerd/containerd/log" "github.com/containerd/containerd/log"
"github.com/containerd/containerd/mount" "github.com/containerd/containerd/mount"
"github.com/containerd/containerd/platforms"
"github.com/containerd/containerd/plugin" "github.com/containerd/containerd/plugin"
"github.com/containerd/containerd/snapshots" "github.com/containerd/containerd/snapshots"
"github.com/containerd/containerd/snapshots/storage" "github.com/containerd/containerd/snapshots/storage"
"github.com/containerd/continuity/fs" "github.com/containerd/continuity/fs"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors" "github.com/pkg/errors"
) )
@ -44,6 +46,7 @@ func init() {
Type: plugin.SnapshotPlugin, Type: plugin.SnapshotPlugin,
ID: "windows", ID: "windows",
InitFn: func(ic *plugin.InitContext) (interface{}, error) { InitFn: func(ic *plugin.InitContext) (interface{}, error) {
ic.Meta.Platforms = []ocispec.Platform{platforms.DefaultSpec()}
return NewSnapshotter(ic.Root) return NewSnapshotter(ic.Root)
}, },
}) })

View File

@ -31,6 +31,7 @@ import (
"github.com/containerd/containerd/images" "github.com/containerd/containerd/images"
"github.com/containerd/containerd/log" "github.com/containerd/containerd/log"
"github.com/containerd/containerd/mount" "github.com/containerd/containerd/mount"
"github.com/containerd/containerd/platforms"
"github.com/containerd/containerd/snapshots" "github.com/containerd/containerd/snapshots"
"github.com/opencontainers/go-digest" "github.com/opencontainers/go-digest"
"github.com/opencontainers/image-spec/identity" "github.com/opencontainers/image-spec/identity"
@ -93,6 +94,17 @@ func (u *unpacker) unpack(
return errors.Errorf("number of layers and diffIDs don't match: %d != %d", len(layers), len(diffIDs)) return errors.Errorf("number of layers and diffIDs don't match: %d != %d", len(layers), len(diffIDs))
} }
if u.config.CheckPlatformSupported {
imgPlatform := platforms.Normalize(ocispec.Platform{OS: i.OS, Architecture: i.Architecture})
snapshotterPlatformMatcher, err := u.c.GetSnapshotterSupportedPlatforms(ctx, u.snapshotter)
if err != nil {
return errors.Wrapf(err, "failed to find supported platforms for snapshotter %s", u.snapshotter)
}
if !snapshotterPlatformMatcher.Match(imgPlatform) {
return fmt.Errorf("snapshotter %s does not support platform %s for image %s", u.snapshotter, imgPlatform, config.Digest)
}
}
var ( var (
sn = u.c.SnapshotService(u.snapshotter) sn = u.c.SnapshotService(u.snapshotter)
a = u.c.DiffService() a = u.c.DiffService()