Skip to content

Commit cfe4d46

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 This optimization significantly speeds up image loading operations when working with remote Podman machines by eliminating redundant file transfers for already-accessible image archives. Fixes: https://issues.redhat.com/browse/RUN-3249 Fixes: #26321 Signed-off-by: Jan Rodák <[email protected]>
1 parent 0a9d5ca commit cfe4d46

File tree

11 files changed

+333
-44
lines changed

11 files changed

+333
-44
lines changed

cmd/podman/compose_machine.go

Lines changed: 5 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -3,57 +3,20 @@
33
package main
44

55
import (
6-
"fmt"
76
"net/url"
8-
"strconv"
97

10-
"github.com/containers/podman/v5/pkg/machine/define"
11-
"github.com/containers/podman/v5/pkg/machine/env"
12-
"github.com/containers/podman/v5/pkg/machine/provider"
13-
"github.com/containers/podman/v5/pkg/machine/vmconfigs"
8+
"github.com/containers/podman/v5/internal/localapi"
149
)
1510

1611
func getMachineConn(connectionURI string, parsedConnection *url.URL) (string, error) {
17-
machineProvider, err := provider.Get()
18-
if err != nil {
19-
return "", fmt.Errorf("getting machine provider: %w", err)
20-
}
21-
dirs, err := env.GetMachineDirs(machineProvider.VMType())
12+
mc, machineProvider, err := localapi.FindMachineByPort(connectionURI, parsedConnection)
2213
if err != nil {
2314
return "", err
2415
}
2516

26-
machineList, err := vmconfigs.LoadMachinesInDir(dirs)
27-
if err != nil {
28-
return "", fmt.Errorf("listing machines: %w", err)
29-
}
30-
31-
// Now we know that the connection points to a machine and we
32-
// can find the machine by looking for the one with the
33-
// matching port.
34-
connectionPort, err := strconv.Atoi(parsedConnection.Port())
17+
podmanSocket, podmanPipe, err := mc.ConnectionInfo(machineProvider.VMType())
3518
if err != nil {
36-
return "", fmt.Errorf("parsing connection port: %w", err)
37-
}
38-
for _, mc := range machineList {
39-
if connectionPort != mc.SSH.Port {
40-
continue
41-
}
42-
43-
state, err := machineProvider.State(mc, false)
44-
if err != nil {
45-
return "", err
46-
}
47-
48-
if state != define.Running {
49-
return "", fmt.Errorf("machine %s is not running but in state %s", mc.Name, state)
50-
}
51-
52-
podmanSocket, podmanPipe, err := mc.ConnectionInfo(machineProvider.VMType())
53-
if err != nil {
54-
return "", err
55-
}
56-
return extractConnectionString(podmanSocket, podmanPipe)
19+
return "", err
5720
}
58-
return "", fmt.Errorf("could not find a matching machine for connection %q", connectionURI)
21+
return extractConnectionString(podmanSocket, podmanPipe)
5922
}

internal/localapi/types.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package localapi
2+
3+
// LocalAPIMap is a map of local paths to their target paths in the VM
4+
type LocalAPIMap struct {
5+
ClientPath string `json:"ClientPath,omitempty"`
6+
RemotePath string `json:"RemotePath,omitempty"`
7+
}

internal/localapi/utils.go

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
//go:build amd64 || arm64
2+
3+
package localapi
4+
5+
import (
6+
"context"
7+
"errors"
8+
"fmt"
9+
"io/fs"
10+
"net/url"
11+
"path/filepath"
12+
"strconv"
13+
"strings"
14+
15+
"github.com/containers/podman/v5/pkg/bindings"
16+
"github.com/containers/podman/v5/pkg/machine/define"
17+
"github.com/containers/podman/v5/pkg/machine/env"
18+
"github.com/containers/podman/v5/pkg/machine/provider"
19+
"github.com/containers/podman/v5/pkg/machine/vmconfigs"
20+
"github.com/containers/podman/v5/pkg/specgen"
21+
"github.com/containers/storage/pkg/fileutils"
22+
"github.com/sirupsen/logrus"
23+
)
24+
25+
// FindMachineByPort finds a running machine that matches the given connection port.
26+
// It returns the machine configuration and provider, or an error if not found.
27+
func FindMachineByPort(connectionURI string, parsedConnection *url.URL) (*vmconfigs.MachineConfig, vmconfigs.VMProvider, error) {
28+
machineProvider, err := provider.Get()
29+
if err != nil {
30+
return nil, nil, fmt.Errorf("getting machine provider: %w", err)
31+
}
32+
33+
dirs, err := env.GetMachineDirs(machineProvider.VMType())
34+
if err != nil {
35+
return nil, nil, err
36+
}
37+
38+
machineList, err := vmconfigs.LoadMachinesInDir(dirs)
39+
if err != nil {
40+
return nil, nil, fmt.Errorf("listing machines: %w", err)
41+
}
42+
43+
// Now we know that the connection points to a machine and we
44+
// can find the machine by looking for the one with the
45+
// matching port.
46+
connectionPort, err := strconv.Atoi(parsedConnection.Port())
47+
if err != nil {
48+
return nil, nil, fmt.Errorf("parsing connection port: %w", err)
49+
}
50+
51+
for _, mc := range machineList {
52+
if connectionPort != mc.SSH.Port {
53+
continue
54+
}
55+
56+
state, err := machineProvider.State(mc, false)
57+
if err != nil {
58+
return nil, nil, err
59+
}
60+
61+
if state != define.Running {
62+
return nil, nil, fmt.Errorf("machine %s is not running but in state %s", mc.Name, state)
63+
}
64+
65+
return mc, machineProvider, nil
66+
}
67+
68+
return nil, nil, fmt.Errorf("could not find a matching machine for connection %q", connectionURI)
69+
}
70+
71+
// getMachineMountsAndVMType retrieves the mounts and VM type of a machine based on the connection URI and parsed URL.
72+
// It returns a slice of mounts, the VM type, or an error if the machine cannot be found or is not running.
73+
func getMachineMountsAndVMType(connectionURI string, parsedConnection *url.URL) ([]*vmconfigs.Mount, define.VMType, error) {
74+
mc, machineProvider, err := FindMachineByPort(connectionURI, parsedConnection)
75+
if err != nil {
76+
return nil, define.UnknownVirt, err
77+
}
78+
return mc.Mounts, machineProvider.VMType(), nil
79+
}
80+
81+
// isPathAvailableOnMachine checks if a local path is available on the machine through mounted directories.
82+
// If the path is available, it returns a LocalAPIMap with the corresponding remote path.
83+
func isPathAvailableOnMachine(mounts []*vmconfigs.Mount, vmType define.VMType, path string) (*LocalAPIMap, bool) {
84+
pathABS, err := filepath.Abs(path)
85+
if err != nil {
86+
logrus.Debugf("Failed to get absolute path for %s: %v", path, err)
87+
return nil, false
88+
}
89+
90+
// WSLVirt is a special case where there is no real concept of doing a mount in WSL,
91+
// WSL by default mounts the drives to /mnt/c, /mnt/d, etc...
92+
if vmType == define.WSLVirt {
93+
converted_path, err := specgen.ConvertWinMountPath(pathABS)
94+
if err != nil {
95+
logrus.Debugf("Failed to convert Windows mount path: %v", err)
96+
return nil, false
97+
}
98+
99+
return &LocalAPIMap{
100+
ClientPath: pathABS,
101+
RemotePath: converted_path,
102+
}, true
103+
}
104+
105+
for _, mount := range mounts {
106+
mountSource := filepath.Clean(mount.Source)
107+
relPath, err := filepath.Rel(mountSource, pathABS)
108+
if err != nil {
109+
logrus.Debugf("Failed to get relative path: %v", err)
110+
continue
111+
}
112+
// If relPath starts with ".." or is absolute, pathABS is not under mountSource
113+
if relPath == "." || (!strings.HasPrefix(relPath, "..") && !filepath.IsAbs(relPath)) {
114+
target := filepath.Join(mount.Target, relPath)
115+
converted_path, err := specgen.ConvertWinMountPath(target)
116+
if err != nil {
117+
logrus.Debugf("Failed to convert Windows mount path: %v", err)
118+
return nil, false
119+
}
120+
logrus.Debugf("Converted client path: %q", converted_path)
121+
return &LocalAPIMap{
122+
ClientPath: pathABS,
123+
RemotePath: converted_path,
124+
}, true
125+
}
126+
}
127+
return nil, false
128+
}
129+
130+
// CheckPathOnRunningMachine is a convenience function that checks if a path is available
131+
// on any currently running machine. It combines machine inspection and path checking.
132+
func CheckPathOnRunningMachine(ctx context.Context, path string) (*LocalAPIMap, bool) {
133+
if err := fileutils.Exists(path); errors.Is(err, fs.ErrNotExist) {
134+
logrus.Debugf("Path %s does not exist locally, skipping machine check", path)
135+
return nil, false
136+
}
137+
138+
if machineMode := bindings.GetMachineMode(ctx); !machineMode {
139+
logrus.Debug("Machine mode is not enabled, skipping machine check")
140+
return nil, false
141+
}
142+
143+
conn, err := bindings.GetClient(ctx)
144+
if err != nil {
145+
logrus.Debugf("Failed to get client connection: %v", err)
146+
return nil, false
147+
}
148+
149+
mounts, vmType, err := getMachineMountsAndVMType(conn.URI.String(), conn.URI)
150+
if err != nil {
151+
logrus.Debugf("Failed to get machine mounts: %v", err)
152+
return nil, false
153+
}
154+
155+
return isPathAvailableOnMachine(mounts, vmType, path)
156+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
//go:build !amd64 && !arm64
2+
3+
package localapi
4+
5+
import (
6+
"context"
7+
8+
"github.com/sirupsen/logrus"
9+
)
10+
11+
func CheckPathOnRunningMachine(ctx context.Context, path string) (*LocalAPIMap, bool) {
12+
logrus.Debug("CheckPathOnRunningMachine is not supported")
13+
return nil, false
14+
}

pkg/api/handlers/libpod/images.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@ import (
88
"errors"
99
"fmt"
1010
"io"
11+
"io/fs"
1112
"net/http"
1213
"os"
14+
"path/filepath"
1315
"strconv"
1416
"strings"
1517

@@ -36,6 +38,7 @@ import (
3638
"github.com/containers/storage"
3739
"github.com/containers/storage/pkg/archive"
3840
"github.com/containers/storage/pkg/chrootarchive"
41+
"github.com/containers/storage/pkg/fileutils"
3942
"github.com/containers/storage/pkg/idtools"
4043
"github.com/docker/docker/pkg/jsonmessage"
4144
"github.com/gorilla/schema"
@@ -374,6 +377,47 @@ func ImagesLoad(w http.ResponseWriter, r *http.Request) {
374377
utils.WriteResponse(w, http.StatusOK, loadReport)
375378
}
376379

380+
func ImagesLocalLoad(w http.ResponseWriter, r *http.Request) {
381+
runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime)
382+
decoder := r.Context().Value(api.DecoderKey).(*schema.Decoder)
383+
query := struct {
384+
Path string `schema:"path"`
385+
}{}
386+
387+
if err := decoder.Decode(&query, r.URL.Query()); err != nil {
388+
utils.Error(w, http.StatusBadRequest, fmt.Errorf("failed to parse parameters for %s: %w", r.URL.String(), err))
389+
return
390+
}
391+
392+
if query.Path == "" {
393+
utils.Error(w, http.StatusBadRequest, fmt.Errorf("path query parameter is required"))
394+
return
395+
}
396+
397+
cleanPath := filepath.Clean(query.Path)
398+
// Check if the path exists on server side.
399+
// Note: fileutils.Exists returns nil if the file exists, not an error.
400+
switch err := fileutils.Exists(cleanPath); {
401+
case err == nil:
402+
// no error -> continue
403+
case errors.Is(err, fs.ErrNotExist):
404+
utils.Error(w, http.StatusNotFound, fmt.Errorf("file does not exist: %q", cleanPath))
405+
return
406+
default:
407+
utils.Error(w, http.StatusInternalServerError, fmt.Errorf("failed to access file: %w", err))
408+
return
409+
}
410+
411+
imageEngine := abi.ImageEngine{Libpod: runtime}
412+
loadOptions := entities.ImageLoadOptions{Input: cleanPath}
413+
loadReport, err := imageEngine.Load(r.Context(), loadOptions)
414+
if err != nil {
415+
utils.Error(w, http.StatusInternalServerError, fmt.Errorf("unable to load image: %w", err))
416+
return
417+
}
418+
utils.WriteResponse(w, http.StatusOK, loadReport)
419+
}
420+
377421
func ImagesImport(w http.ResponseWriter, r *http.Request) {
378422
runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime)
379423
decoder := r.Context().Value(api.DecoderKey).(*schema.Decoder)

pkg/api/server/register_images.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -941,6 +941,30 @@ func (s *APIServer) registerImagesHandlers(r *mux.Router) error {
941941
// 500:
942942
// $ref: '#/responses/internalError'
943943
r.Handle(VersionedPath("/libpod/images/load"), s.APIHandler(libpod.ImagesLoad)).Methods(http.MethodPost)
944+
// swagger:operation POST /libpod/local/images/load libpod LocalImagesLibpod
945+
// ---
946+
// tags:
947+
// - images
948+
// summary: Load image from local path
949+
// description: Load an image (oci-archive or docker-archive) from a file path accessible on the server.
950+
// parameters:
951+
// - in: query
952+
// name: path
953+
// type: string
954+
// required: true
955+
// description: Path to the image archive file on the server filesystem
956+
// produces:
957+
// - application/json
958+
// responses:
959+
// 200:
960+
// $ref: "#/responses/imagesLoadResponseLibpod"
961+
// 400:
962+
// $ref: "#/responses/badParamError"
963+
// 404:
964+
// $ref: "#/responses/imageNotFound"
965+
// 500:
966+
// $ref: '#/responses/internalError'
967+
r.Handle(VersionedPath("/libpod/local/images/load"), s.APIHandler(libpod.ImagesLocalLoad)).Methods(http.MethodPost)
944968
// swagger:operation POST /libpod/images/import libpod ImageImportLibpod
945969
// ---
946970
// tags:

pkg/bindings/connection.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,9 @@ type Connection struct {
3838
type valueKey string
3939

4040
const (
41-
clientKey = valueKey("Client")
42-
versionKey = valueKey("ServiceVersion")
41+
clientKey = valueKey("Client")
42+
versionKey = valueKey("ServiceVersion")
43+
machineModeKey = valueKey("MachineMode")
4344
)
4445

4546
type ConnectError struct {
@@ -66,6 +67,13 @@ func GetClient(ctx context.Context) (*Connection, error) {
6667
return nil, fmt.Errorf("%s not set in context", clientKey)
6768
}
6869

70+
func GetMachineMode(ctx context.Context) bool {
71+
if v, ok := ctx.Value(machineModeKey).(bool); ok {
72+
return v
73+
}
74+
return false
75+
}
76+
6977
// ServiceVersion from context build by NewConnection()
7078
func ServiceVersion(ctx context.Context) *semver.Version {
7179
if v, ok := ctx.Value(versionKey).(*semver.Version); ok {
@@ -142,6 +150,8 @@ func NewConnectionWithIdentity(ctx context.Context, uri string, identity string,
142150
return nil, newConnectError(err)
143151
}
144152
ctx = context.WithValue(ctx, versionKey, serviceVersion)
153+
154+
ctx = context.WithValue(ctx, machineModeKey, machine)
145155
return ctx, nil
146156
}
147157

0 commit comments

Comments
 (0)