package libimage import ( "context" "errors" "fmt" "path/filepath" "sort" "strings" "time" "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/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 *ImageData // Corresponding OCI image. ociv1Image *ociv1.Image // Names() parsed into references. namesReferences []reference.Reference // Calculating the Size() is expensive, so cache it. size *int64 } } // 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 fmt.Errorf("reloading image: %w", err) } i.storageImage = img i.cached.imageSource = nil i.cached.partialInspectData = nil i.cached.completeInspectData = nil i.cached.ociv1Image = nil i.cached.namesReferences = nil i.cached.size = nil return nil } // isCorrupted returns an error if the image may be corrupted. func (i *Image) isCorrupted(name string) error { // If it's a manifest list, we're good for now. if _, err := i.getManifestList(); err == nil { return nil } ref, err := i.StorageReference() if err != nil { return err } if _, err := ref.NewImage(context.Background(), nil); err != nil { if name == "" { name = i.ID()[:12] } return fmt.Errorf("Image %s exists in local storage but may be corrupted (remove the image to resolve the issue): %v", name, err) } 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 } // NamesReferences returns Names() as references. func (i *Image) NamesReferences() ([]reference.Reference, error) { if i.cached.namesReferences != nil { return i.cached.namesReferences, nil } refs := make([]reference.Reference, 0, len(i.Names())) for _, name := range i.Names() { ref, err := reference.Parse(name) if err != nil { return nil, err } refs = append(refs, ref) } i.cached.namesReferences = refs return refs, nil } // 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. Typically it is the digest of one among // possibly many digests that we have stored for the image, so many // applications are better off using the entire list returned by Digests(). 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, that is an untagged image // without children. func (i *Image) IsDangling(ctx context.Context) (bool, error) { return i.isDangling(ctx, nil) } // isDangling returns true if the image is dangling, that is an untagged image // without children. If tree is nil, it will created for this invocation only. func (i *Image) isDangling(ctx context.Context, tree *layerTree) (bool, error) { if len(i.Names()) > 0 { return false, nil } children, err := i.getChildren(ctx, false, tree) if err != nil { return false, err } return len(children) == 0, nil } // IsIntermediate returns true if the image is an intermediate image, that is // an untagged image with children. func (i *Image) IsIntermediate(ctx context.Context) (bool, error) { return i.isIntermediate(ctx, nil) } // isIntermediate returns true if the image is an intermediate image, that is // an untagged image with children. If tree is nil, it will created for this // invocation only. func (i *Image) isIntermediate(ctx context.Context, tree *layerTree) (bool, error) { if len(i.Names()) > 0 { return false, nil } children, err := i.getChildren(ctx, false, tree) if err != nil { return false, err } 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 = fmt.Errorf("fallback error checking whether image is a manifest list: %v: %w", err, 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, nil) 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, nil) 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. If tree is nil, it will be // created for this invocation only. func (i *Image) getChildren(ctx context.Context, all bool, tree *layerTree) ([]*Image, error) { if tree == nil { t, err := i.runtime.layerTree() if err != nil { return nil, err } tree = t } 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(options *RemoveImagesOptions) error { if !options.Force && !options.ExternalContainers { // Nothing to do. return nil } if options.Force && options.RemoveContainerFunc != nil { logrus.Debugf("Removing containers of image %s with custom removal function", i.ID()) if err := options.RemoveContainerFunc(i.ID()); err != nil { return err } } containers, err := i.Containers() if err != nil { return err } if !options.Force && options.ExternalContainers { // All containers must be external ones. for _, cID := range containers { isExternal, err := options.IsExternalContainerFunc(cID) if err != nil { return fmt.Errorf("checking if %s is an external container: %w", cID, err) } if !isExternal { return fmt.Errorf("cannot remove container %s: not an external container", cID) } } } 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.Is(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 // RemoveImagesReport is the assembled data from removing *one* image. type RemoveImageReport struct { // ID of the image. ID string // Image was removed. Removed bool // Size of the removed image. Only set when explicitly requested in // RemoveImagesOptions. Size int64 // The untagged tags. Untagged []string } // 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. Returns IDs of removed/untagged images in order. // // If the image is used by containers return storage.ErrImageUsedByContainer. // Use force to remove these containers. // // NOTE: the rmMap is used to assemble image-removal data across multiple // invocations of this function. The recursive nature requires some // bookkeeping to make sure that all data is aggregated correctly. // // This function is internal. Users of libimage should always use // `(*Runtime).RemoveImages()`. func (i *Image) remove(ctx context.Context, rmMap map[string]*RemoveImageReport, referencedBy string, options *RemoveImagesOptions) ([]string, error) { processedIDs := []string{} return i.removeRecursive(ctx, rmMap, processedIDs, referencedBy, options) } func (i *Image) removeRecursive(ctx context.Context, rmMap map[string]*RemoveImageReport, processedIDs []string, referencedBy string, options *RemoveImagesOptions) ([]string, error) { // If referencedBy is empty, the image is considered to be removed via // `image remove --all` which alters the logic below. // The removal logic below is complex. There is a number of rules // inherited from Podman and Buildah (and Docker). This function // should be the *only* place to extend the removal logic so we keep it // sealed in one place. Make sure to add verbose comments to leave // some breadcrumbs for future readers. logrus.Debugf("Removing image %s", i.ID()) if i.IsReadOnly() { return processedIDs, fmt.Errorf("cannot remove read-only image %q", i.ID()) } if i.runtime.eventChannel != nil { defer i.runtime.writeEvent(&Event{ID: i.ID(), Name: referencedBy, Time: time.Now(), Type: EventTypeImageRemove}) } // Check if already visisted this image. report, exists := rmMap[i.ID()] if exists { // If the image has already been removed, we're done. if report.Removed { return processedIDs, nil } } else { report = &RemoveImageReport{ID: i.ID()} rmMap[i.ID()] = report } // The image may have already been (partially) removed, so we need to // have a closer look at the errors. On top, image removal should be // tolerant toward corrupted images. handleError := func(err error) error { if errors.Is(err, storage.ErrImageUnknown) || errors.Is(err, storage.ErrNotAnImage) || errors.Is(err, storage.ErrLayerUnknown) { // The image or layers of the image may already have been removed // in which case we consider the image to be removed. return nil } return err } // Calculate the size if requested. `podman-image-prune` likes to // report the regained size. if options.WithSize { size, err := i.Size() if handleError(err) != nil { return processedIDs, err } report.Size = size } skipRemove := false numNames := len(i.Names()) // NOTE: the `numNames == 1` check is not only a performance // optimization but also preserves exiting Podman/Docker behaviour. // If image "foo" is used by a container and has only this tag/name, // an `rmi foo` will not untag "foo" but instead attempt to remove the // entire image. If there's a container using "foo", we should get an // error. if referencedBy == "" || numNames == 1 { // DO NOTHING, the image will be removed } else { byID := strings.HasPrefix(i.ID(), referencedBy) byDigest := strings.HasPrefix(referencedBy, "sha256:") if !options.Force { if byID && numNames > 1 { return processedIDs, fmt.Errorf("unable to delete image %q by ID with more than one tag (%s): please force removal", i.ID(), i.Names()) } else if byDigest && numNames > 1 { // FIXME - Docker will remove the digest but containers storage // does not support that yet, so our hands are tied. return processedIDs, fmt.Errorf("unable to delete image %q by digest with more than one tag (%s): please force removal", i.ID(), i.Names()) } } // Only try to untag if we know it's not an ID or digest. if !byID && !byDigest { if err := i.Untag(referencedBy); handleError(err) != nil { return processedIDs, err } report.Untagged = append(report.Untagged, referencedBy) // If there's still tags left, we cannot delete it. skipRemove = len(i.Names()) > 0 } } processedIDs = append(processedIDs, i.ID()) if skipRemove { return processedIDs, nil } // Perform the container removal, if needed. if err := i.removeContainers(options); err != nil { return processedIDs, err } // Podman/Docker compat: we only report an image as removed if it has // no children. Otherwise, the data is effectively still present in the // storage despite the image being removed. hasChildren, err := i.HasChildren(ctx) if err != nil { // We must be tolerant toward corrupted images. // See containers/podman commit fd9dd7065d44. logrus.Warnf("Failed to determine if an image is a parent: %v, ignoring the error", err) hasChildren = false } // If there's a dangling parent that no other image depends on, remove // it recursively. parent, err := i.Parent(ctx) if err != nil { // We must be tolerant toward corrupted images. // See containers/podman commit fd9dd7065d44. logrus.Warnf("Failed to determine parent of image: %v, ignoring the error", err) parent = nil } if _, err := i.runtime.store.DeleteImage(i.ID(), true); handleError(err) != nil { if errors.Is(err, storage.ErrImageUsedByContainer) { err = fmt.Errorf("%w: consider listing external containers and force-removing image", err) } return processedIDs, err } report.Untagged = append(report.Untagged, i.Names()...) if i.runtime.eventChannel != nil { for _, name := range i.Names() { i.runtime.writeEvent(&Event{ID: i.ID(), Name: name, Time: time.Now(), Type: EventTypeImageUntag}) } } if !hasChildren { report.Removed = true } // Do not delete any parents if NoPrune is true if options.NoPrune { return processedIDs, nil } // Check if can remove the parent image. if parent == nil { return processedIDs, nil } // Only remove the parent if it's dangling, that is being untagged and // without children. danglingParent, err := parent.IsDangling(ctx) if err != nil { // See Podman commit fd9dd7065d44: we need to // be tolerant toward corrupted images. logrus.Warnf("Failed to determine if an image is a parent: %v, ignoring the error", err) danglingParent = false } if !danglingParent { return processedIDs, nil } // Recurse into removing the parent. return parent.removeRecursive(ctx, rmMap, processedIDs, "", options) } var errTagDigest = errors.New("tag by digest not supported") // 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 { if strings.HasPrefix(name, "sha256:") { // ambiguous input return fmt.Errorf("%s: %w", name, errTagDigest) } ref, err := NormalizeName(name) if err != nil { return fmt.Errorf("normalizing name %q: %w", name, err) } if _, isDigested := ref.(reference.Digested); isDigested { return fmt.Errorf("%s: %w", name, errTagDigest) } logrus.Debugf("Tagging image %s with %q", i.ID(), ref.String()) if i.runtime.eventChannel != nil { defer i.runtime.writeEvent(&Event{ID: i.ID(), Name: name, Time: time.Now(), Type: EventTypeImageTag}) } newNames := append(i.Names(), ref.String()) if err := i.runtime.store.SetNames(i.ID(), newNames); err != nil { return err } return i.reload() } // to have some symmetry with the errors from containers/storage. var errTagUnknown = errors.New("tag not known") // TODO (@vrothberg) - `docker rmi sha256:` will remove the digest from the // image. However, that's something containers storage does not support. var errUntagDigest = errors.New("untag by digest not supported") // 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 { if strings.HasPrefix(name, "sha256:") { // ambiguous input return fmt.Errorf("%s: %w", name, errUntagDigest) } ref, err := NormalizeName(name) if err != nil { return fmt.Errorf("normalizing name %q: %w", name, err) } // FIXME: this is breaking Podman CI but must be re-enabled once // c/storage supports alterting the digests of an image. Then, // Podman will do the right thing. // // !!! Also make sure to re-enable the tests !!! // // if _, isDigested := ref.(reference.Digested); isDigested { // return fmt.Errorf("%s: %w", name, errUntagDigest) // } name = ref.String() logrus.Debugf("Untagging %q from image %s", ref.String(), i.ID()) if i.runtime.eventChannel != nil { defer i.runtime.writeEvent(&Event{ID: i.ID(), Name: name, Time: time.Now(), Type: EventTypeImageUntag}) } removedName := false newNames := []string{} for _, n := range i.Names() { if n == name { removedName = true continue } newNames = append(newNames, n) } if !removedName { return fmt.Errorf("%s: %w", name, errTagUnknown) } 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 } // NamedTaggedRepoTags returns the repotags associated with the image as a // slice of reference.NamedTagged. func (i *Image) NamedTaggedRepoTags() ([]reference.NamedTagged, error) { repoTags := make([]reference.NamedTagged, 0, len(i.Names())) for _, name := range i.Names() { parsed, err := reference.Parse(name) if err != nil { return nil, err } named, isNamed := parsed.(reference.Named) if !isNamed { continue } tagged, isTagged := named.(reference.NamedTagged) if !isTagged { continue } repoTags = append(repoTags, tagged) } return repoTags, nil } // NamedRepoTags returns the repotags associated with the image as a // slice of reference.Named. func (i *Image) NamedRepoTags() ([]reference.Named, error) { var repoTags []reference.Named for _, name := range i.Names() { parsed, err := reference.Parse(name) if err != nil { return nil, err } if named, isNamed := parsed.(reference.Named); isNamed { repoTags = append(repoTags, named) } } return repoTags, nil } // inRepoTags looks for the specified name/tag pair in the image's repo tags. func (i *Image) inRepoTags(namedTagged reference.NamedTagged) (reference.Named, error) { repoTags, err := i.NamedRepoTags() if err != nil { return nil, err } pairs, err := ToNameTagPairs(repoTags) if err != nil { return nil, err } name := namedTagged.Name() tag := namedTagged.Tag() for _, pair := range pairs { if tag != pair.Tag { continue } if !strings.HasSuffix(pair.Name, name) { continue } if len(pair.Name) == len(name) { // full match return pair.named, nil } if pair.Name[len(pair.Name)-len(name)-1] == '/' { // matches at repo return pair.named, nil } } return nil, nil } // RepoDigests returns a string array of repodigests associated with the image. func (i *Image) RepoDigests() ([]string, error) { 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) { if i.runtime.eventChannel != nil { defer i.runtime.writeEvent(&Event{ID: i.ID(), Name: "", Time: time.Now(), Type: EventTypeImageMount}) } 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 } // Mountpoint returns the path to image's mount point. The path is empty if // the image is not mounted. func (i *Image) Mountpoint() (string, error) { mountedTimes, err := i.runtime.store.Mounted(i.TopLayer()) if err != nil || mountedTimes == 0 { if errors.Is(err, storage.ErrLayerUnknown) { // Can happen, Podman did it, but there's no // explanation why. err = nil } return "", err } layer, err := i.runtime.store.Layer(i.TopLayer()) if err != nil { return "", err } mountPoint, err := filepath.EvalSymlinks(layer.MountPoint) if err != nil { return "", err } return mountPoint, nil } // Unmount the image. Use force to ignore the reference counter and forcefully // unmount. func (i *Image) Unmount(force bool) error { if i.runtime.eventChannel != nil { defer i.runtime.writeEvent(&Event{ID: i.ID(), Name: "", Time: time.Now(), Type: EventTypeImageUnmount}) } logrus.Debugf("Unmounted image %s", i.ID()) _, err := i.runtime.store.UnmountImage(i.ID(), force) return err } // Size computes the size of the image layers and associated data. func (i *Image) Size() (int64, error) { if i.cached.size != nil { return *i.cached.size, nil } size, err := i.runtime.store.ImageSize(i.ID()) i.cached.size = &size return size, err } // HasDifferentDigestOptions allows for customizing the check if another // (remote) image has a different digest. type HasDifferentDigestOptions struct { // containers-auth.json(5) file to use when authenticating against // container registries. AuthFilePath string } // 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, options *HasDifferentDigestOptions) (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.systemContextCopy() 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 } if options != nil && options.AuthFilePath != "" { sys.AuthFilePath = options.AuthFilePath } return i.hasDifferentDigestWithSystemContext(ctx, remoteRef, sys) } func (i *Image) hasDifferentDigestWithSystemContext(ctx context.Context, remoteRef types.ImageReference, sys *types.SystemContext) (bool, error) { remoteImg, err := remoteRef.NewImage(ctx, sys) if err != nil { return false, err } rawManifest, rawManifestMIMEType, err := remoteImg.Manifest(ctx) if err != nil { return false, err } // If the remote ref's manifest is a list, try to zero in on the image // in the list that we would eventually try to pull. var remoteDigest digest.Digest if manifest.MIMETypeIsMultiImage(rawManifestMIMEType) { list, err := manifest.ListFromBlob(rawManifest, rawManifestMIMEType) if err != nil { return false, err } remoteDigest, err = list.ChooseInstance(sys) if err != nil { return false, err } } else { remoteDigest, err = manifest.Digest(rawManifest) if err != nil { return false, err } } // Check if we already have that image's manifest in this image. A // single image can have multiple manifests that describe the same // config blob and layers, so treat any match as a successful match. for _, digest := range append(i.Digests(), i.Digest()) { if digest.Validate() != nil { continue } if digest.String() == remoteDigest.String() { return false, nil } } // No matching digest found in the local image. return true, nil } // driverData gets the driver data from the store on a layer func (i *Image) driverData() (*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 &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 } // 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.systemContextCopy()) if err != nil { return nil, err } i.cached.imageSource = src return src, nil } // rawConfigBlob returns the image's config as a raw byte slice. Users need to // unmarshal it to the corresponding type (OCI, Docker v2s{1,2}) func (i *Image) rawConfigBlob(ctx context.Context) ([]byte, error) { ref, err := i.StorageReference() if err != nil { return nil, err } imageCloser, err := ref.NewImage(ctx, i.runtime.systemContextCopy()) if err != nil { return nil, err } defer imageCloser.Close() return imageCloser.ConfigBlob(ctx) } // Manifest returns the raw data and the MIME type of the image's manifest. func (i *Image) Manifest(ctx context.Context) (rawManifest []byte, mimeType string, err error) { src, err := i.source(ctx) if err != nil { return nil, "", err } return src.GetManifest(ctx, nil) } // getImageID creates an image object and uses the hex value of the config // blob's digest (if it has one) as the image ID for parsing the store reference func getImageID(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 "", fmt.Errorf("getting config info: %w", err) } return "@" + imageDigest.Encoded(), nil }