mirror of
https://github.com/containers/podman.git
synced 2025-09-19 23:03:16 +08:00
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:
82
internal/remote_build_helpers/utils.go
Normal file
82
internal/remote_build_helpers/utils.go
Normal 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)
|
||||||
|
}
|
52
internal/remote_build_helpers/utils_test.go
Normal file
52
internal/remote_build_helpers/utils_test.go
Normal 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)
|
||||||
|
})
|
||||||
|
}
|
@ -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 {
|
||||||
|
Reference in New Issue
Block a user