| package containerd |
| |
| import ( |
| "context" |
| "encoding/json" |
| "fmt" |
| "regexp" |
| "strconv" |
| "sync/atomic" |
| "time" |
| |
| "github.com/containerd/containerd/content" |
| cerrdefs "github.com/containerd/containerd/errdefs" |
| containerdimages "github.com/containerd/containerd/images" |
| cplatforms "github.com/containerd/containerd/platforms" |
| "github.com/docker/distribution/reference" |
| containertypes "github.com/docker/docker/api/types/container" |
| imagetype "github.com/docker/docker/api/types/image" |
| "github.com/docker/docker/daemon/images" |
| "github.com/docker/docker/errdefs" |
| "github.com/docker/docker/image" |
| "github.com/docker/docker/layer" |
| "github.com/docker/docker/pkg/platforms" |
| "github.com/docker/go-connections/nat" |
| "github.com/opencontainers/go-digest" |
| ocispec "github.com/opencontainers/image-spec/specs-go/v1" |
| "github.com/pkg/errors" |
| "github.com/sirupsen/logrus" |
| "golang.org/x/sync/semaphore" |
| ) |
| |
| var truncatedID = regexp.MustCompile(`^([a-f0-9]{4,64})$`) |
| |
| // GetImage returns an image corresponding to the image referred to by refOrID. |
| func (i *ImageService) GetImage(ctx context.Context, refOrID string, options imagetype.GetImageOpts) (*image.Image, error) { |
| desc, err := i.resolveDescriptor(ctx, refOrID) |
| if err != nil { |
| return nil, err |
| } |
| |
| platform := platforms.AllPlatformsWithPreference(cplatforms.Default()) |
| if options.Platform != nil { |
| platform = cplatforms.OnlyStrict(*options.Platform) |
| } |
| |
| cs := i.client.ContentStore() |
| conf, err := containerdimages.Config(ctx, cs, desc, platform) |
| if err != nil { |
| return nil, err |
| } |
| |
| imageConfigBytes, err := content.ReadBlob(ctx, cs, conf) |
| if err != nil { |
| return nil, err |
| } |
| |
| var ociimage ocispec.Image |
| if err := json.Unmarshal(imageConfigBytes, &ociimage); err != nil { |
| return nil, err |
| } |
| |
| rootfs := image.NewRootFS() |
| for _, id := range ociimage.RootFS.DiffIDs { |
| rootfs.Append(layer.DiffID(id)) |
| } |
| exposedPorts := make(nat.PortSet, len(ociimage.Config.ExposedPorts)) |
| for k, v := range ociimage.Config.ExposedPorts { |
| exposedPorts[nat.Port(k)] = v |
| } |
| |
| var imgHistory []image.History |
| for _, h := range ociimage.History { |
| var created time.Time |
| if h.Created != nil { |
| created = *h.Created |
| } |
| imgHistory = append(imgHistory, image.History{ |
| Created: created, |
| Author: h.Author, |
| CreatedBy: h.CreatedBy, |
| Comment: h.Comment, |
| EmptyLayer: h.EmptyLayer, |
| }) |
| } |
| |
| img := image.NewImage(image.ID(desc.Digest)) |
| img.V1Image = image.V1Image{ |
| ID: string(desc.Digest), |
| OS: ociimage.OS, |
| Architecture: ociimage.Architecture, |
| Config: &containertypes.Config{ |
| Entrypoint: ociimage.Config.Entrypoint, |
| Env: ociimage.Config.Env, |
| Cmd: ociimage.Config.Cmd, |
| User: ociimage.Config.User, |
| WorkingDir: ociimage.Config.WorkingDir, |
| ExposedPorts: exposedPorts, |
| Volumes: ociimage.Config.Volumes, |
| Labels: ociimage.Config.Labels, |
| StopSignal: ociimage.Config.StopSignal, |
| }, |
| } |
| |
| img.RootFS = rootfs |
| img.History = imgHistory |
| |
| if options.Details { |
| lastUpdated := time.Unix(0, 0) |
| size, err := i.size(ctx, desc, platform) |
| if err != nil { |
| return nil, err |
| } |
| |
| tagged, err := i.client.ImageService().List(ctx, "target.digest=="+desc.Digest.String()) |
| if err != nil { |
| return nil, err |
| } |
| |
| // Each image will result in 2 references (named and digested). |
| refs := make([]reference.Named, 0, len(tagged)*2) |
| for _, i := range tagged { |
| if i.UpdatedAt.After(lastUpdated) { |
| lastUpdated = i.UpdatedAt |
| } |
| 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. |
| logrus.WithField("refs", tagged).Warn("multiple images have the same target, but one of them is still dangling") |
| } |
| continue |
| } |
| |
| name, err := reference.ParseNamed(i.Name) |
| if err != nil { |
| // This is inconsistent with `docker image ls` which will |
| // still include the malformed name in RepoTags. |
| logrus.WithField("name", name).WithError(err).Error("failed to parse image name as reference") |
| continue |
| } |
| refs = append(refs, name) |
| |
| digested, err := reference.WithDigest(reference.TrimNamed(name), desc.Digest) |
| 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. |
| logrus.WithError(err).Error("failed to create digested reference") |
| continue |
| } |
| refs = append(refs, digested) |
| } |
| |
| img.Details = &image.Details{ |
| References: refs, |
| Size: size, |
| Metadata: nil, |
| Driver: i.snapshotter, |
| LastUpdated: lastUpdated, |
| } |
| } |
| |
| return img, nil |
| } |
| |
| func (i *ImageService) GetImageManifest(ctx context.Context, refOrID string, options imagetype.GetImageOpts) (*ocispec.Descriptor, error) { |
| cs := i.client.ContentStore() |
| |
| desc, err := i.resolveDescriptor(ctx, refOrID) |
| if err != nil { |
| return nil, err |
| } |
| |
| if containerdimages.IsManifestType(desc.MediaType) { |
| return &desc, nil |
| } |
| |
| if containerdimages.IsIndexType(desc.MediaType) { |
| platform := platforms.AllPlatformsWithPreference(cplatforms.Default()) |
| if options.Platform != nil { |
| platform = cplatforms.Only(*options.Platform) |
| } |
| |
| 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")) |
| } |
| |
| // size returns the total size of the image's packed resources. |
| func (i *ImageService) size(ctx context.Context, desc ocispec.Descriptor, platform cplatforms.MatchComparer) (int64, error) { |
| var size int64 |
| |
| cs := i.client.ContentStore() |
| 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) { |
| 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) { |
| parsed, err := reference.ParseAnyReference(refOrID) |
| if err != nil { |
| return containerdimages.Image{}, errdefs.InvalidParameter(err) |
| } |
| |
| is := i.client.ImageService() |
| |
| digested, ok := parsed.(reference.Digested) |
| if ok { |
| imgs, err := is.List(ctx, "target.digest=="+digested.Digest().String()) |
| if err != nil { |
| return containerdimages.Image{}, errors.Wrap(err, "failed to lookup digest") |
| } |
| if len(imgs) == 0 { |
| return containerdimages.Image{}, images.ErrImageDoesNotExist{Ref: parsed} |
| } |
| |
| return imgs[0], nil |
| } |
| |
| ref := reference.TagNameOnly(parsed.(reference.Named)).String() |
| img, err := is.Get(ctx, ref) |
| if err == nil { |
| return img, nil |
| } else { |
| // TODO(containerd): error translation can use common function |
| if !cerrdefs.IsNotFound(err) { |
| return containerdimages.Image{}, err |
| } |
| } |
| |
| // If the identifier could be a short ID, attempt to match |
| if truncatedID.MatchString(refOrID) { |
| filters := []string{ |
| fmt.Sprintf("name==%q", ref), // Or it could just look like one. |
| "target.digest~=" + strconv.Quote(fmt.Sprintf(`^sha256:%s[0-9a-fA-F]{%d}$`, regexp.QuoteMeta(refOrID), 64-len(refOrID))), |
| } |
| imgs, err := is.List(ctx, filters...) |
| if err != nil { |
| return containerdimages.Image{}, err |
| } |
| |
| if len(imgs) == 0 { |
| return containerdimages.Image{}, images.ErrImageDoesNotExist{Ref: parsed} |
| } |
| if len(imgs) > 1 { |
| digests := map[digest.Digest]struct{}{} |
| for _, img := range imgs { |
| if img.Name == ref { |
| return img, nil |
| } |
| digests[img.Target.Digest] = struct{}{} |
| } |
| |
| if len(digests) > 1 { |
| return containerdimages.Image{}, errdefs.NotFound(errors.New("ambiguous reference")) |
| } |
| } |
| |
| return imgs[0], nil |
| } |
| |
| return containerdimages.Image{}, images.ErrImageDoesNotExist{Ref: parsed} |
| } |