github.com/Heebron/moby@v0.0.0-20221111184709-6eab4f55faf7/daemon/images/image.go (about) 1 package images // import "github.com/docker/docker/daemon/images" 2 3 import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 "io" 8 9 "github.com/containerd/containerd/content" 10 c8derrdefs "github.com/containerd/containerd/errdefs" 11 "github.com/containerd/containerd/images" 12 "github.com/containerd/containerd/leases" 13 "github.com/containerd/containerd/platforms" 14 "github.com/docker/distribution/reference" 15 imagetypes "github.com/docker/docker/api/types/image" 16 "github.com/docker/docker/errdefs" 17 "github.com/docker/docker/image" 18 "github.com/opencontainers/go-digest" 19 specs "github.com/opencontainers/image-spec/specs-go/v1" 20 "github.com/pkg/errors" 21 "github.com/sirupsen/logrus" 22 ) 23 24 // ErrImageDoesNotExist is error returned when no image can be found for a reference. 25 type ErrImageDoesNotExist struct { 26 ref reference.Reference 27 } 28 29 func (e ErrImageDoesNotExist) Error() string { 30 ref := e.ref 31 if named, ok := ref.(reference.Named); ok { 32 ref = reference.TagNameOnly(named) 33 } 34 return fmt.Sprintf("No such image: %s", reference.FamiliarString(ref)) 35 } 36 37 // NotFound implements the NotFound interface 38 func (e ErrImageDoesNotExist) NotFound() {} 39 40 type manifestList struct { 41 Manifests []specs.Descriptor `json:"manifests"` 42 } 43 44 type manifest struct { 45 Config specs.Descriptor `json:"config"` 46 } 47 48 func (i *ImageService) manifestMatchesPlatform(ctx context.Context, img *image.Image, platform specs.Platform) (bool, error) { 49 logger := logrus.WithField("image", img.ID).WithField("desiredPlatform", platforms.Format(platform)) 50 51 ls, leaseErr := i.leases.ListResources(ctx, leases.Lease{ID: imageKey(img.ID().Digest())}) 52 if leaseErr != nil { 53 logger.WithError(leaseErr).Error("Error looking up image leases") 54 return false, leaseErr 55 } 56 57 // Note we are comparing against manifest lists here, which we expect to always have a CPU variant set (where applicable). 58 // So there is no need for the fallback matcher here. 59 comparer := platforms.Only(platform) 60 61 var ( 62 ml manifestList 63 m manifest 64 ) 65 66 makeRdr := func(ra content.ReaderAt) io.Reader { 67 return io.LimitReader(io.NewSectionReader(ra, 0, ra.Size()), 1e6) 68 } 69 70 for _, r := range ls { 71 logger := logger.WithField("resourceID", r.ID).WithField("resourceType", r.Type) 72 logger.Debug("Checking lease resource for platform match") 73 if r.Type != "content" { 74 continue 75 } 76 77 ra, err := i.content.ReaderAt(ctx, specs.Descriptor{Digest: digest.Digest(r.ID)}) 78 if err != nil { 79 if c8derrdefs.IsNotFound(err) { 80 continue 81 } 82 logger.WithError(err).Error("Error looking up referenced manifest list for image") 83 continue 84 } 85 86 data, err := io.ReadAll(makeRdr(ra)) 87 ra.Close() 88 89 if err != nil { 90 logger.WithError(err).Error("Error reading manifest list for image") 91 continue 92 } 93 94 ml.Manifests = nil 95 96 if err := json.Unmarshal(data, &ml); err != nil { 97 logger.WithError(err).Error("Error unmarshalling content") 98 continue 99 } 100 101 for _, md := range ml.Manifests { 102 switch md.MediaType { 103 case specs.MediaTypeImageManifest, images.MediaTypeDockerSchema2Manifest: 104 default: 105 continue 106 } 107 108 p := specs.Platform{ 109 Architecture: md.Platform.Architecture, 110 OS: md.Platform.OS, 111 Variant: md.Platform.Variant, 112 } 113 if !comparer.Match(p) { 114 logger.WithField("otherPlatform", platforms.Format(p)).Debug("Manifest is not a match") 115 continue 116 } 117 118 // Here we have a platform match for the referenced manifest, let's make sure the manifest is actually for the image config we are using. 119 120 ra, err := i.content.ReaderAt(ctx, specs.Descriptor{Digest: md.Digest}) 121 if err != nil { 122 logger.WithField("otherDigest", md.Digest).WithError(err).Error("Could not get reader for manifest") 123 continue 124 } 125 126 data, err := io.ReadAll(makeRdr(ra)) 127 ra.Close() 128 if err != nil { 129 logger.WithError(err).Error("Error reading manifest for image") 130 continue 131 } 132 133 if err := json.Unmarshal(data, &m); err != nil { 134 logger.WithError(err).Error("Error desserializing manifest") 135 continue 136 } 137 138 if m.Config.Digest == img.ID().Digest() { 139 logger.WithField("manifestDigest", md.Digest).Debug("Found matching manifest for image") 140 return true, nil 141 } 142 143 logger.WithField("otherDigest", md.Digest).Debug("Skipping non-matching manifest") 144 } 145 } 146 147 return false, nil 148 } 149 150 // GetImage returns an image corresponding to the image referred to by refOrID. 151 func (i *ImageService) GetImage(ctx context.Context, refOrID string, options imagetypes.GetImageOpts) (retImg *image.Image, retErr error) { 152 defer func() { 153 if retErr != nil || retImg == nil || options.Platform == nil { 154 return 155 } 156 157 imgPlat := specs.Platform{ 158 OS: retImg.OS, 159 Architecture: retImg.Architecture, 160 Variant: retImg.Variant, 161 } 162 p := *options.Platform 163 // Note that `platforms.Only` will fuzzy match this for us 164 // For example: an armv6 image will run just fine on an armv7 CPU, without emulation or anything. 165 if OnlyPlatformWithFallback(p).Match(imgPlat) { 166 return 167 } 168 // In some cases the image config can actually be wrong (e.g. classic `docker build` may not handle `--platform` correctly) 169 // So we'll look up the manifest list that corresponds to this image to check if at least the manifest list says it is the correct image. 170 var matches bool 171 matches, retErr = i.manifestMatchesPlatform(ctx, retImg, p) 172 if matches || retErr != nil { 173 return 174 } 175 176 // This allows us to tell clients that we don't have the image they asked for 177 // Where this gets hairy is the image store does not currently support multi-arch images, e.g.: 178 // An image `foo` may have a multi-arch manifest, but the image store only fetches the image for a specific platform 179 // The image store does not store the manifest list and image tags are assigned to architecture specific images. 180 // So we can have a `foo` image that is amd64 but the user requested armv7. If the user looks at the list of images. 181 // This may be confusing. 182 // The alternative to this is to return an errdefs.Conflict error with a helpful message, but clients will not be 183 // able to automatically tell what causes the conflict. 184 retErr = errdefs.NotFound(errors.Errorf("image with reference %s was found but does not match the specified platform: wanted %s, actual: %s", refOrID, platforms.Format(p), platforms.Format(imgPlat))) 185 }() 186 ref, err := reference.ParseAnyReference(refOrID) 187 if err != nil { 188 return nil, errdefs.InvalidParameter(err) 189 } 190 namedRef, ok := ref.(reference.Named) 191 if !ok { 192 digested, ok := ref.(reference.Digested) 193 if !ok { 194 return nil, ErrImageDoesNotExist{ref} 195 } 196 id := image.IDFromDigest(digested.Digest()) 197 if img, err := i.imageStore.Get(id); err == nil { 198 return img, nil 199 } 200 return nil, ErrImageDoesNotExist{ref} 201 } 202 203 if digest, err := i.referenceStore.Get(namedRef); err == nil { 204 // Search the image stores to get the operating system, defaulting to host OS. 205 id := image.IDFromDigest(digest) 206 if img, err := i.imageStore.Get(id); err == nil { 207 return img, nil 208 } 209 } 210 211 // Search based on ID 212 if id, err := i.imageStore.Search(refOrID); err == nil { 213 img, err := i.imageStore.Get(id) 214 if err != nil { 215 return nil, ErrImageDoesNotExist{ref} 216 } 217 return img, nil 218 } 219 220 return nil, ErrImageDoesNotExist{ref} 221 } 222 223 // OnlyPlatformWithFallback uses `platforms.Only` with a fallback to handle the case where the platform 224 // being matched does not have a CPU variant. 225 // 226 // The reason for this is that CPU variant is not even if the official image config spec as of this writing. 227 // See: https://github.com/opencontainers/image-spec/pull/809 228 // Since Docker tends to compare platforms from the image config, we need to handle this case. 229 func OnlyPlatformWithFallback(p specs.Platform) platforms.Matcher { 230 return &onlyFallbackMatcher{only: platforms.Only(p), p: platforms.Normalize(p)} 231 } 232 233 type onlyFallbackMatcher struct { 234 only platforms.Matcher 235 p specs.Platform 236 } 237 238 func (m *onlyFallbackMatcher) Match(other specs.Platform) bool { 239 if m.only.Match(other) { 240 // It matches, no reason to fallback 241 return true 242 } 243 if other.Variant != "" { 244 // If there is a variant then this fallback does not apply, and there is no match 245 return false 246 } 247 otherN := platforms.Normalize(other) 248 otherN.Variant = "" // normalization adds a default variant... which is the whole problem with `platforms.Only` 249 250 return m.p.OS == otherN.OS && 251 m.p.Architecture == otherN.Architecture 252 }