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