Skip to content

Commit 038c711

Browse files
committed
Optimize image loading for Podman machines
Add support for loading images directly from machine paths to avoid unnecessary file transfers when the image archive is already accessible on the running machine through mounted directories. Changes include: - New /libpod/local/images/load API endpoint for direct machine loading - Machine detection and path mapping functionality - Fallback in tunnel mode to try optimized loading first - Improved error handling for non-JSON API responses This optimization significantly speeds up image loading operations when working with remote Podman machines by eliminating redundant file transfers for already-accessible image archives. Signed-off-by: Jan Rodák <[email protected]>
1 parent cc84e29 commit 038c711

File tree

8 files changed

+317
-3
lines changed

8 files changed

+317
-3
lines changed

pkg/api/handlers/libpod/images.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"io"
1111
"net/http"
1212
"os"
13+
"path/filepath"
1314
"strconv"
1415
"strings"
1516

@@ -31,6 +32,7 @@ import (
3132
"github.com/containers/podman/v5/pkg/domain/infra/abi"
3233
domainUtils "github.com/containers/podman/v5/pkg/domain/utils"
3334
"github.com/containers/podman/v5/pkg/errorhandling"
35+
"github.com/containers/podman/v5/pkg/machine"
3436
"github.com/containers/podman/v5/pkg/util"
3537
utils2 "github.com/containers/podman/v5/utils"
3638
"github.com/containers/storage"
@@ -374,6 +376,41 @@ func ImagesLoad(w http.ResponseWriter, r *http.Request) {
374376
utils.WriteResponse(w, http.StatusOK, loadReport)
375377
}
376378

379+
func ImagesLocalLoad(w http.ResponseWriter, r *http.Request) {
380+
runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime)
381+
382+
localMap := machine.LocalAPIMap{}
383+
if err := json.NewDecoder(r.Body).Decode(&localMap); err != nil {
384+
utils.Error(w, http.StatusBadRequest, fmt.Errorf("failed to decode request body: %w", err))
385+
return
386+
}
387+
388+
if localMap.RemotePath == "" {
389+
utils.Error(w, http.StatusBadRequest, fmt.Errorf("RemotePath is required"))
390+
return
391+
}
392+
393+
cleanPath := filepath.Clean(localMap.RemotePath)
394+
switch _, err := os.Stat(cleanPath); {
395+
case err == nil:
396+
case os.IsNotExist(err):
397+
utils.Error(w, http.StatusNotFound, fmt.Errorf("file does not exist: %q", cleanPath))
398+
return
399+
default:
400+
utils.Error(w, http.StatusInternalServerError, fmt.Errorf("failed to access file: %w", err))
401+
return
402+
}
403+
404+
imageEngine := abi.ImageEngine{Libpod: runtime}
405+
loadOptions := entities.ImageLoadOptions{Input: cleanPath}
406+
loadReport, err := imageEngine.Load(r.Context(), loadOptions)
407+
if err != nil {
408+
utils.Error(w, http.StatusInternalServerError, fmt.Errorf("unable to load image: %w", err))
409+
return
410+
}
411+
utils.WriteResponse(w, http.StatusOK, loadReport)
412+
}
413+
377414
func ImagesImport(w http.ResponseWriter, r *http.Request) {
378415
runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime)
379416
decoder := r.Context().Value(api.DecoderKey).(*schema.Decoder)

pkg/api/server/register_images.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -955,6 +955,40 @@ func (s *APIServer) registerImagesHandlers(r *mux.Router) error {
955955
// 500:
956956
// $ref: '#/responses/internalError'
957957
r.Handle(VersionedPath("/libpod/images/load"), s.APIHandler(libpod.ImagesLoad)).Methods(http.MethodPost)
958+
// swagger:operation POST /libpod/local/images/load libpod ImageLoadLocalLibpod
959+
// ---
960+
// tags:
961+
// - images
962+
// summary: Load image from path on remote
963+
// description: Load an image (oci-archive or docker-archive) from a file path on the remote server.
964+
// parameters:
965+
// - in: body
966+
// name: localMap
967+
// required: true
968+
// description: JSON object containing the file path on the remote server
969+
// schema:
970+
// type: object
971+
// properties:
972+
// RemotePath:
973+
// type: string
974+
// description: Path to the image archive file on the remote server
975+
// example: "/tmp/image.tar"
976+
// required:
977+
// - RemotePath
978+
// consumes:
979+
// - application/json
980+
// produces:
981+
// - application/json
982+
// responses:
983+
// 200:
984+
// $ref: "#/responses/imagesLoadResponseLibpod"
985+
// 400:
986+
// $ref: "#/responses/badParamError"
987+
// 404:
988+
// $ref: "#/responses/imageNotFound"
989+
// 500:
990+
// $ref: '#/responses/internalError'
991+
r.Handle(VersionedPath("/libpod/local/images/load"), s.APIHandler(libpod.ImagesLocalLoad)).Methods(http.MethodPost)
958992
// swagger:operation POST /libpod/images/import libpod ImageImportLibpod
959993
// ---
960994
// tags:

pkg/bindings/errors.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,13 @@ func (h *APIResponse) ProcessWithError(unmarshalInto interface{}, unmarshalError
4747
if h.IsConflictError() {
4848
return handleError(data, unmarshalErrorInto)
4949
}
50-
51-
// TODO should we add a debug here with the response code?
52-
return handleError(data, &errorhandling.ErrorModel{})
50+
if h.Response.Header.Get("Content-Type") == "application/json" {
51+
return handleError(data, &errorhandling.ErrorModel{})
52+
}
53+
return &errorhandling.ErrorModel{
54+
Message: string(data),
55+
ResponseCode: h.Response.StatusCode,
56+
}
5357
}
5458

5559
func CheckResponseCode(inError error) (int, error) {

pkg/bindings/images/images.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,16 @@ import (
88
"net/http"
99
"net/url"
1010
"strconv"
11+
"strings"
1112

1213
imageTypes "github.com/containers/image/v5/types"
1314
handlersTypes "github.com/containers/podman/v5/pkg/api/handlers/types"
1415
"github.com/containers/podman/v5/pkg/auth"
1516
"github.com/containers/podman/v5/pkg/bindings"
1617
"github.com/containers/podman/v5/pkg/domain/entities/reports"
1718
"github.com/containers/podman/v5/pkg/domain/entities/types"
19+
"github.com/containers/podman/v5/pkg/machine"
20+
jsoniter "github.com/json-iterator/go"
1821
)
1922

2023
// Exists a lightweight way to determine if an image exists in local storage. It returns a
@@ -139,6 +142,27 @@ func Load(ctx context.Context, r io.Reader) (*types.ImageLoadReport, error) {
139142
return &report, response.Process(&report)
140143
}
141144

145+
func LoadLocal(ctx context.Context, m *machine.LocalAPIMap) (*types.ImageLoadReport, error) {
146+
var report types.ImageLoadReport
147+
conn, err := bindings.GetClient(ctx)
148+
if err != nil {
149+
return nil, err
150+
}
151+
152+
localAPImapString, err := jsoniter.MarshalToString(m)
153+
if err != nil {
154+
return nil, err
155+
}
156+
157+
response, err := conn.DoRequest(ctx, strings.NewReader(localAPImapString), http.MethodPost, "/local/images/load", nil, nil)
158+
if err != nil {
159+
return nil, err
160+
}
161+
defer response.Body.Close()
162+
163+
return &report, response.Process(&report)
164+
}
165+
142166
// Export saves images from local storage as a tarball or image archive. The optional format
143167
// parameter is used to change the format of the output.
144168
func Export(ctx context.Context, nameOrIDs []string, w io.Writer, options *ExportOptions) error {

pkg/domain/infra/tunnel/images.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"errors"
66
"fmt"
7+
"net/http"
78
"os"
89
"strconv"
910
"strings"
@@ -20,6 +21,7 @@ import (
2021
"github.com/containers/podman/v5/pkg/domain/entities/reports"
2122
"github.com/containers/podman/v5/pkg/domain/utils"
2223
"github.com/containers/podman/v5/pkg/errorhandling"
24+
"github.com/containers/podman/v5/pkg/machine/shim"
2325
"github.com/containers/storage/pkg/archive"
2426
)
2527

@@ -221,6 +223,22 @@ func (ir *ImageEngine) Inspect(ctx context.Context, namesOrIDs []string, opts en
221223
}
222224

223225
func (ir *ImageEngine) Load(ctx context.Context, opts entities.ImageLoadOptions) (*entities.ImageLoadReport, error) {
226+
if localMap, ok := shim.CheckPathOnRunningMachine(ir.ClientCtx, opts.Input); ok {
227+
report, err := images.LoadLocal(ir.ClientCtx, localMap)
228+
if err == nil {
229+
return report, nil
230+
}
231+
errModel, ok := err.(*errorhandling.ErrorModel)
232+
if !ok {
233+
return nil, err
234+
}
235+
switch errModel.ResponseCode {
236+
case http.StatusNotFound, http.StatusMethodNotAllowed:
237+
default:
238+
return nil, err
239+
}
240+
}
241+
224242
f, err := os.Open(opts.Input)
225243
if err != nil {
226244
return nil, err

pkg/machine/config.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,13 @@ type InspectInfo struct {
114114
UserModeNetworking bool
115115
Rootful bool
116116
Rosetta bool
117+
Mounts []*vmconfigs.Mount
118+
}
119+
120+
// LocalAPIMap is a map of local paths to their target paths in the VM
121+
type LocalAPIMap struct {
122+
ClientPath string `json:"ClientPath,omitempty"`
123+
RemotePath string `json:"RemotePath,omitempty"`
117124
}
118125

119126
// ImageConfig describes the bootable image for the VM

pkg/machine/shim/host.go

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package shim
22

33
import (
44
"bufio"
5+
"context"
56
"errors"
67
"fmt"
78
"io"
@@ -14,6 +15,8 @@ import (
1415
"syscall"
1516
"time"
1617

18+
"github.com/containers/common/pkg/config"
19+
"github.com/containers/podman/v5/pkg/bindings"
1720
"github.com/containers/podman/v5/pkg/machine"
1821
"github.com/containers/podman/v5/pkg/machine/connection"
1922
machineDefine "github.com/containers/podman/v5/pkg/machine/define"
@@ -840,3 +843,159 @@ func validateDestinationPaths(dest string) error {
840843
}
841844
return nil
842845
}
846+
847+
// InspectRunningMachine finds and returns information about the currently running machine.
848+
// It searches across all VM providers to find a machine in the running state.
849+
// Returns nil if no running machine is found.
850+
func InspectRunningMachine(vmstubbers []vmconfigs.VMProvider, _ machine.ListOptions) (*machine.InspectInfo, error) {
851+
for _, s := range vmstubbers {
852+
dirs, err := env.GetMachineDirs(s.VMType())
853+
if err != nil {
854+
return nil, err
855+
}
856+
mcs, err := vmconfigs.LoadMachinesInDir(dirs)
857+
if err != nil {
858+
return nil, err
859+
}
860+
for _, mc := range mcs {
861+
state, err := s.State(mc, false)
862+
if err != nil {
863+
return nil, err
864+
}
865+
if state != machineDefine.Running {
866+
continue
867+
}
868+
869+
podmanSocket, podmanPipe, err := mc.ConnectionInfo(s.VMType())
870+
if err != nil {
871+
return nil, err
872+
}
873+
874+
rosetta, err := s.GetRosetta(mc)
875+
if err != nil {
876+
return nil, err
877+
}
878+
879+
return &machine.InspectInfo{
880+
ConfigDir: *dirs.ConfigDir,
881+
ConnectionInfo: machine.ConnectionConfig{
882+
PodmanSocket: podmanSocket,
883+
PodmanPipe: podmanPipe,
884+
},
885+
Created: mc.Created,
886+
LastUp: mc.LastUp,
887+
Name: mc.Name,
888+
Resources: mc.Resources,
889+
SSHConfig: mc.SSH,
890+
State: state,
891+
UserModeNetworking: s.UserModeNetworkEnabled(mc),
892+
Rootful: mc.HostUser.Rootful,
893+
Rosetta: rosetta,
894+
Mounts: mc.Mounts,
895+
}, nil
896+
}
897+
}
898+
return nil, nil
899+
}
900+
901+
// isConnectionToMachine checks if the current connection context is connected to the specified machine.
902+
// It compares connection URIs and socket paths to determine if they match.
903+
func isConnectionToMachine(ctx context.Context, machineInfo *machine.InspectInfo) bool {
904+
conn, err := bindings.GetClient(ctx)
905+
if err != nil {
906+
logrus.Warnf("Error getting client connection: %v", err)
907+
return false
908+
}
909+
logrus.Debugf("Checking connection URI: %s", conn.URI.String())
910+
cfg, err := config.Default()
911+
if err != nil {
912+
logrus.Warnf("Error getting default config: %v", err)
913+
return false
914+
}
915+
cons, err := cfg.GetAllConnections()
916+
if err != nil {
917+
logrus.Warnf("Error getting connections: %v", err)
918+
return false
919+
}
920+
921+
for _, con := range cons {
922+
if con.URI == conn.URI.String() && con.IsMachine {
923+
return true
924+
}
925+
}
926+
927+
if machineInfo.ConnectionInfo.PodmanSocket != nil {
928+
if filepath.Clean(machineInfo.ConnectionInfo.PodmanSocket.Path) == filepath.Clean(conn.URI.Path) {
929+
return true
930+
}
931+
}
932+
933+
if machineInfo.ConnectionInfo.PodmanPipe != nil {
934+
if filepath.Clean(machineInfo.ConnectionInfo.PodmanPipe.Path) == filepath.Clean(conn.URI.Path) {
935+
return true
936+
}
937+
}
938+
return false
939+
}
940+
941+
// IsPathAvailableOnMachine checks if a local path is available on the machine through mounted directories.
942+
// If the path is available, it returns a LocalAPIMap with the corresponding remote path.
943+
func IsPathAvailableOnMachine(ctx context.Context, machineInfo *machine.InspectInfo, path string) (*machine.LocalAPIMap, bool) {
944+
if machineInfo == nil || path == "" {
945+
return nil, false
946+
}
947+
948+
if !isConnectionToMachine(ctx, machineInfo) {
949+
return nil, false
950+
}
951+
952+
pathABS, err := filepath.Abs(path)
953+
if err != nil {
954+
logrus.Debugf("Failed to get absolute path for %s: %v", path, err)
955+
return nil, false
956+
}
957+
958+
for _, mount := range machineInfo.Mounts {
959+
mountSource := filepath.Clean(mount.Source)
960+
if strings.HasPrefix(pathABS, mountSource) {
961+
// Ensure we're matching directory boundaries, not just prefixes
962+
// e.g., /home/user should not match /home/username
963+
if len(pathABS) > len(mountSource) && pathABS[len(mountSource)] != filepath.Separator {
964+
continue
965+
}
966+
967+
relPath, err := filepath.Rel(mountSource, pathABS)
968+
if err != nil {
969+
logrus.Debugf("Failed to get relative path: %v", err)
970+
continue
971+
}
972+
target := filepath.Join(mount.Target, relPath)
973+
return &machine.LocalAPIMap{
974+
ClientPath: pathABS,
975+
RemotePath: target,
976+
}, true
977+
}
978+
}
979+
return nil, false
980+
}
981+
982+
// CheckPathOnRunningMachine is a convenience function that checks if a path is available
983+
// on any currently running machine. It combines machine inspection and path checking.
984+
func CheckPathOnRunningMachine(ctx context.Context, path string) (*machine.LocalAPIMap, bool) {
985+
if _, err := os.Stat(path); os.IsNotExist(err) {
986+
logrus.Debugf("Path %s does not exist locally, skipping machine check", path)
987+
return nil, false
988+
}
989+
990+
allProviders := provider.GetAll()
991+
machineInfo, err := InspectRunningMachine(allProviders, machine.ListOptions{})
992+
if err != nil {
993+
logrus.Debugf("Error inspecting running machine: %v", err)
994+
return nil, false
995+
}
996+
if machineInfo == nil {
997+
logrus.Debug("No running machine found")
998+
return nil, false
999+
}
1000+
return IsPathAvailableOnMachine(ctx, machineInfo, path)
1001+
}

0 commit comments

Comments
 (0)