From f8992f451cf61dcf754f7f5a68ff1a756aed7610 Mon Sep 17 00:00:00 2001 From: Kathryn Baldauf Date: Thu, 29 Aug 2019 15:57:12 -0700 Subject: [PATCH] add optional check that snapshotter supports the image platform when unpacking Signed-off-by: Kathryn Baldauf --- client.go | 31 ++++++++++++ client_test.go | 2 +- image.go | 67 +++++++++++++++++++++++-- image_test.go | 39 ++++++++++++++ services/introspection/introspection.go | 2 + snapshots/windows/windows.go | 3 ++ unpacker.go | 12 +++++ 7 files changed, 150 insertions(+), 6 deletions(-) diff --git a/client.go b/client.go index e72433c9b..977e9c8e2 100644 --- a/client.go +++ b/client.go @@ -39,6 +39,7 @@ import ( snapshotsapi "github.com/containerd/containerd/api/services/snapshots/v1" "github.com/containerd/containerd/api/services/tasks/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/content" contentproxy "github.com/containerd/containerd/content/proxy" @@ -782,3 +783,33 @@ func CheckRuntime(current, expected string) bool { } 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 +} diff --git a/client_test.go b/client_test.go index 4d6ff964b..3cc726120 100644 --- a/client_test.go +++ b/client_test.go @@ -137,7 +137,7 @@ func TestMain(m *testing.M) { }).Info("running tests against containerd") // 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 { fmt.Fprintf(os.Stderr, "%s: %s\n", err, buf.String()) ctrd.Kill() diff --git a/image.go b/image.go index c96b79301..f35f0edca 100644 --- a/image.go +++ b/image.go @@ -18,6 +18,7 @@ package containerd import ( "context" + "encoding/json" "fmt" "strings" "sync/atomic" @@ -281,11 +282,22 @@ type UnpackConfig struct { ApplyOpts []diff.ApplyOpt // SnapshotOpts for configuring a snapshotter 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 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 { ctx, done, err := i.client.WithLease(ctx) 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 { return err } @@ -320,6 +337,12 @@ func (i *image) Unpack(ctx context.Context, snapshotterName string, opts ...Unpa if err != nil { return err } + if config.CheckPlatformSupported { + if err := i.checkSnapshotterSupport(ctx, snapshotterName, manifest); err != nil { + return err + } + } + for _, layer := range layers { unpacked, err = rootfs.ApplyLayerWithOpts(ctx, layer, chain, sn, a, config.SnapshotOpts, config.ApplyOpts) if err != nil { @@ -361,14 +384,17 @@ func (i *image) Unpack(ctx context.Context, snapshotterName string, opts ...Unpa return err } -func (i *image) getLayers(ctx context.Context, platform platforms.MatchComparer) ([]rootfs.Layer, error) { - cs := i.client.ContentStore() - +func (i *image) getManifest(ctx context.Context, platform platforms.MatchComparer) (ocispec.Manifest, error) { + cs := i.ContentStore() manifest, err := images.Manifest(ctx, cs, i.i.Target, platform) 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) if err != nil { 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 } +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 { return i.client.ContentStore() } diff --git a/image_test.go b/image_test.go index 38aa5fbb3..939e4b4d4 100644 --- a/image_test.go +++ b/image_test.go @@ -81,6 +81,9 @@ func TestImageIsUnpacked(t *testing.T) { } func TestImagePullWithDistSourceLabel(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip() + } var ( source = "docker.io" 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) } } + +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) + } +} diff --git a/services/introspection/introspection.go b/services/introspection/introspection.go index 5024788b5..69465d84c 100644 --- a/services/introspection/introspection.go +++ b/services/introspection/introspection.go @@ -21,6 +21,7 @@ import ( api "github.com/containerd/containerd/api/services/introspection/v1" "github.com/containerd/containerd/errdefs" + "github.com/containerd/containerd/log" 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) { + log.G(ctx).WithField("filters", filters).Info("remote introspection plugin filters") resp, err := i.client.Plugins(ctx, &api.PluginsRequest{ Filters: filters, }) diff --git a/snapshots/windows/windows.go b/snapshots/windows/windows.go index 647800623..d4668a230 100644 --- a/snapshots/windows/windows.go +++ b/snapshots/windows/windows.go @@ -32,10 +32,12 @@ import ( "github.com/containerd/containerd/errdefs" "github.com/containerd/containerd/log" "github.com/containerd/containerd/mount" + "github.com/containerd/containerd/platforms" "github.com/containerd/containerd/plugin" "github.com/containerd/containerd/snapshots" "github.com/containerd/containerd/snapshots/storage" "github.com/containerd/continuity/fs" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" "github.com/pkg/errors" ) @@ -44,6 +46,7 @@ func init() { Type: plugin.SnapshotPlugin, ID: "windows", InitFn: func(ic *plugin.InitContext) (interface{}, error) { + ic.Meta.Platforms = []ocispec.Platform{platforms.DefaultSpec()} return NewSnapshotter(ic.Root) }, }) diff --git a/unpacker.go b/unpacker.go index 1395fc6d9..76f5d7b0c 100644 --- a/unpacker.go +++ b/unpacker.go @@ -31,6 +31,7 @@ import ( "github.com/containerd/containerd/images" "github.com/containerd/containerd/log" "github.com/containerd/containerd/mount" + "github.com/containerd/containerd/platforms" "github.com/containerd/containerd/snapshots" "github.com/opencontainers/go-digest" "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)) } + 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 ( sn = u.c.SnapshotService(u.snapshotter) a = u.c.DiffService()