//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(©Opts) 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 }