diff options
author | Matthew Heon <matthew.heon@gmail.com> | 2017-11-01 11:24:59 -0400 |
---|---|---|
committer | Matthew Heon <matthew.heon@gmail.com> | 2017-11-01 11:24:59 -0400 |
commit | a031b83a09a8628435317a03f199cdc18b78262f (patch) | |
tree | bc017a96769ce6de33745b8b0b1304ccf38e9df0 /libpod/runtime_img.go | |
parent | 2b74391cd5281f6fdf391ff8ad50fd1490f6bf89 (diff) | |
download | podman-a031b83a09a8628435317a03f199cdc18b78262f.tar.gz podman-a031b83a09a8628435317a03f199cdc18b78262f.tar.bz2 podman-a031b83a09a8628435317a03f199cdc18b78262f.zip |
Initial checkin from CRI-O repo
Signed-off-by: Matthew Heon <matthew.heon@gmail.com>
Diffstat (limited to 'libpod/runtime_img.go')
-rw-r--r-- | libpod/runtime_img.go | 823 |
1 files changed, 823 insertions, 0 deletions
diff --git a/libpod/runtime_img.go b/libpod/runtime_img.go new file mode 100644 index 000000000..9e7ad3106 --- /dev/null +++ b/libpod/runtime_img.go @@ -0,0 +1,823 @@ +package libpod + +import ( + "encoding/json" + "fmt" + "io" + "os" + "strings" + "syscall" + "time" + + cp "github.com/containers/image/copy" + dockerarchive "github.com/containers/image/docker/archive" + "github.com/containers/image/docker/reference" + "github.com/containers/image/docker/tarfile" + ociarchive "github.com/containers/image/oci/archive" + "github.com/containers/image/pkg/sysregistries" + "github.com/containers/image/signature" + is "github.com/containers/image/storage" + "github.com/containers/image/transports" + "github.com/containers/image/transports/alltransports" + "github.com/containers/image/types" + "github.com/containers/storage" + "github.com/containers/storage/pkg/archive" + "github.com/kubernetes-incubator/cri-o/libpod/common" + digest "github.com/opencontainers/go-digest" + ociv1 "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/pkg/errors" +) + +// Runtime API + +const ( + // DefaultRegistry is a prefix that we apply to an image name + // to check docker hub first for the image + DefaultRegistry = "docker://" +) + +var ( + // DockerArchive is the transport we prepend to an image name + // when saving to docker-archive + DockerArchive = dockerarchive.Transport.Name() + // OCIArchive is the transport we prepend to an image name + // when saving to oci-archive + OCIArchive = ociarchive.Transport.Name() + // DirTransport is the transport for pushing and pulling + // images to and from a directory + DirTransport = "dir" +) + +// CopyOptions contains the options given when pushing or pulling images +type CopyOptions struct { + // Compression specifies the type of compression which is applied to + // layer blobs. The default is to not use compression, but + // archive.Gzip is recommended. + Compression archive.Compression + // DockerRegistryOptions encapsulates settings that affect how we + // connect or authenticate to a remote registry to which we want to + // push the image. + common.DockerRegistryOptions + // SigningOptions encapsulates settings that control whether or not we + // strip or add signatures to the image when pushing (uploading) the + // image to a registry. + common.SigningOptions + + // SigningPolicyPath this points to a alternative signature policy file, used mainly for testing + SignaturePolicyPath string + // AuthFile is the path of the cached credentials file defined by the user + AuthFile string + // Writer is the reportWriter for the output + Writer io.Writer +} + +// Image API + +// ImageFilterParams contains the filter options that may be given when outputting images +type ImageFilterParams struct { + Dangling string + Label string + BeforeImage time.Time + SinceImage time.Time + ReferencePattern string + ImageName string + ImageInput string +} + +// struct for when a user passes a short or incomplete +// image name +type imageDecomposeStruct struct { + imageName string + tag string + registry string + hasRegistry bool + transport string +} + +// ImageFilter is a function to determine whether an 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(*storage.Image, *types.ImageInspectInfo) bool + +func (ips imageDecomposeStruct) returnFQName() string { + return fmt.Sprintf("%s%s/%s:%s", ips.transport, ips.registry, ips.imageName, ips.tag) +} + +func getRegistriesToTry(image string, store storage.Store) ([]*pullStruct, error) { + var pStructs []*pullStruct + var imageError = fmt.Sprintf("unable to parse '%s'\n", image) + imgRef, err := reference.Parse(image) + if err != nil { + return nil, errors.Wrapf(err, imageError) + } + tagged, isTagged := imgRef.(reference.NamedTagged) + tag := "latest" + if isTagged { + tag = tagged.Tag() + } + hasDomain := true + registry := reference.Domain(imgRef.(reference.Named)) + if registry == "" { + hasDomain = false + } + imageName := reference.Path(imgRef.(reference.Named)) + pImage := imageDecomposeStruct{ + imageName, + tag, + registry, + hasDomain, + "docker://", + } + if pImage.hasRegistry { + // If input has a registry, we have to assume they included an image + // name but maybe not a tag + srcRef, err := alltransports.ParseImageName(pImage.returnFQName()) + if err != nil { + return nil, errors.Errorf(imageError) + } + pStruct := &pullStruct{ + image: srcRef.DockerReference().String(), + srcRef: srcRef, + } + pStructs = append(pStructs, pStruct) + } else { + // No registry means we check the globals registries configuration file + // and assemble a list of candidate sources to try + registryConfigPath := "" + envOverride := os.Getenv("REGISTRIES_CONFIG_PATH") + if len(envOverride) > 0 { + registryConfigPath = envOverride + } + searchRegistries, err := sysregistries.GetRegistries(&types.SystemContext{SystemRegistriesConfPath: registryConfigPath}) + if err != nil { + fmt.Println(err) + return nil, errors.Errorf("unable to parse the registries.conf file and"+ + " the image name '%s' is incomplete.", imageName) + } + for _, searchRegistry := range searchRegistries { + pImage.registry = searchRegistry + srcRef, err := alltransports.ParseImageName(pImage.returnFQName()) + if err != nil { + return nil, errors.Errorf("unable to parse '%s'", pImage.returnFQName()) + } + pStruct := &pullStruct{ + image: srcRef.DockerReference().String(), + srcRef: srcRef, + } + pStructs = append(pStructs, pStruct) + } + } + + for _, pStruct := range pStructs { + destRef, err := is.Transport.ParseStoreReference(store, pStruct.image) + if err != nil { + return nil, errors.Errorf("error parsing dest reference name: %v", err) + } + pStruct.dstRef = destRef + } + return pStructs, nil +} + +type pullStruct struct { + image string + srcRef types.ImageReference + dstRef types.ImageReference +} + +func (r *Runtime) getPullStruct(srcRef types.ImageReference, destName string) (*pullStruct, error) { + reference := destName + if srcRef.DockerReference() != nil { + reference = srcRef.DockerReference().String() + } + destRef, err := is.Transport.ParseStoreReference(r.store, reference) + if err != nil { + return nil, errors.Errorf("error parsing dest reference name: %v", err) + } + return &pullStruct{ + image: destName, + srcRef: srcRef, + dstRef: destRef, + }, nil +} + +// returns a list of pullStruct with the srcRef and DstRef based on the transport being used +func (r *Runtime) getPullListFromRef(srcRef types.ImageReference, imgName string, sc *types.SystemContext) ([]*pullStruct, error) { + var pullStructs []*pullStruct + splitArr := strings.Split(imgName, ":") + archFile := splitArr[len(splitArr)-1] + + // supports pulling from docker-archive, oci, and registries + if srcRef.Transport().Name() == DockerArchive { + tarSource := tarfile.NewSource(archFile) + manifest, err := tarSource.LoadTarManifest() + if err != nil { + return nil, errors.Errorf("error retrieving manifest.json: %v", err) + } + // to pull the first image stored in the tar file + if len(manifest) == 0 { + // create an image object and use the hex value of the digest as the image ID + // for parsing the store reference + newImg, err := srcRef.NewImage(sc) + if err != nil { + return nil, err + } + defer newImg.Close() + digest := newImg.ConfigInfo().Digest + if err := digest.Validate(); err == nil { + pullInfo, err := r.getPullStruct(srcRef, "@"+digest.Hex()) + if err != nil { + return nil, err + } + pullStructs = append(pullStructs, pullInfo) + } else { + return nil, errors.Wrapf(err, "error getting config info") + } + } else { + pullInfo, err := r.getPullStruct(srcRef, manifest[0].RepoTags[0]) + if err != nil { + return nil, err + } + pullStructs = append(pullStructs, pullInfo) + } + + } else if srcRef.Transport().Name() == OCIArchive { + // retrieve the manifest from index.json to access the image name + manifest, err := ociarchive.LoadManifestDescriptor(srcRef) + if err != nil { + return nil, errors.Wrapf(err, "error loading manifest for %q", srcRef) + } + + if manifest.Annotations == nil || manifest.Annotations["org.opencontainers.image.ref.name"] == "" { + return nil, errors.Errorf("error, archive doesn't have a name annotation. Cannot store image with no name") + } + pullInfo, err := r.getPullStruct(srcRef, manifest.Annotations["org.opencontainers.image.ref.name"]) + if err != nil { + return nil, err + } + pullStructs = append(pullStructs, pullInfo) + } else if srcRef.Transport().Name() == DirTransport { + // supports pull from a directory + image := splitArr[1] + // remove leading "/" + if image[:1] == "/" { + image = image[1:] + } + pullInfo, err := r.getPullStruct(srcRef, image) + if err != nil { + return nil, err + } + pullStructs = append(pullStructs, pullInfo) + } else { + pullInfo, err := r.getPullStruct(srcRef, imgName) + if err != nil { + return nil, err + } + pullStructs = append(pullStructs, pullInfo) + } + return pullStructs, nil +} + +// PullImage pulls an image from configured registries +// By default, only the latest tag (or a specific tag if requested) will be +// pulled. If allTags is true, all tags for the requested image will be pulled. +// Signature validation will be performed if the Runtime has been appropriately +// configured +func (r *Runtime) PullImage(imgName string, options CopyOptions) error { + r.lock.Lock() + defer r.lock.Unlock() + + if !r.valid { + return ErrRuntimeStopped + } + + // PullImage copies the image from the source to the destination + var pullStructs []*pullStruct + + signaturePolicyPath := r.config.SignaturePolicyPath + if options.SignaturePolicyPath != "" { + signaturePolicyPath = options.SignaturePolicyPath + } + + sc := common.GetSystemContext(signaturePolicyPath, options.AuthFile) + + srcRef, err := alltransports.ParseImageName(imgName) + if err != nil { + // could be trying to pull from registry with short name + pullStructs, err = getRegistriesToTry(imgName, r.store) + if err != nil { + return errors.Wrap(err, "error getting default registries to try") + } + } else { + pullStructs, err = r.getPullListFromRef(srcRef, imgName, sc) + if err != nil { + return errors.Wrapf(err, "error getting pullStruct info to pull image %q", imgName) + } + } + + policy, err := signature.DefaultPolicy(sc) + if err != nil { + return err + } + + policyContext, err := signature.NewPolicyContext(policy) + if err != nil { + return err + } + defer policyContext.Destroy() + + copyOptions := common.GetCopyOptions(options.Writer, signaturePolicyPath, &options.DockerRegistryOptions, nil, options.SigningOptions, options.AuthFile) + + for _, imageInfo := range pullStructs { + fmt.Printf("Trying to pull %s...\n", imageInfo.image) + if err = cp.Image(policyContext, imageInfo.dstRef, imageInfo.srcRef, copyOptions); err != nil { + fmt.Println("Failed") + } else { + return nil + } + } + return errors.Wrapf(err, "error pulling image from %q", imgName) +} + +// PushImage pushes the given image to a location described by the given path +func (r *Runtime) PushImage(source string, destination string, options CopyOptions) error { + r.lock.Lock() + defer r.lock.Unlock() + + if !r.valid { + return ErrRuntimeStopped + } + + // PushImage pushes the src image to the destination + //func PushImage(source, destination string, options CopyOptions) error { + if source == "" || destination == "" { + return errors.Wrapf(syscall.EINVAL, "source and destination image names must be specified") + } + + // Get the destination Image Reference + dest, err := alltransports.ParseImageName(destination) + if err != nil { + return errors.Wrapf(err, "error getting destination imageReference for %q", destination) + } + + signaturePolicyPath := r.config.SignaturePolicyPath + if options.SignaturePolicyPath != "" { + signaturePolicyPath = options.SignaturePolicyPath + } + + sc := common.GetSystemContext(signaturePolicyPath, options.AuthFile) + + policy, err := signature.DefaultPolicy(sc) + if err != nil { + return err + } + + policyContext, err := signature.NewPolicyContext(policy) + 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(r.store, source) + if err != nil { + return errors.Wrapf(err, "error getting source imageReference for %q", source) + } + + copyOptions := common.GetCopyOptions(options.Writer, signaturePolicyPath, nil, &options.DockerRegistryOptions, options.SigningOptions, options.AuthFile) + + // Copy the image to the remote destination + err = cp.Image(policyContext, dest, src, copyOptions) + if err != nil { + return errors.Wrapf(err, "Error copying image to the remote destination") + } + return nil +} + +// TagImage adds a tag to the given image +func (r *Runtime) TagImage(image *storage.Image, tag string) error { + r.lock.Lock() + defer r.lock.Unlock() + + if !r.valid { + return ErrRuntimeStopped + } + + tags, err := r.store.Names(image.ID) + if err != nil { + return err + } + for _, key := range tags { + if key == tag { + return nil + } + } + tags = append(tags, tag) + return r.store.SetNames(image.ID, tags) +} + +// UntagImage removes a tag from the given image +func (r *Runtime) UntagImage(image *storage.Image, tag string) (string, error) { + r.lock.Lock() + defer r.lock.Unlock() + + if !r.valid { + return "", ErrRuntimeStopped + } + + tags, err := r.store.Names(image.ID) + if err != nil { + return "", err + } + for i, key := range tags { + if key == tag { + tags[i] = tags[len(tags)-1] + tags = tags[:len(tags)-1] + break + } + } + if err = r.store.SetNames(image.ID, tags); err != nil { + return "", err + } + return tag, nil +} + +// RemoveImage deletes an image from local storage +// Images being used by running containers can only be removed if force=true +func (r *Runtime) RemoveImage(image *storage.Image, force bool) (string, error) { + r.lock.Lock() + defer r.lock.Unlock() + + if !r.valid { + return "", ErrRuntimeStopped + } + + containersWithImage, err := r.getContainersWithImage(image.ID) + if err != nil { + return "", errors.Wrapf(err, "error getting containers for image %q", image.ID) + } + if len(containersWithImage) > 0 && len(image.Names) <= 1 { + if force { + if err := r.removeMultipleContainers(containersWithImage); err != nil { + return "", err + } + } else { + for _, ctr := range containersWithImage { + return "", fmt.Errorf("Could not remove image %q (must force) - container %q is using its reference image", image.ID, ctr.ImageID) + } + } + } + + if len(image.Names) > 1 && !force { + return "", fmt.Errorf("unable to delete %s (must force) - image is referred to in multiple tags", image.ID) + } + // If it is forced, we have to untag the image so that it can be deleted + image.Names = image.Names[:0] + + _, err = r.store.DeleteImage(image.ID, true) + if err != nil { + return "", err + } + return image.ID, 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 (r *Runtime) GetImage(image string) (*storage.Image, error) { + r.lock.Lock() + defer r.lock.Unlock() + + if !r.valid { + return nil, ErrRuntimeStopped + } + return r.getImage(image) +} + +func (r *Runtime) getImage(image string) (*storage.Image, error) { + var img *storage.Image + ref, err := is.Transport.ParseStoreReference(r.store, image) + if err == nil { + img, err = is.Transport.GetStoreImage(r.store, ref) + } + if err != nil { + img2, err2 := r.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 +} + +// GetImageRef searches for and returns a new types.Image matching the given name or ID in the given store. +func (r *Runtime) GetImageRef(image string) (types.Image, error) { + r.lock.Lock() + defer r.lock.Unlock() + + if !r.valid { + return nil, ErrRuntimeStopped + } + return r.getImageRef(image) + +} + +func (r *Runtime) getImageRef(image string) (types.Image, error) { + img, err := r.getImage(image) + if err != nil { + return nil, errors.Wrapf(err, "unable to locate image %q", image) + } + ref, err := is.Transport.ParseStoreReference(r.store, "@"+img.ID) + if err != nil { + return nil, errors.Wrapf(err, "error parsing reference to image %q", img.ID) + } + imgRef, err := ref.NewImage(nil) + if err != nil { + return nil, errors.Wrapf(err, "error reading image %q", img.ID) + } + return imgRef, nil +} + +// GetImages retrieves all images present in storage +// Filters can be provided which will determine which images are included in the +// output. Multiple filters are handled by ANDing their output, so only images +// matching all filters are included +func (r *Runtime) GetImages(params *ImageFilterParams, filters ...ImageFilter) ([]*storage.Image, error) { + r.lock.RLock() + defer r.lock.RUnlock() + + if !r.valid { + return nil, ErrRuntimeStopped + } + + images, err := r.store.Images() + if err != nil { + return nil, err + } + + var imagesFiltered []*storage.Image + + for _, img := range images { + info, err := r.getImageInspectInfo(img) + if err != nil { + return nil, err + } + var names []string + if len(img.Names) > 0 { + names = img.Names + } else { + names = append(names, "<none>") + } + for _, name := range names { + include := true + if params != nil { + params.ImageName = name + } + for _, filter := range filters { + include = include && filter(&img, info) + } + + if include { + newImage := img + newImage.Names = []string{name} + imagesFiltered = append(imagesFiltered, &newImage) + } + } + } + + return imagesFiltered, nil +} + +// GetHistory gets the history of an image and information about its layers +func (r *Runtime) GetHistory(image string) ([]ociv1.History, []types.BlobInfo, string, error) { + r.lock.RLock() + defer r.lock.RUnlock() + + if !r.valid { + return nil, nil, "", ErrRuntimeStopped + } + + img, err := r.getImage(image) + if err != nil { + return nil, nil, "", errors.Wrapf(err, "no such image %q", image) + } + + src, err := r.getImageRef(image) + if err != nil { + return nil, nil, "", errors.Wrapf(err, "error instantiating image %q", image) + } + + oci, err := src.OCIConfig() + if err != nil { + return nil, nil, "", err + } + + return oci.History, src.LayerInfos(), img.ID, nil +} + +// ImportImage imports an OCI format image archive into storage as an image +func (r *Runtime) ImportImage(path string) (*storage.Image, error) { + return nil, ErrNotImplemented +} + +// GetImageInspectInfo returns the inspect information of an image +func (r *Runtime) GetImageInspectInfo(image storage.Image) (*types.ImageInspectInfo, error) { + r.lock.RLock() + defer r.lock.RUnlock() + + if !r.valid { + return nil, ErrRuntimeStopped + } + return r.getImageInspectInfo(image) +} + +func (r *Runtime) getImageInspectInfo(image storage.Image) (*types.ImageInspectInfo, error) { + img, err := r.getImageRef(image.ID) + if err != nil { + return nil, err + } + return img.Inspect() +} + +// ParseImageFilter takes a set of images and a filter string as input, and returns the libpod.ImageFilterParams struct +func (r *Runtime) ParseImageFilter(imageInput, filter string) (*ImageFilterParams, error) { + r.lock.RLock() + defer r.lock.RUnlock() + + if !r.valid { + return nil, ErrRuntimeStopped + } + + if filter == "" && imageInput == "" { + return nil, nil + } + + var params ImageFilterParams + params.ImageInput = imageInput + + if filter == "" && imageInput != "" { + return ¶ms, nil + } + + images, err := r.store.Images() + if err != nil { + return nil, err + } + + filterStrings := strings.Split(filter, ",") + for _, param := range filterStrings { + pair := strings.SplitN(param, "=", 2) + switch strings.TrimSpace(pair[0]) { + case "dangling": + if common.IsValidBool(pair[1]) { + params.Dangling = pair[1] + } else { + return nil, fmt.Errorf("invalid filter: '%s=[%s]'", pair[0], pair[1]) + } + case "label": + params.Label = pair[1] + case "before": + if img, err := findImageInSlice(images, pair[1]); err == nil { + info, err := r.GetImageInspectInfo(img) + if err != nil { + return nil, err + } + params.BeforeImage = info.Created + } else { + return nil, fmt.Errorf("no such id: %s", pair[0]) + } + case "since": + if img, err := findImageInSlice(images, pair[1]); err == nil { + info, err := r.GetImageInspectInfo(img) + if err != nil { + return nil, err + } + params.SinceImage = info.Created + } else { + return nil, fmt.Errorf("no such id: %s``", pair[0]) + } + case "reference": + params.ReferencePattern = pair[1] + default: + return nil, fmt.Errorf("invalid filter: '%s'", pair[0]) + } + } + return ¶ms, nil +} + +// InfoAndDigestAndSize returns the inspection info and size of the image in the given +// store and the digest of its manifest, if it has one, or "" if it doesn't. +func (r *Runtime) InfoAndDigestAndSize(img storage.Image) (*types.ImageInspectInfo, digest.Digest, int64, error) { + r.lock.RLock() + defer r.lock.RUnlock() + + if !r.valid { + return nil, "", -1, ErrRuntimeStopped + } + + imgRef, err := r.getImageRef("@" + img.ID) + if err != nil { + return nil, "", -1, errors.Wrapf(err, "error reading image %q", img.ID) + } + defer imgRef.Close() + return infoAndDigestAndSize(imgRef) +} + +func infoAndDigestAndSize(imgRef types.Image) (*types.ImageInspectInfo, digest.Digest, int64, error) { + imgSize, err := imgRef.Size() + if err != nil { + return nil, "", -1, errors.Wrapf(err, "error reading size of image %q", transports.ImageName(imgRef.Reference())) + } + manifest, _, err := imgRef.Manifest() + if err != nil { + return nil, "", -1, errors.Wrapf(err, "error reading manifest for image %q", transports.ImageName(imgRef.Reference())) + } + manifestDigest := digest.Digest("") + if len(manifest) > 0 { + manifestDigest = digest.Canonical.FromBytes(manifest) + } + info, err := imgRef.Inspect() + if err != nil { + return nil, "", -1, errors.Wrapf(err, "error inspecting image %q", transports.ImageName(imgRef.Reference())) + } + return info, manifestDigest, imgSize, nil +} + +// MatchesID returns true if argID is a full or partial match for id +func MatchesID(id, argID string) bool { + return strings.HasPrefix(argID, id) +} + +// MatchesReference returns true if argName is a full or partial match for name +// Partial matches will register only if they match the most specific part of the name available +// For example, take the image docker.io/library/redis:latest +// redis, library/redis, docker.io/library/redis, redis:latest, etc. will match +// But redis:alpine, ry/redis, library, and io/library/redis will not +func MatchesReference(name, argName string) bool { + if argName == "" { + return false + } + splitName := strings.Split(name, ":") + // If the arg contains a tag, we handle it differently than if it does not + if strings.Contains(argName, ":") { + splitArg := strings.Split(argName, ":") + return strings.HasSuffix(splitName[0], splitArg[0]) && (splitName[1] == splitArg[1]) + } + return strings.HasSuffix(splitName[0], argName) +} + +// ParseImageNames parses the names we've stored with an image into a list of +// tagged references and a list of references which contain digests. +func ParseImageNames(names []string) (tags, digests []string, err error) { + for _, name := range names { + if named, err := reference.ParseNamed(name); err == nil { + if digested, ok := named.(reference.Digested); ok { + canonical, err := reference.WithDigest(named, digested.Digest()) + if err == nil { + digests = append(digests, canonical.String()) + } + } else { + if reference.IsNameOnly(named) { + named = reference.TagNameOnly(named) + } + if tagged, ok := named.(reference.Tagged); ok { + namedTagged, err := reference.WithTag(named, tagged.Tag()) + if err == nil { + tags = append(tags, namedTagged.String()) + } + } + } + } + } + return tags, digests, nil +} + +func annotations(manifest []byte, manifestType string) map[string]string { + 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 +} + +func findImageInSlice(images []storage.Image, ref string) (storage.Image, error) { + for _, image := range images { + if MatchesID(image.ID, ref) { + return image, nil + } + for _, name := range image.Names { + if MatchesReference(name, ref) { + return image, nil + } + } + } + return storage.Image{}, errors.New("could not find image") +} |