refactor: Modularize binding build functions

- Split the monolithic Build() function into focused helper functions.
- Add a TempFileManager for proper temporary file lifecycle management.

This refactoring is in preparation for implementing a local build API.

Signed-off-by: Jan Rodák <hony.com@seznam.cz>
This commit is contained in:
Jan Rodák
2025-08-20 18:41:01 +02:00
parent 8900d8e77b
commit c70c0ac13e
3 changed files with 505 additions and 259 deletions

View File

@ -0,0 +1,82 @@
package remote_build_helpers
import (
"errors"
"fmt"
"io"
"os"
"github.com/sirupsen/logrus"
)
// TempFileManager manages temporary files created during image build.
// It maintains a list of created temporary files and provides cleanup functionality
// to ensure proper resource management.
type TempFileManager struct {
files []string
}
func NewTempFileManager() *TempFileManager {
return &TempFileManager{}
}
func (t *TempFileManager) AddFile(filename string) {
t.files = append(t.files, filename)
}
func (t *TempFileManager) Cleanup() {
for _, file := range t.files {
if err := os.Remove(file); err != nil && !errors.Is(err, os.ErrNotExist) {
logrus.Errorf("Failed to remove temp file %s: %v", file, err)
}
}
t.files = t.files[:0] // Reset slice
}
// CreateTempFileFromReader creates a temporary file in the specified destination directory
// with the given pattern, and copies content from the provided reader into the file.
// The created temporary file is automatically added to the manager's cleanup list.
//
// Parameters:
// - dest: The directory where the temporary file should be created
// - pattern: The pattern for naming the temporary file
// - reader: The io.Reader from which to read content to write into the temporary file
//
// Returns:
// - string: The path to the created temporary file
// - error: Any error encountered during the operation
func (t *TempFileManager) CreateTempFileFromReader(dest string, pattern string, reader io.Reader) (string, error) {
tmpFile, err := os.CreateTemp(dest, pattern)
if err != nil {
return "", fmt.Errorf("creating temp file: %w", err)
}
defer tmpFile.Close()
t.AddFile(tmpFile.Name())
if _, err := io.Copy(tmpFile, reader); err != nil {
return "", fmt.Errorf("copying stdin content: %w", err)
}
return tmpFile.Name(), nil
}
// CreateTempSecret creates a temporary copy of a secret file in the specified
// context directory. The original secret file is copied to a new temporary file
// which is automatically added to the manager's cleanup list.
//
// Parameters:
// - secretPath: The path to the source secret file to copy
// - contextDir: The directory where the temporary secret file should be created
//
// Returns:
// - string: The path to the created temporary secret file
// - error: Any error encountered during the operation
func (t *TempFileManager) CreateTempSecret(secretPath, contextDir string) (string, error) {
secretFile, err := os.Open(secretPath)
if err != nil {
return "", fmt.Errorf("opening secret file %s: %w", secretPath, err)
}
defer secretFile.Close()
return t.CreateTempFileFromReader(contextDir, "podman-build-secret-*", secretFile)
}

View File

@ -0,0 +1,52 @@
package remote_build_helpers
import (
"os"
"path/filepath"
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
func TestTempFileManager(t *testing.T) {
manager := NewTempFileManager()
t.Run("CreateTempFileFromReader", func(t *testing.T) {
content := "test content"
r := strings.NewReader(content)
filename, err := manager.CreateTempFileFromReader("", "podman-build-stdin-*", r)
assert.NoError(t, err)
assert.FileExists(t, filename)
data, err := os.ReadFile(filename)
assert.NoError(t, err)
assert.Equal(t, content, string(data))
manager.Cleanup()
assert.NoFileExists(t, filename)
})
t.Run("CreateTempSecret", func(t *testing.T) {
tempdir := t.TempDir()
secretPath := filepath.Join(tempdir, "secret.txt")
content := "test secret"
err := os.WriteFile(secretPath, []byte(content), 0600)
assert.NoError(t, err)
filename, err := manager.CreateTempSecret(secretPath, tempdir)
assert.NoError(t, err)
assert.FileExists(t, filename)
data, err := os.ReadFile(filename)
assert.NoError(t, err)
assert.Equal(t, content, string(data))
manager.Cleanup()
assert.NoFileExists(t, filename)
})
}

View File

@ -19,6 +19,7 @@ import (
"github.com/blang/semver/v4" "github.com/blang/semver/v4"
"github.com/containers/buildah/define" "github.com/containers/buildah/define"
"github.com/containers/podman/v5/internal/remote_build_helpers"
ldefine "github.com/containers/podman/v5/libpod/define" ldefine "github.com/containers/podman/v5/libpod/define"
"github.com/containers/podman/v5/pkg/auth" "github.com/containers/podman/v5/pkg/auth"
"github.com/containers/podman/v5/pkg/bindings" "github.com/containers/podman/v5/pkg/bindings"
@ -53,6 +54,21 @@ type BuildResponse struct {
Aux json.RawMessage `json:"aux,omitempty"` Aux json.RawMessage `json:"aux,omitempty"`
} }
// BuildFilePaths contains the file paths and exclusion patterns for the build context.
type BuildFilePaths struct {
tarContent []string
newContainerFiles []string // dockerfile paths, relative to context dir, ToSlash()ed
dontexcludes []string
excludes []string
}
// RequestParts contains the components of an HTTP request for the build API.
type RequestParts struct {
Headers http.Header
Params url.Values
Body io.ReadCloser
}
// Modify the build contexts that uses a local windows path. The windows path is // Modify the build contexts that uses a local windows path. The windows path is
// converted into the corresping guest path in the default Windows machine // converted into the corresping guest path in the default Windows machine
// (e.g. C:\test ==> /mnt/c/test). // (e.g. C:\test ==> /mnt/c/test).
@ -89,12 +105,32 @@ func convertVolumeSrcPath(volume string) string {
} }
} }
// Build creates an image using a containerfile reference // isSupportedVersion checks if the server version is greater than or equal to the specified minimum version.
func Build(ctx context.Context, containerFiles []string, options types.BuildOptions) (*types.BuildReport, error) { // It extracts version numbers from the server version string, removing any suffixes like -dev or -rc,
if options.CommonBuildOpts == nil { // and compares them using semantic versioning.
options.CommonBuildOpts = new(define.CommonBuildOptions) func isSupportedVersion(ctx context.Context, minVersion string) (bool, error) {
serverVersion := bindings.ServiceVersion(ctx)
// Extract just the version numbers (remove -dev, -rc, etc)
versionStr := serverVersion.String()
if idx := strings.Index(versionStr, "-"); idx > 0 {
versionStr = versionStr[:idx]
} }
serverVer, err := semver.ParseTolerant(versionStr)
if err != nil {
return false, fmt.Errorf("parsing server version %q: %w", serverVersion, err)
}
minMultipartVersion, _ := semver.ParseTolerant(minVersion)
return serverVer.GTE(minMultipartVersion), nil
}
// prepareParams converts BuildOptions into URL parameters for the build API request.
// It handles various build options including capabilities, annotations, CPU settings,
// devices, labels, platforms, volumes, and other build configuration parameters.
func prepareParams(options types.BuildOptions) (url.Values, error) {
params := url.Values{} params := url.Values{}
if caps := options.AddCapabilities; len(caps) > 0 { if caps := options.AddCapabilities; len(caps) > 0 {
@ -399,12 +435,6 @@ func Build(ctx context.Context, containerFiles []string, options types.BuildOpti
params.Add("groupadd", group) params.Add("groupadd", group)
} }
var err error
var contextDir string
if contextDir, err = filepath.EvalSymlinks(options.ContextDirectory); err == nil {
options.ContextDirectory = contextDir
}
params.Set("pullpolicy", options.PullPolicy.String()) params.Set("pullpolicy", options.PullPolicy.String())
switch options.CommonBuildOpts.IdentityLabel { switch options.CommonBuildOpts.IdentityLabel {
@ -486,59 +516,55 @@ func Build(ctx context.Context, containerFiles []string, options types.BuildOpti
params.Add("unsetannotation", uannotation) params.Add("unsetannotation", uannotation)
} }
var ( return params, nil
headers http.Header }
)
if options.SystemContext != nil { // prepareAuthHeaders sets up authentication headers for the build request.
// It handles Docker authentication configuration and TLS verification settings
// from the system context.
func prepareAuthHeaders(options types.BuildOptions, requestParts *RequestParts) (*RequestParts, error) {
var err error
if options.SystemContext == nil {
return requestParts, err
}
if options.SystemContext.DockerAuthConfig != nil { if options.SystemContext.DockerAuthConfig != nil {
headers, err = auth.MakeXRegistryAuthHeader(options.SystemContext, options.SystemContext.DockerAuthConfig.Username, options.SystemContext.DockerAuthConfig.Password) requestParts.Headers, err = auth.MakeXRegistryAuthHeader(options.SystemContext, options.SystemContext.DockerAuthConfig.Username, options.SystemContext.DockerAuthConfig.Password)
} else { } else {
headers, err = auth.MakeXRegistryConfigHeader(options.SystemContext, "", "") requestParts.Headers, err = auth.MakeXRegistryConfigHeader(options.SystemContext, "", "")
} }
if options.SystemContext.DockerInsecureSkipTLSVerify == imageTypes.OptionalBoolTrue { if options.SystemContext.DockerInsecureSkipTLSVerify == imageTypes.OptionalBoolTrue {
params.Set("tlsVerify", "false") requestParts.Params.Set("tlsVerify", "false")
}
}
if err != nil {
return nil, err
} }
stdout := io.Writer(os.Stdout) return requestParts, err
if options.Out != nil { }
stdout = options.Out
// prepareContainerFiles processes container files (Dockerfiles/Containerfiles) for the build.
// It handles URLs, stdin input, symlinks, and determines which files need to be included
// in the tar archive versus which are already in the context directory.
// WARNING: Caller must ensure tempManager.Cleanup() is called to remove any temporary files created.
func prepareContainerFiles(containerFiles []string, contextDir string, options *BuildOptions, tempManager *remote_build_helpers.TempFileManager) (*BuildFilePaths, error) {
out := BuildFilePaths{
tarContent: []string{options.ContextDirectory},
newContainerFiles: []string{}, // dockerfile paths, relative to context dir, ToSlash()ed
dontexcludes: []string{"!Dockerfile", "!Containerfile", "!.dockerignore", "!.containerignore"},
excludes: []string{},
} }
contextDir, err = filepath.Abs(options.ContextDirectory)
if err != nil {
logrus.Errorf("Cannot find absolute path of %v: %v", options.ContextDirectory, err)
return nil, err
}
tarContent := []string{options.ContextDirectory}
newContainerFiles := []string{} // dockerfile paths, relative to context dir, ToSlash()ed
dontexcludes := []string{"!Dockerfile", "!Containerfile", "!.dockerignore", "!.containerignore"}
for _, c := range containerFiles { for _, c := range containerFiles {
// Don not add path to containerfile if it is a URL // Don not add path to containerfile if it is a URL
if strings.HasPrefix(c, "http://") || strings.HasPrefix(c, "https://") { if strings.HasPrefix(c, "http://") || strings.HasPrefix(c, "https://") {
newContainerFiles = append(newContainerFiles, c) out.newContainerFiles = append(out.newContainerFiles, c)
continue continue
} }
if c == "/dev/stdin" { if c == "/dev/stdin" {
content, err := io.ReadAll(os.Stdin) stdinFile, err := tempManager.CreateTempFileFromReader("", "podman-build-stdin-*", os.Stdin)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("processing stdin: %w", err)
} }
tmpFile, err := os.CreateTemp("", "build") c = stdinFile
if err != nil {
return nil, err
}
defer os.Remove(tmpFile.Name()) // clean up
defer tmpFile.Close()
if _, err := tmpFile.Write(content); err != nil {
return nil, err
}
c = tmpFile.Name()
} }
c = filepath.Clean(c) c = filepath.Clean(c)
cfDir := filepath.Dir(c) cfDir := filepath.Dir(c)
@ -557,9 +583,9 @@ func Build(ctx context.Context, containerFiles []string, options types.BuildOpti
// Do NOT add to tarfile // Do NOT add to tarfile
if strings.HasPrefix(containerfile, contextDir+string(filepath.Separator)) { if strings.HasPrefix(containerfile, contextDir+string(filepath.Separator)) {
containerfile = strings.TrimPrefix(containerfile, contextDir+string(filepath.Separator)) containerfile = strings.TrimPrefix(containerfile, contextDir+string(filepath.Separator))
dontexcludes = append(dontexcludes, "!"+containerfile) out.dontexcludes = append(out.dontexcludes, "!"+containerfile)
dontexcludes = append(dontexcludes, "!"+containerfile+".dockerignore") out.dontexcludes = append(out.dontexcludes, "!"+containerfile+".dockerignore")
dontexcludes = append(dontexcludes, "!"+containerfile+".containerignore") out.dontexcludes = append(out.dontexcludes, "!"+containerfile+".containerignore")
} else { } else {
// If Containerfile does not exist, assume it is in context directory and do Not add to tarfile // If Containerfile does not exist, assume it is in context directory and do Not add to tarfile
if err := fileutils.Lexists(containerfile); err != nil { if err := fileutils.Lexists(containerfile); err != nil {
@ -567,46 +593,34 @@ func Build(ctx context.Context, containerFiles []string, options types.BuildOpti
return nil, err return nil, err
} }
containerfile = c containerfile = c
dontexcludes = append(dontexcludes, "!"+containerfile) out.dontexcludes = append(out.dontexcludes, "!"+containerfile)
dontexcludes = append(dontexcludes, "!"+containerfile+".dockerignore") out.dontexcludes = append(out.dontexcludes, "!"+containerfile+".dockerignore")
dontexcludes = append(dontexcludes, "!"+containerfile+".containerignore") out.dontexcludes = append(out.dontexcludes, "!"+containerfile+".containerignore")
} else { } else {
// If Containerfile does exist and not in the context directory, add it to the tarfile // If Containerfile does exist and not in the context directory, add it to the tarfile
tarContent = append(tarContent, containerfile) out.tarContent = append(out.tarContent, containerfile)
} }
} }
newContainerFiles = append(newContainerFiles, filepath.ToSlash(containerfile)) out.newContainerFiles = append(out.newContainerFiles, filepath.ToSlash(containerfile))
} }
if len(newContainerFiles) > 0 { return &out, nil
cFileJSON, err := json.Marshal(newContainerFiles) }
if err != nil {
return nil, err // prepareSecrets processes build secrets by creating temporary files for them.
} // It moves secrets to the context directory and modifies the secret configuration
params.Set("dockerfile", string(cFileJSON)) // to use relative paths suitable for remote builds.
// WARNING: Caller must ensure tempManager.Cleanup() is called to remove any temporary files created.
func prepareSecrets(secrets []string, contextDir string, tempManager *remote_build_helpers.TempFileManager) ([]string, []string, error) {
if len(secrets) == 0 {
return nil, nil, nil
} }
excludes := options.Excludes
if len(excludes) == 0 {
excludes, _, err = util.ParseDockerignore(newContainerFiles, options.ContextDirectory)
if err != nil {
return nil, err
}
}
saveFormat := ldefine.OCIArchive
if options.OutputFormat == define.Dockerv2ImageManifest {
saveFormat = ldefine.V2s2Archive
}
// build secrets are usually absolute host path or relative to context dir on host
// in any case move secret to current context and ship the tar.
if secrets := options.CommonBuildOpts.Secrets; len(secrets) > 0 {
secretsForRemote := []string{} secretsForRemote := []string{}
tarContent := []string{}
for _, secret := range secrets { for _, secret := range secrets {
secretOpt := strings.Split(secret, ",") secretOpt := strings.Split(secret, ",")
if len(secretOpt) > 0 {
modifiedOpt := []string{} modifiedOpt := []string{}
for _, token := range secretOpt { for _, token := range secretOpt {
opt, val, hasVal := strings.Cut(token, "=") opt, val, hasVal := strings.Cut(token, "=")
@ -614,26 +628,15 @@ func Build(ctx context.Context, containerFiles []string, options types.BuildOpti
if opt == "src" { if opt == "src" {
// read specified secret into a tmp file // read specified secret into a tmp file
// move tmp file to tar and change secret source to relative tmp file // move tmp file to tar and change secret source to relative tmp file
tmpSecretFile, err := os.CreateTemp(options.ContextDirectory, "podman-build-secret") tmpSecretFilePath, err := tempManager.CreateTempSecret(val, contextDir)
if err != nil { if err != nil {
return nil, err return nil, nil, err
}
defer os.Remove(tmpSecretFile.Name()) // clean up
defer tmpSecretFile.Close()
srcSecretFile, err := os.Open(val)
if err != nil {
return nil, err
}
defer srcSecretFile.Close()
_, err = io.Copy(tmpSecretFile, srcSecretFile)
if err != nil {
return nil, err
} }
// add tmp file to context dir // add tmp file to context dir
tarContent = append(tarContent, tmpSecretFile.Name()) tarContent = append(tarContent, tmpSecretFilePath)
modifiedSrc := fmt.Sprintf("src=%s", filepath.Base(tmpSecretFile.Name())) modifiedSrc := fmt.Sprintf("src=%s", filepath.Base(tmpSecretFilePath))
modifiedOpt = append(modifiedOpt, modifiedSrc) modifiedOpt = append(modifiedOpt, modifiedSrc)
} else { } else {
modifiedOpt = append(modifiedOpt, token) modifiedOpt = append(modifiedOpt, token)
@ -642,49 +645,50 @@ func Build(ctx context.Context, containerFiles []string, options types.BuildOpti
} }
secretsForRemote = append(secretsForRemote, strings.Join(modifiedOpt, ",")) secretsForRemote = append(secretsForRemote, strings.Join(modifiedOpt, ","))
} }
}
c, err := jsoniter.MarshalToString(secretsForRemote) return secretsForRemote, tarContent, nil
}
// prepareRequestBody creates the request body for the build API call.
// It handles both simple tar archives and multipart form data for builds with
// additional build contexts, supporting URLs, images, and local directories.
// WARNING: Caller must close request body.
func prepareRequestBody(ctx context.Context, requestParts *RequestParts, buildFilePaths *BuildFilePaths, options types.BuildOptions) (*RequestParts, error) {
tarfile, err := nTar(append(buildFilePaths.excludes, buildFilePaths.dontexcludes...), buildFilePaths.tarContent...)
if err != nil { if err != nil {
logrus.Errorf("Cannot tar container entries %v error: %v", buildFilePaths.tarContent, err)
return nil, err return nil, err
} }
params.Add("secrets", c)
}
tarfile, err := nTar(append(excludes, dontexcludes...), tarContent...)
if err != nil {
logrus.Errorf("Cannot tar container entries %v error: %v", tarContent, err)
return nil, err
}
defer func() {
if err := tarfile.Close(); err != nil {
logrus.Errorf("%v\n", err)
}
}()
var requestBody io.Reader
var contentType string var contentType string
// If there are additional build contexts, we need to handle them based on the server version // If there are additional build contexts, we need to handle them based on the server version
// podman version >= 5.6.0 supports multipart/form-data for additional build contexts that // podman version >= 5.6.0 supports multipart/form-data for additional build contexts that
// are local directories or archives. URLs and images are still sent as query parameters. // are local directories or archives. URLs and images are still sent as query parameters.
if len(options.AdditionalBuildContexts) > 0 { isSupported, err := isSupportedVersion(ctx, "5.6.0")
serverVersion := bindings.ServiceVersion(ctx)
// Extract just the version numbers (remove -dev, -rc, etc)
versionStr := serverVersion.String()
if idx := strings.Index(versionStr, "-"); idx > 0 {
versionStr = versionStr[:idx]
}
serverVer, err := semver.ParseTolerant(versionStr)
if err != nil { if err != nil {
return nil, fmt.Errorf("parsing server version %q: %w", serverVersion, err) return nil, err
} }
minMultipartVersion, _ := semver.ParseTolerant("5.6.0") if len(options.AdditionalBuildContexts) == 0 {
requestParts.Body = tarfile
logrus.Debugf("Using main build context: %q", options.ContextDirectory)
return requestParts, nil
}
if !isSupported {
convertAdditionalBuildContexts(options.AdditionalBuildContexts)
additionalBuildContextMap, err := jsoniter.Marshal(options.AdditionalBuildContexts)
if err != nil {
return nil, err
}
requestParts.Params.Set("additionalbuildcontexts", string(additionalBuildContextMap))
requestParts.Body = tarfile
logrus.Debugf("Using main build context: %q", options.ContextDirectory)
return requestParts, nil
}
if serverVer.GTE(minMultipartVersion) {
imageContexts := make(map[string]string) imageContexts := make(map[string]string)
urlContexts := make(map[string]string) urlContexts := make(map[string]string)
localContexts := make(map[string]*define.AdditionalBuildContext) localContexts := make(map[string]*define.AdditionalBuildContext)
@ -702,15 +706,19 @@ func Build(ctx context.Context, containerFiles []string, options types.BuildOpti
logrus.Debugf("URL Contexts: %v", urlContexts) logrus.Debugf("URL Contexts: %v", urlContexts)
for name, url := range urlContexts { for name, url := range urlContexts {
params.Add("additionalbuildcontexts", fmt.Sprintf("%s=url:%s", name, url)) requestParts.Params.Add("additionalbuildcontexts", fmt.Sprintf("%s=url:%s", name, url))
} }
logrus.Debugf("Image Contexts: %v", imageContexts) logrus.Debugf("Image Contexts: %v", imageContexts)
for name, imageRef := range imageContexts { for name, imageRef := range imageContexts {
params.Add("additionalbuildcontexts", fmt.Sprintf("%s=image:%s", name, imageRef)) requestParts.Params.Add("additionalbuildcontexts", fmt.Sprintf("%s=image:%s", name, imageRef))
} }
if len(localContexts) > 0 { if len(localContexts) == 0 {
requestParts.Body = tarfile
logrus.Debugf("Using main build context: %q", options.ContextDirectory)
return requestParts, nil
}
// Multipart request structure: // Multipart request structure:
// - "MainContext": The main build context as a tar file // - "MainContext": The main build context as a tar file
// - "build-context-<name>": Each additional local context as a tar file // - "build-context-<name>": Each additional local context as a tar file
@ -718,12 +726,12 @@ func Build(ctx context.Context, containerFiles []string, options types.BuildOpti
pr, pw := io.Pipe() pr, pw := io.Pipe()
writer := multipart.NewWriter(pw) writer := multipart.NewWriter(pw)
contentType = writer.FormDataContentType() contentType = writer.FormDataContentType()
requestBody = pr requestParts.Body = pr
if headers == nil { if requestParts.Headers == nil {
headers = make(http.Header) requestParts.Headers = make(http.Header)
} }
headers.Set("Content-Type", contentType) requestParts.Headers.Set("Content-Type", contentType)
go func() { go func() {
defer pw.Close() defer pw.Close()
@ -740,6 +748,12 @@ func Build(ctx context.Context, containerFiles []string, options types.BuildOpti
return return
} }
defer func() {
if err := tarfile.Close(); err != nil {
logrus.Errorf("failed to close context tarfile: %v\n", err)
}
}()
for name, context := range localContexts { for name, context := range localContexts {
logrus.Debugf("Processing additional local context: %s", name) logrus.Debugf("Processing additional local context: %s", name)
part, err := writer.CreateFormFile(fmt.Sprintf("build-context-%s", name), name) part, err := writer.CreateFormFile(fmt.Sprintf("build-context-%s", name), name)
@ -778,40 +792,35 @@ func Build(ctx context.Context, containerFiles []string, options types.BuildOpti
} }
}() }()
logrus.Debugf("Multipart body is created with content type: %s", contentType) logrus.Debugf("Multipart body is created with content type: %s", contentType)
} else {
requestBody = tarfile
logrus.Debugf("Using main build context: %q", options.ContextDirectory)
}
} else {
convertAdditionalBuildContexts(options.AdditionalBuildContexts)
additionalBuildContextMap, err := jsoniter.Marshal(options.AdditionalBuildContexts)
if err != nil {
return nil, err
}
params.Set("additionalbuildcontexts", string(additionalBuildContextMap))
requestBody = tarfile return requestParts, nil
logrus.Debugf("Using main build context: %q", options.ContextDirectory) }
}
} else {
requestBody = tarfile
logrus.Debugf("Using main build context: %q", options.ContextDirectory)
}
// executeBuildRequest sends the build request to the API endpoint and returns the response.
// It handles the HTTP request creation and error checking for the build operation.
// WARNING: Caller must close the response body.
func executeBuildRequest(ctx context.Context, endpoint string, requestParts *RequestParts) (*bindings.APIResponse, error) {
conn, err := bindings.GetClient(ctx) conn, err := bindings.GetClient(ctx)
if err != nil { if err != nil {
return nil, err return nil, err
} }
response, err := conn.DoRequest(ctx, requestBody, http.MethodPost, "/build", params, headers)
response, err := conn.DoRequest(ctx, requestParts.Body, http.MethodPost, endpoint, requestParts.Params, requestParts.Headers)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer response.Body.Close()
if !response.IsSuccess() { if !response.IsSuccess() {
return nil, response.Process(err) return nil, response.Process(err)
} }
return response, nil
}
// processBuildResponse processes the streaming build response from the API.
// It reads the JSON stream, extracts build output and errors, writes to stdout,
// and returns a build report with the final image ID.
func processBuildResponse(response *bindings.APIResponse, stdout io.Writer, saveFormat string) (*types.BuildReport, error) {
body := response.Body.(io.Reader) body := response.Body.(io.Reader)
if logrus.IsLevelEnabled(logrus.DebugLevel) { if logrus.IsLevelEnabled(logrus.DebugLevel) {
if v, found := os.LookupEnv("PODMAN_RETAIN_BUILD_ARTIFACT"); found { if v, found := os.LookupEnv("PODMAN_RETAIN_BUILD_ARTIFACT"); found {
@ -868,6 +877,109 @@ func Build(ctx context.Context, containerFiles []string, options types.BuildOpti
return &types.BuildReport{ID: id, SaveFormat: saveFormat}, nil return &types.BuildReport{ID: id, SaveFormat: saveFormat}, nil
} }
func Build(ctx context.Context, containerFiles []string, options types.BuildOptions) (*types.BuildReport, error) {
if options.CommonBuildOpts == nil {
options.CommonBuildOpts = new(define.CommonBuildOptions)
}
tempManager := remote_build_helpers.NewTempFileManager()
defer tempManager.Cleanup()
params_, err := prepareParams(options)
if err != nil {
return nil, err
}
var headers http.Header
var requestBody io.ReadCloser
requestParts := &RequestParts{
Params: params_,
Headers: headers,
Body: requestBody,
}
var contextDir string
if contextDir, err = filepath.EvalSymlinks(options.ContextDirectory); err == nil {
options.ContextDirectory = contextDir
}
requestParts, err = prepareAuthHeaders(options, requestParts)
if err != nil {
return nil, err
}
contextDirAbs, err := filepath.Abs(options.ContextDirectory)
if err != nil {
logrus.Errorf("Cannot find absolute path of %v: %v", options.ContextDirectory, err)
return nil, err
}
buildFilePaths, err := prepareContainerFiles(containerFiles, contextDirAbs, &options, tempManager)
if err != nil {
return nil, err
}
if len(buildFilePaths.newContainerFiles) > 0 {
cFileJSON, err := json.Marshal(buildFilePaths.newContainerFiles)
if err != nil {
return nil, err
}
requestParts.Params.Set("dockerfile", string(cFileJSON))
}
buildFilePaths.excludes = options.Excludes
if len(buildFilePaths.excludes) == 0 {
buildFilePaths.excludes, _, err = util.ParseDockerignore(buildFilePaths.newContainerFiles, options.ContextDirectory)
if err != nil {
return nil, err
}
}
// build secrets are usually absolute host path or relative to context dir on host
// in any case move secret to current context and ship the tar.
secretsForRemote, secretsTarContent, err := prepareSecrets(options.CommonBuildOpts.Secrets, options.ContextDirectory, tempManager)
if err != nil {
return nil, err
}
if len(secretsForRemote) > 0 {
c, err := jsoniter.MarshalToString(secretsForRemote)
if err != nil {
return nil, err
}
requestParts.Params.Add("secrets", c)
buildFilePaths.tarContent = append(buildFilePaths.tarContent, secretsTarContent...)
}
requestParts, err = prepareRequestBody(ctx, requestParts, buildFilePaths, options)
if err != nil {
return nil, fmt.Errorf("building tar file: %w", err)
}
defer func() {
if err := requestParts.Body.Close(); err != nil {
logrus.Errorf("failed to close build request body: %v\n", err)
}
}()
response, err := executeBuildRequest(ctx, "/build", requestParts)
if err != nil {
return nil, err
}
defer response.Body.Close()
saveFormat := ldefine.OCIArchive
if options.OutputFormat == define.Dockerv2ImageManifest {
saveFormat = ldefine.V2s2Archive
}
stdout := io.Writer(os.Stdout)
if options.Out != nil {
stdout = options.Out
}
return processBuildResponse(response, stdout, saveFormat)
}
func nTar(excludes []string, sources ...string) (io.ReadCloser, error) { func nTar(excludes []string, sources ...string) (io.ReadCloser, error) {
pm, err := fileutils.NewPatternMatcher(excludes) pm, err := fileutils.NewPatternMatcher(excludes)
if err != nil { if err != nil {