Skip to content

Commit 08d3c3c

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 39e9e7a commit 08d3c3c

File tree

11 files changed

+348
-44
lines changed

11 files changed

+348
-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/local_utils"
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 := local_utils.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/local_utils/local_types.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package local_utils
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/local_utils/local_utils.go

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
//go:build amd64 || arm64
2+
3+
package local_utils
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+
// getMachineMounts retrieves the mounts of a machine based on the connection URI and parsed URL.
72+
// It returns a slice of mounts or an error if the machine cannot be found or is not running.
73+
func getMachineMounts(connectionURI string, parsedConnection *url.URL, machineProvider vmconfigs.VMProvider) ([]*vmconfigs.Mount, error) {
74+
mc, _, err := FindMachineByPort(connectionURI, parsedConnection)
75+
if err != nil {
76+
return nil, err
77+
}
78+
return mc.Mounts, 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+
if strings.HasPrefix(pathABS, mountSource) {
108+
// Ensure we're matching directory boundaries, not just prefixes
109+
// e.g., /home/user should not match /home/username
110+
if len(pathABS) > len(mountSource) && pathABS[len(mountSource)] != filepath.Separator {
111+
continue
112+
}
113+
114+
relPath, err := filepath.Rel(mountSource, pathABS)
115+
if err != nil {
116+
logrus.Debugf("Failed to get relative path: %v", err)
117+
continue
118+
}
119+
target := filepath.Join(mount.Target, relPath)
120+
121+
converted_path, err := specgen.ConvertWinMountPath(target)
122+
if err != nil {
123+
logrus.Debugf("Failed to convert Windows mount path: %v", err)
124+
return nil, false
125+
}
126+
logrus.Debugf("Converted client path: %q", converted_path)
127+
return &LocalAPIMap{
128+
ClientPath: pathABS,
129+
RemotePath: converted_path,
130+
}, true
131+
}
132+
}
133+
return nil, false
134+
}
135+
136+
// CheckPathOnRunningMachine is a convenience function that checks if a path is available
137+
// on any currently running machine. It combines machine inspection and path checking.
138+
func CheckPathOnRunningMachine(ctx context.Context, path string) (*LocalAPIMap, bool) {
139+
if err := fileutils.Exists(path); errors.Is(err, fs.ErrNotExist) {
140+
logrus.Debugf("Path %s does not exist locally, skipping machine check", path)
141+
return nil, false
142+
}
143+
144+
if machineMode := bindings.GetMachineMode(ctx); !machineMode {
145+
logrus.Debug("Machine mode is not enabled, skipping machine check")
146+
return nil, false
147+
}
148+
149+
conn, err := bindings.GetClient(ctx)
150+
if err != nil {
151+
logrus.Debugf("Failed to get client connection: %v", err)
152+
return nil, false
153+
}
154+
155+
machineProvider, err := provider.Get()
156+
if err != nil {
157+
return nil, false
158+
}
159+
160+
mounts, err := getMachineMounts(conn.URI.String(), conn.URI, machineProvider)
161+
if err != nil {
162+
logrus.Debugf("Failed to get machine mounts: %v", err)
163+
return nil, false
164+
}
165+
166+
return isPathAvailableOnMachine(mounts, machineProvider.VMType(), path)
167+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
//go:build !amd64 && !arm64
2+
3+
package local_utils
4+
5+
import (
6+
"context"
7+
"net/url"
8+
9+
"github.com/containers/podman/v5/pkg/machine/vmconfigs"
10+
"github.com/sirupsen/logrus"
11+
)
12+
13+
func CheckPathOnRunningMachine(ctx context.Context, path string) (*LocalAPIMap, bool) {
14+
logrus.Debug("CheckPathOnRunningMachine is not supported")
15+
return nil, false
16+
}
17+
18+
func FindMachineByPort(connectionURI string, parsedConnection *url.URL) (*vmconfigs.MachineConfig, vmconfigs.VMProvider, error) {
19+
logrus.Debug("FindMachineByPort is not supported")
20+
return nil, nil, nil
21+
}

pkg/api/handlers/libpod/images.go

Lines changed: 41 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,44 @@ 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+
switch err := fileutils.Exists(cleanPath); {
399+
case err == nil:
400+
case errors.Is(err, fs.ErrNotExist):
401+
utils.Error(w, http.StatusNotFound, fmt.Errorf("file does not exist: %q", cleanPath))
402+
return
403+
default:
404+
utils.Error(w, http.StatusInternalServerError, fmt.Errorf("failed to access file: %w", err))
405+
return
406+
}
407+
408+
imageEngine := abi.ImageEngine{Libpod: runtime}
409+
loadOptions := entities.ImageLoadOptions{Input: cleanPath}
410+
loadReport, err := imageEngine.Load(r.Context(), loadOptions)
411+
if err != nil {
412+
utils.Error(w, http.StatusInternalServerError, fmt.Errorf("unable to load image: %w", err))
413+
return
414+
}
415+
utils.WriteResponse(w, http.StatusOK, loadReport)
416+
}
417+
377418
func ImagesImport(w http.ResponseWriter, r *http.Request) {
378419
runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime)
379420
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)