Skip to content

Commit 9e81b73

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 machone without tar uploads. Fixes: https://issues.redhat.com/browse/RUN-3249 Signed-off-by: Jan Rodák <[email protected]>
1 parent e03a5d2 commit 9e81b73

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)