2022-07-18 10:51:49 +00:00
package containerd
import (
2022-08-09 12:42:50 +00:00
"context"
2022-07-06 12:24:38 +00:00
"fmt"
"regexp"
2023-06-26 14:14:38 +00:00
"sort"
2022-07-06 12:24:38 +00:00
"strconv"
2023-09-08 12:07:11 +00:00
"strings"
2022-07-06 12:24:38 +00:00
"sync/atomic"
"time"
2022-08-09 12:42:50 +00:00
2022-07-06 12:24:38 +00:00
cerrdefs "github.com/containerd/containerd/errdefs"
containerdimages "github.com/containerd/containerd/images"
2024-01-20 21:11:53 +00:00
"github.com/containerd/containerd/platforms"
2023-09-13 15:41:45 +00:00
"github.com/containerd/log"
2023-08-30 16:31:46 +00:00
"github.com/distribution/reference"
2024-01-20 15:01:43 +00:00
"github.com/docker/docker/api/types/backend"
2022-07-06 12:24:38 +00:00
"github.com/docker/docker/daemon/images"
2022-08-10 13:22:32 +00:00
"github.com/docker/docker/errdefs"
2022-07-18 10:51:49 +00:00
"github.com/docker/docker/image"
2024-02-08 21:47:28 +00:00
imagespec "github.com/moby/docker-image-spec/specs-go/v1"
2022-07-06 12:24:38 +00:00
"github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
"golang.org/x/sync/semaphore"
2022-07-18 10:51:49 +00:00
)
2023-09-08 12:07:11 +00:00
var truncatedID = regexp . MustCompile ( ` ^(sha256:)?([a-f0-9] { 4,64})$ ` )
2022-07-06 12:24:38 +00:00
2023-11-17 05:33:11 +00:00
var errInconsistentData error = errors . New ( "consistency error: data changed during operation, retry" )
2022-07-18 10:51:49 +00:00
// GetImage returns an image corresponding to the image referred to by refOrID.
2024-01-20 15:01:43 +00:00
func ( i * ImageService ) GetImage ( ctx context . Context , refOrID string , options backend . GetImageOpts ) ( * image . Image , error ) {
2023-06-26 14:14:38 +00:00
desc , err := i . resolveImage ( ctx , refOrID )
2022-07-06 12:24:38 +00:00
if err != nil {
return nil , err
}
2024-01-20 21:11:53 +00:00
platform := matchAllWithPreference ( platforms . Default ( ) )
2022-07-06 12:24:38 +00:00
if options . Platform != nil {
2024-01-20 21:11:53 +00:00
platform = platforms . OnlyStrict ( * options . Platform )
2022-07-06 12:24:38 +00:00
}
2024-02-06 11:26:50 +00:00
presentImages , err := i . presentImages ( ctx , desc , refOrID , platform )
2022-07-06 12:24:38 +00:00
if err != nil {
return nil , err
}
2024-01-19 11:50:13 +00:00
ociImage := presentImages [ 0 ]
2023-06-26 14:14:38 +00:00
2024-01-19 11:50:13 +00:00
img := dockerOciImageToDockerImagePartial ( image . ID ( desc . Target . Digest ) , ociImage )
2023-12-08 15:47:56 +00:00
parent , err := i . getImageLabelByDigest ( ctx , desc . Target . Digest , imageLabelClassicBuilderParent )
if err != nil {
log . G ( ctx ) . WithError ( err ) . Warn ( "failed to determine Parent property" )
} else {
img . Parent = image . ID ( parent )
}
2022-07-06 12:24:38 +00:00
if options . Details {
lastUpdated := time . Unix ( 0 , 0 )
2023-06-26 14:14:38 +00:00
size , err := i . size ( ctx , desc . Target , platform )
2022-07-06 12:24:38 +00:00
if err != nil {
return nil , err
}
2023-11-17 05:55:54 +00:00
tagged , err := i . images . List ( ctx , "target.digest==" + desc . Target . Digest . String ( ) )
2022-07-06 12:24:38 +00:00
if err != nil {
return nil , err
}
2023-03-08 10:56:16 +00:00
2023-07-18 10:33:17 +00:00
// Usually each image will result in 2 references (named and digested).
2023-03-08 10:56:16 +00:00
refs := make ( [ ] reference . Named , 0 , len ( tagged ) * 2 )
2022-07-06 12:24:38 +00:00
for _ , i := range tagged {
if i . UpdatedAt . After ( lastUpdated ) {
lastUpdated = i . UpdatedAt
}
2023-03-30 08:34:38 +00:00
if isDanglingImage ( i ) {
if len ( tagged ) > 1 {
// This is unexpected - dangling image should be deleted
// as soon as another image with the same target is created.
// Log a warning, but don't error out the whole operation.
2023-06-23 00:33:17 +00:00
log . G ( ctx ) . WithField ( "refs" , tagged ) . Warn ( "multiple images have the same target, but one of them is still dangling" )
2023-03-30 08:34:38 +00:00
}
continue
}
2022-07-06 12:24:38 +00:00
name , err := reference . ParseNamed ( i . Name )
if err != nil {
2023-03-30 08:34:38 +00:00
// This is inconsistent with `docker image ls` which will
// still include the malformed name in RepoTags.
2023-06-23 00:33:17 +00:00
log . G ( ctx ) . WithField ( "name" , name ) . WithError ( err ) . Error ( "failed to parse image name as reference" )
2023-03-30 08:34:38 +00:00
continue
2022-07-06 12:24:38 +00:00
}
2023-03-08 10:56:16 +00:00
refs = append ( refs , name )
2023-07-18 10:33:17 +00:00
if _ , ok := name . ( reference . Digested ) ; ok {
// Image name already contains a digest, so no need to create a digested reference.
continue
}
2023-06-26 14:14:38 +00:00
digested , err := reference . WithDigest ( reference . TrimNamed ( name ) , desc . Target . Digest )
2023-03-08 10:56:16 +00:00
if err != nil {
// This could only happen if digest is invalid, but considering that
// we get it from the Descriptor it's highly unlikely.
// Log error just in case.
2023-06-23 00:33:17 +00:00
log . G ( ctx ) . WithError ( err ) . Error ( "failed to create digested reference" )
2023-03-08 10:56:16 +00:00
continue
}
refs = append ( refs , digested )
2022-07-06 12:24:38 +00:00
}
img . Details = & image . Details {
2023-03-08 10:56:16 +00:00
References : refs ,
2022-07-06 12:24:38 +00:00
Size : size ,
Metadata : nil ,
Driver : i . snapshotter ,
LastUpdated : lastUpdated ,
}
}
return img , nil
}
2024-02-06 11:26:50 +00:00
// presentImages returns the images that are present in the content store,
// manifests without a config are ignored.
// The images are filtered and sorted by platform preference.
func ( i * ImageService ) presentImages ( ctx context . Context , desc containerdimages . Image , refOrID string , platform platforms . MatchComparer ) ( [ ] imagespec . DockerOCIImage , error ) {
var presentImages [ ] imagespec . DockerOCIImage
err := i . walkImageManifests ( ctx , desc , func ( img * ImageManifest ) error {
conf , err := img . Config ( ctx )
if err != nil {
if cerrdefs . IsNotFound ( err ) {
log . G ( ctx ) . WithFields ( log . Fields {
"manifestDescriptor" : img . Target ( ) ,
} ) . Debug ( "manifest was present, but accessing its config failed, ignoring" )
return nil
}
return errdefs . System ( fmt . Errorf ( "failed to get config descriptor: %w" , err ) )
}
var ociimage imagespec . DockerOCIImage
if err := readConfig ( ctx , i . content , conf , & ociimage ) ; err != nil {
if errdefs . IsNotFound ( err ) {
log . G ( ctx ) . WithFields ( log . Fields {
"manifestDescriptor" : img . Target ( ) ,
"configDescriptor" : conf ,
} ) . Debug ( "manifest present, but its config is missing, ignoring" )
return nil
}
return errdefs . System ( fmt . Errorf ( "failed to read config of the manifest %v: %w" , img . Target ( ) . Digest , err ) )
}
if platform . Match ( ociimage . Platform ) {
presentImages = append ( presentImages , ociimage )
}
return nil
} )
if err != nil {
return nil , err
}
if len ( presentImages ) == 0 {
ref , _ := reference . ParseAnyReference ( refOrID )
return nil , images . ErrImageDoesNotExist { Ref : ref }
}
sort . SliceStable ( presentImages , func ( i , j int ) bool {
return platform . Less ( presentImages [ i ] . Platform , presentImages [ j ] . Platform )
} )
return presentImages , nil
}
2024-01-20 15:01:43 +00:00
func ( i * ImageService ) GetImageManifest ( ctx context . Context , refOrID string , options backend . GetImageOpts ) ( * ocispec . Descriptor , error ) {
2024-01-20 21:11:53 +00:00
platform := matchAllWithPreference ( platforms . Default ( ) )
2023-09-16 11:23:32 +00:00
if options . Platform != nil {
2024-01-20 21:11:53 +00:00
platform = platforms . Only ( * options . Platform )
2023-09-16 11:23:32 +00:00
}
2024-02-26 16:31:16 +00:00
cs := i . content
2023-02-25 17:18:05 +00:00
2023-09-26 09:16:34 +00:00
img , err := i . resolveImage ( ctx , refOrID )
2023-02-25 17:18:05 +00:00
if err != nil {
return nil , err
}
2023-09-26 09:16:34 +00:00
desc := img . Target
2023-02-25 17:18:05 +00:00
if containerdimages . IsManifestType ( desc . MediaType ) {
2023-09-26 09:16:34 +00:00
plat := desc . Platform
if plat == nil {
config , err := img . Config ( ctx , cs , platform )
if err != nil {
return nil , err
}
var configPlatform ocispec . Platform
if err := readConfig ( ctx , cs , config , & configPlatform ) ; err != nil {
return nil , err
}
plat = & configPlatform
}
2023-09-16 11:23:32 +00:00
if options . Platform != nil {
2023-09-26 09:16:34 +00:00
if plat == nil {
2024-01-20 21:11:53 +00:00
return nil , errdefs . NotFound ( errors . Errorf ( "image with reference %s was found but does not match the specified platform: wanted %s, actual: nil" , refOrID , platforms . Format ( * options . Platform ) ) )
2023-09-26 09:16:34 +00:00
} else if ! platform . Match ( * plat ) {
2024-01-20 21:11:53 +00:00
return nil , errdefs . NotFound ( errors . Errorf ( "image with reference %s was found but does not match the specified platform: wanted %s, actual: %s" , refOrID , platforms . Format ( * options . Platform ) , platforms . Format ( * plat ) ) )
2023-09-16 11:23:32 +00:00
}
}
2023-02-25 17:18:05 +00:00
return & desc , nil
}
if containerdimages . IsIndexType ( desc . MediaType ) {
childManifests , err := containerdimages . LimitManifests ( containerdimages . ChildrenHandler ( cs ) , platform , 1 ) ( ctx , desc )
if err != nil {
if cerrdefs . IsNotFound ( err ) {
return nil , errdefs . NotFound ( err )
}
return nil , errdefs . System ( err )
}
// len(childManifests) == 1 since we requested 1 and if none
// were found LimitManifests would have thrown an error
if ! containerdimages . IsManifestType ( childManifests [ 0 ] . MediaType ) {
return nil , errdefs . NotFound ( fmt . Errorf ( "manifest has incorrect mediatype: %s" , childManifests [ 0 ] . MediaType ) )
}
return & childManifests [ 0 ] , nil
}
return nil , errdefs . NotFound ( errors . New ( "failed to find manifest" ) )
}
2022-07-06 12:24:38 +00:00
// size returns the total size of the image's packed resources.
2024-01-20 21:11:53 +00:00
func ( i * ImageService ) size ( ctx context . Context , desc ocispec . Descriptor , platform platforms . MatchComparer ) ( int64 , error ) {
2022-07-06 12:24:38 +00:00
var size int64
2024-02-26 16:31:16 +00:00
cs := i . content
2022-07-06 12:24:38 +00:00
handler := containerdimages . LimitManifests ( containerdimages . ChildrenHandler ( cs ) , platform , 1 )
var wh containerdimages . HandlerFunc = func ( ctx context . Context , desc ocispec . Descriptor ) ( [ ] ocispec . Descriptor , error ) {
children , err := handler ( ctx , desc )
if err != nil {
if ! cerrdefs . IsNotFound ( err ) {
return nil , err
}
}
atomic . AddInt64 ( & size , desc . Size )
return children , nil
}
l := semaphore . NewWeighted ( 3 )
if err := containerdimages . Dispatch ( ctx , wh , l , desc ) ; err != nil {
return 0 , err
}
return size , nil
}
// resolveDescriptor searches for a descriptor based on the given
// reference or identifier. Returns the descriptor of
// the image, which could be a manifest list, manifest, or config.
func ( i * ImageService ) resolveDescriptor ( ctx context . Context , refOrID string ) ( ocispec . Descriptor , error ) {
2023-04-20 15:23:21 +00:00
img , err := i . resolveImage ( ctx , refOrID )
if err != nil {
return ocispec . Descriptor { } , err
}
return img . Target , nil
}
func ( i * ImageService ) resolveImage ( ctx context . Context , refOrID string ) ( containerdimages . Image , error ) {
2022-07-06 12:24:38 +00:00
parsed , err := reference . ParseAnyReference ( refOrID )
if err != nil {
2023-04-20 15:23:21 +00:00
return containerdimages . Image { } , errdefs . InvalidParameter ( err )
2022-07-06 12:24:38 +00:00
}
digested , ok := parsed . ( reference . Digested )
if ok {
2023-11-17 05:55:54 +00:00
imgs , err := i . images . List ( ctx , "target.digest==" + digested . Digest ( ) . String ( ) )
2022-07-06 12:24:38 +00:00
if err != nil {
2023-04-20 15:23:21 +00:00
return containerdimages . Image { } , errors . Wrap ( err , "failed to lookup digest" )
2022-07-06 12:24:38 +00:00
}
if len ( imgs ) == 0 {
2023-04-20 15:23:21 +00:00
return containerdimages . Image { } , images . ErrImageDoesNotExist { Ref : parsed }
2022-07-06 12:24:38 +00:00
}
2023-07-18 12:21:00 +00:00
// If reference is both Named and Digested, make sure we don't match
// images with a different repository even if digest matches.
// For example, busybox@sha256:abcdef..., shouldn't match asdf@sha256:abcdef...
if parsedNamed , ok := parsed . ( reference . Named ) ; ok {
for _ , img := range imgs {
imgNamed , err := reference . ParseNormalizedNamed ( img . Name )
if err != nil {
log . G ( ctx ) . WithError ( err ) . WithField ( "image" , img . Name ) . Warn ( "image with invalid name encountered" )
continue
}
if parsedNamed . Name ( ) == imgNamed . Name ( ) {
return img , nil
}
}
return containerdimages . Image { } , images . ErrImageDoesNotExist { Ref : parsed }
}
2023-04-20 15:23:21 +00:00
return imgs [ 0 ] , nil
2022-07-06 12:24:38 +00:00
}
ref := reference . TagNameOnly ( parsed . ( reference . Named ) ) . String ( )
2023-11-17 05:55:54 +00:00
img , err := i . images . Get ( ctx , ref )
2023-04-20 15:23:21 +00:00
if err == nil {
return img , nil
} else {
// TODO(containerd): error translation can use common function
if ! cerrdefs . IsNotFound ( err ) {
return containerdimages . Image { } , err
}
}
2022-07-06 12:24:38 +00:00
// If the identifier could be a short ID, attempt to match
if truncatedID . MatchString ( refOrID ) {
2023-09-08 12:07:11 +00:00
idWithoutAlgo := strings . TrimPrefix ( refOrID , "sha256:" )
2022-07-06 12:24:38 +00:00
filters := [ ] string {
fmt . Sprintf ( "name==%q" , ref ) , // Or it could just look like one.
2023-09-08 12:07:11 +00:00
"target.digest~=" + strconv . Quote ( fmt . Sprintf ( ` ^sha256:%s[0-9a-fA-F] { %d}$ ` , regexp . QuoteMeta ( idWithoutAlgo ) , 64 - len ( idWithoutAlgo ) ) ) ,
2022-07-06 12:24:38 +00:00
}
2023-11-17 05:55:54 +00:00
imgs , err := i . images . List ( ctx , filters ... )
2022-07-06 12:24:38 +00:00
if err != nil {
2023-04-20 15:23:21 +00:00
return containerdimages . Image { } , err
2022-07-06 12:24:38 +00:00
}
if len ( imgs ) == 0 {
2023-04-20 15:23:21 +00:00
return containerdimages . Image { } , images . ErrImageDoesNotExist { Ref : parsed }
2022-07-06 12:24:38 +00:00
}
if len ( imgs ) > 1 {
digests := map [ digest . Digest ] struct { } { }
for _ , img := range imgs {
if img . Name == ref {
2023-04-20 15:23:21 +00:00
return img , nil
2022-07-06 12:24:38 +00:00
}
digests [ img . Target . Digest ] = struct { } { }
}
if len ( digests ) > 1 {
2023-04-20 15:23:21 +00:00
return containerdimages . Image { } , errdefs . NotFound ( errors . New ( "ambiguous reference" ) )
2022-07-06 12:24:38 +00:00
}
}
2023-04-20 15:23:21 +00:00
return imgs [ 0 ] , nil
2022-07-06 12:24:38 +00:00
}
2023-04-20 15:23:21 +00:00
return containerdimages . Image { } , images . ErrImageDoesNotExist { Ref : parsed }
2022-07-18 10:51:49 +00:00
}
2023-09-22 11:17:19 +00:00
// getAllImagesWithRepository returns a slice of images which name is a reference
// pointing to the same repository as the given reference.
func ( i * ImageService ) getAllImagesWithRepository ( ctx context . Context , ref reference . Named ) ( [ ] containerdimages . Image , error ) {
nameFilter := "^" + regexp . QuoteMeta ( ref . Name ( ) ) + ":" + reference . TagRegexp . String ( ) + "$"
2024-02-26 16:31:16 +00:00
return i . images . List ( ctx , "name~=" + strconv . Quote ( nameFilter ) )
2023-09-22 11:17:19 +00:00
}
2023-12-08 12:51:20 +00:00
func imageFamiliarName ( img containerdimages . Image ) string {
if isDanglingImage ( img ) {
return img . Target . Digest . String ( )
}
if ref , err := reference . ParseNamed ( img . Name ) ; err == nil {
return reference . FamiliarString ( ref )
}
return img . Name
}
2023-12-08 15:47:56 +00:00
// getImageLabelByDigest will return the value of the label for images
// targeting the specified digest.
// If images have different values, an errdefs.Conflict error will be returned.
func ( i * ImageService ) getImageLabelByDigest ( ctx context . Context , target digest . Digest , labelKey string ) ( string , error ) {
2024-02-26 16:31:16 +00:00
imgs , err := i . images . List ( ctx , "target.digest==" + target . String ( ) + ",labels." + labelKey )
2023-12-08 15:47:56 +00:00
if err != nil {
return "" , errdefs . System ( err )
}
var value string
for _ , img := range imgs {
if v , ok := img . Labels [ labelKey ] ; ok {
if value != "" && value != v {
return value , errdefs . Conflict ( fmt . Errorf ( "conflicting label value %q and %q" , value , v ) )
}
value = v
}
}
return value , nil
}
2023-11-17 05:33:11 +00:00
func convertError ( err error ) error {
// TODO: Convert containerd error to Docker error
return err
}
// resolveAllReferences resolves the reference name or ID to an image and returns all the images with
// the same target.
//
// Returns:
//
// 1: *(github.com/containerd/containerd/images).Image
//
// An image match from the image store with the provided refOrID
//
// 2: [](github.com/containerd/containerd/images).Image
//
// List of all images with the same target that matches the refOrID. If the first argument is
// non-nil, the image list will all have the same target as the matched image. If the first
// argument is nil but the list is non-empty, this value is a list of all the images with a
// target that matches the digest provided in the refOrID, but none are an image name match
// to refOrID.
//
// 3: error
//
// An error looking up refOrID or no images found with matching name or target. Note that the first
// argument may be nil with a nil error if the second argument is non-empty.
func ( i * ImageService ) resolveAllReferences ( ctx context . Context , refOrID string ) ( * containerdimages . Image , [ ] containerdimages . Image , error ) {
parsed , err := reference . ParseAnyReference ( refOrID )
if err != nil {
return nil , nil , errdefs . InvalidParameter ( err )
}
var dgst digest . Digest
var img * containerdimages . Image
if truncatedID . MatchString ( refOrID ) {
if d , ok := parsed . ( reference . Digested ) ; ok {
if cimg , err := i . images . Get ( ctx , d . String ( ) ) ; err == nil {
img = & cimg
dgst = d . Digest ( )
if cimg . Target . Digest != dgst {
// Ambiguous image reference, use reference name
log . G ( ctx ) . WithField ( "image" , refOrID ) . WithField ( "target" , cimg . Target . Digest ) . Warn ( "digest reference points to image with a different digest" )
dgst = cimg . Target . Digest
}
} else if ! cerrdefs . IsNotFound ( err ) {
return nil , nil , convertError ( err )
} else {
dgst = d . Digest ( )
}
} else {
idWithoutAlgo := strings . TrimPrefix ( refOrID , "sha256:" )
name := reference . TagNameOnly ( parsed . ( reference . Named ) ) . String ( )
filters := [ ] string {
fmt . Sprintf ( "name==%q" , name ) , // Or it could just look like one.
"target.digest~=" + strconv . Quote ( fmt . Sprintf ( ` ^sha256:%s[0-9a-fA-F] { %d}$ ` , regexp . QuoteMeta ( idWithoutAlgo ) , 64 - len ( idWithoutAlgo ) ) ) ,
}
imgs , err := i . images . List ( ctx , filters ... )
if err != nil {
return nil , nil , convertError ( err )
}
if len ( imgs ) == 0 {
return nil , nil , images . ErrImageDoesNotExist { Ref : parsed }
}
for _ , limg := range imgs {
if limg . Name == name {
copyImg := limg
img = & copyImg
}
if dgst != "" {
if limg . Target . Digest != dgst {
return nil , nil , errdefs . NotFound ( errors . New ( "ambiguous reference" ) )
}
} else {
dgst = limg . Target . Digest
}
}
// Return immediately if target digest matches already included
if img == nil || len ( imgs ) > 1 {
return img , imgs , nil
}
}
} else {
named , ok := parsed . ( reference . Named )
if ! ok {
return nil , nil , errdefs . InvalidParameter ( errors . New ( "invalid name reference" ) )
}
digested , ok := parsed . ( reference . Digested )
if ok {
dgst = digested . Digest ( )
}
name := reference . TagNameOnly ( named ) . String ( )
cimg , err := i . images . Get ( ctx , name )
if err != nil {
if ! cerrdefs . IsNotFound ( err ) {
return nil , nil , convertError ( err )
}
2023-12-12 06:12:00 +00:00
// If digest is given, continue looking up for matching targets.
// There will be no exact match found but the caller may attempt
// to match across images with the matching target.
if dgst == "" {
return nil , nil , images . ErrImageDoesNotExist { Ref : parsed }
}
2023-11-17 05:33:11 +00:00
} else {
img = & cimg
if dgst != "" && img . Target . Digest != dgst {
// Ambiguous image reference, use reference name
log . G ( ctx ) . WithField ( "image" , name ) . WithField ( "target" , cimg . Target . Digest ) . Warn ( "digest reference points to image with a different digest" )
}
dgst = img . Target . Digest
}
}
// Lookup up all associated images and check for consistency with first reference
// Ideally operations dependent on multiple values will rely on the garbage collector,
// this logic will just check for consistency and throw an error
imgs , err := i . images . List ( ctx , "target.digest==" + dgst . String ( ) )
if err != nil {
return nil , nil , errors . Wrap ( err , "failed to lookup digest" )
}
if len ( imgs ) == 0 {
if img == nil {
return nil , nil , images . ErrImageDoesNotExist { Ref : parsed }
}
err = errInconsistentData
} else if img != nil {
// Check to ensure the original img is in the list still
err = errInconsistentData
for _ , rimg := range imgs {
if rimg . Name == img . Name {
err = nil
break
}
}
}
if errors . Is ( err , errInconsistentData ) {
if retries , ok := ctx . Value ( errInconsistentData ) . ( int ) ; ! ok || retries < 3 {
log . G ( ctx ) . WithFields ( log . Fields { "retry" : retries , "ref" : refOrID } ) . Info ( "image changed during lookup, retrying" )
return i . resolveAllReferences ( context . WithValue ( ctx , errInconsistentData , retries + 1 ) , refOrID )
}
return nil , nil , err
}
return img , imgs , nil
}