Skip to content

Commit faab050

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 de754d3 commit faab050

File tree

11 files changed

+303
-2
lines changed

11 files changed

+303
-2
lines changed

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: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
//go:build remote && (amd64 || arm64)
2+
3+
package local_utils
4+
5+
import (
6+
"context"
7+
"errors"
8+
"io/fs"
9+
"path/filepath"
10+
"strings"
11+
12+
"github.com/containers/podman/v5/pkg/bindings"
13+
machine "github.com/containers/podman/v5/pkg/machine"
14+
"github.com/containers/podman/v5/pkg/machine/provider"
15+
"github.com/containers/podman/v5/pkg/machine/shim"
16+
"github.com/containers/storage/pkg/fileutils"
17+
"github.com/sirupsen/logrus"
18+
)
19+
20+
// isPathAvailableOnMachine checks if a local path is available on the machine through mounted directories.
21+
// If the path is available, it returns a LocalAPIMap with the corresponding remote path.
22+
func isPathAvailableOnMachine(machineInfo *machine.InternalInspectInfo, path string) (*LocalAPIMap, bool) {
23+
if machineInfo == nil || path == "" {
24+
return nil, false
25+
}
26+
27+
pathABS, err := filepath.Abs(path)
28+
if err != nil {
29+
logrus.Debugf("Failed to get absolute path for %s: %v", path, err)
30+
return nil, false
31+
}
32+
33+
for _, mount := range machineInfo.Mounts {
34+
mountSource := filepath.Clean(mount.Source)
35+
if strings.HasPrefix(pathABS, mountSource) {
36+
// Ensure we're matching directory boundaries, not just prefixes
37+
// e.g., /home/user should not match /home/username
38+
if len(pathABS) > len(mountSource) && pathABS[len(mountSource)] != filepath.Separator {
39+
continue
40+
}
41+
42+
relPath, err := filepath.Rel(mountSource, pathABS)
43+
if err != nil {
44+
logrus.Debugf("Failed to get relative path: %v", err)
45+
continue
46+
}
47+
target := filepath.Join(mount.Target, relPath)
48+
return &LocalAPIMap{
49+
ClientPath: pathABS,
50+
RemotePath: target,
51+
}, true
52+
}
53+
}
54+
return nil, false
55+
}
56+
57+
// CheckPathOnRunningMachine is a convenience function that checks if a path is available
58+
// on any currently running machine. It combines machine inspection and path checking.
59+
func CheckPathOnRunningMachine(ctx context.Context, path string) (*LocalAPIMap, bool) {
60+
if err := fileutils.Exists(path); errors.Is(err, fs.ErrNotExist) {
61+
logrus.Debugf("Path %s does not exist locally, skipping machine check", path)
62+
return nil, false
63+
}
64+
65+
if machineMode := bindings.GetMachineMode(ctx); !machineMode {
66+
logrus.Debug("Machine mode is not enabled, skipping machine check")
67+
return nil, false
68+
}
69+
70+
allProviders := provider.GetAll()
71+
machineInfo, err := shim.InspectRunningMachine(allProviders, machine.ListOptions{})
72+
if err != nil {
73+
logrus.Debugf("Error inspecting running machine: %v", err)
74+
return nil, false
75+
}
76+
if machineInfo == nil {
77+
logrus.Debug("No running machine found")
78+
return nil, false
79+
}
80+
return isPathAvailableOnMachine(machineInfo, path)
81+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
//go:build !remote || (!amd64 && !arm64)
2+
3+
package local_utils
4+
5+
import (
6+
"context"
7+
)
8+
9+
func CheckPathOnRunningMachine(ctx context.Context, path string) (*LocalAPIMap, bool) {
10+
return nil, false
11+
}

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

pkg/bindings/images/images.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"strconv"
1111

1212
imageTypes "github.com/containers/image/v5/types"
13+
"github.com/containers/podman/v5/internal/local_utils"
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"
@@ -139,6 +140,25 @@ func Load(ctx context.Context, r io.Reader) (*types.ImageLoadReport, error) {
139140
return &report, response.Process(&report)
140141
}
141142

143+
func LoadLocal(ctx context.Context, m *local_utils.LocalAPIMap) (*types.ImageLoadReport, error) {
144+
var report types.ImageLoadReport
145+
conn, err := bindings.GetClient(ctx)
146+
if err != nil {
147+
return nil, err
148+
}
149+
150+
params := url.Values{}
151+
params.Set("path", m.RemotePath)
152+
153+
response, err := conn.DoRequest(ctx, nil, http.MethodPost, "/local/images/load", params, nil)
154+
if err != nil {
155+
return nil, err
156+
}
157+
defer response.Body.Close()
158+
159+
return &report, response.Process(&report)
160+
}
161+
142162
// Export saves images from local storage as a tarball or image archive. The optional format
143163
// parameter is used to change the format of the output.
144164
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"
@@ -14,6 +15,7 @@ import (
1415
"github.com/containers/common/pkg/config"
1516
"github.com/containers/image/v5/docker/reference"
1617
"github.com/containers/image/v5/types"
18+
"github.com/containers/podman/v5/internal/local_utils"
1719
"github.com/containers/podman/v5/libpod/define"
1820
"github.com/containers/podman/v5/pkg/bindings/images"
1921
"github.com/containers/podman/v5/pkg/domain/entities"
@@ -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 := local_utils.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: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,11 @@ type InspectInfo struct {
116116
Rosetta bool
117117
}
118118

119+
type InternalInspectInfo struct {
120+
InspectInfo
121+
Mounts []*vmconfigs.Mount
122+
}
123+
119124
// ImageConfig describes the bootable image for the VM
120125
type ImageConfig struct {
121126
// IgnitionFile is the path to the filesystem where the

pkg/machine/shim/host.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -840,3 +840,59 @@ func validateDestinationPaths(dest string) error {
840840
}
841841
return nil
842842
}
843+
844+
// InspectRunningMachine finds and returns information about the currently running machine.
845+
// It searches across all VM providers to find a machine in the running state.
846+
// Returns nil if no running machine is found.
847+
func InspectRunningMachine(vmstubbers []vmconfigs.VMProvider, _ machine.ListOptions) (*machine.InternalInspectInfo, error) {
848+
for _, s := range vmstubbers {
849+
dirs, err := env.GetMachineDirs(s.VMType())
850+
if err != nil {
851+
return nil, err
852+
}
853+
mcs, err := vmconfigs.LoadMachinesInDir(dirs)
854+
if err != nil {
855+
return nil, err
856+
}
857+
for _, mc := range mcs {
858+
state, err := s.State(mc, false)
859+
if err != nil {
860+
return nil, err
861+
}
862+
if state != machineDefine.Running {
863+
continue
864+
}
865+
866+
podmanSocket, podmanPipe, err := mc.ConnectionInfo(s.VMType())
867+
if err != nil {
868+
return nil, err
869+
}
870+
871+
rosetta, err := s.GetRosetta(mc)
872+
if err != nil {
873+
return nil, err
874+
}
875+
876+
return &machine.InternalInspectInfo{
877+
InspectInfo: machine.InspectInfo{
878+
ConfigDir: *dirs.ConfigDir,
879+
ConnectionInfo: machine.ConnectionConfig{
880+
PodmanSocket: podmanSocket,
881+
PodmanPipe: podmanPipe,
882+
},
883+
Created: mc.Created,
884+
LastUp: mc.LastUp,
885+
Name: mc.Name,
886+
Resources: mc.Resources,
887+
SSHConfig: mc.SSH,
888+
State: state,
889+
UserModeNetworking: s.UserModeNetworkEnabled(mc),
890+
Rootful: mc.HostUser.Rootful,
891+
Rosetta: rosetta,
892+
},
893+
Mounts: mc.Mounts,
894+
}, nil
895+
}
896+
}
897+
return nil, nil
898+
}

0 commit comments

Comments
 (0)