mirror of
https://github.com/containers/podman.git
synced 2025-06-30 15:49:03 +08:00

Currently in rootless containers, we end up not using the blob cache. We also don't store the blob cache based on the users specified graph storage. This change will cause the cache directory to be stored with the rest of the containe images. While doing this patch, I found that we had duplicated GetSystemContext in two places in libpod. I cleaned this up. Signed-off-by: Daniel J Walsh <dwalsh@redhat.com>
1280 lines
37 KiB
Go
1280 lines
37 KiB
Go
package image
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
|
|
types2 "github.com/containernetworking/cni/pkg/types"
|
|
cp "github.com/containers/image/copy"
|
|
"github.com/containers/image/directory"
|
|
dockerarchive "github.com/containers/image/docker/archive"
|
|
"github.com/containers/image/docker/reference"
|
|
"github.com/containers/image/manifest"
|
|
ociarchive "github.com/containers/image/oci/archive"
|
|
is "github.com/containers/image/storage"
|
|
"github.com/containers/image/tarball"
|
|
"github.com/containers/image/transports"
|
|
"github.com/containers/image/transports/alltransports"
|
|
"github.com/containers/image/types"
|
|
"github.com/containers/libpod/libpod/driver"
|
|
"github.com/containers/libpod/libpod/events"
|
|
"github.com/containers/libpod/pkg/inspect"
|
|
"github.com/containers/libpod/pkg/registries"
|
|
"github.com/containers/libpod/pkg/util"
|
|
"github.com/containers/storage"
|
|
"github.com/containers/storage/pkg/reexec"
|
|
"github.com/opencontainers/go-digest"
|
|
imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1"
|
|
ociv1 "github.com/opencontainers/image-spec/specs-go/v1"
|
|
"github.com/opentracing/opentracing-go"
|
|
"github.com/pkg/errors"
|
|
"github.com/sirupsen/logrus"
|
|
)
|
|
|
|
// imageConversions is used to cache image "cast" types
|
|
type imageConversions struct {
|
|
imgRef types.Image
|
|
storeRef types.ImageReference
|
|
}
|
|
|
|
// 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.
|
|
imageConversions
|
|
inspect.ImageData
|
|
inspect.ImageResult
|
|
inspectInfo *types.ImageInspectInfo
|
|
InputName string
|
|
Local bool
|
|
//runtime *libpod.Runtime
|
|
image *storage.Image
|
|
imageruntime *Runtime
|
|
repotagsMap map[string][]string
|
|
}
|
|
|
|
// Runtime contains the store
|
|
type Runtime struct {
|
|
store storage.Store
|
|
SignaturePolicyPath string
|
|
EventsLogFilePath string
|
|
}
|
|
|
|
// ErrRepoTagNotFound is the error returned when the image id given doesn't match a rep tag in store
|
|
var ErrRepoTagNotFound = errors.New("unable to match user input to any specific repotag")
|
|
|
|
// 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) {
|
|
if reexec.Init() {
|
|
return nil, errors.Errorf("unable to reexec")
|
|
}
|
|
store, err := setStore(options)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &Runtime{
|
|
store: 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
|
|
}
|
|
|
|
// newFromStorage creates a new image object from a storage.Image
|
|
func (ir *Runtime) newFromStorage(img *storage.Image) *Image {
|
|
image := Image{
|
|
InputName: img.ID,
|
|
Local: true,
|
|
imageruntime: ir,
|
|
image: img,
|
|
}
|
|
return &image
|
|
}
|
|
|
|
// 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) {
|
|
image := Image{
|
|
InputName: name,
|
|
Local: true,
|
|
imageruntime: ir,
|
|
}
|
|
localImage, err := image.getLocalImage()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
image.image = localImage
|
|
return &image, 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, forcePull bool, label *string) (*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
|
|
newImage := Image{
|
|
InputName: name,
|
|
Local: false,
|
|
imageruntime: ir,
|
|
}
|
|
if !forcePull {
|
|
localImage, err := newImage.getLocalImage()
|
|
if err == nil {
|
|
newImage.Local = true
|
|
newImage.image = localImage
|
|
return &newImage, nil
|
|
}
|
|
}
|
|
|
|
// 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.InputName = imageName[0]
|
|
img, err := newImage.getLocalImage()
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "error retrieving local image after pulling %s", name)
|
|
}
|
|
newImage.image = img
|
|
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) {
|
|
var newImages []*Image
|
|
|
|
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))
|
|
}
|
|
|
|
for _, name := range imageNames {
|
|
newImage := Image{
|
|
InputName: name,
|
|
Local: true,
|
|
imageruntime: ir,
|
|
}
|
|
img, err := newImage.getLocalImage()
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "error retrieving local image after pulling %s", name)
|
|
}
|
|
newImage.image = img
|
|
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
|
|
}
|
|
|
|
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.image
|
|
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 a storage.Image or an error. It is used by NewFromLocal.
|
|
func (i *Image) getLocalImage() (*storage.Image, error) {
|
|
imageError := fmt.Sprintf("unable to find '%s' in local storage", i.InputName)
|
|
if i.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(i.InputName)
|
|
if err == nil && dest.DockerReference() != nil {
|
|
i.InputName = dest.DockerReference().String()
|
|
}
|
|
|
|
img, err := i.imageruntime.getImage(stripSha256(i.InputName))
|
|
if err == nil {
|
|
return img.image, 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(i.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 = i.imageruntime.getImage(ref.String())
|
|
if err == nil {
|
|
return img.image, err
|
|
}
|
|
|
|
// grab all the local images
|
|
images, err := i.imageruntime.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 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
|
|
}
|
|
|
|
// Digest returns the image's digest
|
|
func (i *Image) Digest() digest.Digest {
|
|
return i.image.Digest
|
|
}
|
|
|
|
// Manifest returns the image's manifest as a byte array
|
|
// and manifest type as a string. The manifest type is
|
|
// MediaTypeImageManifest from ociv1.
|
|
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
|
|
func (i *Image) Names() []string {
|
|
return i.image.Names
|
|
}
|
|
|
|
// RepoDigests returns a string array of repodigests associated with the image
|
|
func (i *Image) RepoDigests() ([]string, error) {
|
|
var repoDigests []string
|
|
digest := i.Digest()
|
|
|
|
for _, name := range i.Names() {
|
|
named, err := reference.ParseNormalizedNamed(name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
canonical, err := reference.WithDigest(reference.TrimNamed(named), digest)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
repoDigests = append(repoDigests, canonical.String())
|
|
}
|
|
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(force bool) error {
|
|
parent, err := i.GetParent()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if _, err := i.imageruntime.store.DeleteImage(i.ID(), true); err != nil {
|
|
return err
|
|
}
|
|
for parent != nil {
|
|
nextParent, err := parent.GetParent()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
children, err := parent.GetChildren()
|
|
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
|
|
}
|
|
defer i.newImageEvent(events.Remove)
|
|
return nil
|
|
}
|
|
|
|
// Decompose an Image
|
|
func (i *Image) Decompose() error {
|
|
return types2.NotImplementedError
|
|
}
|
|
|
|
// TODO: Rework this method to not require an assembly of the fq name with transport
|
|
/*
|
|
// GetManifest tries to GET an images manifest, returns nil on success and err on failure
|
|
func (i *Image) GetManifest() error {
|
|
pullRef, err := alltransports.ParseImageName(i.assembleFqNameTransport())
|
|
if err != nil {
|
|
return errors.Errorf("unable to parse '%s'", i.Names()[0])
|
|
}
|
|
imageSource, err := pullRef.NewImageSource(nil)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "unable to create new image source")
|
|
}
|
|
_, _, err = imageSource.GetManifest(nil)
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
*/
|
|
|
|
// 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) (*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
|
|
}
|
|
newImage := ir.newFromStorage(img)
|
|
return newImage, nil
|
|
}
|
|
|
|
// GetImages retrieves all images present in storage
|
|
func (ir *Runtime) GetImages() ([]*Image, error) {
|
|
var newImages []*Image
|
|
images, err := ir.store.Images()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, i := range images {
|
|
// 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 newImg.Close()
|
|
digest := newImg.ConfigInfo().Digest
|
|
if err = digest.Validate(); err != nil {
|
|
return "", errors.Wrapf(err, "error getting config info")
|
|
}
|
|
return "@" + digest.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 {
|
|
i.reloadImage()
|
|
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
|
|
}
|
|
i.reloadImage()
|
|
defer i.newImageEvent(events.Tag)
|
|
return nil
|
|
}
|
|
|
|
// UntagImage removes a tag from the given image
|
|
func (i *Image) UntagImage(tag string) error {
|
|
i.reloadImage()
|
|
var newTags []string
|
|
tags := i.Names()
|
|
if !util.StringInSlice(tag, tags) {
|
|
return nil
|
|
}
|
|
for _, t := range tags {
|
|
if tag != t {
|
|
newTags = append(newTags, t)
|
|
}
|
|
}
|
|
if err := i.imageruntime.store.SetNames(i.ID(), newTags); err != nil {
|
|
return err
|
|
}
|
|
i.reloadImage()
|
|
defer 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, 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, 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, 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 policyContext.Destroy()
|
|
|
|
// 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
|
|
_, err = cp.Image(ctx, policyContext, dest, src, copyOptions)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "Error copying image to the remote destination")
|
|
}
|
|
defer i.newImageEvent(events.Push)
|
|
return nil
|
|
}
|
|
|
|
// MatchesID returns a bool based on if the input id
|
|
// matches the image's id
|
|
func (i *Image) MatchesID(id string) bool {
|
|
return strings.HasPrefix(i.ID(), id)
|
|
}
|
|
|
|
// toStorageReference returns a *storageReference from an Image
|
|
func (i *Image) toStorageReference() (types.ImageReference, error) {
|
|
var lookupName string
|
|
if i.storeRef == nil {
|
|
if i.image != nil {
|
|
lookupName = i.ID()
|
|
} else {
|
|
lookupName = i.InputName
|
|
}
|
|
storeRef, err := is.Transport.ParseStoreReference(i.imageruntime.store, lookupName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
i.storeRef = storeRef
|
|
}
|
|
return i.storeRef, nil
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
// 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")
|
|
}
|
|
if i.imgRef == 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())
|
|
}
|
|
imgRef, err := ref.NewImage(ctx, nil)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "error reading image %q", i.ID())
|
|
}
|
|
i.imgRef = imgRef
|
|
}
|
|
return i.imgRef, nil
|
|
}
|
|
|
|
// sizer knows its size.
|
|
type sizer interface {
|
|
Size() (int64, error)
|
|
}
|
|
|
|
//Size returns the size of the image
|
|
func (i *Image) Size(ctx context.Context) (*uint64, error) {
|
|
storeRef, err := is.Transport.ParseStoreReference(i.imageruntime.store, i.ID())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
systemContext := &types.SystemContext{}
|
|
img, err := storeRef.NewImageSource(ctx, systemContext)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if s, ok := img.(sizer); ok {
|
|
if sum, err := s.Size(); err == nil {
|
|
usum := uint64(sum)
|
|
return &usum, nil
|
|
}
|
|
}
|
|
return nil, errors.Errorf("unable to determine size")
|
|
}
|
|
|
|
// DriverData gets the driver data from the store on a layer
|
|
func (i *Image) DriverData() (*inspect.Data, error) {
|
|
topLayer, err := i.Layer()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return driver.GetDriverData(i.imageruntime.store, topLayer.ID)
|
|
}
|
|
|
|
// 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"`
|
|
}
|
|
|
|
// History gets the history of an image and information about its layers
|
|
func (i *Image) History(ctx context.Context) ([]*History, error) {
|
|
img, err := i.toImageRef(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
oci, err := img.OCIConfig(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Get the IDs of the images making up the history layers
|
|
// if the images exist locally in the store
|
|
images, err := i.imageruntime.GetImages()
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "error getting images from store")
|
|
}
|
|
imageIDs := []string{i.ID()}
|
|
if err := i.historyLayerIDs(i.TopLayer(), images, &imageIDs); err != nil {
|
|
return nil, errors.Wrap(err, "error getting image IDs for layers in history")
|
|
}
|
|
|
|
var (
|
|
imageID string
|
|
imgIDCount = 0
|
|
size int64
|
|
sizeCount = 1
|
|
allHistory []*History
|
|
)
|
|
|
|
for i := len(oci.History) - 1; i >= 0; i-- {
|
|
if imgIDCount < len(imageIDs) {
|
|
imageID = imageIDs[imgIDCount]
|
|
imgIDCount++
|
|
} else {
|
|
imageID = "<missing>"
|
|
}
|
|
if !oci.History[i].EmptyLayer {
|
|
size = img.LayerInfos()[len(img.LayerInfos())-sizeCount].Size
|
|
sizeCount++
|
|
}
|
|
allHistory = append(allHistory, &History{
|
|
ID: imageID,
|
|
Created: oci.History[i].Created,
|
|
CreatedBy: oci.History[i].CreatedBy,
|
|
Size: size,
|
|
Comment: oci.History[i].Comment,
|
|
})
|
|
}
|
|
return allHistory, nil
|
|
}
|
|
|
|
// historyLayerIDs goes through the images in store and checks if the top layer of an image
|
|
// is the same as the parent of topLayerID
|
|
func (i *Image) historyLayerIDs(topLayerID string, images []*Image, IDs *[]string) error {
|
|
for _, image := range images {
|
|
// Get the layer info of topLayerID
|
|
layer, err := i.imageruntime.store.Layer(topLayerID)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "error getting layer info %q", topLayerID)
|
|
}
|
|
// Check if the parent of layer is equal to the image's top layer
|
|
// If so add the image ID to the list of IDs and find the parent of
|
|
// the top layer of the image ID added to the list
|
|
// Since we are checking for parent, each top layer can only have one parent
|
|
if layer.Parent == image.TopLayer() {
|
|
*IDs = append(*IDs, image.ID())
|
|
return i.historyLayerIDs(image.TopLayer(), images, IDs)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Dangling returns a bool if the image is "dangling"
|
|
func (i *Image) Dangling() bool {
|
|
return len(i.Names()) == 0
|
|
}
|
|
|
|
// 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, nil
|
|
}
|
|
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) {
|
|
imageLabels, err := i.Labels(ctx)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
for k, v := range imageLabels {
|
|
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) {
|
|
manifest, manifestType, err := i.Manifest(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
annotations := make(map[string]string)
|
|
switch manifestType {
|
|
case ociv1.MediaTypeImageManifest:
|
|
var m ociv1.Manifest
|
|
if err := json.Unmarshal(manifest, &m); err == nil {
|
|
for k, v := range m.Annotations {
|
|
annotations[k] = v
|
|
}
|
|
}
|
|
}
|
|
return annotations, nil
|
|
}
|
|
|
|
// ociv1Image converts and image to an imgref and then 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 {
|
|
sr, err := i.toStorageReference()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ic, err := sr.NewImage(ctx, &types.SystemContext{})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
imgInspect, err := ic.Inspect(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
i.inspectInfo = imgInspect
|
|
}
|
|
return i.inspectInfo, 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()
|
|
|
|
ociv1Img, err := i.ociv1Image(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
info, err := i.imageInspectInfo(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
annotations, err := i.Annotations(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
size, err := i.Size(ctx)
|
|
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.Manifest(ctx)
|
|
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: i.Names(),
|
|
RepoDigests: repoDigests,
|
|
Comment: comment,
|
|
Created: ociv1Img.Created,
|
|
Author: ociv1Img.Author,
|
|
Architecture: ociv1Img.Architecture,
|
|
Os: ociv1Img.OS,
|
|
Config: &ociv1Img.Config,
|
|
Version: info.DockerVersion,
|
|
Size: int64(*size),
|
|
VirtualSize: int64(*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,
|
|
}
|
|
return data, nil
|
|
}
|
|
|
|
// 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 policyContext.Destroy()
|
|
copyOptions := getCopyOptions(sc, writer, nil, nil, signingOptions, "", nil)
|
|
dest, err := is.Transport.ParseStoreReference(ir.store, reference)
|
|
if err != 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 {
|
|
defer 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() (bool, error) {
|
|
children, err := i.GetChildren()
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return len(children) > 0, nil
|
|
}
|
|
|
|
// GetParent returns the image ID of the parent. Return nil if a parent is not found.
|
|
func (i *Image) GetParent() (*Image, error) {
|
|
images, err := i.imageruntime.GetImages()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
layer, err := i.imageruntime.store.Layer(i.TopLayer())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, img := range images {
|
|
if img.TopLayer() == layer.Parent {
|
|
return img, nil
|
|
}
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
// GetChildren returns a list of the imageIDs that depend on the image
|
|
func (i *Image) GetChildren() ([]string, error) {
|
|
var children []string
|
|
images, err := i.imageruntime.GetImages()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
layers, err := i.imageruntime.store.Layers()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, layer := range layers {
|
|
if layer.Parent == i.TopLayer() {
|
|
if imageID := getImageOfTopLayer(images, layer.ID); len(imageID) > 0 {
|
|
children = append(children, imageID...)
|
|
}
|
|
}
|
|
}
|
|
return children, nil
|
|
}
|
|
|
|
// getImageOfTopLayer returns the image ID where layer is the top layer of the image
|
|
func getImageOfTopLayer(images []*Image, layer string) []string {
|
|
var matches []string
|
|
for _, img := range images {
|
|
if img.TopLayer() == layer {
|
|
matches = append(matches, img.ID())
|
|
}
|
|
}
|
|
return matches
|
|
}
|
|
|
|
// 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 {
|
|
return "", err
|
|
}
|
|
return ociv1Img.History[0].Comment, 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", "":
|
|
dst := output
|
|
destImageName := imageNameForSaveDestination(i, source)
|
|
if destImageName != "" {
|
|
dst = fmt.Sprintf("%s:%s", dst, destImageName)
|
|
}
|
|
destRef, err = dockerarchive.ParseReference(dst) // FIXME? Add dockerarchive.NewReference
|
|
if err != nil {
|
|
return errors.Wrapf(err, "error getting Docker archive ImageReference for %q", dst)
|
|
}
|
|
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)
|
|
}
|
|
defer i.newImageEvent(events.Save)
|
|
return 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 := e.Write(ir.EventsLogFilePath); 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 := e.Write(i.imageruntime.EventsLogFilePath); 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 childs 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
|
|
}
|
|
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
|
|
}
|