Windows: Block pulling uplevel images

Signed-off-by: John Howard <jhoward@microsoft.com>
This commit is contained in:
John Howard 2018-02-15 13:17:27 -08:00
parent 68c3201626
commit 83908836d3
9 changed files with 107 additions and 36 deletions

View File

@ -19,6 +19,7 @@ import (
"github.com/docker/docker/registry"
"github.com/docker/libtrust"
"github.com/opencontainers/go-digest"
specs "github.com/opencontainers/image-spec/specs-go/v1"
"golang.org/x/net/context"
)
@ -86,7 +87,8 @@ type ImagePushConfig struct {
type ImageConfigStore interface {
Put([]byte) (digest.Digest, error)
Get(digest.Digest) ([]byte, error)
RootFSAndOSFromConfig([]byte) (*image.RootFS, string, error)
RootFSFromConfig([]byte) (*image.RootFS, error)
PlatformFromConfig([]byte) (*specs.Platform, error)
}
// PushLayerProvider provides layers to be pushed by ChainID.
@ -140,18 +142,26 @@ func (s *imageConfigStore) Get(d digest.Digest) ([]byte, error) {
return img.RawJSON(), nil
}
func (s *imageConfigStore) RootFSAndOSFromConfig(c []byte) (*image.RootFS, string, error) {
func (s *imageConfigStore) RootFSFromConfig(c []byte) (*image.RootFS, error) {
var unmarshalledConfig image.Image
if err := json.Unmarshal(c, &unmarshalledConfig); err != nil {
return nil, "", err
return nil, err
}
return unmarshalledConfig.RootFS, nil
}
func (s *imageConfigStore) PlatformFromConfig(c []byte) (*specs.Platform, error) {
var unmarshalledConfig image.Image
if err := json.Unmarshal(c, &unmarshalledConfig); err != nil {
return nil, err
}
// fail immediately on Windows when downloading a non-Windows image
// and vice versa. Exception on Windows if Linux Containers are enabled.
if runtime.GOOS == "windows" && unmarshalledConfig.OS == "linux" && !system.LCOWSupported() {
return nil, "", fmt.Errorf("image operating system %q cannot be used on this platform", unmarshalledConfig.OS)
return nil, fmt.Errorf("image operating system %q cannot be used on this platform", unmarshalledConfig.OS)
} else if runtime.GOOS != "windows" && unmarshalledConfig.OS == "windows" {
return nil, "", fmt.Errorf("image operating system %q cannot be used on this platform", unmarshalledConfig.OS)
return nil, fmt.Errorf("image operating system %q cannot be used on this platform", unmarshalledConfig.OS)
}
os := unmarshalledConfig.OS
@ -159,9 +169,9 @@ func (s *imageConfigStore) RootFSAndOSFromConfig(c []byte) (*image.RootFS, strin
os = runtime.GOOS
}
if !system.IsOSSupported(os) {
return nil, "", system.ErrNotSupportedOperatingSystem
return nil, system.ErrNotSupportedOperatingSystem
}
return unmarshalledConfig.RootFS, os, nil
return &specs.Platform{OS: os, OSVersion: unmarshalledConfig.OSVersion}, nil
}
type storeLayerProvider struct {

View File

@ -30,6 +30,7 @@ import (
refstore "github.com/docker/docker/reference"
"github.com/docker/docker/registry"
digest "github.com/opencontainers/go-digest"
specs "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"golang.org/x/net/context"
@ -584,11 +585,11 @@ func (p *v2Puller) pullSchema2(ctx context.Context, ref reference.Named, mfst *s
}()
var (
configJSON []byte // raw serialized image config
downloadedRootFS *image.RootFS // rootFS from registered layers
configRootFS *image.RootFS // rootFS from configuration
release func() // release resources from rootFS download
configOS string // for LCOW when registering downloaded layers
configJSON []byte // raw serialized image config
downloadedRootFS *image.RootFS // rootFS from registered layers
configRootFS *image.RootFS // rootFS from configuration
release func() // release resources from rootFS download
configPlatform *specs.Platform // for LCOW when registering downloaded layers
)
// https://github.com/docker/docker/issues/24766 - Err on the side of caution,
@ -600,14 +601,16 @@ func (p *v2Puller) pullSchema2(ctx context.Context, ref reference.Named, mfst *s
// check to block Windows images being pulled on Linux is implemented, it
// may be necessary to perform the same type of serialisation.
if runtime.GOOS == "windows" {
configJSON, configRootFS, configOS, err = receiveConfig(p.config.ImageStore, configChan, configErrChan)
configJSON, configRootFS, configPlatform, err = receiveConfig(p.config.ImageStore, configChan, configErrChan)
if err != nil {
return "", "", err
}
if configRootFS == nil {
return "", "", errRootFSInvalid
}
if err := checkImageCompatibility(configPlatform.OS, configPlatform.OSVersion); err != nil {
return "", "", err
}
if len(descriptors) != len(configRootFS.DiffIDs) {
return "", "", errRootFSMismatch
@ -615,8 +618,8 @@ func (p *v2Puller) pullSchema2(ctx context.Context, ref reference.Named, mfst *s
// Early bath if the requested OS doesn't match that of the configuration.
// This avoids doing the download, only to potentially fail later.
if !strings.EqualFold(configOS, requestedOS) {
return "", "", fmt.Errorf("cannot download image with operating system %q when requesting %q", configOS, requestedOS)
if !strings.EqualFold(configPlatform.OS, requestedOS) {
return "", "", fmt.Errorf("cannot download image with operating system %q when requesting %q", configPlatform.OS, requestedOS)
}
// Populate diff ids in descriptors to avoid downloading foreign layers
@ -698,16 +701,20 @@ func (p *v2Puller) pullSchema2(ctx context.Context, ref reference.Named, mfst *s
return imageID, manifestDigest, nil
}
func receiveConfig(s ImageConfigStore, configChan <-chan []byte, errChan <-chan error) ([]byte, *image.RootFS, string, error) {
func receiveConfig(s ImageConfigStore, configChan <-chan []byte, errChan <-chan error) ([]byte, *image.RootFS, *specs.Platform, error) {
select {
case configJSON := <-configChan:
rootfs, os, err := s.RootFSAndOSFromConfig(configJSON)
rootfs, err := s.RootFSFromConfig(configJSON)
if err != nil {
return nil, nil, "", err
return nil, nil, nil, err
}
return configJSON, rootfs, os, nil
platform, err := s.PlatformFromConfig(configJSON)
if err != nil {
return nil, nil, nil, err
}
return configJSON, rootfs, platform, nil
case err := <-errChan:
return nil, nil, "", err
return nil, nil, nil, err
// Don't need a case for ctx.Done in the select because cancellation
// will trigger an error in p.pullSchema2ImageConfig.
}
@ -736,6 +743,10 @@ func (p *v2Puller) pullManifestList(ctx context.Context, ref reference.Named, mf
}
manifestDigest := manifestMatches[0].Digest
if err := checkImageCompatibility(manifestMatches[0].Platform.OS, manifestMatches[0].Platform.OSVersion); err != nil {
return "", "", err
}
manSvc, err := p.repo.Manifests(ctx)
if err != nil {
return "", "", err

View File

@ -27,3 +27,8 @@ func filterManifests(manifests []manifestlist.ManifestDescriptor, os string) []m
}
return matches
}
// checkImageCompatibility is a Windows-specific function. No-op on Linux
func checkImageCompatibility(imageOS, imageOSVersion string) error {
return nil
}

View File

@ -1,11 +1,13 @@
package distribution // import "github.com/docker/docker/distribution"
import (
"errors"
"fmt"
"net/http"
"os"
"runtime"
"sort"
"strconv"
"strings"
"github.com/docker/distribution"
@ -63,7 +65,6 @@ func (ld *v2LayerDescriptor) open(ctx context.Context) (distribution.ReadSeekClo
func filterManifests(manifests []manifestlist.ManifestDescriptor, os string) []manifestlist.ManifestDescriptor {
osVersion := ""
if os == "windows" {
// TODO: Add UBR (Update Build Release) component after build
version := system.GetOSVersion()
osVersion = fmt.Sprintf("%d.%d.%d", version.MajorVersion, version.MinorVersion, version.Build)
logrus.Debugf("will prefer entries with version %s", osVersion)
@ -71,10 +72,11 @@ func filterManifests(manifests []manifestlist.ManifestDescriptor, os string) []m
var matches []manifestlist.ManifestDescriptor
for _, manifestDescriptor := range manifests {
// TODO: Consider filtering out greater versions, including only greater UBR
if manifestDescriptor.Platform.Architecture == runtime.GOARCH && manifestDescriptor.Platform.OS == os {
matches = append(matches, manifestDescriptor)
logrus.Debugf("found match for %s/%s with media type %s, digest %s", os, runtime.GOARCH, manifestDescriptor.MediaType, manifestDescriptor.Digest.String())
logrus.Debugf("found match for %s/%s %s with media type %s, digest %s", os, runtime.GOARCH, manifestDescriptor.Platform.OSVersion, manifestDescriptor.MediaType, manifestDescriptor.Digest.String())
} else {
logrus.Debugf("ignoring %s/%s %s with media type %s, digest %s", os, runtime.GOARCH, manifestDescriptor.Platform.OSVersion, manifestDescriptor.MediaType, manifestDescriptor.Digest.String())
}
}
if os == "windows" {
@ -107,3 +109,22 @@ func (mbv manifestsByVersion) Len() int {
func (mbv manifestsByVersion) Swap(i, j int) {
mbv.list[i], mbv.list[j] = mbv.list[j], mbv.list[i]
}
// checkImageCompatibility blocks pulling incompatible images based on a later OS build
// Fixes https://github.com/moby/moby/issues/36184.
func checkImageCompatibility(imageOS, imageOSVersion string) error {
if imageOS == "windows" {
hostOSV := system.GetOSVersion()
splitImageOSVersion := strings.Split(imageOSVersion, ".") // eg 10.0.16299.nnnn
if len(splitImageOSVersion) >= 3 {
if imageOSBuild, err := strconv.Atoi(splitImageOSVersion[2]); err == nil {
if imageOSBuild > int(hostOSV.Build) {
errMsg := fmt.Sprintf("a Windows version %s.%s.%s-based image is incompatible with a %s host", splitImageOSVersion[0], splitImageOSVersion[1], splitImageOSVersion[2], hostOSV.ToString())
logrus.Debugf(errMsg)
return errors.New(errMsg)
}
}
}
}
return nil
}

View File

@ -118,12 +118,17 @@ func (p *v2Pusher) pushV2Tag(ctx context.Context, ref reference.NamedTagged, id
return fmt.Errorf("could not find image from tag %s: %v", reference.FamiliarString(ref), err)
}
rootfs, os, err := p.config.ImageStore.RootFSAndOSFromConfig(imgConfig)
rootfs, err := p.config.ImageStore.RootFSFromConfig(imgConfig)
if err != nil {
return fmt.Errorf("unable to get rootfs for image %s: %s", reference.FamiliarString(ref), err)
}
l, err := p.config.LayerStores[os].Get(rootfs.ChainID())
platform, err := p.config.ImageStore.PlatformFromConfig(imgConfig)
if err != nil {
return fmt.Errorf("unable to get platform for image %s: %s", reference.FamiliarString(ref), err)
}
l, err := p.config.LayerStores[platform.OS].Get(rootfs.ChainID())
if err != nil {
return fmt.Errorf("failed to get top layer from image: %v", err)
}

View File

@ -1,6 +1,7 @@
package system // import "github.com/docker/docker/pkg/system"
import (
"fmt"
"unsafe"
"github.com/sirupsen/logrus"
@ -53,6 +54,10 @@ func GetOSVersion() OSVersion {
return osv
}
func (osv OSVersion) ToString() string {
return fmt.Sprintf("%d.%d.%d", osv.MajorVersion, osv.MinorVersion, osv.Build)
}
// IsWindowsClient returns true if the SKU is client
// @engine maintainers - this function should not be removed or modified as it
// is used to enforce licensing restrictions on Windows.

View File

@ -33,6 +33,7 @@ import (
"github.com/docker/docker/plugin/v2"
refstore "github.com/docker/docker/reference"
digest "github.com/opencontainers/go-digest"
specs "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"golang.org/x/net/context"
@ -146,10 +147,15 @@ func (s *tempConfigStore) Get(d digest.Digest) ([]byte, error) {
return s.config, nil
}
func (s *tempConfigStore) RootFSAndOSFromConfig(c []byte) (*image.RootFS, string, error) {
func (s *tempConfigStore) RootFSFromConfig(c []byte) (*image.RootFS, error) {
return configToRootFS(c)
}
func (s *tempConfigStore) PlatformFromConfig(c []byte) (*specs.Platform, error) {
// TODO: LCOW/Plugins. This will need revisiting. For now use the runtime OS
return &specs.Platform{OS: runtime.GOOS}, nil
}
func computePrivileges(c types.PluginConfig) types.PluginPrivileges {
var privileges types.PluginPrivileges
if c.Network.Type != "null" && c.Network.Type != "bridge" && c.Network.Type != "" {
@ -534,10 +540,15 @@ func (s *pluginConfigStore) Get(d digest.Digest) ([]byte, error) {
return ioutil.ReadAll(rwc)
}
func (s *pluginConfigStore) RootFSAndOSFromConfig(c []byte) (*image.RootFS, string, error) {
func (s *pluginConfigStore) RootFSFromConfig(c []byte) (*image.RootFS, error) {
return configToRootFS(c)
}
func (s *pluginConfigStore) PlatformFromConfig(c []byte) (*specs.Platform, error) {
// TODO: LCOW/Plugins. This will need revisiting. For now use the runtime OS
return &specs.Platform{OS: runtime.GOOS}, nil
}
type pluginLayerProvider struct {
pm *Manager
plugin *v2.Plugin

View File

@ -6,6 +6,7 @@ import (
"io/ioutil"
"os"
"path/filepath"
"runtime"
"github.com/docker/docker/distribution/xfer"
"github.com/docker/docker/image"
@ -14,6 +15,7 @@ import (
"github.com/docker/docker/pkg/chrootarchive"
"github.com/docker/docker/pkg/progress"
"github.com/opencontainers/go-digest"
specs "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"golang.org/x/net/context"
@ -178,6 +180,10 @@ func (dm *downloadManager) Put(dt []byte) (digest.Digest, error) {
func (dm *downloadManager) Get(d digest.Digest) ([]byte, error) {
return nil, fmt.Errorf("digest not found")
}
func (dm *downloadManager) RootFSAndOSFromConfig(c []byte) (*image.RootFS, string, error) {
func (dm *downloadManager) RootFSFromConfig(c []byte) (*image.RootFS, error) {
return configToRootFS(c)
}
func (dm *downloadManager) PlatformFromConfig(c []byte) (*specs.Platform, error) {
// TODO: LCOW/Plugins. This will need revisiting. For now use the runtime OS
return &specs.Platform{OS: runtime.GOOS}, nil
}

View File

@ -8,7 +8,6 @@ import (
"path/filepath"
"reflect"
"regexp"
"runtime"
"sort"
"strings"
"sync"
@ -375,19 +374,17 @@ func isEqualPrivilege(a, b types.PluginPrivilege) bool {
return reflect.DeepEqual(a.Value, b.Value)
}
func configToRootFS(c []byte) (*image.RootFS, string, error) {
// TODO @jhowardmsft LCOW - Will need to revisit this.
os := runtime.GOOS
func configToRootFS(c []byte) (*image.RootFS, error) {
var pluginConfig types.PluginConfig
if err := json.Unmarshal(c, &pluginConfig); err != nil {
return nil, "", err
return nil, err
}
// validation for empty rootfs is in distribution code
if pluginConfig.Rootfs == nil {
return nil, os, nil
return nil, nil
}
return rootFSFromPlugin(pluginConfig.Rootfs), os, nil
return rootFSFromPlugin(pluginConfig.Rootfs), nil
}
func rootFSFromPlugin(pluginfs *types.PluginConfigRootfs) *image.RootFS {