Skip to content

Commit 46d2fc6

Browse files
committed
Add local build API for direct filesystem builds on MacOS and Windows
Adds /libpod/local/build endpoint, client bindings, and path translation utilities to enable container builds from mounted directories to podman machine without tar uploads. This optimization significantly speeds up build operations when working with remote Podman machines by eliminating redundant file transfers for already-accessible files. Fixes: https://issues.redhat.com/browse/RUN-3249 Signed-off-by: Jan Rodák <[email protected]>
1 parent e03a5d2 commit 46d2fc6

File tree

9 files changed

+705
-11
lines changed

9 files changed

+705
-11
lines changed

internal/localapi/types.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,5 @@ type LocalAPIMap struct {
55
ClientPath string `json:"ClientPath,omitempty"`
66
RemotePath string `json:"RemotePath,omitempty"`
77
}
8+
9+
type TranslationLocalAPIMap map[string]string

internal/localapi/utils.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,3 +154,40 @@ func CheckPathOnRunningMachine(ctx context.Context, path string) (*LocalAPIMap,
154154

155155
return isPathAvailableOnMachine(mounts, vmType, path)
156156
}
157+
158+
// CheckMultiplePathsOnRunningMachine checks if multiple paths are available on a running machine.
159+
// Returns a translation map of local-to-remote paths if all paths are accessible, false otherwise.
160+
func CheckMultiplePathsOnRunningMachine(ctx context.Context, paths []string) (TranslationLocalAPIMap, bool) {
161+
result := TranslationLocalAPIMap{}
162+
163+
if machineMode := bindings.GetMachineMode(ctx); !machineMode {
164+
logrus.Debug("Machine mode is not enabled, skipping machine check")
165+
return nil, false
166+
}
167+
168+
conn, err := bindings.GetClient(ctx)
169+
if err != nil {
170+
logrus.Debugf("Failed to get client connection: %v", err)
171+
return nil, false
172+
}
173+
174+
mounts, vmType, err := getMachineMountsAndVMType(conn.URI.String(), conn.URI)
175+
if err != nil {
176+
logrus.Debugf("Failed to get machine mounts: %v", err)
177+
return nil, false
178+
}
179+
180+
for _, path := range paths {
181+
if err := fileutils.Exists(path); errors.Is(err, fs.ErrNotExist) {
182+
logrus.Debugf("Path %s does not exist locally, skipping machine check", path)
183+
return nil, false
184+
}
185+
mapping, found := isPathAvailableOnMachine(mounts, vmType, path)
186+
if !found {
187+
logrus.Debugf("Path %s is not available on the running machine", path)
188+
return nil, false
189+
}
190+
result[path] = mapping.RemotePath
191+
}
192+
return result, true
193+
}

internal/localapi/utils_unsupported.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,8 @@ func CheckPathOnRunningMachine(ctx context.Context, path string) (*LocalAPIMap,
1212
logrus.Debug("CheckPathOnRunningMachine is not supported")
1313
return nil, false
1414
}
15+
16+
func CheckMultiplePathsOnRunningMachine(ctx context.Context, paths []string) (TranslationLocalAPIMap, bool) {
17+
logrus.Debug("CheckMultiplePathsOnRunningMachine is not supported")
18+
return nil, false
19+
}

pkg/api/handlers/compat/images_build.go

Lines changed: 140 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,40 @@ type BuildContext struct {
135135
IgnoreFile string
136136
}
137137

138+
func (b *BuildContext) validatePaths() error {
139+
if err := validateLocalPath(b.ContextDirectory); err != nil {
140+
return err
141+
}
142+
143+
for _, containerfile := range b.ContainerFiles {
144+
if err := validateLocalPath(containerfile); err != nil {
145+
return err
146+
}
147+
}
148+
149+
for _, ctx := range b.AdditionalBuildContexts {
150+
if ctx.IsURL || ctx.IsImage {
151+
continue
152+
}
153+
if err := validateLocalPath(ctx.Value); err != nil {
154+
return err
155+
}
156+
}
157+
158+
return nil
159+
}
160+
161+
func validateLocalPath(path string) error {
162+
if !filepath.IsAbs(path) {
163+
return fmt.Errorf("path %q is not absolute", path)
164+
}
165+
166+
if err := fileutils.Exists(path); err != nil {
167+
return err
168+
}
169+
return nil
170+
}
171+
138172
// genSpaceErr wraps filesystem errors to provide more context for disk space issues.
139173
func genSpaceErr(err error) error {
140174
if errors.Is(err, syscall.ENOSPC) {
@@ -257,10 +291,17 @@ func processBuildContext(query url.Values, r *http.Request, buildContext *BuildC
257291

258292
for _, containerfile := range m {
259293
// Add path to containerfile iff it is not URL
260-
if !strings.HasPrefix(containerfile, "http://") && !strings.HasPrefix(containerfile, "https://") {
294+
if strings.HasPrefix(containerfile, "http://") || strings.HasPrefix(containerfile, "https://") {
295+
continue
296+
}
297+
298+
if filepath.IsAbs(containerfile) {
299+
containerfile = filepath.Clean(filepath.FromSlash(containerfile))
300+
} else {
261301
containerfile = filepath.Join(buildContext.ContextDirectory,
262302
filepath.Clean(filepath.FromSlash(containerfile)))
263303
}
304+
264305
buildContext.ContainerFiles = append(buildContext.ContainerFiles, containerfile)
265306
}
266307
dockerFileSet = true
@@ -819,7 +860,104 @@ func executeBuild(runtime *libpod.Runtime, w http.ResponseWriter, r *http.Reques
819860
}
820861
}
821862

863+
// handleLocalBuildContexts processes build contexts for local builds using filesystem paths.
864+
// It extracts the local context directory and processes additional build contexts.
865+
func handleLocalBuildContexts(query url.Values, anchorDir string) (*BuildContext, error) {
866+
localContextDir := query.Get("localcontextdir")
867+
if localContextDir == "" {
868+
return nil, fmt.Errorf("localcontextdir cannot be empty")
869+
}
870+
871+
out := &BuildContext{
872+
ContextDirectory: filepath.Clean(localContextDir),
873+
AdditionalBuildContexts: make(map[string]*buildahDefine.AdditionalBuildContext),
874+
}
875+
876+
for _, url := range query["additionalbuildcontexts"] {
877+
name, value, found := strings.Cut(url, "=")
878+
if !found {
879+
return nil, fmt.Errorf("invalid additional build context format: %q", url)
880+
}
881+
882+
logrus.Debugf("name: %q, context: %q", name, value)
883+
884+
switch {
885+
case strings.HasPrefix(value, "url:"):
886+
value = strings.TrimPrefix(value, "url:")
887+
tempDir, subdir, err := buildahDefine.TempDirForURL(anchorDir, "buildah", value)
888+
if err != nil {
889+
return nil, fmt.Errorf("downloading URL %q: %w", name, err)
890+
}
891+
892+
contextPath := filepath.Join(tempDir, subdir)
893+
out.AdditionalBuildContexts[name] = &buildahDefine.AdditionalBuildContext{
894+
IsURL: true,
895+
IsImage: false,
896+
Value: contextPath,
897+
DownloadedCache: contextPath,
898+
}
899+
case strings.HasPrefix(value, "image:"):
900+
value = strings.TrimPrefix(value, "image:")
901+
out.AdditionalBuildContexts[name] = &buildahDefine.AdditionalBuildContext{
902+
IsURL: false,
903+
IsImage: true,
904+
Value: value,
905+
}
906+
case strings.HasPrefix(value, "localpath:"):
907+
value = strings.TrimPrefix(value, "localpath:")
908+
fmt.Printf("Using local path %q for additional build context %q\n", value, name)
909+
out.AdditionalBuildContexts[name] = &buildahDefine.AdditionalBuildContext{
910+
IsURL: false,
911+
IsImage: false,
912+
Value: filepath.Clean(value),
913+
}
914+
}
915+
}
916+
return out, nil
917+
}
918+
919+
// getLocalBuildContext processes build contexts from HTTP request to a BuildContext struct.
920+
func getLocalBuildContext(r *http.Request, query url.Values, anchorDir string, multipart bool) (*BuildContext, error) {
921+
// Handle build contexts (extract from tar/multipart)
922+
buildContext, err := handleLocalBuildContexts(query, anchorDir)
923+
if err != nil {
924+
return nil, utils.GetInternalServerError(genSpaceErr(err))
925+
}
926+
927+
// Process build context and container files
928+
buildContext, err = processBuildContext(query, r, buildContext, anchorDir)
929+
if err != nil {
930+
return nil, err
931+
}
932+
933+
if err := buildContext.validatePaths(); err != nil {
934+
if errors.Is(err, os.ErrNotExist) {
935+
return nil, utils.GetFileNotFoundError(err)
936+
}
937+
return nil, utils.GetGenericBadRequestError(err)
938+
}
939+
940+
// Process dockerignore
941+
_, ignoreFile, err := util.ParseDockerignore(buildContext.ContainerFiles, buildContext.ContextDirectory)
942+
if err != nil {
943+
return nil, utils.GetInternalServerError(fmt.Errorf("processing ignore file: %w", err))
944+
}
945+
buildContext.IgnoreFile = ignoreFile
946+
947+
return buildContext, nil
948+
}
949+
950+
func LocalBuildImage(w http.ResponseWriter, r *http.Request) {
951+
buildImage(w, r, getLocalBuildContext)
952+
}
953+
822954
func BuildImage(w http.ResponseWriter, r *http.Request) {
955+
buildImage(w, r, getBuildContext)
956+
}
957+
958+
type getBuildContextFunc func(r *http.Request, query url.Values, anchorDir string, multipart bool) (*BuildContext, error)
959+
960+
func buildImage(w http.ResponseWriter, r *http.Request, getBuildContextFunc getBuildContextFunc) {
823961
// Create temporary directory for build context
824962
anchorDir, err := os.MkdirTemp(parse.GetTempDir(), "libpod_builder")
825963
if err != nil {
@@ -850,7 +988,7 @@ func BuildImage(w http.ResponseWriter, r *http.Request) {
850988
}
851989
queryValues := r.URL.Query()
852990

853-
buildContext, err := getBuildContext(r, queryValues, anchorDir, multipart)
991+
buildContext, err := getBuildContextFunc(r, queryValues, anchorDir, multipart)
854992
if err != nil {
855993
utils.ProcessBuildError(w, err)
856994
return

pkg/api/handlers/swagger/errors.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,13 @@ type imageNotFound struct {
1616
Body errorhandling.ErrorModel
1717
}
1818

19+
// No such file
20+
// swagger:response
21+
type fileNotFound struct {
22+
// in:body
23+
Body errorhandling.ErrorModel
24+
}
25+
1926
// No such container
2027
// swagger:response
2128
type containerNotFound struct {

pkg/api/handlers/utils/errors.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,10 @@ func (e *BuildError) Error() string {
118118
return e.err.Error()
119119
}
120120

121+
func GetFileNotFoundError(err error) *BuildError {
122+
return &BuildError{code: http.StatusNotFound, err: err}
123+
}
124+
121125
func GetBadRequestError(key, value string, err error) *BuildError {
122126
return &BuildError{code: http.StatusBadRequest, err: fmt.Errorf("failed to parse query parameter '%s': %q: %w", key, value, err)}
123127
}

0 commit comments

Comments
 (0)