github.com/tonistiigi/docker@v0.10.1-0.20240229224939-974013b0dc6a/distribution/pull_v2_windows.go (about)

     1  package distribution // import "github.com/docker/docker/distribution"
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"io"
     8  	"net/http"
     9  	"runtime"
    10  	"sort"
    11  	"strconv"
    12  	"strings"
    13  
    14  	"github.com/Microsoft/hcsshim/osversion"
    15  	"github.com/containerd/containerd/platforms"
    16  	"github.com/containerd/log"
    17  	"github.com/docker/distribution"
    18  	"github.com/docker/distribution/manifest/manifestlist"
    19  	"github.com/docker/distribution/manifest/schema2"
    20  	"github.com/docker/distribution/registry/client/transport"
    21  	"github.com/docker/docker/image"
    22  	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
    23  )
    24  
    25  var _ distribution.Describable = &layerDescriptor{}
    26  
    27  func (ld *layerDescriptor) Descriptor() distribution.Descriptor {
    28  	if ld.src.MediaType == schema2.MediaTypeForeignLayer && len(ld.src.URLs) > 0 {
    29  		return ld.src
    30  	}
    31  	return distribution.Descriptor{}
    32  }
    33  
    34  func (ld *layerDescriptor) open(ctx context.Context) (distribution.ReadSeekCloser, error) {
    35  	blobs := ld.repo.Blobs(ctx)
    36  	rsc, err := blobs.Open(ctx, ld.digest)
    37  
    38  	if len(ld.src.URLs) == 0 {
    39  		return rsc, err
    40  	}
    41  
    42  	// We're done if the registry has this blob.
    43  	if err == nil {
    44  		// Seek does an HTTP GET.  If it succeeds, the blob really is accessible.
    45  		if _, err = rsc.Seek(0, io.SeekStart); err == nil {
    46  			return rsc, nil
    47  		}
    48  		rsc.Close()
    49  	}
    50  
    51  	// Find the first URL that results in a 200 result code.
    52  	for _, url := range ld.src.URLs {
    53  		log.G(ctx).Debugf("Pulling %v from foreign URL %v", ld.digest, url)
    54  		rsc = transport.NewHTTPReadSeeker(http.DefaultClient, url, nil)
    55  
    56  		// Seek does an HTTP GET.  If it succeeds, the blob really is accessible.
    57  		_, err = rsc.Seek(0, io.SeekStart)
    58  		if err == nil {
    59  			break
    60  		}
    61  		log.G(ctx).Debugf("Download for %v failed: %v", ld.digest, err)
    62  		rsc.Close()
    63  		rsc = nil
    64  	}
    65  	return rsc, err
    66  }
    67  
    68  func filterManifests(manifests []manifestlist.ManifestDescriptor, p ocispec.Platform) []manifestlist.ManifestDescriptor {
    69  	version := osversion.Get()
    70  	osVersion := fmt.Sprintf("%d.%d.%d", version.MajorVersion, version.MinorVersion, version.Build)
    71  	log.G(context.TODO()).Debugf("will prefer Windows entries with version %s", osVersion)
    72  
    73  	var matches []manifestlist.ManifestDescriptor
    74  	foundWindowsMatch := false
    75  	for _, manifestDescriptor := range manifests {
    76  		skip := func() {
    77  			log.G(context.TODO()).Debugf("ignoring %s/%s %s with media type %s, digest %s", manifestDescriptor.Platform.OS, manifestDescriptor.Platform.Architecture, manifestDescriptor.Platform.OSVersion, manifestDescriptor.MediaType, manifestDescriptor.Digest.String())
    78  		}
    79  		// TODO(thaJeztah): should we also check for the user-provided architecture (if any)?
    80  		if manifestDescriptor.Platform.Architecture != runtime.GOARCH {
    81  			skip()
    82  			continue
    83  		}
    84  		os := manifestDescriptor.Platform.OS
    85  		if p.OS != "" {
    86  			// Explicit user request for an OS
    87  			os = p.OS
    88  		}
    89  		if err := image.CheckOS(os); err != nil {
    90  			skip()
    91  			continue
    92  		}
    93  		// TODO(thaJeztah): should we also take the user-provided platform into account (if any)?
    94  		if strings.EqualFold("windows", manifestDescriptor.Platform.OS) {
    95  			if err := checkImageCompatibility("windows", manifestDescriptor.Platform.OSVersion); err != nil {
    96  				skip()
    97  				continue
    98  			}
    99  			foundWindowsMatch = true
   100  		}
   101  		matches = append(matches, manifestDescriptor)
   102  		log.G(context.TODO()).Debugf("found match %s/%s %s with media type %s, digest %s", manifestDescriptor.Platform.OS, runtime.GOARCH, manifestDescriptor.Platform.OSVersion, manifestDescriptor.MediaType, manifestDescriptor.Digest.String())
   103  	}
   104  	if foundWindowsMatch {
   105  		sort.Stable(manifestsByVersion{osVersion, matches})
   106  	}
   107  	return matches
   108  }
   109  
   110  func versionMatch(actual, expected string) bool {
   111  	// Check whether the version matches up to the build, ignoring UBR
   112  	return strings.HasPrefix(actual, expected+".")
   113  }
   114  
   115  type manifestsByVersion struct {
   116  	version string
   117  	list    []manifestlist.ManifestDescriptor
   118  }
   119  
   120  func (mbv manifestsByVersion) Less(i, j int) bool {
   121  	// TODO: Split version by parts and compare
   122  	// TODO: Prefer versions which have a greater version number
   123  	// Move compatible versions to the top, with no other ordering changes
   124  	return (strings.EqualFold("windows", mbv.list[i].Platform.OS) && !strings.EqualFold("windows", mbv.list[j].Platform.OS)) ||
   125  		(versionMatch(mbv.list[i].Platform.OSVersion, mbv.version) && !versionMatch(mbv.list[j].Platform.OSVersion, mbv.version))
   126  }
   127  
   128  func (mbv manifestsByVersion) Len() int {
   129  	return len(mbv.list)
   130  }
   131  
   132  func (mbv manifestsByVersion) Swap(i, j int) {
   133  	mbv.list[i], mbv.list[j] = mbv.list[j], mbv.list[i]
   134  }
   135  
   136  // checkImageCompatibility blocks pulling incompatible images based on a later OS build
   137  // Fixes https://github.com/moby/moby/issues/36184.
   138  func checkImageCompatibility(imageOS, imageOSVersion string) error {
   139  	if imageOS == "windows" {
   140  		hostOSV := osversion.Get()
   141  		splitImageOSVersion := strings.Split(imageOSVersion, ".") // eg 10.0.16299.nnnn
   142  		if len(splitImageOSVersion) >= 3 {
   143  			if imageOSBuild, err := strconv.Atoi(splitImageOSVersion[2]); err == nil {
   144  				if imageOSBuild > int(hostOSV.Build) {
   145  					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())
   146  					log.G(context.TODO()).Debugf(errMsg)
   147  					return errors.New(errMsg)
   148  				}
   149  			}
   150  		}
   151  	}
   152  	return nil
   153  }
   154  
   155  func formatPlatform(platform ocispec.Platform) string {
   156  	if platform.OS == "" {
   157  		platform = platforms.DefaultSpec()
   158  	}
   159  	return fmt.Sprintf("%s %s", platforms.Format(platform), osversion.Get().ToString())
   160  }