automation-tests/common/libimage/save.go

230 lines
6.6 KiB
Go

//go:build !remote
package libimage
import (
"context"
"errors"
"fmt"
"strings"
"time"
dirTransport "github.com/containers/image/v5/directory"
dockerArchiveTransport "github.com/containers/image/v5/docker/archive"
"github.com/containers/image/v5/docker/reference"
"github.com/containers/image/v5/manifest"
ociArchiveTransport "github.com/containers/image/v5/oci/archive"
ociTransport "github.com/containers/image/v5/oci/layout"
"github.com/containers/image/v5/types"
ociv1 "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/sirupsen/logrus"
)
// SaveOptions allow for customizing saving images.
type SaveOptions struct {
CopyOptions
// AdditionalTags for the saved image. Incompatible when saving
// multiple images.
AdditionalTags []string
}
// Save saves one or more images indicated by `names` in the specified `format`
// to `path`. Supported formats are oci-archive, docker-archive, oci-dir and
// docker-dir. The latter two adhere to the dir transport in the corresponding
// oci or docker v2s2 format. Please note that only docker-archive supports
// saving more than one images. Other formats will yield an error attempting
// to save more than one.
func (r *Runtime) Save(ctx context.Context, names []string, format, path string, options *SaveOptions) error {
logrus.Debugf("Saving one more images (%s) to %q", names, path)
if options == nil {
options = &SaveOptions{}
}
// First some sanity checks to simplify subsequent code.
switch len(names) {
case 0:
return errors.New("no image specified for saving images")
case 1:
// All formats support saving 1.
default:
if format != "docker-archive" {
return fmt.Errorf("unsupported format %q for saving multiple images (only docker-archive)", format)
}
if len(options.AdditionalTags) > 0 {
return errors.New("cannot save multiple images with multiple tags")
}
}
// Dispatch the save operations.
switch format {
case "oci-archive", "oci-dir", "docker-dir":
if len(names) > 1 {
return fmt.Errorf("%q does not support saving multiple images (%v)", format, names)
}
return r.saveSingleImage(ctx, names[0], format, path, options)
case "docker-archive":
options.ManifestMIMEType = manifest.DockerV2Schema2MediaType
return r.saveDockerArchive(ctx, names, path, options)
}
return fmt.Errorf("unsupported format %q for saving images", format)
}
// saveSingleImage saves the specified image name to the specified path.
// Supported formats are "oci-archive", "oci-dir" and "docker-dir".
func (r *Runtime) saveSingleImage(ctx context.Context, name, format, path string, options *SaveOptions) error {
image, imageName, err := r.LookupImage(name, nil)
if err != nil {
return err
}
if r.eventChannel != nil {
defer r.writeEvent(&Event{ID: image.ID(), Name: path, Time: time.Now(), Type: EventTypeImageSave})
}
// Unless the image was referenced by ID, use the resolved name as a
// tag.
var tag string
if !strings.HasPrefix(image.ID(), imageName) {
tag = imageName
}
srcRef, err := image.StorageReference()
if err != nil {
return err
}
// Prepare the destination reference.
var destRef types.ImageReference
switch format {
case "oci-archive":
destRef, err = ociArchiveTransport.NewReference(path, tag)
case "oci-dir":
destRef, err = ociTransport.NewReference(path, tag)
options.ManifestMIMEType = ociv1.MediaTypeImageManifest
case "docker-dir":
destRef, err = dirTransport.NewReference(path)
options.ManifestMIMEType = manifest.DockerV2Schema2MediaType
default:
return fmt.Errorf("unsupported format %q for saving images", format)
}
if err != nil {
return err
}
c, err := r.newCopier(&options.CopyOptions)
if err != nil {
return err
}
defer c.Close()
_, err = c.Copy(ctx, srcRef, destRef)
return err
}
// saveDockerArchive saves the specified images indicated by names to the path.
// It loads all images from the local containers storage and assembles the meta
// data needed to properly save images. Since multiple names could refer to
// the *same* image, we need to dance a bit and store additional "names".
// Those can then be used as additional tags when copying.
func (r *Runtime) saveDockerArchive(ctx context.Context, names []string, path string, options *SaveOptions) error {
type localImage struct {
image *Image
tags []reference.NamedTagged
}
additionalTags := []reference.NamedTagged{}
for _, tag := range options.AdditionalTags {
named, err := NormalizeName(tag)
if err == nil {
tagged, withTag := named.(reference.NamedTagged)
if !withTag {
return fmt.Errorf("invalid additional tag %q: normalized to untagged %q", tag, named.String())
}
additionalTags = append(additionalTags, tagged)
}
}
orderedIDs := []string{} // to preserve the relative order
localImages := make(map[string]*localImage) // to assemble tags
visitedNames := make(map[string]bool) // filters duplicate names
for _, name := range names {
// Look up local images.
image, imageName, err := r.LookupImage(name, nil)
if err != nil {
return err
}
// Make sure to filter duplicates purely based on the resolved
// name.
if _, exists := visitedNames[imageName]; exists {
continue
}
visitedNames[imageName] = true
// Extract and assemble the data.
local, exists := localImages[image.ID()]
if !exists {
local = &localImage{image: image}
local.tags = additionalTags
orderedIDs = append(orderedIDs, image.ID())
}
// Add the tag if the locally resolved name is properly tagged
// (which it should unless we looked it up by ID).
named, err := reference.ParseNamed(imageName)
if err == nil {
tagged, withTag := named.(reference.NamedTagged)
if withTag {
local.tags = append(local.tags, tagged)
}
}
localImages[image.ID()] = local
if r.eventChannel != nil {
defer r.writeEvent(&Event{ID: image.ID(), Name: path, Time: time.Now(), Type: EventTypeImageSave})
}
}
writer, err := dockerArchiveTransport.NewWriter(r.systemContextCopy(), path)
if err != nil {
return err
}
defer writer.Close()
for _, id := range orderedIDs {
local, exists := localImages[id]
if !exists {
return fmt.Errorf("internal error: saveDockerArchive: ID %s not found in local map", id)
}
copyOpts := options.CopyOptions
copyOpts.dockerArchiveAdditionalTags = local.tags
c, err := r.newCopier(&copyOpts)
if err != nil {
return err
}
defer c.Close()
destRef, err := writer.NewReference(nil)
if err != nil {
return err
}
srcRef, err := local.image.StorageReference()
if err != nil {
return err
}
if _, err := c.Copy(ctx, srcRef, destRef); err != nil {
return err
}
}
return nil
}