mirror of https://github.com/containers/podman.git
589 lines
18 KiB
Go
589 lines
18 KiB
Go
//go:build !remote
|
||
|
||
package compat
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"fmt"
|
||
"net/http"
|
||
"os"
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/containers/buildah"
|
||
"github.com/containers/common/libimage"
|
||
"github.com/containers/common/pkg/config"
|
||
"github.com/containers/common/pkg/filters"
|
||
"github.com/containers/image/v5/manifest"
|
||
"github.com/containers/podman/v5/libpod"
|
||
"github.com/containers/podman/v5/pkg/api/handlers"
|
||
"github.com/containers/podman/v5/pkg/api/handlers/utils"
|
||
api "github.com/containers/podman/v5/pkg/api/types"
|
||
"github.com/containers/podman/v5/pkg/auth"
|
||
"github.com/containers/podman/v5/pkg/domain/entities"
|
||
"github.com/containers/podman/v5/pkg/domain/infra/abi"
|
||
"github.com/containers/podman/v5/pkg/util"
|
||
"github.com/containers/storage"
|
||
dockerContainer "github.com/docker/docker/api/types/container"
|
||
dockerImage "github.com/docker/docker/api/types/image"
|
||
dockerStorage "github.com/docker/docker/api/types/storage"
|
||
dockerSpec "github.com/moby/docker-image-spec/specs-go/v1"
|
||
"github.com/opencontainers/go-digest"
|
||
imageSpec "github.com/opencontainers/image-spec/specs-go/v1"
|
||
"github.com/sirupsen/logrus"
|
||
)
|
||
|
||
// mergeNameAndTagOrDigest creates an image reference as string from the
|
||
// provided image name and tagOrDigest which can be a tag, a digest or empty.
|
||
func mergeNameAndTagOrDigest(name, tagOrDigest string) string {
|
||
if len(tagOrDigest) == 0 {
|
||
return name
|
||
}
|
||
|
||
separator := ":" // default to tag
|
||
if _, err := digest.Parse(tagOrDigest); err == nil {
|
||
// We have a digest, so let's change the separator.
|
||
separator = "@"
|
||
}
|
||
return fmt.Sprintf("%s%s%s", name, separator, tagOrDigest)
|
||
}
|
||
|
||
func ExportImage(w http.ResponseWriter, r *http.Request) {
|
||
// 200 ok
|
||
// 500 server
|
||
runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime)
|
||
|
||
tmpfile, err := os.CreateTemp("", "api.tar")
|
||
if err != nil {
|
||
utils.Error(w, http.StatusInternalServerError, fmt.Errorf("unable to create tempfile: %w", err))
|
||
return
|
||
}
|
||
defer os.Remove(tmpfile.Name())
|
||
|
||
name := utils.GetName(r)
|
||
possiblyNormalizedName, err := utils.NormalizeToDockerHub(r, name)
|
||
if err != nil {
|
||
utils.Error(w, http.StatusInternalServerError, fmt.Errorf("normalizing image: %w", err))
|
||
return
|
||
}
|
||
|
||
imageEngine := abi.ImageEngine{Libpod: runtime}
|
||
|
||
saveOptions := entities.ImageSaveOptions{
|
||
Format: "docker-archive",
|
||
Output: tmpfile.Name(),
|
||
}
|
||
|
||
if err := imageEngine.Save(r.Context(), possiblyNormalizedName, nil, saveOptions); err != nil {
|
||
if errors.Is(err, storage.ErrImageUnknown) {
|
||
utils.ImageNotFound(w, name, fmt.Errorf("failed to find image %s: %w", name, err))
|
||
return
|
||
}
|
||
utils.Error(w, http.StatusInternalServerError, fmt.Errorf("unable to create tempfile: %w", err))
|
||
return
|
||
}
|
||
|
||
if err := tmpfile.Close(); err != nil {
|
||
utils.Error(w, http.StatusInternalServerError, fmt.Errorf("unable to close tempfile: %w", err))
|
||
return
|
||
}
|
||
|
||
rdr, err := os.Open(tmpfile.Name())
|
||
if err != nil {
|
||
utils.Error(w, http.StatusInternalServerError, fmt.Errorf("failed to read the exported tarfile: %w", err))
|
||
return
|
||
}
|
||
defer rdr.Close()
|
||
utils.WriteResponse(w, http.StatusOK, rdr)
|
||
}
|
||
|
||
func CommitContainer(w http.ResponseWriter, r *http.Request) {
|
||
decoder := utils.GetDecoder(r)
|
||
runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime)
|
||
|
||
query := struct {
|
||
Author string `schema:"author"`
|
||
Changes []string `schema:"changes"`
|
||
Comment string `schema:"comment"`
|
||
Container string `schema:"container"`
|
||
Pause bool `schema:"pause"`
|
||
Squash bool `schema:"squash"`
|
||
Repo string `schema:"repo"`
|
||
Tag string `schema:"tag"`
|
||
// fromSrc string # fromSrc is currently unused
|
||
}{
|
||
Tag: "latest",
|
||
}
|
||
|
||
if err := decoder.Decode(&query, r.URL.Query()); err != nil {
|
||
utils.Error(w, http.StatusBadRequest, fmt.Errorf("failed to parse parameters for %s: %w", r.URL.String(), err))
|
||
return
|
||
}
|
||
rtc, err := runtime.GetConfig()
|
||
if err != nil {
|
||
utils.Error(w, http.StatusInternalServerError, fmt.Errorf("Decode(): %w", err))
|
||
return
|
||
}
|
||
sc := runtime.SystemContext()
|
||
options := libpod.ContainerCommitOptions{
|
||
Pause: true,
|
||
}
|
||
options.CommitOptions = buildah.CommitOptions{
|
||
SignaturePolicyPath: rtc.Engine.SignaturePolicyPath,
|
||
ReportWriter: os.Stderr,
|
||
SystemContext: sc,
|
||
PreferredManifestType: manifest.DockerV2Schema2MediaType,
|
||
}
|
||
|
||
options.Message = query.Comment
|
||
options.Author = query.Author
|
||
options.Pause = query.Pause
|
||
options.Squash = query.Squash
|
||
options.Changes = util.DecodeChanges(query.Changes)
|
||
if r.Body != nil {
|
||
defer r.Body.Close()
|
||
if options.CommitOptions.OverrideConfig, err = abi.DecodeOverrideConfig(r.Body); err != nil {
|
||
utils.Error(w, http.StatusBadRequest, err)
|
||
return
|
||
}
|
||
}
|
||
ctr, err := runtime.LookupContainer(query.Container)
|
||
if err != nil {
|
||
utils.Error(w, http.StatusNotFound, err)
|
||
return
|
||
}
|
||
|
||
var destImage string
|
||
if len(query.Repo) > 1 {
|
||
destImage = fmt.Sprintf("%s:%s", query.Repo, query.Tag)
|
||
possiblyNormalizedName, err := utils.NormalizeToDockerHub(r, destImage)
|
||
if err != nil {
|
||
utils.Error(w, http.StatusInternalServerError, fmt.Errorf("normalizing image: %w", err))
|
||
return
|
||
}
|
||
destImage = possiblyNormalizedName
|
||
}
|
||
|
||
commitImage, err := ctr.Commit(r.Context(), destImage, options)
|
||
if err != nil && !strings.Contains(err.Error(), "is not running") {
|
||
utils.Error(w, http.StatusInternalServerError, err)
|
||
return
|
||
}
|
||
utils.WriteResponse(w, http.StatusCreated, entities.IDResponse{ID: commitImage.ID()})
|
||
}
|
||
|
||
func CreateImageFromSrc(w http.ResponseWriter, r *http.Request) {
|
||
// 200 no error
|
||
// 404 repo does not exist or no read access
|
||
// 500 internal
|
||
decoder := utils.GetDecoder(r)
|
||
runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime)
|
||
|
||
query := struct {
|
||
Changes []string `schema:"changes"`
|
||
FromSrc string `schema:"fromSrc"`
|
||
Message string `schema:"message"`
|
||
Platform string `schema:"platform"`
|
||
Repo string `schema:"repo"`
|
||
Tag string `schema:"tag"`
|
||
}{
|
||
// This is where you can override the golang default value for one of fields
|
||
}
|
||
|
||
if err := decoder.Decode(&query, r.URL.Query()); err != nil {
|
||
utils.Error(w, http.StatusBadRequest, fmt.Errorf("failed to parse parameters for %s: %w", r.URL.String(), err))
|
||
return
|
||
}
|
||
// fromSrc – Source to import. The value may be a URL from which the image can be retrieved or - to read the image from the request body. This parameter may only be used when importing an image.
|
||
source := query.FromSrc
|
||
if source == "-" {
|
||
f, err := os.CreateTemp("", "api_load.tar")
|
||
if err != nil {
|
||
utils.Error(w, http.StatusInternalServerError, fmt.Errorf("failed to create tempfile: %w", err))
|
||
return
|
||
}
|
||
|
||
source = f.Name()
|
||
if err := SaveFromBody(f, r); err != nil {
|
||
utils.Error(w, http.StatusInternalServerError, fmt.Errorf("failed to write temporary file: %w", err))
|
||
return
|
||
}
|
||
}
|
||
|
||
reference := query.Repo
|
||
if query.Repo != "" {
|
||
possiblyNormalizedName, err := utils.NormalizeToDockerHub(r, mergeNameAndTagOrDigest(reference, query.Tag))
|
||
if err != nil {
|
||
utils.Error(w, http.StatusInternalServerError, fmt.Errorf("normalizing image: %w", err))
|
||
return
|
||
}
|
||
reference = possiblyNormalizedName
|
||
}
|
||
|
||
platformSpecs := strings.Split(query.Platform, "/")
|
||
opts := entities.ImageImportOptions{
|
||
Source: source,
|
||
Changes: query.Changes,
|
||
Message: query.Message,
|
||
Reference: reference,
|
||
OS: platformSpecs[0],
|
||
}
|
||
if len(platformSpecs) > 1 {
|
||
opts.Architecture = platformSpecs[1]
|
||
}
|
||
|
||
imageEngine := abi.ImageEngine{Libpod: runtime}
|
||
report, err := imageEngine.Import(r.Context(), opts)
|
||
if err != nil {
|
||
utils.Error(w, http.StatusInternalServerError, fmt.Errorf("unable to import tarball: %w", err))
|
||
return
|
||
}
|
||
// Success
|
||
utils.WriteResponse(w, http.StatusOK, struct {
|
||
Status string `json:"status"`
|
||
Progress string `json:"progress"`
|
||
ProgressDetail map[string]string `json:"progressDetail"`
|
||
Id string `json:"id"`
|
||
}{
|
||
Status: report.Id,
|
||
ProgressDetail: map[string]string{},
|
||
Id: report.Id,
|
||
})
|
||
}
|
||
|
||
func CreateImageFromImage(w http.ResponseWriter, r *http.Request) {
|
||
// 200 no error
|
||
// 404 repo does not exist or no read access
|
||
// 500 internal
|
||
decoder := utils.GetDecoder(r)
|
||
runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime)
|
||
|
||
query := struct {
|
||
FromImage string `schema:"fromImage"`
|
||
Tag string `schema:"tag"`
|
||
Platform string `schema:"platform"`
|
||
Retry uint `schema:"retry"`
|
||
RetryDelay string `schema:"retryDelay"`
|
||
}{
|
||
// This is where you can override the golang default value for one of fields
|
||
}
|
||
if err := decoder.Decode(&query, r.URL.Query()); err != nil {
|
||
utils.Error(w, http.StatusBadRequest, fmt.Errorf("failed to parse parameters for %s: %w", r.URL.String(), err))
|
||
return
|
||
}
|
||
|
||
possiblyNormalizedName, err := utils.NormalizeToDockerHub(r, mergeNameAndTagOrDigest(query.FromImage, query.Tag))
|
||
if err != nil {
|
||
utils.Error(w, http.StatusInternalServerError, fmt.Errorf("normalizing image: %w", err))
|
||
return
|
||
}
|
||
|
||
authConf, authfile, err := auth.GetCredentials(r)
|
||
if err != nil {
|
||
utils.Error(w, http.StatusBadRequest, err)
|
||
return
|
||
}
|
||
defer auth.RemoveAuthfile(authfile)
|
||
|
||
pullOptions := &libimage.PullOptions{}
|
||
pullOptions.AuthFilePath = authfile
|
||
if authConf != nil {
|
||
pullOptions.Username = authConf.Username
|
||
pullOptions.Password = authConf.Password
|
||
pullOptions.IdentityToken = authConf.IdentityToken
|
||
}
|
||
pullOptions.Writer = os.Stderr // allows for debugging on the server
|
||
|
||
if _, found := r.URL.Query()["retry"]; found {
|
||
pullOptions.MaxRetries = &query.Retry
|
||
}
|
||
|
||
if _, found := r.URL.Query()["retryDelay"]; found {
|
||
duration, err := time.ParseDuration(query.RetryDelay)
|
||
if err != nil {
|
||
utils.Error(w, http.StatusBadRequest, err)
|
||
return
|
||
}
|
||
pullOptions.RetryDelay = &duration
|
||
}
|
||
|
||
// Handle the platform.
|
||
platformSpecs := strings.Split(query.Platform, "/")
|
||
pullOptions.OS = platformSpecs[0] // may be empty
|
||
if len(platformSpecs) > 1 {
|
||
pullOptions.Architecture = platformSpecs[1]
|
||
if len(platformSpecs) > 2 {
|
||
pullOptions.Variant = platformSpecs[2]
|
||
}
|
||
}
|
||
|
||
utils.CompatPull(r.Context(), w, runtime, possiblyNormalizedName, config.PullPolicyAlways, pullOptions)
|
||
}
|
||
|
||
func GetImage(w http.ResponseWriter, r *http.Request) {
|
||
// 200 no error
|
||
// 404 no such
|
||
// 500 internal
|
||
name := utils.GetName(r)
|
||
possiblyNormalizedName, err := utils.NormalizeToDockerHub(r, name)
|
||
if err != nil {
|
||
utils.Error(w, http.StatusInternalServerError, fmt.Errorf("normalizing image: %w", err))
|
||
return
|
||
}
|
||
|
||
newImage, err := utils.GetImage(r, possiblyNormalizedName)
|
||
if err != nil {
|
||
// Here we need to fiddle with the error message because docker-py is looking for "No
|
||
// such image" to determine on how to raise the correct exception.
|
||
errMsg := strings.ReplaceAll(err.Error(), "image not known", "No such image")
|
||
utils.Error(w, http.StatusNotFound, fmt.Errorf("failed to find image %s: %s", name, errMsg))
|
||
return
|
||
}
|
||
inspect, err := imageDataToImageInspect(r.Context(), newImage)
|
||
if err != nil {
|
||
utils.Error(w, http.StatusInternalServerError, fmt.Errorf("failed to convert ImageData to ImageInspect '%s': %w", name, err))
|
||
return
|
||
}
|
||
utils.WriteResponse(w, http.StatusOK, inspect)
|
||
}
|
||
|
||
func imageDataToImageInspect(ctx context.Context, l *libimage.Image) (*handlers.ImageInspect, error) {
|
||
options := &libimage.InspectOptions{WithParent: true, WithSize: true}
|
||
info, err := l.Inspect(ctx, options)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// TODO: many fields in Config still need wiring
|
||
config := dockerSpec.DockerOCIImageConfig{
|
||
ImageConfig: imageSpec.ImageConfig{
|
||
User: info.User,
|
||
ExposedPorts: info.Config.ExposedPorts,
|
||
Env: info.Config.Env,
|
||
Cmd: info.Config.Cmd,
|
||
Volumes: info.Config.Volumes,
|
||
WorkingDir: info.Config.WorkingDir,
|
||
Entrypoint: info.Config.Entrypoint,
|
||
Labels: info.Labels,
|
||
StopSignal: info.Config.StopSignal,
|
||
},
|
||
}
|
||
|
||
rootfs := dockerImage.RootFS{}
|
||
if info.RootFS != nil {
|
||
rootfs.Type = info.RootFS.Type
|
||
rootfs.Layers = make([]string, 0, len(info.RootFS.Layers))
|
||
for _, layer := range info.RootFS.Layers {
|
||
rootfs.Layers = append(rootfs.Layers, string(layer))
|
||
}
|
||
}
|
||
|
||
graphDriver := dockerStorage.DriverData{
|
||
Name: info.GraphDriver.Name,
|
||
Data: info.GraphDriver.Data,
|
||
}
|
||
// Add in basic ContainerConfig to satisfy docker-compose
|
||
cc := new(dockerContainer.Config)
|
||
cc.Hostname = info.ID[0:11] // short ID is the hostname
|
||
cc.Volumes = info.Config.Volumes
|
||
|
||
dockerImageInspect := dockerImage.InspectResponse{
|
||
Architecture: info.Architecture,
|
||
Author: info.Author,
|
||
Comment: info.Comment,
|
||
Config: &config,
|
||
ContainerConfig: cc,
|
||
Created: l.Created().Format(time.RFC3339Nano),
|
||
DockerVersion: info.Version,
|
||
GraphDriver: graphDriver,
|
||
ID: "sha256:" + l.ID(),
|
||
Metadata: dockerImage.Metadata{},
|
||
Os: info.Os,
|
||
OsVersion: info.Version,
|
||
Parent: info.Parent,
|
||
RepoDigests: info.RepoDigests,
|
||
RepoTags: info.RepoTags,
|
||
RootFS: rootfs,
|
||
Size: info.Size,
|
||
Variant: "",
|
||
VirtualSize: info.VirtualSize,
|
||
}
|
||
return &handlers.ImageInspect{InspectResponse: dockerImageInspect}, nil
|
||
}
|
||
|
||
func GetImages(w http.ResponseWriter, r *http.Request) {
|
||
decoder := utils.GetDecoder(r)
|
||
runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime)
|
||
query := struct {
|
||
All bool
|
||
Digests bool
|
||
Filter string // Docker 1.24 compatibility
|
||
}{
|
||
// This is where you can override the golang default value for one of fields
|
||
}
|
||
|
||
if err := decoder.Decode(&query, r.URL.Query()); err != nil {
|
||
utils.Error(w, http.StatusBadRequest,
|
||
fmt.Errorf("failed to parse parameters for %s: %w", r.URL.String(), err))
|
||
return
|
||
}
|
||
if _, found := r.URL.Query()["digests"]; found && query.Digests {
|
||
utils.UnSupportedParameter("digests")
|
||
return
|
||
}
|
||
|
||
var filterList []string
|
||
var err error
|
||
if utils.IsLibpodRequest(r) {
|
||
// Podman clients split the filter map as `"{"label":["version","1.0"]}`
|
||
filterList, err = filters.FiltersFromRequest(r)
|
||
if err != nil {
|
||
utils.Error(w, http.StatusInternalServerError, err)
|
||
return
|
||
}
|
||
} else {
|
||
// Docker clients split the filter map as `"{"label":["version=1.0"]}`
|
||
filterList, err = util.FiltersFromRequest(r)
|
||
if err != nil {
|
||
utils.Error(w, http.StatusInternalServerError, err)
|
||
return
|
||
}
|
||
if len(query.Filter) > 0 { // Docker 1.24 compatibility
|
||
filterList = append(filterList, "reference="+query.Filter)
|
||
}
|
||
filterList = append(filterList, "manifest=false")
|
||
}
|
||
|
||
imageEngine := abi.ImageEngine{Libpod: runtime}
|
||
|
||
listOptions := entities.ImageListOptions{All: query.All, Filter: filterList, ExtendedAttributes: utils.IsLibpodRequest(r)}
|
||
summaries, err := imageEngine.List(r.Context(), listOptions)
|
||
if err != nil {
|
||
utils.Error(w, http.StatusInternalServerError, err)
|
||
return
|
||
}
|
||
|
||
if !utils.IsLibpodRequest(r) {
|
||
// docker adds sha256: in front of the ID
|
||
for _, s := range summaries {
|
||
s.ID = "sha256:" + s.ID
|
||
}
|
||
}
|
||
utils.WriteResponse(w, http.StatusOK, summaries)
|
||
}
|
||
|
||
func LoadImages(w http.ResponseWriter, r *http.Request) {
|
||
decoder := utils.GetDecoder(r)
|
||
runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime)
|
||
|
||
query := struct {
|
||
Changes map[string]string `json:"changes"` // Ignored
|
||
Message string `json:"message"` // Ignored
|
||
Quiet bool `json:"quiet"` // Ignored
|
||
}{
|
||
// This is where you can override the golang default value for one of fields
|
||
}
|
||
|
||
if err := decoder.Decode(&query, r.URL.Query()); err != nil {
|
||
utils.Error(w, http.StatusBadRequest, fmt.Errorf("failed to parse parameters for %s: %w", r.URL.String(), err))
|
||
return
|
||
}
|
||
|
||
// First write the body to a temporary file that we can later attempt
|
||
// to load.
|
||
f, err := os.CreateTemp("", "api_load.tar")
|
||
if err != nil {
|
||
utils.Error(w, http.StatusInternalServerError, fmt.Errorf("failed to create tempfile: %w", err))
|
||
return
|
||
}
|
||
defer func() {
|
||
err := os.Remove(f.Name())
|
||
if err != nil {
|
||
logrus.Errorf("Failed to remove temporary file: %v.", err)
|
||
}
|
||
}()
|
||
if err := SaveFromBody(f, r); err != nil {
|
||
utils.Error(w, http.StatusInternalServerError, fmt.Errorf("failed to write temporary file: %w", err))
|
||
return
|
||
}
|
||
|
||
imageEngine := abi.ImageEngine{Libpod: runtime}
|
||
|
||
loadOptions := entities.ImageLoadOptions{Input: f.Name()}
|
||
loadReport, err := imageEngine.Load(r.Context(), loadOptions)
|
||
if err != nil {
|
||
utils.Error(w, http.StatusInternalServerError, fmt.Errorf("failed to load image: %w", err))
|
||
return
|
||
}
|
||
|
||
if len(loadReport.Names) < 1 {
|
||
utils.Error(w, http.StatusInternalServerError, fmt.Errorf("one or more images are required"))
|
||
return
|
||
}
|
||
|
||
utils.WriteResponse(w, http.StatusOK, struct {
|
||
Stream string `json:"stream"`
|
||
}{
|
||
Stream: fmt.Sprintf("Loaded image: %s", strings.Join(loadReport.Names, ",")),
|
||
})
|
||
}
|
||
|
||
func ExportImages(w http.ResponseWriter, r *http.Request) {
|
||
// 200 OK
|
||
// 500 Error
|
||
decoder := utils.GetDecoder(r)
|
||
runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime)
|
||
|
||
query := struct {
|
||
Names []string `schema:"names"`
|
||
}{
|
||
// This is where you can override the golang default value for one of fields
|
||
}
|
||
if err := decoder.Decode(&query, r.URL.Query()); err != nil {
|
||
utils.Error(w, http.StatusBadRequest, fmt.Errorf("failed to parse parameters for %s: %w", r.URL.String(), err))
|
||
return
|
||
}
|
||
if len(query.Names) == 0 {
|
||
utils.Error(w, http.StatusBadRequest, fmt.Errorf("no images to download"))
|
||
return
|
||
}
|
||
|
||
images := make([]string, len(query.Names))
|
||
for i, img := range query.Names {
|
||
possiblyNormalizedName, err := utils.NormalizeToDockerHub(r, img)
|
||
if err != nil {
|
||
utils.Error(w, http.StatusInternalServerError, fmt.Errorf("normalizing image: %w", err))
|
||
return
|
||
}
|
||
images[i] = possiblyNormalizedName
|
||
}
|
||
|
||
tmpfile, err := os.CreateTemp("", "api.tar")
|
||
if err != nil {
|
||
utils.Error(w, http.StatusInternalServerError, fmt.Errorf("unable to create tempfile: %w", err))
|
||
return
|
||
}
|
||
defer os.Remove(tmpfile.Name())
|
||
if err := tmpfile.Close(); err != nil {
|
||
utils.Error(w, http.StatusInternalServerError, fmt.Errorf("unable to close tempfile: %w", err))
|
||
return
|
||
}
|
||
|
||
imageEngine := abi.ImageEngine{Libpod: runtime}
|
||
|
||
saveOptions := entities.ImageSaveOptions{Format: "docker-archive", Output: tmpfile.Name(), MultiImageArchive: true}
|
||
if err := imageEngine.Save(r.Context(), images[0], images[1:], saveOptions); err != nil {
|
||
utils.InternalServerError(w, err)
|
||
return
|
||
}
|
||
|
||
rdr, err := os.Open(tmpfile.Name())
|
||
if err != nil {
|
||
utils.Error(w, http.StatusInternalServerError, fmt.Errorf("failed to read the exported tarfile: %w", err))
|
||
return
|
||
}
|
||
defer rdr.Close()
|
||
utils.WriteResponse(w, http.StatusOK, rdr)
|
||
}
|