image.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343
  1. package containerd
  2. import (
  3. "context"
  4. "fmt"
  5. "regexp"
  6. "sort"
  7. "strconv"
  8. "strings"
  9. "sync/atomic"
  10. "time"
  11. cerrdefs "github.com/containerd/containerd/errdefs"
  12. containerdimages "github.com/containerd/containerd/images"
  13. cplatforms "github.com/containerd/containerd/platforms"
  14. "github.com/containerd/log"
  15. "github.com/distribution/reference"
  16. imagetype "github.com/docker/docker/api/types/image"
  17. "github.com/docker/docker/daemon/images"
  18. "github.com/docker/docker/errdefs"
  19. "github.com/docker/docker/image"
  20. imagespec "github.com/docker/docker/image/spec/specs-go/v1"
  21. "github.com/docker/docker/pkg/platforms"
  22. "github.com/opencontainers/go-digest"
  23. ocispec "github.com/opencontainers/image-spec/specs-go/v1"
  24. "github.com/pkg/errors"
  25. "golang.org/x/sync/semaphore"
  26. )
  27. var truncatedID = regexp.MustCompile(`^(sha256:)?([a-f0-9]{4,64})$`)
  28. // GetImage returns an image corresponding to the image referred to by refOrID.
  29. func (i *ImageService) GetImage(ctx context.Context, refOrID string, options imagetype.GetImageOpts) (*image.Image, error) {
  30. desc, err := i.resolveImage(ctx, refOrID)
  31. if err != nil {
  32. return nil, err
  33. }
  34. platform := platforms.AllPlatformsWithPreference(cplatforms.Default())
  35. if options.Platform != nil {
  36. platform = cplatforms.OnlyStrict(*options.Platform)
  37. }
  38. cs := i.client.ContentStore()
  39. var presentImages []imagespec.DockerOCIImage
  40. err = i.walkImageManifests(ctx, desc, func(img *ImageManifest) error {
  41. conf, err := img.Config(ctx)
  42. if err != nil {
  43. if cerrdefs.IsNotFound(err) {
  44. log.G(ctx).WithFields(log.Fields{
  45. "manifestDescriptor": img.Target(),
  46. }).Debug("manifest was present, but accessing its config failed, ignoring")
  47. return nil
  48. }
  49. return errdefs.System(fmt.Errorf("failed to get config descriptor: %w", err))
  50. }
  51. var ociimage imagespec.DockerOCIImage
  52. if err := readConfig(ctx, cs, conf, &ociimage); err != nil {
  53. if cerrdefs.IsNotFound(err) {
  54. log.G(ctx).WithFields(log.Fields{
  55. "manifestDescriptor": img.Target(),
  56. "configDescriptor": conf,
  57. }).Debug("manifest present, but its config is missing, ignoring")
  58. return nil
  59. }
  60. return errdefs.System(fmt.Errorf("failed to read config of the manifest %v: %w", img.Target().Digest, err))
  61. }
  62. presentImages = append(presentImages, ociimage)
  63. return nil
  64. })
  65. if err != nil {
  66. return nil, err
  67. }
  68. if len(presentImages) == 0 {
  69. ref, _ := reference.ParseAnyReference(refOrID)
  70. return nil, images.ErrImageDoesNotExist{Ref: ref}
  71. }
  72. sort.SliceStable(presentImages, func(i, j int) bool {
  73. return platform.Less(presentImages[i].Platform, presentImages[j].Platform)
  74. })
  75. ociimage := presentImages[0]
  76. img := dockerOciImageToDockerImagePartial(image.ID(desc.Target.Digest), ociimage)
  77. if options.Details {
  78. lastUpdated := time.Unix(0, 0)
  79. size, err := i.size(ctx, desc.Target, platform)
  80. if err != nil {
  81. return nil, err
  82. }
  83. tagged, err := i.client.ImageService().List(ctx, "target.digest=="+desc.Target.Digest.String())
  84. if err != nil {
  85. return nil, err
  86. }
  87. // Usually each image will result in 2 references (named and digested).
  88. refs := make([]reference.Named, 0, len(tagged)*2)
  89. for _, i := range tagged {
  90. if i.UpdatedAt.After(lastUpdated) {
  91. lastUpdated = i.UpdatedAt
  92. }
  93. if isDanglingImage(i) {
  94. if len(tagged) > 1 {
  95. // This is unexpected - dangling image should be deleted
  96. // as soon as another image with the same target is created.
  97. // Log a warning, but don't error out the whole operation.
  98. log.G(ctx).WithField("refs", tagged).Warn("multiple images have the same target, but one of them is still dangling")
  99. }
  100. continue
  101. }
  102. name, err := reference.ParseNamed(i.Name)
  103. if err != nil {
  104. // This is inconsistent with `docker image ls` which will
  105. // still include the malformed name in RepoTags.
  106. log.G(ctx).WithField("name", name).WithError(err).Error("failed to parse image name as reference")
  107. continue
  108. }
  109. refs = append(refs, name)
  110. if _, ok := name.(reference.Digested); ok {
  111. // Image name already contains a digest, so no need to create a digested reference.
  112. continue
  113. }
  114. digested, err := reference.WithDigest(reference.TrimNamed(name), desc.Target.Digest)
  115. if err != nil {
  116. // This could only happen if digest is invalid, but considering that
  117. // we get it from the Descriptor it's highly unlikely.
  118. // Log error just in case.
  119. log.G(ctx).WithError(err).Error("failed to create digested reference")
  120. continue
  121. }
  122. refs = append(refs, digested)
  123. }
  124. img.Details = &image.Details{
  125. References: refs,
  126. Size: size,
  127. Metadata: nil,
  128. Driver: i.snapshotter,
  129. LastUpdated: lastUpdated,
  130. }
  131. }
  132. return img, nil
  133. }
  134. func (i *ImageService) GetImageManifest(ctx context.Context, refOrID string, options imagetype.GetImageOpts) (*ocispec.Descriptor, error) {
  135. platform := platforms.AllPlatformsWithPreference(cplatforms.Default())
  136. if options.Platform != nil {
  137. platform = cplatforms.Only(*options.Platform)
  138. }
  139. cs := i.client.ContentStore()
  140. img, err := i.resolveImage(ctx, refOrID)
  141. if err != nil {
  142. return nil, err
  143. }
  144. desc := img.Target
  145. if containerdimages.IsManifestType(desc.MediaType) {
  146. plat := desc.Platform
  147. if plat == nil {
  148. config, err := img.Config(ctx, cs, platform)
  149. if err != nil {
  150. return nil, err
  151. }
  152. var configPlatform ocispec.Platform
  153. if err := readConfig(ctx, cs, config, &configPlatform); err != nil {
  154. return nil, err
  155. }
  156. plat = &configPlatform
  157. }
  158. if options.Platform != nil {
  159. if plat == nil {
  160. return nil, errdefs.NotFound(errors.Errorf("image with reference %s was found but does not match the specified platform: wanted %s, actual: nil", refOrID, cplatforms.Format(*options.Platform)))
  161. } else if !platform.Match(*plat) {
  162. return nil, errdefs.NotFound(errors.Errorf("image with reference %s was found but does not match the specified platform: wanted %s, actual: %s", refOrID, cplatforms.Format(*options.Platform), cplatforms.Format(*plat)))
  163. }
  164. }
  165. return &desc, nil
  166. }
  167. if containerdimages.IsIndexType(desc.MediaType) {
  168. childManifests, err := containerdimages.LimitManifests(containerdimages.ChildrenHandler(cs), platform, 1)(ctx, desc)
  169. if err != nil {
  170. if cerrdefs.IsNotFound(err) {
  171. return nil, errdefs.NotFound(err)
  172. }
  173. return nil, errdefs.System(err)
  174. }
  175. // len(childManifests) == 1 since we requested 1 and if none
  176. // were found LimitManifests would have thrown an error
  177. if !containerdimages.IsManifestType(childManifests[0].MediaType) {
  178. return nil, errdefs.NotFound(fmt.Errorf("manifest has incorrect mediatype: %s", childManifests[0].MediaType))
  179. }
  180. return &childManifests[0], nil
  181. }
  182. return nil, errdefs.NotFound(errors.New("failed to find manifest"))
  183. }
  184. // size returns the total size of the image's packed resources.
  185. func (i *ImageService) size(ctx context.Context, desc ocispec.Descriptor, platform cplatforms.MatchComparer) (int64, error) {
  186. var size int64
  187. cs := i.client.ContentStore()
  188. handler := containerdimages.LimitManifests(containerdimages.ChildrenHandler(cs), platform, 1)
  189. var wh containerdimages.HandlerFunc = func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
  190. children, err := handler(ctx, desc)
  191. if err != nil {
  192. if !cerrdefs.IsNotFound(err) {
  193. return nil, err
  194. }
  195. }
  196. atomic.AddInt64(&size, desc.Size)
  197. return children, nil
  198. }
  199. l := semaphore.NewWeighted(3)
  200. if err := containerdimages.Dispatch(ctx, wh, l, desc); err != nil {
  201. return 0, err
  202. }
  203. return size, nil
  204. }
  205. // resolveDescriptor searches for a descriptor based on the given
  206. // reference or identifier. Returns the descriptor of
  207. // the image, which could be a manifest list, manifest, or config.
  208. func (i *ImageService) resolveDescriptor(ctx context.Context, refOrID string) (ocispec.Descriptor, error) {
  209. img, err := i.resolveImage(ctx, refOrID)
  210. if err != nil {
  211. return ocispec.Descriptor{}, err
  212. }
  213. return img.Target, nil
  214. }
  215. func (i *ImageService) resolveImage(ctx context.Context, refOrID string) (containerdimages.Image, error) {
  216. parsed, err := reference.ParseAnyReference(refOrID)
  217. if err != nil {
  218. return containerdimages.Image{}, errdefs.InvalidParameter(err)
  219. }
  220. is := i.client.ImageService()
  221. digested, ok := parsed.(reference.Digested)
  222. if ok {
  223. imgs, err := is.List(ctx, "target.digest=="+digested.Digest().String())
  224. if err != nil {
  225. return containerdimages.Image{}, errors.Wrap(err, "failed to lookup digest")
  226. }
  227. if len(imgs) == 0 {
  228. return containerdimages.Image{}, images.ErrImageDoesNotExist{Ref: parsed}
  229. }
  230. // If reference is both Named and Digested, make sure we don't match
  231. // images with a different repository even if digest matches.
  232. // For example, busybox@sha256:abcdef..., shouldn't match asdf@sha256:abcdef...
  233. if parsedNamed, ok := parsed.(reference.Named); ok {
  234. for _, img := range imgs {
  235. imgNamed, err := reference.ParseNormalizedNamed(img.Name)
  236. if err != nil {
  237. log.G(ctx).WithError(err).WithField("image", img.Name).Warn("image with invalid name encountered")
  238. continue
  239. }
  240. if parsedNamed.Name() == imgNamed.Name() {
  241. return img, nil
  242. }
  243. }
  244. return containerdimages.Image{}, images.ErrImageDoesNotExist{Ref: parsed}
  245. }
  246. return imgs[0], nil
  247. }
  248. ref := reference.TagNameOnly(parsed.(reference.Named)).String()
  249. img, err := is.Get(ctx, ref)
  250. if err == nil {
  251. return img, nil
  252. } else {
  253. // TODO(containerd): error translation can use common function
  254. if !cerrdefs.IsNotFound(err) {
  255. return containerdimages.Image{}, err
  256. }
  257. }
  258. // If the identifier could be a short ID, attempt to match
  259. if truncatedID.MatchString(refOrID) {
  260. idWithoutAlgo := strings.TrimPrefix(refOrID, "sha256:")
  261. filters := []string{
  262. fmt.Sprintf("name==%q", ref), // Or it could just look like one.
  263. "target.digest~=" + strconv.Quote(fmt.Sprintf(`^sha256:%s[0-9a-fA-F]{%d}$`, regexp.QuoteMeta(idWithoutAlgo), 64-len(idWithoutAlgo))),
  264. }
  265. imgs, err := is.List(ctx, filters...)
  266. if err != nil {
  267. return containerdimages.Image{}, err
  268. }
  269. if len(imgs) == 0 {
  270. return containerdimages.Image{}, images.ErrImageDoesNotExist{Ref: parsed}
  271. }
  272. if len(imgs) > 1 {
  273. digests := map[digest.Digest]struct{}{}
  274. for _, img := range imgs {
  275. if img.Name == ref {
  276. return img, nil
  277. }
  278. digests[img.Target.Digest] = struct{}{}
  279. }
  280. if len(digests) > 1 {
  281. return containerdimages.Image{}, errdefs.NotFound(errors.New("ambiguous reference"))
  282. }
  283. }
  284. return imgs[0], nil
  285. }
  286. return containerdimages.Image{}, images.ErrImageDoesNotExist{Ref: parsed}
  287. }
  288. // getAllImagesWithRepository returns a slice of images which name is a reference
  289. // pointing to the same repository as the given reference.
  290. func (i *ImageService) getAllImagesWithRepository(ctx context.Context, ref reference.Named) ([]containerdimages.Image, error) {
  291. nameFilter := "^" + regexp.QuoteMeta(ref.Name()) + ":" + reference.TagRegexp.String() + "$"
  292. return i.client.ImageService().List(ctx, "name~="+strconv.Quote(nameFilter))
  293. }