mirror of
https://github.com/podman-container-tools/podman.git
synced 2026-08-11 11:25:45 +00:00
With the advent of Podman 2.0.0 we crossed the magical barrier of go modules. While we were able to continue importing all packages inside of the project, the project could not be vendored anymore from the outside. Move the go module to new major version and change all imports to github.com/containers/libpod/v2. The renaming of the imports was done via gomove [1]. [1] https://github.com/KSubedi/gomove Signed-off-by: Valentin Rothberg <rothberg@redhat.com>
1678 lines
49 KiB
Go
1678 lines
49 KiB
Go
package image
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
stderrors "errors"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
|
|
cp "github.com/containers/image/v5/copy"
|
|
"github.com/containers/image/v5/directory"
|
|
dockerarchive "github.com/containers/image/v5/docker/archive"
|
|
"github.com/containers/image/v5/docker/reference"
|
|
"github.com/containers/image/v5/image"
|
|
"github.com/containers/image/v5/manifest"
|
|
ociarchive "github.com/containers/image/v5/oci/archive"
|
|
is "github.com/containers/image/v5/storage"
|
|
"github.com/containers/image/v5/tarball"
|
|
"github.com/containers/image/v5/transports"
|
|
"github.com/containers/image/v5/transports/alltransports"
|
|
"github.com/containers/image/v5/types"
|
|
"github.com/containers/libpod/v2/libpod/driver"
|
|
"github.com/containers/libpod/v2/libpod/events"
|
|
"github.com/containers/libpod/v2/pkg/inspect"
|
|
"github.com/containers/libpod/v2/pkg/registries"
|
|
"github.com/containers/libpod/v2/pkg/util"
|
|
"github.com/containers/storage"
|
|
digest "github.com/opencontainers/go-digest"
|
|
imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1"
|
|
ociv1 "github.com/opencontainers/image-spec/specs-go/v1"
|
|
opentracing "github.com/opentracing/opentracing-go"
|
|
"github.com/pkg/errors"
|
|
"github.com/sirupsen/logrus"
|
|
)
|
|
|
|
// Image is the primary struct for dealing with images
|
|
// It is still very much a work in progress
|
|
type Image struct {
|
|
// Adding these two structs for now but will cull when we near
|
|
// completion of this library.
|
|
imgRef types.Image
|
|
imgSrcRef types.ImageSource
|
|
inspect.ImageData
|
|
inspect.ImageResult
|
|
inspectInfo *types.ImageInspectInfo
|
|
InputName string
|
|
image *storage.Image
|
|
imageruntime *Runtime
|
|
}
|
|
|
|
// Runtime contains the store
|
|
type Runtime struct {
|
|
store storage.Store
|
|
SignaturePolicyPath string
|
|
EventsLogFilePath string
|
|
EventsLogger string
|
|
Eventer events.Eventer
|
|
}
|
|
|
|
// InfoImage keep information of Image along with all associated layers
|
|
type InfoImage struct {
|
|
// ID of image
|
|
ID string
|
|
// Tags of image
|
|
Tags []string
|
|
// Layers stores all layers of image.
|
|
Layers []LayerInfo
|
|
}
|
|
|
|
// ImageFilter is a function to determine whether a image is included
|
|
// in command output. Images to be outputted are tested using the function.
|
|
// A true return will include the image, a false return will exclude it.
|
|
type ImageFilter func(*Image) bool //nolint
|
|
|
|
// ErrRepoTagNotFound is the error returned when the image id given doesn't match a rep tag in store
|
|
var ErrRepoTagNotFound = stderrors.New("unable to match user input to any specific repotag")
|
|
|
|
// ErrImageIsBareList is the error returned when the image is just a list or index
|
|
var ErrImageIsBareList = stderrors.New("image contains a manifest list or image index, but no runnable image")
|
|
|
|
// NewImageRuntimeFromStore creates an ImageRuntime based on a provided store
|
|
func NewImageRuntimeFromStore(store storage.Store) *Runtime {
|
|
return &Runtime{
|
|
store: store,
|
|
}
|
|
}
|
|
|
|
// NewImageRuntimeFromOptions creates an Image Runtime including the store given
|
|
// store options
|
|
func NewImageRuntimeFromOptions(options storage.StoreOptions) (*Runtime, error) {
|
|
store, err := setStore(options)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return NewImageRuntimeFromStore(store), nil
|
|
}
|
|
|
|
func setStore(options storage.StoreOptions) (storage.Store, error) {
|
|
store, err := storage.GetStore(options)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
is.Transport.SetStore(store)
|
|
return store, nil
|
|
}
|
|
|
|
// newImage creates a new image object given an "input name" and a storage.Image
|
|
func (ir *Runtime) newImage(inputName string, img *storage.Image) *Image {
|
|
return &Image{
|
|
InputName: inputName,
|
|
imageruntime: ir,
|
|
image: img,
|
|
}
|
|
}
|
|
|
|
// newFromStorage creates a new image object from a storage.Image. Its "input name" will be its ID.
|
|
func (ir *Runtime) newFromStorage(img *storage.Image) *Image {
|
|
return ir.newImage(img.ID, img)
|
|
}
|
|
|
|
// NewFromLocal creates a new image object that is intended
|
|
// to only deal with local images already in the store (or
|
|
// its aliases)
|
|
func (ir *Runtime) NewFromLocal(name string) (*Image, error) {
|
|
updatedInputName, localImage, err := ir.getLocalImage(name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return ir.newImage(updatedInputName, localImage), nil
|
|
}
|
|
|
|
// New creates a new image object where the image could be local
|
|
// or remote
|
|
func (ir *Runtime) New(ctx context.Context, name, signaturePolicyPath, authfile string, writer io.Writer, dockeroptions *DockerRegistryOptions, signingoptions SigningOptions, label *string, pullType util.PullType) (*Image, error) {
|
|
span, _ := opentracing.StartSpanFromContext(ctx, "newImage")
|
|
span.SetTag("type", "runtime")
|
|
defer span.Finish()
|
|
|
|
// We don't know if the image is local or not ... check local first
|
|
if pullType != util.PullImageAlways {
|
|
newImage, err := ir.NewFromLocal(name)
|
|
if err == nil {
|
|
return newImage, nil
|
|
} else if pullType == util.PullImageNever {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// The image is not local
|
|
if signaturePolicyPath == "" {
|
|
signaturePolicyPath = ir.SignaturePolicyPath
|
|
}
|
|
imageName, err := ir.pullImageFromHeuristicSource(ctx, name, writer, authfile, signaturePolicyPath, signingoptions, dockeroptions, label)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "unable to pull %s", name)
|
|
}
|
|
|
|
newImage, err := ir.NewFromLocal(imageName[0])
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "error retrieving local image after pulling %s", name)
|
|
}
|
|
return newImage, nil
|
|
}
|
|
|
|
// LoadFromArchiveReference creates a new image object for images pulled from a tar archive and the like (podman load)
|
|
// This function is needed because it is possible for a tar archive to have multiple tags for one image
|
|
func (ir *Runtime) LoadFromArchiveReference(ctx context.Context, srcRef types.ImageReference, signaturePolicyPath string, writer io.Writer) ([]*Image, error) {
|
|
if signaturePolicyPath == "" {
|
|
signaturePolicyPath = ir.SignaturePolicyPath
|
|
}
|
|
imageNames, err := ir.pullImageFromReference(ctx, srcRef, writer, "", signaturePolicyPath, SigningOptions{}, &DockerRegistryOptions{})
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "unable to pull %s", transports.ImageName(srcRef))
|
|
}
|
|
|
|
newImages := make([]*Image, 0, len(imageNames))
|
|
for _, name := range imageNames {
|
|
newImage, err := ir.NewFromLocal(name)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "error retrieving local image after pulling %s", name)
|
|
}
|
|
newImages = append(newImages, newImage)
|
|
}
|
|
ir.newImageEvent(events.LoadFromArchive, "")
|
|
return newImages, nil
|
|
}
|
|
|
|
// Shutdown closes down the storage and require a bool arg as to
|
|
// whether it should do so forcibly.
|
|
func (ir *Runtime) Shutdown(force bool) error {
|
|
_, err := ir.store.Shutdown(force)
|
|
return err
|
|
}
|
|
|
|
// GetImagesWithFilters gets images with a series of filters applied
|
|
func (ir *Runtime) GetImagesWithFilters(filters []string) ([]*Image, error) {
|
|
filterFuncs, err := ir.createFilterFuncs(filters, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
images, err := ir.GetImages()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return FilterImages(images, filterFuncs), nil
|
|
}
|
|
|
|
func (i *Image) reloadImage() error {
|
|
newImage, err := i.imageruntime.getImage(i.ID())
|
|
if err != nil {
|
|
return errors.Wrapf(err, "unable to reload image")
|
|
}
|
|
i.image = newImage
|
|
return nil
|
|
}
|
|
|
|
// stringSha256 strips sha256 from user input
|
|
func stripSha256(name string) string {
|
|
if strings.HasPrefix(name, "sha256:") && len(name) > 7 {
|
|
return name[7:]
|
|
}
|
|
return name
|
|
}
|
|
|
|
// getLocalImage resolves an unknown input describing an image and
|
|
// returns an updated input name, and a storage.Image, or an error. It is used by NewFromLocal.
|
|
func (ir *Runtime) getLocalImage(inputName string) (string, *storage.Image, error) {
|
|
imageError := fmt.Sprintf("unable to find '%s' in local storage", inputName)
|
|
if inputName == "" {
|
|
return "", nil, errors.Errorf("input name is blank")
|
|
}
|
|
// Check if the input name has a transport and if so strip it
|
|
dest, err := alltransports.ParseImageName(inputName)
|
|
if err == nil && dest.DockerReference() != nil {
|
|
inputName = dest.DockerReference().String()
|
|
}
|
|
|
|
img, err := ir.getImage(stripSha256(inputName))
|
|
if err == nil {
|
|
return inputName, img, err
|
|
}
|
|
|
|
// container-storage wasn't able to find it in its current form
|
|
// check if the input name has a tag, and if not, run it through
|
|
// again
|
|
decomposedImage, err := decompose(inputName)
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
|
|
// The image has a registry name in it and we made sure we looked for it locally
|
|
// with a tag. It cannot be local.
|
|
if decomposedImage.hasRegistry {
|
|
return "", nil, errors.Wrapf(ErrNoSuchImage, imageError)
|
|
}
|
|
// if the image is saved with the repository localhost, searching with localhost prepended is necessary
|
|
// We don't need to strip the sha because we have already determined it is not an ID
|
|
ref, err := decomposedImage.referenceWithRegistry(DefaultLocalRegistry)
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
img, err = ir.getImage(ref.String())
|
|
if err == nil {
|
|
return inputName, img, err
|
|
}
|
|
|
|
// grab all the local images
|
|
images, err := ir.GetImages()
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
|
|
// check the repotags of all images for a match
|
|
repoImage, err := findImageInRepotags(decomposedImage, images)
|
|
if err == nil {
|
|
return inputName, repoImage, nil
|
|
}
|
|
|
|
return "", nil, errors.Wrapf(ErrNoSuchImage, err.Error())
|
|
}
|
|
|
|
// ID returns the image ID as a string
|
|
func (i *Image) ID() string {
|
|
return i.image.ID
|
|
}
|
|
|
|
// IsReadOnly returns whether the image ID comes from a local store
|
|
func (i *Image) IsReadOnly() bool {
|
|
return i.image.ReadOnly
|
|
}
|
|
|
|
// Digest returns the image's digest
|
|
func (i *Image) Digest() digest.Digest {
|
|
return i.image.Digest
|
|
}
|
|
|
|
// Digests returns the image's digests
|
|
func (i *Image) Digests() []digest.Digest {
|
|
return i.image.Digests
|
|
}
|
|
|
|
// GetManifest returns the image's manifest as a byte array
|
|
// and manifest type as a string.
|
|
func (i *Image) GetManifest(ctx context.Context, instanceDigest *digest.Digest) ([]byte, string, error) {
|
|
imgSrcRef, err := i.toImageSourceRef(ctx)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
return imgSrcRef.GetManifest(ctx, instanceDigest)
|
|
}
|
|
|
|
// Manifest returns the image's manifest as a byte array
|
|
// and manifest type as a string.
|
|
func (i *Image) Manifest(ctx context.Context) ([]byte, string, error) {
|
|
imgRef, err := i.toImageRef(ctx)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
return imgRef.Manifest(ctx)
|
|
}
|
|
|
|
// Names returns a string array of names associated with the image, which may be a mixture of tags and digests
|
|
func (i *Image) Names() []string {
|
|
return i.image.Names
|
|
}
|
|
|
|
// NamesHistory returns a string array of names previously associated with the
|
|
// image, which may be a mixture of tags and digests
|
|
func (i *Image) NamesHistory() []string {
|
|
if len(i.image.Names) > 0 && len(i.image.NamesHistory) > 0 &&
|
|
// We compare the latest (time-referenced) tags for equality and skip
|
|
// it in the history if they match to not display them twice. We have
|
|
// to compare like this, because `i.image.Names` (latest last) gets
|
|
// appended on retag, whereas `i.image.NamesHistory` gets prepended
|
|
// (latest first)
|
|
i.image.Names[len(i.image.Names)-1] == i.image.NamesHistory[0] {
|
|
return i.image.NamesHistory[1:]
|
|
}
|
|
return i.image.NamesHistory
|
|
}
|
|
|
|
// RepoTags returns a string array of repotags associated with the image
|
|
func (i *Image) RepoTags() ([]string, error) {
|
|
var repoTags []string
|
|
for _, name := range i.Names() {
|
|
named, err := reference.ParseNormalizedNamed(name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if tagged, isTagged := named.(reference.NamedTagged); isTagged {
|
|
repoTags = append(repoTags, tagged.String())
|
|
}
|
|
}
|
|
return repoTags, nil
|
|
}
|
|
|
|
// RepoDigests returns a string array of repodigests associated with the image
|
|
func (i *Image) RepoDigests() ([]string, error) {
|
|
var repoDigests []string
|
|
added := make(map[string]struct{})
|
|
|
|
for _, name := range i.Names() {
|
|
for _, imageDigest := range append(i.Digests(), i.Digest()) {
|
|
if imageDigest == "" {
|
|
continue
|
|
}
|
|
|
|
named, err := reference.ParseNormalizedNamed(name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
canonical, err := reference.WithDigest(reference.TrimNamed(named), imageDigest)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if _, alreadyInList := added[canonical.String()]; !alreadyInList {
|
|
repoDigests = append(repoDigests, canonical.String())
|
|
added[canonical.String()] = struct{}{}
|
|
}
|
|
}
|
|
}
|
|
sort.Strings(repoDigests)
|
|
return repoDigests, nil
|
|
}
|
|
|
|
// Created returns the time the image was created
|
|
func (i *Image) Created() time.Time {
|
|
return i.image.Created
|
|
}
|
|
|
|
// TopLayer returns the top layer id as a string
|
|
func (i *Image) TopLayer() string {
|
|
return i.image.TopLayer
|
|
}
|
|
|
|
// Remove an image; container removal for the image must be done
|
|
// outside the context of images
|
|
// TODO: the force param does nothing as of now. Need to move container
|
|
// handling logic here eventually.
|
|
func (i *Image) Remove(ctx context.Context, force bool) error {
|
|
parent, err := i.GetParent(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if _, err := i.imageruntime.store.DeleteImage(i.ID(), true); err != nil {
|
|
return err
|
|
}
|
|
i.newImageEvent(events.Remove)
|
|
for parent != nil {
|
|
nextParent, err := parent.GetParent(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
children, err := parent.GetChildren(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// Do not remove if image is a base image and is not untagged, or if
|
|
// the image has more children.
|
|
if len(children) > 0 || len(parent.Names()) > 0 {
|
|
return nil
|
|
}
|
|
id := parent.ID()
|
|
if _, err := i.imageruntime.store.DeleteImage(id, true); err != nil {
|
|
logrus.Debugf("unable to remove intermediate image %q: %v", id, err)
|
|
} else {
|
|
fmt.Println(id)
|
|
}
|
|
parent = nextParent
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// getImage retrieves an image matching the given name or hash from system
|
|
// storage
|
|
// If no matching image can be found, an error is returned
|
|
func (ir *Runtime) getImage(image string) (*storage.Image, error) {
|
|
var img *storage.Image
|
|
ref, err := is.Transport.ParseStoreReference(ir.store, image)
|
|
if err == nil {
|
|
img, err = is.Transport.GetStoreImage(ir.store, ref)
|
|
}
|
|
if err != nil {
|
|
img2, err2 := ir.store.Image(image)
|
|
if err2 != nil {
|
|
if ref == nil {
|
|
return nil, errors.Wrapf(err, "error parsing reference to image %q", image)
|
|
}
|
|
return nil, errors.Wrapf(err, "unable to locate image %q", image)
|
|
}
|
|
img = img2
|
|
}
|
|
return img, nil
|
|
}
|
|
|
|
// GetImages retrieves all images present in storage
|
|
func (ir *Runtime) GetImages() ([]*Image, error) {
|
|
return ir.getImages(false)
|
|
}
|
|
|
|
// GetRWImages retrieves all read/write images present in storage
|
|
func (ir *Runtime) GetRWImages() ([]*Image, error) {
|
|
return ir.getImages(true)
|
|
}
|
|
|
|
// getImages retrieves all images present in storage
|
|
func (ir *Runtime) getImages(rwOnly bool) ([]*Image, error) {
|
|
images, err := ir.store.Images()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
newImages := []*Image{}
|
|
for _, i := range images {
|
|
if rwOnly && i.ReadOnly {
|
|
continue
|
|
}
|
|
// iterating over these, be careful to not iterate on the literal
|
|
// pointer.
|
|
image := i
|
|
img := ir.newFromStorage(&image)
|
|
newImages = append(newImages, img)
|
|
}
|
|
return newImages, nil
|
|
}
|
|
|
|
// getImageDigest creates an image object and uses the hex value of the digest as the image ID
|
|
// for parsing the store reference
|
|
func getImageDigest(ctx context.Context, src types.ImageReference, sc *types.SystemContext) (string, error) {
|
|
newImg, err := src.NewImage(ctx, sc)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer func() {
|
|
if err := newImg.Close(); err != nil {
|
|
logrus.Errorf("failed to close image: %q", err)
|
|
}
|
|
}()
|
|
imageDigest := newImg.ConfigInfo().Digest
|
|
if err = imageDigest.Validate(); err != nil {
|
|
return "", errors.Wrapf(err, "error getting config info")
|
|
}
|
|
return "@" + imageDigest.Hex(), nil
|
|
}
|
|
|
|
// NormalizedTag returns the canonical version of tag for use in Image.Names()
|
|
func NormalizedTag(tag string) (reference.Named, error) {
|
|
decomposedTag, err := decompose(tag)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// If the input doesn't specify a registry, set the registry to localhost
|
|
var ref reference.Named
|
|
if !decomposedTag.hasRegistry {
|
|
ref, err = decomposedTag.referenceWithRegistry(DefaultLocalRegistry)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
} else {
|
|
ref, err = decomposedTag.normalizedReference()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
// If the input does not have a tag, we need to add one (latest)
|
|
ref = reference.TagNameOnly(ref)
|
|
return ref, nil
|
|
}
|
|
|
|
// TagImage adds a tag to the given image
|
|
func (i *Image) TagImage(tag string) error {
|
|
if err := i.reloadImage(); err != nil {
|
|
return err
|
|
}
|
|
ref, err := NormalizedTag(tag)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
tags := i.Names()
|
|
if util.StringInSlice(ref.String(), tags) {
|
|
return nil
|
|
}
|
|
tags = append(tags, ref.String())
|
|
if err := i.imageruntime.store.SetNames(i.ID(), tags); err != nil {
|
|
return err
|
|
}
|
|
if err := i.reloadImage(); err != nil {
|
|
return err
|
|
}
|
|
i.newImageEvent(events.Tag)
|
|
return nil
|
|
}
|
|
|
|
// UntagImage removes the specified tag from the image.
|
|
// If the tag does not exist, ErrNoSuchTag is returned.
|
|
func (i *Image) UntagImage(tag string) error {
|
|
if err := i.reloadImage(); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Normalize the tag as we do with TagImage.
|
|
ref, err := NormalizedTag(tag)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
tag = ref.String()
|
|
|
|
var newTags []string
|
|
tags := i.Names()
|
|
if !util.StringInSlice(tag, tags) {
|
|
return errors.Wrapf(ErrNoSuchTag, "%q", tag)
|
|
}
|
|
for _, t := range tags {
|
|
if tag != t {
|
|
newTags = append(newTags, t)
|
|
}
|
|
}
|
|
if err := i.imageruntime.store.SetNames(i.ID(), newTags); err != nil {
|
|
return err
|
|
}
|
|
if err := i.reloadImage(); err != nil {
|
|
return err
|
|
}
|
|
i.newImageEvent(events.Untag)
|
|
return nil
|
|
}
|
|
|
|
// PushImageToHeuristicDestination pushes the given image to "destination", which is heuristically parsed.
|
|
// Use PushImageToReference if the destination is known precisely.
|
|
func (i *Image) PushImageToHeuristicDestination(ctx context.Context, destination, manifestMIMEType, authFile, digestFile, signaturePolicyPath string, writer io.Writer, forceCompress bool, signingOptions SigningOptions, dockerRegistryOptions *DockerRegistryOptions, additionalDockerArchiveTags []reference.NamedTagged) error {
|
|
if destination == "" {
|
|
return errors.Wrapf(syscall.EINVAL, "destination image name must be specified")
|
|
}
|
|
|
|
// Get the destination Image Reference
|
|
dest, err := alltransports.ParseImageName(destination)
|
|
if err != nil {
|
|
if hasTransport(destination) {
|
|
return errors.Wrapf(err, "error getting destination imageReference for %q", destination)
|
|
}
|
|
// Try adding the images default transport
|
|
destination2 := DefaultTransport + destination
|
|
dest, err = alltransports.ParseImageName(destination2)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return i.PushImageToReference(ctx, dest, manifestMIMEType, authFile, digestFile, signaturePolicyPath, writer, forceCompress, signingOptions, dockerRegistryOptions, additionalDockerArchiveTags)
|
|
}
|
|
|
|
// PushImageToReference pushes the given image to a location described by the given path
|
|
func (i *Image) PushImageToReference(ctx context.Context, dest types.ImageReference, manifestMIMEType, authFile, digestFile, signaturePolicyPath string, writer io.Writer, forceCompress bool, signingOptions SigningOptions, dockerRegistryOptions *DockerRegistryOptions, additionalDockerArchiveTags []reference.NamedTagged) error {
|
|
sc := GetSystemContext(signaturePolicyPath, authFile, forceCompress)
|
|
sc.BlobInfoCacheDir = filepath.Join(i.imageruntime.store.GraphRoot(), "cache")
|
|
|
|
policyContext, err := getPolicyContext(sc)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() {
|
|
if err := policyContext.Destroy(); err != nil {
|
|
logrus.Errorf("failed to destroy policy context: %q", err)
|
|
}
|
|
}()
|
|
|
|
// Look up the source image, expecting it to be in local storage
|
|
src, err := is.Transport.ParseStoreReference(i.imageruntime.store, i.ID())
|
|
if err != nil {
|
|
return errors.Wrapf(err, "error getting source imageReference for %q", i.InputName)
|
|
}
|
|
copyOptions := getCopyOptions(sc, writer, nil, dockerRegistryOptions, signingOptions, manifestMIMEType, additionalDockerArchiveTags)
|
|
copyOptions.DestinationCtx.SystemRegistriesConfPath = registries.SystemRegistriesConfPath() // FIXME: Set this more globally. Probably no reason not to have it in every types.SystemContext, and to compute the value just once in one place.
|
|
// Copy the image to the remote destination
|
|
manifestBytes, err := cp.Image(ctx, policyContext, dest, src, copyOptions)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "Error copying image to the remote destination")
|
|
}
|
|
digest, err := manifest.Digest(manifestBytes)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "error computing digest of manifest of new image %q", transports.ImageName(dest))
|
|
}
|
|
|
|
logrus.Debugf("Successfully pushed %s with digest %s", transports.ImageName(dest), digest.String())
|
|
|
|
if digestFile != "" {
|
|
if err = ioutil.WriteFile(digestFile, []byte(digest.String()), 0644); err != nil {
|
|
return errors.Wrapf(err, "failed to write digest to file %q", digestFile)
|
|
}
|
|
}
|
|
i.newImageEvent(events.Push)
|
|
return nil
|
|
}
|
|
|
|
// MatchesID returns a bool based on if the input id
|
|
// matches the image's id
|
|
// TODO: This isn't used anywhere, so remove it
|
|
func (i *Image) MatchesID(id string) bool {
|
|
return strings.HasPrefix(i.ID(), id)
|
|
}
|
|
|
|
// ToImageRef returns an image reference type from an image
|
|
// TODO: Hopefully we can remove this exported function for mheon
|
|
func (i *Image) ToImageRef(ctx context.Context) (types.Image, error) {
|
|
return i.toImageRef(ctx)
|
|
}
|
|
|
|
// toImageSourceRef returns an ImageSource Reference type from an image
|
|
func (i *Image) toImageSourceRef(ctx context.Context) (types.ImageSource, error) {
|
|
if i == nil {
|
|
return nil, errors.Errorf("cannot convert nil image to image source reference")
|
|
}
|
|
if i.imgSrcRef == nil {
|
|
ref, err := is.Transport.ParseStoreReference(i.imageruntime.store, "@"+i.ID())
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "error parsing reference to image %q", i.ID())
|
|
}
|
|
imgSrcRef, err := ref.NewImageSource(ctx, nil)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "error reading image %q as image source", i.ID())
|
|
}
|
|
i.imgSrcRef = imgSrcRef
|
|
}
|
|
return i.imgSrcRef, nil
|
|
}
|
|
|
|
//Size returns the size of the image
|
|
func (i *Image) Size(ctx context.Context) (*uint64, error) {
|
|
sum, err := i.imageruntime.store.ImageSize(i.ID())
|
|
if err == nil && sum >= 0 {
|
|
usum := uint64(sum)
|
|
return &usum, nil
|
|
}
|
|
return nil, errors.Wrap(err, "unable to determine size")
|
|
}
|
|
|
|
// toImageRef returns an Image Reference type from an image
|
|
func (i *Image) toImageRef(ctx context.Context) (types.Image, error) {
|
|
if i == nil {
|
|
return nil, errors.Errorf("cannot convert nil image to image reference")
|
|
}
|
|
imgSrcRef, err := i.toImageSourceRef(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if i.imgRef == nil {
|
|
systemContext := &types.SystemContext{}
|
|
unparsedDefaultInstance := image.UnparsedInstance(imgSrcRef, nil)
|
|
imgRef, err := image.FromUnparsedImage(ctx, systemContext, unparsedDefaultInstance)
|
|
if err != nil {
|
|
// check for a "tried-to-treat-a-bare-list-like-a-runnable-image" problem, else
|
|
// return info about the not-a-bare-list runnable image part of this storage.Image
|
|
if manifestBytes, manifestType, err2 := imgSrcRef.GetManifest(ctx, nil); err2 == nil {
|
|
if manifest.MIMETypeIsMultiImage(manifestType) {
|
|
if list, err3 := manifest.ListFromBlob(manifestBytes, manifestType); err3 == nil {
|
|
switch manifestType {
|
|
case ociv1.MediaTypeImageIndex:
|
|
err = errors.Wrapf(ErrImageIsBareList, "%q is an image index", i.InputName)
|
|
case manifest.DockerV2ListMediaType:
|
|
err = errors.Wrapf(ErrImageIsBareList, "%q is a manifest list", i.InputName)
|
|
default:
|
|
err = errors.Wrapf(ErrImageIsBareList, "%q", i.InputName)
|
|
}
|
|
for _, instanceDigest := range list.Instances() {
|
|
instance := instanceDigest
|
|
unparsedInstance := image.UnparsedInstance(imgSrcRef, &instance)
|
|
if imgRef2, err4 := image.FromUnparsedImage(ctx, systemContext, unparsedInstance); err4 == nil {
|
|
imgRef = imgRef2
|
|
err = nil
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "error reading image %q as image", i.ID())
|
|
}
|
|
}
|
|
i.imgRef = imgRef
|
|
}
|
|
return i.imgRef, nil
|
|
}
|
|
|
|
// DriverData gets the driver data from the store on a layer
|
|
func (i *Image) DriverData() (*driver.Data, error) {
|
|
return driver.GetDriverData(i.imageruntime.store, i.TopLayer())
|
|
}
|
|
|
|
// Layer returns the image's top layer
|
|
func (i *Image) Layer() (*storage.Layer, error) {
|
|
return i.imageruntime.store.Layer(i.image.TopLayer)
|
|
}
|
|
|
|
// History contains the history information of an image
|
|
type History struct {
|
|
ID string `json:"id"`
|
|
Created *time.Time `json:"created"`
|
|
CreatedBy string `json:"createdBy"`
|
|
Size int64 `json:"size"`
|
|
Comment string `json:"comment"`
|
|
Tags []string `json:"tags"`
|
|
}
|
|
|
|
// History gets the history of an image and the IDs of images that are part of
|
|
// its history
|
|
func (i *Image) History(ctx context.Context) ([]*History, error) {
|
|
img, err := i.toImageRef(ctx)
|
|
if err != nil {
|
|
if errors.Cause(err) == ErrImageIsBareList {
|
|
return nil, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
oci, err := img.OCIConfig(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Build a mapping from top-layer to image ID.
|
|
images, err := i.imageruntime.GetImages()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
topLayerMap := make(map[string]string)
|
|
for _, image := range images {
|
|
if _, exists := topLayerMap[image.TopLayer()]; !exists {
|
|
topLayerMap[image.TopLayer()] = image.ID()
|
|
}
|
|
}
|
|
|
|
var allHistory []*History
|
|
var layer *storage.Layer
|
|
|
|
// Check if we have an actual top layer to prevent lookup errors.
|
|
if i.TopLayer() != "" {
|
|
layer, err = i.imageruntime.store.Layer(i.TopLayer())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// Iterate in reverse order over the history entries, and lookup the
|
|
// corresponding image ID, size and get the next later if needed.
|
|
numHistories := len(oci.History) - 1
|
|
for x := numHistories; x >= 0; x-- {
|
|
var size int64
|
|
|
|
id := "<missing>"
|
|
if x == numHistories {
|
|
id = i.ID()
|
|
}
|
|
if layer != nil {
|
|
if !oci.History[x].EmptyLayer {
|
|
size = layer.UncompressedSize
|
|
}
|
|
if imageID, exists := topLayerMap[layer.ID]; exists {
|
|
id = imageID
|
|
// Delete the entry to avoid reusing it for following history items.
|
|
delete(topLayerMap, layer.ID)
|
|
}
|
|
}
|
|
h := History{
|
|
ID: id,
|
|
Created: oci.History[x].Created,
|
|
CreatedBy: oci.History[x].CreatedBy,
|
|
Size: size,
|
|
Comment: oci.History[x].Comment,
|
|
}
|
|
if layer != nil {
|
|
h.Tags = layer.Names
|
|
}
|
|
allHistory = append(allHistory, &h)
|
|
|
|
if layer != nil && layer.Parent != "" && !oci.History[x].EmptyLayer {
|
|
layer, err = i.imageruntime.store.Layer(layer.Parent)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
}
|
|
|
|
return allHistory, nil
|
|
}
|
|
|
|
// Dangling returns a bool if the image is "dangling"
|
|
func (i *Image) Dangling() bool {
|
|
return len(i.Names()) == 0
|
|
}
|
|
|
|
// Intermediate returns true if the image is cache or intermediate image.
|
|
// Cache image has parent and child.
|
|
func (i *Image) Intermediate(ctx context.Context) (bool, error) {
|
|
parent, err := i.IsParent(ctx)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if !parent {
|
|
return false, nil
|
|
}
|
|
img, err := i.GetParent(ctx)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if img != nil {
|
|
return true, nil
|
|
}
|
|
return false, nil
|
|
}
|
|
|
|
// User returns the image's user
|
|
func (i *Image) User(ctx context.Context) (string, error) {
|
|
imgInspect, err := i.inspect(ctx, false)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return imgInspect.Config.User, nil
|
|
}
|
|
|
|
// StopSignal returns the image's StopSignal
|
|
func (i *Image) StopSignal(ctx context.Context) (string, error) {
|
|
imgInspect, err := i.inspect(ctx, false)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return imgInspect.Config.StopSignal, nil
|
|
}
|
|
|
|
// WorkingDir returns the image's WorkingDir
|
|
func (i *Image) WorkingDir(ctx context.Context) (string, error) {
|
|
imgInspect, err := i.inspect(ctx, false)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return imgInspect.Config.WorkingDir, nil
|
|
}
|
|
|
|
// Cmd returns the image's cmd
|
|
func (i *Image) Cmd(ctx context.Context) ([]string, error) {
|
|
imgInspect, err := i.inspect(ctx, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return imgInspect.Config.Cmd, nil
|
|
}
|
|
|
|
// Entrypoint returns the image's entrypoint
|
|
func (i *Image) Entrypoint(ctx context.Context) ([]string, error) {
|
|
imgInspect, err := i.inspect(ctx, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return imgInspect.Config.Entrypoint, nil
|
|
}
|
|
|
|
// Env returns the image's env
|
|
func (i *Image) Env(ctx context.Context) ([]string, error) {
|
|
imgInspect, err := i.imageInspectInfo(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return imgInspect.Env, nil
|
|
}
|
|
|
|
// Labels returns the image's labels
|
|
func (i *Image) Labels(ctx context.Context) (map[string]string, error) {
|
|
imgInspect, err := i.imageInspectInfo(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return imgInspect.Labels, nil
|
|
}
|
|
|
|
// GetLabel Returns a case-insensitive match of a given label
|
|
func (i *Image) GetLabel(ctx context.Context, label string) (string, error) {
|
|
labels, err := i.Labels(ctx)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
for k, v := range labels {
|
|
if strings.ToLower(k) == strings.ToLower(label) {
|
|
return v, nil
|
|
}
|
|
}
|
|
return "", nil
|
|
}
|
|
|
|
// Annotations returns the annotations of an image
|
|
func (i *Image) Annotations(ctx context.Context) (map[string]string, error) {
|
|
imageManifest, manifestType, err := i.Manifest(ctx)
|
|
if err != nil {
|
|
imageManifest, manifestType, err = i.GetManifest(ctx, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
annotations := make(map[string]string)
|
|
if manifestType == ociv1.MediaTypeImageManifest {
|
|
var m ociv1.Manifest
|
|
if err := json.Unmarshal(imageManifest, &m); err == nil {
|
|
for k, v := range m.Annotations {
|
|
annotations[k] = v
|
|
}
|
|
}
|
|
}
|
|
return annotations, nil
|
|
}
|
|
|
|
// ociv1Image converts an image to an imgref and then returns its config blob
|
|
// converted to an ociv1 image type
|
|
func (i *Image) ociv1Image(ctx context.Context) (*ociv1.Image, error) {
|
|
imgRef, err := i.toImageRef(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return imgRef.OCIConfig(ctx)
|
|
}
|
|
|
|
func (i *Image) imageInspectInfo(ctx context.Context) (*types.ImageInspectInfo, error) {
|
|
if i.inspectInfo == nil {
|
|
ic, err := i.toImageRef(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
imgInspect, err := ic.Inspect(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
i.inspectInfo = imgInspect
|
|
}
|
|
return i.inspectInfo, nil
|
|
}
|
|
|
|
func (i *Image) inspect(ctx context.Context, calculateSize bool) (*inspect.ImageData, error) {
|
|
ociv1Img, err := i.ociv1Image(ctx)
|
|
if err != nil {
|
|
ociv1Img = &ociv1.Image{}
|
|
}
|
|
info, err := i.imageInspectInfo(ctx)
|
|
if err != nil {
|
|
info = &types.ImageInspectInfo{}
|
|
}
|
|
annotations, err := i.Annotations(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
size := int64(-1)
|
|
if calculateSize {
|
|
if usize, err := i.Size(ctx); err == nil {
|
|
size = int64(*usize)
|
|
}
|
|
}
|
|
|
|
repoTags, err := i.RepoTags()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
repoDigests, err := i.RepoDigests()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
driver, err := i.DriverData()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
_, manifestType, err := i.GetManifest(ctx, nil)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "unable to determine manifest type")
|
|
}
|
|
comment, err := i.Comment(ctx, manifestType)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
data := &inspect.ImageData{
|
|
ID: i.ID(),
|
|
RepoTags: repoTags,
|
|
RepoDigests: repoDigests,
|
|
Comment: comment,
|
|
Created: ociv1Img.Created,
|
|
Author: ociv1Img.Author,
|
|
Architecture: ociv1Img.Architecture,
|
|
Os: ociv1Img.OS,
|
|
Config: &ociv1Img.Config,
|
|
Version: info.DockerVersion,
|
|
Size: size,
|
|
VirtualSize: size,
|
|
Annotations: annotations,
|
|
Digest: i.Digest(),
|
|
Labels: info.Labels,
|
|
RootFS: &inspect.RootFS{
|
|
Type: ociv1Img.RootFS.Type,
|
|
Layers: ociv1Img.RootFS.DiffIDs,
|
|
},
|
|
GraphDriver: driver,
|
|
ManifestType: manifestType,
|
|
User: ociv1Img.Config.User,
|
|
History: ociv1Img.History,
|
|
NamesHistory: i.NamesHistory(),
|
|
}
|
|
if manifestType == manifest.DockerV2Schema2MediaType {
|
|
hc, err := i.GetHealthCheck(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if hc != nil {
|
|
data.HealthCheck = hc
|
|
}
|
|
}
|
|
return data, nil
|
|
}
|
|
|
|
// Inspect returns an image's inspect data
|
|
func (i *Image) Inspect(ctx context.Context) (*inspect.ImageData, error) {
|
|
span, _ := opentracing.StartSpanFromContext(ctx, "imageInspect")
|
|
|
|
span.SetTag("type", "image")
|
|
defer span.Finish()
|
|
|
|
return i.inspect(ctx, true)
|
|
}
|
|
|
|
// InspectNoSize returns an image's inspect data without calculating the size for the image
|
|
func (i *Image) InspectNoSize(ctx context.Context) (*inspect.ImageData, error) {
|
|
span, _ := opentracing.StartSpanFromContext(ctx, "imageInspectNoSize")
|
|
|
|
span.SetTag("type", "image")
|
|
defer span.Finish()
|
|
|
|
return i.inspect(ctx, false)
|
|
}
|
|
|
|
// Import imports and image into the store and returns an image
|
|
func (ir *Runtime) Import(ctx context.Context, path, reference string, writer io.Writer, signingOptions SigningOptions, imageConfig ociv1.Image) (*Image, error) {
|
|
src, err := tarball.Transport.ParseReference(path)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "error parsing image name %q", path)
|
|
}
|
|
|
|
updater, ok := src.(tarball.ConfigUpdater)
|
|
if !ok {
|
|
return nil, errors.Wrapf(err, "unexpected type, a tarball reference should implement tarball.ConfigUpdater")
|
|
}
|
|
|
|
annotations := make(map[string]string)
|
|
|
|
// config imgspecv1.Image
|
|
err = updater.ConfigUpdate(imageConfig, annotations)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "error updating image config")
|
|
}
|
|
|
|
sc := GetSystemContext("", "", false)
|
|
|
|
// if reference not given, get the image digest
|
|
if reference == "" {
|
|
reference, err = getImageDigest(ctx, src, sc)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
policyContext, err := getPolicyContext(sc)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() {
|
|
if err := policyContext.Destroy(); err != nil {
|
|
logrus.Errorf("failed to destroy policy context: %q", err)
|
|
}
|
|
}()
|
|
copyOptions := getCopyOptions(sc, writer, nil, nil, signingOptions, "", nil)
|
|
dest, err := is.Transport.ParseStoreReference(ir.store, reference)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "error getting image reference for %q", reference)
|
|
}
|
|
_, err = cp.Image(ctx, policyContext, dest, src, copyOptions)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
newImage, err := ir.NewFromLocal(reference)
|
|
if err == nil {
|
|
newImage.newImageEvent(events.Import)
|
|
}
|
|
return newImage, err
|
|
}
|
|
|
|
// MatchRepoTag takes a string and tries to match it against an
|
|
// image's repotags
|
|
func (i *Image) MatchRepoTag(input string) (string, error) {
|
|
results := make(map[int][]string)
|
|
var maxCount int
|
|
// first check if we have an exact match with the input
|
|
if util.StringInSlice(input, i.Names()) {
|
|
return input, nil
|
|
}
|
|
// next check if we are missing the tag
|
|
dcImage, err := decompose(input)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
imageRegistry, imageName, imageSuspiciousTagValueForSearch := dcImage.suspiciousRefNameTagValuesForSearch()
|
|
for _, repoName := range i.Names() {
|
|
count := 0
|
|
dcRepoName, err := decompose(repoName)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
repoNameRegistry, repoNameName, repoNameSuspiciousTagValueForSearch := dcRepoName.suspiciousRefNameTagValuesForSearch()
|
|
if repoNameRegistry == imageRegistry && imageRegistry != "" {
|
|
count++
|
|
}
|
|
if repoNameName == imageName && imageName != "" {
|
|
count++
|
|
} else if splitString(repoNameName) == splitString(imageName) {
|
|
count++
|
|
}
|
|
if repoNameSuspiciousTagValueForSearch == imageSuspiciousTagValueForSearch {
|
|
count++
|
|
}
|
|
results[count] = append(results[count], repoName)
|
|
if count > maxCount {
|
|
maxCount = count
|
|
}
|
|
}
|
|
if maxCount == 0 {
|
|
return "", ErrRepoTagNotFound
|
|
}
|
|
if len(results[maxCount]) > 1 {
|
|
return "", errors.Errorf("user input matched multiple repotags for the image")
|
|
}
|
|
return results[maxCount][0], nil
|
|
}
|
|
|
|
// splitString splits input string by / and returns the last array item
|
|
func splitString(input string) string {
|
|
split := strings.Split(input, "/")
|
|
return split[len(split)-1]
|
|
}
|
|
|
|
// IsParent goes through the layers in the store and checks if i.TopLayer is
|
|
// the parent of any other layer in store. Double check that image with that
|
|
// layer exists as well.
|
|
func (i *Image) IsParent(ctx context.Context) (bool, error) {
|
|
children, err := i.getChildren(ctx, 1)
|
|
if err != nil {
|
|
if errors.Cause(err) == ErrImageIsBareList {
|
|
return false, nil
|
|
}
|
|
return false, err
|
|
}
|
|
return len(children) > 0, nil
|
|
}
|
|
|
|
// historiesMatch returns the number of entries in the histories which have the
|
|
// same contents
|
|
func historiesMatch(a, b []imgspecv1.History) int {
|
|
i := 0
|
|
for i < len(a) && i < len(b) {
|
|
if a[i].Created != nil && b[i].Created == nil {
|
|
return i
|
|
}
|
|
if a[i].Created == nil && b[i].Created != nil {
|
|
return i
|
|
}
|
|
if a[i].Created != nil && b[i].Created != nil {
|
|
if !a[i].Created.Equal(*(b[i].Created)) {
|
|
return i
|
|
}
|
|
}
|
|
if a[i].CreatedBy != b[i].CreatedBy {
|
|
return i
|
|
}
|
|
if a[i].Author != b[i].Author {
|
|
return i
|
|
}
|
|
if a[i].Comment != b[i].Comment {
|
|
return i
|
|
}
|
|
if a[i].EmptyLayer != b[i].EmptyLayer {
|
|
return i
|
|
}
|
|
i++
|
|
}
|
|
return i
|
|
}
|
|
|
|
// areParentAndChild checks diff ID and history in the two images and return
|
|
// true if the second should be considered to be directly based on the first
|
|
func areParentAndChild(parent, child *imgspecv1.Image) bool {
|
|
// the child and candidate parent should share all of the
|
|
// candidate parent's diff IDs, which together would have
|
|
// controlled which layers were used
|
|
if len(parent.RootFS.DiffIDs) > len(child.RootFS.DiffIDs) {
|
|
return false
|
|
}
|
|
childUsesCandidateDiffs := true
|
|
for i := range parent.RootFS.DiffIDs {
|
|
if child.RootFS.DiffIDs[i] != parent.RootFS.DiffIDs[i] {
|
|
childUsesCandidateDiffs = false
|
|
break
|
|
}
|
|
}
|
|
if !childUsesCandidateDiffs {
|
|
return false
|
|
}
|
|
// the child should have the same history as the parent, plus
|
|
// one more entry
|
|
if len(parent.History)+1 != len(child.History) {
|
|
return false
|
|
}
|
|
if historiesMatch(parent.History, child.History) != len(parent.History) {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
// GetParent returns the image ID of the parent. Return nil if a parent is not found.
|
|
func (i *Image) GetParent(ctx context.Context) (*Image, error) {
|
|
var childLayer *storage.Layer
|
|
images, err := i.imageruntime.GetImages()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if i.TopLayer() != "" {
|
|
if childLayer, err = i.imageruntime.store.Layer(i.TopLayer()); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
// fetch the configuration for the child image
|
|
child, err := i.ociv1Image(ctx)
|
|
if err != nil {
|
|
if errors.Cause(err) == ErrImageIsBareList {
|
|
return nil, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
for _, img := range images {
|
|
if img.ID() == i.ID() {
|
|
continue
|
|
}
|
|
candidateLayer := img.TopLayer()
|
|
// as a child, our top layer, if we have one, is either the
|
|
// candidate parent's layer, or one that's derived from it, so
|
|
// skip over any candidate image where we know that isn't the
|
|
// case
|
|
if childLayer != nil {
|
|
// The child has at least one layer, so a parent would
|
|
// have a top layer that's either the same as the child's
|
|
// top layer or the top layer's recorded parent layer,
|
|
// which could be an empty value.
|
|
if candidateLayer != childLayer.Parent && candidateLayer != childLayer.ID {
|
|
continue
|
|
}
|
|
} else {
|
|
// The child has no layers, but the candidate does.
|
|
if candidateLayer != "" {
|
|
continue
|
|
}
|
|
}
|
|
// fetch the configuration for the candidate image
|
|
candidate, err := img.ociv1Image(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// compare them
|
|
if areParentAndChild(candidate, child) {
|
|
return img, nil
|
|
}
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
// GetChildren returns a list of the imageIDs that depend on the image
|
|
func (i *Image) GetChildren(ctx context.Context) ([]string, error) {
|
|
children, err := i.getChildren(ctx, 0)
|
|
if err != nil {
|
|
if errors.Cause(err) == ErrImageIsBareList {
|
|
return nil, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
return children, nil
|
|
}
|
|
|
|
// getChildren returns a list of at most "max" imageIDs that depend on the image
|
|
func (i *Image) getChildren(ctx context.Context, max int) ([]string, error) {
|
|
var children []string
|
|
|
|
if _, err := i.toImageRef(ctx); err != nil {
|
|
return nil, nil
|
|
}
|
|
|
|
images, err := i.imageruntime.GetImages()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// fetch the configuration for the parent image
|
|
parent, err := i.ociv1Image(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
parentLayer := i.TopLayer()
|
|
|
|
for _, img := range images {
|
|
if img.ID() == i.ID() {
|
|
continue
|
|
}
|
|
if img.TopLayer() == "" {
|
|
if parentLayer != "" {
|
|
// this image has no layers, but we do, so
|
|
// it can't be derived from this one
|
|
continue
|
|
}
|
|
} else {
|
|
candidateLayer, err := img.Layer()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// if this image's top layer is not our top layer, and is not
|
|
// based on our top layer, we can skip it
|
|
if candidateLayer.Parent != parentLayer && candidateLayer.ID != parentLayer {
|
|
continue
|
|
}
|
|
}
|
|
// fetch the configuration for the candidate image
|
|
candidate, err := img.ociv1Image(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// compare them
|
|
if areParentAndChild(parent, candidate) {
|
|
children = append(children, img.ID())
|
|
}
|
|
// if we're not building an exhaustive list, maybe we're done?
|
|
if max > 0 && len(children) >= max {
|
|
break
|
|
}
|
|
}
|
|
return children, nil
|
|
}
|
|
|
|
// InputIsID returns a bool if the user input for an image
|
|
// is the image's partial or full id
|
|
func (i *Image) InputIsID() bool {
|
|
return strings.HasPrefix(i.ID(), i.InputName)
|
|
}
|
|
|
|
// Containers a list of container IDs associated with the image
|
|
func (i *Image) Containers() ([]string, error) {
|
|
containers, err := i.imageruntime.store.Containers()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var imageContainers []string
|
|
for _, c := range containers {
|
|
if c.ImageID == i.ID() {
|
|
imageContainers = append(imageContainers, c.ID)
|
|
}
|
|
}
|
|
return imageContainers, err
|
|
}
|
|
|
|
// Comment returns the Comment for an image depending on its ManifestType
|
|
func (i *Image) Comment(ctx context.Context, manifestType string) (string, error) {
|
|
if manifestType == manifest.DockerV2Schema2MediaType {
|
|
imgRef, err := i.toImageRef(ctx)
|
|
if err != nil {
|
|
return "", errors.Wrapf(err, "unable to create image reference from image")
|
|
}
|
|
blob, err := imgRef.ConfigBlob(ctx)
|
|
if err != nil {
|
|
return "", errors.Wrapf(err, "unable to get config blob from image")
|
|
}
|
|
b := manifest.Schema2Image{}
|
|
if err := json.Unmarshal(blob, &b); err != nil {
|
|
return "", err
|
|
}
|
|
return b.Comment, nil
|
|
}
|
|
ociv1Img, err := i.ociv1Image(ctx)
|
|
if err != nil {
|
|
if errors.Cause(err) == ErrImageIsBareList {
|
|
return "", nil
|
|
}
|
|
return "", err
|
|
}
|
|
if len(ociv1Img.History) > 0 {
|
|
return ociv1Img.History[0].Comment, nil
|
|
}
|
|
return "", nil
|
|
}
|
|
|
|
// Save writes a container image to the filesystem
|
|
func (i *Image) Save(ctx context.Context, source, format, output string, moreTags []string, quiet, compress bool) error {
|
|
var (
|
|
writer io.Writer
|
|
destRef types.ImageReference
|
|
manifestType string
|
|
err error
|
|
)
|
|
|
|
if quiet {
|
|
writer = os.Stderr
|
|
}
|
|
switch format {
|
|
case "oci-archive":
|
|
destImageName := imageNameForSaveDestination(i, source)
|
|
destRef, err = ociarchive.NewReference(output, destImageName) // destImageName may be ""
|
|
if err != nil {
|
|
return errors.Wrapf(err, "error getting OCI archive ImageReference for (%q, %q)", output, destImageName)
|
|
}
|
|
case "oci-dir":
|
|
destRef, err = directory.NewReference(output)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "error getting directory ImageReference for %q", output)
|
|
}
|
|
manifestType = imgspecv1.MediaTypeImageManifest
|
|
case "docker-dir":
|
|
destRef, err = directory.NewReference(output)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "error getting directory ImageReference for %q", output)
|
|
}
|
|
manifestType = manifest.DockerV2Schema2MediaType
|
|
case "docker-archive", "":
|
|
destImageName := imageNameForSaveDestination(i, source)
|
|
ref, err := dockerArchiveDstReference(destImageName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
destRef, err = dockerarchive.NewReference(output, ref)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "error getting Docker archive ImageReference for %s:%v", output, ref)
|
|
}
|
|
default:
|
|
return errors.Errorf("unknown format option %q", format)
|
|
}
|
|
// supports saving multiple tags to the same tar archive
|
|
var additionaltags []reference.NamedTagged
|
|
if len(moreTags) > 0 {
|
|
additionaltags, err = GetAdditionalTags(moreTags)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if err := i.PushImageToReference(ctx, destRef, manifestType, "", "", "", writer, compress, SigningOptions{}, &DockerRegistryOptions{}, additionaltags); err != nil {
|
|
return errors.Wrapf(err, "unable to save %q", source)
|
|
}
|
|
i.newImageEvent(events.Save)
|
|
return nil
|
|
}
|
|
|
|
// dockerArchiveDestReference returns a NamedTagged reference for a tagged image and nil for untagged image.
|
|
func dockerArchiveDstReference(normalizedInput string) (reference.NamedTagged, error) {
|
|
if normalizedInput == "" {
|
|
return nil, nil
|
|
}
|
|
ref, err := reference.ParseNormalizedNamed(normalizedInput)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "docker-archive parsing reference %s", normalizedInput)
|
|
}
|
|
ref = reference.TagNameOnly(ref)
|
|
namedTagged, isTagged := ref.(reference.NamedTagged)
|
|
if !isTagged {
|
|
namedTagged = nil
|
|
}
|
|
return namedTagged, nil
|
|
}
|
|
|
|
// GetConfigBlob returns a schema2image. If the image is not a schema2, then
|
|
// it will return an error
|
|
func (i *Image) GetConfigBlob(ctx context.Context) (*manifest.Schema2Image, error) {
|
|
imageRef, err := i.toImageRef(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
b, err := imageRef.ConfigBlob(ctx)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "unable to get config blob for %s", i.ID())
|
|
}
|
|
blob := manifest.Schema2Image{}
|
|
if err := json.Unmarshal(b, &blob); err != nil {
|
|
return nil, errors.Wrapf(err, "unable to parse image blob for %s", i.ID())
|
|
}
|
|
return &blob, nil
|
|
|
|
}
|
|
|
|
// GetHealthCheck returns a HealthConfig for an image. This function only works with
|
|
// schema2 images.
|
|
func (i *Image) GetHealthCheck(ctx context.Context) (*manifest.Schema2HealthConfig, error) {
|
|
configBlob, err := i.GetConfigBlob(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return configBlob.ContainerConfig.Healthcheck, nil
|
|
}
|
|
|
|
// newImageEvent creates a new event based on an image
|
|
func (ir *Runtime) newImageEvent(status events.Status, name string) {
|
|
e := events.NewEvent(status)
|
|
e.Type = events.Image
|
|
e.Name = name
|
|
if err := ir.Eventer.Write(e); err != nil {
|
|
logrus.Infof("unable to write event to %s", ir.EventsLogFilePath)
|
|
}
|
|
}
|
|
|
|
// newImageEvent creates a new event based on an image
|
|
func (i *Image) newImageEvent(status events.Status) {
|
|
e := events.NewEvent(status)
|
|
e.ID = i.ID()
|
|
e.Type = events.Image
|
|
if len(i.Names()) > 0 {
|
|
e.Name = i.Names()[0]
|
|
}
|
|
if err := i.imageruntime.Eventer.Write(e); err != nil {
|
|
logrus.Infof("unable to write event to %s", i.imageruntime.EventsLogFilePath)
|
|
}
|
|
}
|
|
|
|
// LayerInfo keeps information of single layer
|
|
type LayerInfo struct {
|
|
// Layer ID
|
|
ID string
|
|
// Parent ID of current layer.
|
|
ParentID string
|
|
// ChildID of current layer.
|
|
// there can be multiple children in case of fork
|
|
ChildID []string
|
|
// RepoTag will have image repo names, if layer is top layer of image
|
|
RepoTags []string
|
|
// Size stores Uncompressed size of layer.
|
|
Size int64
|
|
}
|
|
|
|
// GetLayersMapWithImageInfo returns map of image-layers, with associated information like RepoTags, parent and list of child layers.
|
|
func GetLayersMapWithImageInfo(imageruntime *Runtime) (map[string]*LayerInfo, error) {
|
|
|
|
// Memory allocated to store map of layers with key LayerID.
|
|
// Map will build dependency chain with ParentID and ChildID(s)
|
|
layerInfoMap := make(map[string]*LayerInfo)
|
|
|
|
// scan all layers & fill size and parent id for each layer in layerInfoMap
|
|
layers, err := imageruntime.store.Layers()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, layer := range layers {
|
|
_, ok := layerInfoMap[layer.ID]
|
|
if !ok {
|
|
layerInfoMap[layer.ID] = &LayerInfo{
|
|
ID: layer.ID,
|
|
Size: layer.UncompressedSize,
|
|
ParentID: layer.Parent,
|
|
}
|
|
} else {
|
|
return nil, fmt.Errorf("detected multiple layers with the same ID %q", layer.ID)
|
|
}
|
|
}
|
|
|
|
// scan all layers & add all childid's for each layers to layerInfo
|
|
for _, layer := range layers {
|
|
_, ok := layerInfoMap[layer.ID]
|
|
if ok {
|
|
if layer.Parent != "" {
|
|
layerInfoMap[layer.Parent].ChildID = append(layerInfoMap[layer.Parent].ChildID, layer.ID)
|
|
}
|
|
} else {
|
|
return nil, fmt.Errorf("lookup error: layer-id %s, not found", layer.ID)
|
|
}
|
|
}
|
|
|
|
// Add the Repo Tags to Top layer of each image.
|
|
imgs, err := imageruntime.store.Images()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
layerInfoMap[""] = &LayerInfo{}
|
|
for _, img := range imgs {
|
|
e, ok := layerInfoMap[img.TopLayer]
|
|
if !ok {
|
|
return nil, fmt.Errorf("top-layer for image %s not found local store", img.ID)
|
|
}
|
|
e.RepoTags = append(e.RepoTags, img.Names...)
|
|
}
|
|
return layerInfoMap, nil
|
|
}
|
|
|
|
// BuildImageHierarchyMap stores hierarchy of images such that all parent layers using which image is built are stored in imageInfo
|
|
// Layers are added such that (Start)RootLayer->...intermediate Parent Layer(s)-> TopLayer(End)
|
|
func BuildImageHierarchyMap(imageInfo *InfoImage, layerMap map[string]*LayerInfo, layerID string) error {
|
|
if layerID == "" {
|
|
return nil
|
|
}
|
|
ll, ok := layerMap[layerID]
|
|
if !ok {
|
|
return fmt.Errorf("lookup error: layerid %s not found", layerID)
|
|
}
|
|
if err := BuildImageHierarchyMap(imageInfo, layerMap, ll.ParentID); err != nil {
|
|
return err
|
|
}
|
|
|
|
imageInfo.Layers = append(imageInfo.Layers, *ll)
|
|
return nil
|
|
}
|