github.com/kaisenlinux/docker.io@v0.0.0-20230510090727-ea55db55fac7/engine/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 "github.com/docker/docker/errdefs" 16 "github.com/docker/docker/image" 17 digest "github.com/opencontainers/go-digest" 18 specs "github.com/opencontainers/image-spec/specs-go/v1" 19 "github.com/pkg/errors" 20 "github.com/sirupsen/logrus" 21 ) 22 23 // ErrImageDoesNotExist is error returned when no image can be found for a reference. 24 type ErrImageDoesNotExist struct { 25 ref reference.Reference 26 } 27 28 func (e ErrImageDoesNotExist) Error() string { 29 ref := e.ref 30 if named, ok := ref.(reference.Named); ok { 31 ref = reference.TagNameOnly(named) 32 } 33 return fmt.Sprintf("No such image: %s", reference.FamiliarString(ref)) 34 } 35 36 // NotFound implements the NotFound interface 37 func (e ErrImageDoesNotExist) NotFound() {} 38 39 type manifestList struct { 40 Manifests []specs.Descriptor `json:"manifests"` 41 } 42 43 type manifest struct { 44 Config specs.Descriptor `json:"config"` 45 } 46 47 func (i *ImageService) manifestMatchesPlatform(img *image.Image, platform specs.Platform) bool { 48 ctx := context.TODO() 49 logger := logrus.WithField("image", img.ID).WithField("desiredPlatform", platforms.Format(platform)) 50 51 ls, leaseErr := i.leases.ListResources(context.TODO(), leases.Lease{ID: imageKey(img.ID().Digest())}) 52 if leaseErr != nil { 53 logger.WithError(leaseErr).Error("Error looking up image leases") 54 return false 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 141 } 142 143 logger.WithField("otherDigest", md.Digest).Debug("Skipping non-matching manifest") 144 } 145 } 146 147 return false 148 } 149 150 // GetImage returns an image corresponding to the image referred to by refOrID. 151 func (i *ImageService) GetImage(refOrID string, platform *specs.Platform) (retImg *image.Image, retErr error) { 152 defer func() { 153 if retErr != nil || retImg == nil || 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 := *platform 163 // Note that `platforms.Only` will fuzzy match this for us 164 // For example: an armv6 image will run just fine an 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 coresponds to this imaage to check if at least the manifest list says it is the correct image. 170 if i.manifestMatchesPlatform(retImg, p) { 171 return 172 } 173 174 // This allows us to tell clients that we don't have the image they asked for 175 // Where this gets hairy is the image store does not currently support multi-arch images, e.g.: 176 // An image `foo` may have a multi-arch manifest, but the image store only fetches the image for a specific platform 177 // The image store does not store the manifest list and image tags are assigned to architecture specific images. 178 // So we can have a `foo` image that is amd64 but the user requested armv7. If the user looks at the list of images. 179 // This may be confusing. 180 // The alternative to this is to return a errdefs.Conflict error with a helpful message, but clients will not be 181 // able to automatically tell what causes the conflict. 182 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))) 183 }() 184 ref, err := reference.ParseAnyReference(refOrID) 185 if err != nil { 186 return nil, errdefs.InvalidParameter(err) 187 } 188 namedRef, ok := ref.(reference.Named) 189 if !ok { 190 digested, ok := ref.(reference.Digested) 191 if !ok { 192 return nil, ErrImageDoesNotExist{ref} 193 } 194 id := image.IDFromDigest(digested.Digest()) 195 if img, err := i.imageStore.Get(id); err == nil { 196 return img, nil 197 } 198 return nil, ErrImageDoesNotExist{ref} 199 } 200 201 if digest, err := i.referenceStore.Get(namedRef); err == nil { 202 // Search the image stores to get the operating system, defaulting to host OS. 203 id := image.IDFromDigest(digest) 204 if img, err := i.imageStore.Get(id); err == nil { 205 return img, nil 206 } 207 } 208 209 // Search based on ID 210 if id, err := i.imageStore.Search(refOrID); err == nil { 211 img, err := i.imageStore.Get(id) 212 if err != nil { 213 return nil, ErrImageDoesNotExist{ref} 214 } 215 return img, nil 216 } 217 218 return nil, ErrImageDoesNotExist{ref} 219 } 220 221 // OnlyPlatformWithFallback uses `platforms.Only` with a fallback to handle the case where the platform 222 // being matched does not have a CPU variant. 223 // 224 // The reason for this is that CPU variant is not even if the official image config spec as of this writing. 225 // See: https://github.com/opencontainers/image-spec/pull/809 226 // Since Docker tends to compare platforms from the image config, we need to handle this case. 227 func OnlyPlatformWithFallback(p specs.Platform) platforms.Matcher { 228 return &onlyFallbackMatcher{only: platforms.Only(p), p: platforms.Normalize(p)} 229 } 230 231 type onlyFallbackMatcher struct { 232 only platforms.Matcher 233 p specs.Platform 234 } 235 236 func (m *onlyFallbackMatcher) Match(other specs.Platform) bool { 237 if m.only.Match(other) { 238 // It matches, no reason to fallback 239 return true 240 } 241 if other.Variant != "" { 242 // If there is a variant then this fallback does not apply, and there is no match 243 return false 244 } 245 otherN := platforms.Normalize(other) 246 otherN.Variant = "" // normalization adds a default variant... which is the whole problem with `platforms.Only` 247 248 return m.p.OS == otherN.OS && 249 m.p.Architecture == otherN.Architecture 250 }