automation-tests/common/libimage/image.go

583 lines
16 KiB
Go

package libimage
import (
"context"
"path/filepath"
"sort"
"time"
libimageTypes "github.com/containers/common/libimage/types"
"github.com/containers/image/v5/docker/reference"
"github.com/containers/image/v5/manifest"
storageTransport "github.com/containers/image/v5/storage"
"github.com/containers/image/v5/types"
"github.com/containers/storage"
"github.com/hashicorp/go-multierror"
"github.com/opencontainers/go-digest"
ociv1 "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
// Image represents an image in the containers storage and allows for further
// operations and data manipulation.
type Image struct {
// Backwards pointer to the runtime.
runtime *Runtime
// Counterpart in the local containers storage.
storageImage *storage.Image
// Image reference to the containers storage.
storageReference types.ImageReference
// All fields in the below structure are cached. They may be cleared
// at any time. When adding a new field, please make sure to clear
// it in `(*Image).reload()`.
cached struct {
// Image source. Cached for performance reasons.
imageSource types.ImageSource
// Inspect data we get from containers/image.
partialInspectData *types.ImageInspectInfo
// Fully assembled image data.
completeInspectData *libimageTypes.ImageData
// Corresponding OCI image.
ociv1Image *ociv1.Image
}
}
// reload the image and pessimitically clear all cached data.
func (i *Image) reload() error {
logrus.Tracef("Reloading image %s", i.ID())
img, err := i.runtime.store.Image(i.ID())
if err != nil {
return errors.Wrap(err, "error reloading image")
}
i.storageImage = img
i.cached.imageSource = nil
i.cached.partialInspectData = nil
i.cached.completeInspectData = nil
i.cached.ociv1Image = nil
return nil
}
// Names returns associated names with the image which may be a mix of tags and
// digests.
func (i *Image) Names() []string {
return i.storageImage.Names
}
// StorageImage returns the underlying storage.Image.
func (i *Image) StorageImage() *storage.Image {
return i.storageImage
}
// 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 {
return i.storageImage.NamesHistory
}
// ID returns the ID of the image.
func (i *Image) ID() string {
return i.storageImage.ID
}
// Digest is a digest value that we can use to locate the image, if one was
// specified at creation-time.
func (i *Image) Digest() digest.Digest {
return i.storageImage.Digest
}
// Digests is a list of digest values of the image's manifests, and possibly a
// manually-specified value, that we can use to locate the image. If Digest is
// set, its value is also in this list.
func (i *Image) Digests() []digest.Digest {
return i.storageImage.Digests
}
// IsReadOnly returns whether the image is set read only.
func (i *Image) IsReadOnly() bool {
return i.storageImage.ReadOnly
}
// IsDangling returns true if the image is dangling. An image is considered
// dangling if no names are associated with it in the containers storage.
func (i *Image) IsDangling() bool {
return len(i.Names()) == 0
}
// IsIntermediate returns true if the image is an intermediate image, that is
// a dangling image without children.
func (i *Image) IsIntermediate(ctx context.Context) (bool, error) {
// If the image has tags, it's not an intermediate one.
if !i.IsDangling() {
return false, nil
}
children, err := i.getChildren(ctx, false)
if err != nil {
return false, err
}
// No tags, no children -> intermediate!
return len(children) != 0, nil
}
// Created returns the time the image was created.
func (i *Image) Created() time.Time {
return i.storageImage.Created
}
// Labels returns the label of the image.
func (i *Image) Labels(ctx context.Context) (map[string]string, error) {
data, err := i.inspectInfo(ctx)
if err != nil {
isManifestList, listErr := i.isManifestList(ctx)
if listErr != nil {
err = errors.Wrapf(err, "fallback error checking whether image is a manifest list: %v", err)
} else if isManifestList {
logrus.Debugf("Ignoring error: cannot return labels for manifest list or image index %s", i.ID())
return nil, nil
}
return nil, err
}
return data.Labels, nil
}
// TopLayer returns the top layer id as a string
func (i *Image) TopLayer() string {
return i.storageImage.TopLayer
}
// Parent returns the parent image or nil if there is none
func (i *Image) Parent(ctx context.Context) (*Image, error) {
tree, err := i.runtime.layerTree()
if err != nil {
return nil, err
}
return tree.parent(ctx, i)
}
// HasChildren returns indicates if the image has children.
func (i *Image) HasChildren(ctx context.Context) (bool, error) {
children, err := i.getChildren(ctx, false)
if err != nil {
return false, err
}
return len(children) > 0, nil
}
// Children returns the image's children.
func (i *Image) Children(ctx context.Context) ([]*Image, error) {
children, err := i.getChildren(ctx, true)
if err != nil {
return nil, err
}
return children, nil
}
// getChildren returns a list of imageIDs that depend on the image. If all is
// false, only the first child image is returned.
func (i *Image) getChildren(ctx context.Context, all bool) ([]*Image, error) {
tree, err := i.runtime.layerTree()
if err != nil {
return nil, err
}
return tree.children(ctx, i, all)
}
// Containers returns a list of containers using the image.
func (i *Image) Containers() ([]string, error) {
var containerIDs []string
containers, err := i.runtime.store.Containers()
if err != nil {
return nil, err
}
imageID := i.ID()
for i := range containers {
if containers[i].ImageID == imageID {
containerIDs = append(containerIDs, containers[i].ID)
}
}
return containerIDs, nil
}
// removeContainers removes all containers using the image.
func (i *Image) removeContainers(fn RemoveContainerFunc) error {
// Execute the custom removal func if specified.
if fn != nil {
logrus.Debugf("Removing containers of image %s with custom removal function", i.ID())
return fn(i.ID())
}
containers, err := i.Containers()
if err != nil {
return err
}
logrus.Debugf("Removing containers of image %s from the local containers storage", i.ID())
var multiE error
for _, cID := range containers {
if err := i.runtime.store.DeleteContainer(cID); err != nil {
// If the container does not exist anymore, we're good.
if errors.Cause(err) != storage.ErrContainerUnknown {
multiE = multierror.Append(multiE, err)
}
}
}
return multiE
}
// RemoveContainerFunc allows for customizing the removal of containers using
// an image specified by imageID.
type RemoveContainerFunc func(imageID string) error
// RemoveImageOptions allow for customizing image removal.
type RemoveImageOptions struct {
// Force will remove all containers from the local storage that are
// using a removed image. Use RemoveContainerFunc for a custom logic.
// If set, all child images will be removed as well.
Force bool
// RemoveContainerFunc allows for a custom logic for removing
// containers using a specific image. By default, all containers in
// the local containers storage will be removed (if Force is set).
RemoveContainerFunc RemoveContainerFunc
}
// Remove removes the image along with all dangling parent images that no other
// image depends on. The image must not be set read-only and not be used by
// containers. Callers must make sure to remove containers before image
// removal and may use `(*Image).Containers()` to get a list of containers
// using the image.
//
// If the image is used by containers return storage.ErrImageUsedByContainer.
// Use force to remove these containers.
func (i *Image) Remove(ctx context.Context, options *RemoveImageOptions) error {
logrus.Debugf("Removing image %s", i.ID())
if i.IsReadOnly() {
return errors.Errorf("cannot remove read-only image %q", i.ID())
}
if options == nil {
options = &RemoveImageOptions{}
}
if options.Force {
if err := i.removeContainers(options.RemoveContainerFunc); err != nil {
return err
}
}
// If there's a dangling parent that no other image depends on, remove
// it recursively.
parent, err := i.Parent(ctx)
if err != nil {
return err
}
if _, err := i.runtime.store.DeleteImage(i.ID(), true); err != nil {
return err
}
delete(i.runtime.imageIDmap, i.ID())
if parent == nil || !parent.IsDangling() {
return nil
}
return parent.Remove(ctx, options)
}
// Tag the image with the specified name and store it in the local containers
// storage. The name is normalized according to the rules of NormalizeName.
func (i *Image) Tag(name string) error {
ref, err := NormalizeName(name)
if err != nil {
return errors.Wrapf(err, "error normalizing name %q", name)
}
logrus.Debugf("Tagging image %s with %q", i.ID(), ref.String())
newNames := append(i.Names(), ref.String())
if err := i.runtime.store.SetNames(i.ID(), newNames); err != nil {
return err
}
return i.reload()
}
// Untag the image with the specified name and make the change persistent in
// the local containers storage. The name is normalized according to the rules
// of NormalizeName.
func (i *Image) Untag(name string) error {
ref, err := NormalizeName(name)
if err != nil {
return errors.Wrapf(err, "error normalizing name %q", name)
}
name = ref.String()
removedName := false
newNames := []string{}
for _, n := range i.Names() {
if n == name {
removedName = true
continue
}
newNames = append(newNames, n)
}
if !removedName {
return nil
}
logrus.Debugf("Untagging %q from image %s", ref.String(), i.ID())
if err := i.runtime.store.SetNames(i.ID(), newNames); err != nil {
return err
}
return i.reload()
}
// RepoTags returns a string slice of repotags associated with the image.
func (i *Image) RepoTags() ([]string, error) {
namedTagged, err := i.NamedTaggedRepoTags()
if err != nil {
return nil, err
}
repoTags := make([]string, len(namedTagged))
for i := range namedTagged {
repoTags[i] = namedTagged[i].String()
}
return repoTags, nil
}
// NammedTaggedRepoTags returns the repotags associated with the image as a
// slice of reference.NamedTagged.
func (i *Image) NamedTaggedRepoTags() ([]reference.NamedTagged, error) {
var repoTags []reference.NamedTagged
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)
}
}
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
}
// Mount the image with the specified mount options and label, both of which
// are directly passed down to the containers storage. Returns the fully
// evaluated path to the mount point.
func (i *Image) Mount(ctx context.Context, mountOptions []string, mountLabel string) (string, error) {
mountPoint, err := i.runtime.store.MountImage(i.ID(), mountOptions, mountLabel)
if err != nil {
return "", err
}
mountPoint, err = filepath.EvalSymlinks(mountPoint)
if err != nil {
return "", err
}
logrus.Debugf("Mounted image %s at %q", i.ID(), mountPoint)
return mountPoint, nil
}
// Unmount the image. Use force to ignore the reference counter and forcefully
// unmount.
func (i *Image) Unmount(force bool) error {
logrus.Debugf("Unmounted image %s", i.ID())
_, err := i.runtime.store.UnmountImage(i.ID(), force)
return err
}
// MountPoint returns the fully-evaluated mount point of the image. If the
// image isn't mounted, an empty string is returned.
func (i *Image) MountPoint() (string, error) {
counter, err := i.runtime.store.Mounted(i.TopLayer())
if err != nil {
return "", err
}
if counter == 0 {
return "", nil
}
layer, err := i.runtime.store.Layer(i.TopLayer())
if err != nil {
return "", err
}
return filepath.EvalSymlinks(layer.MountPoint)
}
// Size computes the size of the image layers and associated data.
func (i *Image) Size() (int64, error) {
return i.runtime.store.ImageSize(i.ID())
}
// HasDifferentDigest returns true if the image specified by `remoteRef` has a
// different digest than the local one. This check can be useful to check for
// updates on remote registries.
func (i *Image) HasDifferentDigest(ctx context.Context, remoteRef types.ImageReference) (bool, error) {
// We need to account for the arch that the image uses. It seems
// common on ARM to tweak this option to pull the correct image. See
// github.com/containers/podman/issues/6613.
inspectInfo, err := i.inspectInfo(ctx)
if err != nil {
return false, err
}
sys := i.runtime.systemContext
sys.ArchitectureChoice = inspectInfo.Architecture
// OS and variant may not be set, so let's check to avoid accidental
// overrides of the runtime settings.
if inspectInfo.Os != "" {
sys.OSChoice = inspectInfo.Os
}
if inspectInfo.Variant != "" {
sys.VariantChoice = inspectInfo.Variant
}
remoteImg, err := remoteRef.NewImage(ctx, &sys)
if err != nil {
return false, err
}
rawManifest, _, err := remoteImg.Manifest(ctx)
if err != nil {
return false, err
}
remoteDigest, err := manifest.Digest(rawManifest)
if err != nil {
return false, err
}
return i.Digest().String() != remoteDigest.String(), nil
}
// driverData gets the driver data from the store on a layer
func (i *Image) driverData() (*libimageTypes.DriverData, error) {
store := i.runtime.store
layerID := i.TopLayer()
driver, err := store.GraphDriver()
if err != nil {
return nil, err
}
metaData, err := driver.Metadata(layerID)
if err != nil {
return nil, err
}
if mountTimes, err := store.Mounted(layerID); mountTimes == 0 || err != nil {
delete(metaData, "MergedDir")
}
return &libimageTypes.DriverData{
Name: driver.String(),
Data: metaData,
}, nil
}
// StorageReference returns the image's reference to the containers storage
// using the image ID.
func (i *Image) StorageReference() (types.ImageReference, error) {
if i.storageReference != nil {
return i.storageReference, nil
}
ref, err := storageTransport.Transport.ParseStoreReference(i.runtime.store, "@"+i.ID())
if err != nil {
return nil, err
}
i.storageReference = ref
return ref, nil
}
// isManifestList returns true if the image is a manifest list (Docker) or an
// image index (OCI). This information may be useful to make certain execution
// paths more robust.
// NOTE: please use this function only to optimize specific execution paths.
// In general, errors should only be suppressed when necessary.
func (i *Image) isManifestList(ctx context.Context) (bool, error) {
ref, err := i.StorageReference()
if err != nil {
return false, err
}
imgRef, err := ref.NewImageSource(ctx, &i.runtime.systemContext)
if err != nil {
return false, err
}
_, manifestType, err := imgRef.GetManifest(ctx, nil)
if err != nil {
return false, err
}
return manifest.MIMETypeIsMultiImage(manifestType), nil
}
// source returns the possibly cached image reference.
func (i *Image) source(ctx context.Context) (types.ImageSource, error) {
if i.cached.imageSource != nil {
return i.cached.imageSource, nil
}
ref, err := i.StorageReference()
if err != nil {
return nil, err
}
src, err := ref.NewImageSource(ctx, &i.runtime.systemContext)
if err != nil {
return nil, err
}
i.cached.imageSource = src
return src, 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, sys *types.SystemContext) (string, error) {
newImg, err := src.NewImage(ctx, sys)
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
}