Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 19 additions & 25 deletions add.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package buildah
import (
"archive/tar"
"context"
"crypto/tls"
"errors"
"fmt"
"io"
Expand All @@ -21,17 +20,17 @@ import (

"github.com/containers/buildah/copier"
"github.com/containers/buildah/define"
"github.com/containers/buildah/internal/httpclient"
"github.com/containers/buildah/internal/tmpdir"
"github.com/containers/buildah/pkg/chrootuser"
"github.com/docker/go-connections/tlsconfig"
tmpdirpkg "github.com/containers/buildah/pkg/tmpdir"
"github.com/hashicorp/go-multierror"
"github.com/moby/sys/userns"
digest "github.com/opencontainers/go-digest"
v1 "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/opencontainers/runtime-spec/specs-go"
"github.com/sirupsen/logrus"
"go.podman.io/common/pkg/retry"
"go.podman.io/image/v5/pkg/tlsclientconfig"
"go.podman.io/image/v5/types"
"go.podman.io/storage/pkg/fileutils"
"go.podman.io/storage/pkg/idtools"
Expand Down Expand Up @@ -112,6 +111,9 @@ type AddAndCopyOptions struct {
// inheritAnnotations, newAnnotations). This field is internally managed and should
// not be set by external API users.
BuildMetadata string
// Callback which controls which, if any, proxy server to use when retrieving HTTP or
// HTTPS sources. Used to construct an http.Client's Transport.
Proxy func(*http.Request) (*url.URL, error)
}

// gitURLFragmentSuffix matches fragments to use as Git reference and build
Expand All @@ -138,30 +140,12 @@ func sourceIsRemote(source string) bool {
}

// getURL writes a tar archive containing the named content
func getURL(src string, chown *idtools.IDPair, mountpoint, renameTarget string, writer io.Writer, chmod *os.FileMode, srcDigest digest.Digest, certPath string, insecureSkipTLSVerify types.OptionalBool, timestamp *time.Time) error {
func getURL(src string, chown *idtools.IDPair, mountpoint, renameTarget string, writer io.Writer, chmod *os.FileMode, srcDigest digest.Digest, timestamp *time.Time, client *http.Client) error {
url, err := url.Parse(src)
if err != nil {
return err
}
tlsClientConfig := &tls.Config{
// As of 2025-08, tlsconfig.ClientDefault() differs from Go 1.23 defaults only in CipherSuites;
// so, limit us to only using that value. If go-connections/tlsconfig changes its policy, we
// will want to consider that and make a decision whether to follow suit.
// There is some chance that eventually the Go default will be to require TLS 1.3, and that point
// we might want to drop the dependency on go-connections entirely.
CipherSuites: tlsconfig.ClientDefault().CipherSuites,
}
if err := tlsclientconfig.SetupCertificates(certPath, tlsClientConfig); err != nil {
return err
}
tlsClientConfig.InsecureSkipVerify = insecureSkipTLSVerify == types.OptionalBoolTrue

tr := &http.Transport{
TLSClientConfig: tlsClientConfig,
Proxy: http.ProxyFromEnvironment,
}
httpClient := &http.Client{Transport: tr}
response, err := httpClient.Get(src)
response, err := client.Get(src)
if err != nil {
return err
}
Expand Down Expand Up @@ -584,6 +568,16 @@ func (b *Builder) Add(destination string, extract bool, options AddAndCopyOption
putDir = extractDirectory
}

urlOptions := tmpdirpkg.URLOptions{
CertPath: options.CertPath,
InsecureSkipTLSVerify: options.InsecureSkipTLSVerify,
Proxy: options.Proxy,
}
httpClient, err := httpclient.ForURLOptions(urlOptions)
if err != nil {
return fmt.Errorf("setting up http client options: %w", err)
}

// Copy each source in turn.
for _, src := range sources {
var multiErr *multierror.Error
Expand All @@ -605,7 +599,7 @@ func (b *Builder) Add(destination string, extract bool, options AddAndCopyOption
defer wg.Done()
defer pipeWriter.Close()
var cloneDir, subdir string
cloneDir, subdir, getErr = define.TempDirForURL(tmpdir.GetTempDir(), "", src)
cloneDir, subdir, getErr = tmpdirpkg.ForURL(tmpdir.GetTempDir(), "", src, &urlOptions)
if getErr != nil {
return
}
Expand All @@ -630,7 +624,7 @@ func (b *Builder) Add(destination string, extract bool, options AddAndCopyOption
} else {
go func() {
getErr = retry.IfNecessary(context.TODO(), func() error {
return getURL(src, chownFiles, mountPoint, renameTarget, pipeWriter, chmodDirsFiles, srcDigest, options.CertPath, options.InsecureSkipTLSVerify, options.Timestamp)
return getURL(src, chownFiles, mountPoint, renameTarget, pipeWriter, chmodDirsFiles, srcDigest, options.Timestamp, httpClient)
}, &retry.Options{
MaxRetry: options.MaxRetries,
Delay: options.RetryDelay,
Expand Down
2 changes: 2 additions & 0 deletions cmd/buildah/addcopy.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package main
import (
"errors"
"fmt"
"net/http"
"os"
"path/filepath"
"strconv"
Expand Down Expand Up @@ -266,6 +267,7 @@ func addAndCopyCmd(c *cobra.Command, args []string, verb string, iopts addCopyRe
Parents: iopts.parents,
Timestamp: timestamp,
Link: iopts.link,
Proxy: http.ProxyFromEnvironment,
}
if iopts.contextdir != "" {
var excludes []string
Expand Down
5 changes: 5 additions & 0 deletions define/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package define

import (
"io"
"net/http"
"net/url"
"time"

encconfig "github.com/containers/ocicrypt/config"
Expand Down Expand Up @@ -421,4 +423,7 @@ type BuildOptions struct {
// MetadataFile is the name of a file to which the builder should write a JSON map
// containing metadata about the built image.
MetadataFile string
// Proxy controls how we retrieve HTTP or HTTPS build contexts and
// sources to ADD.
Proxy func(req *http.Request) (*url.URL, error)
}
201 changes: 7 additions & 194 deletions define/types.go
Original file line number Diff line number Diff line change
@@ -1,26 +1,11 @@
package define

import (
"bufio"
"bytes"
"errors"
"fmt"
"io"
"net/http"
urlpkg "net/url"
"os"
"os/exec"
"path"
"path/filepath"
"strings"

"github.com/containers/buildah/pkg/tmpdir"
v1 "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/opencontainers/runtime-spec/specs-go"
"github.com/sirupsen/logrus"
"go.podman.io/image/v5/manifest"
"go.podman.io/storage/pkg/archive"
"go.podman.io/storage/pkg/chrootarchive"
"go.podman.io/storage/pkg/ioutils"
"go.podman.io/storage/types"
)

Expand Down Expand Up @@ -173,182 +158,10 @@ type SBOMScanOptions struct {
// is, TempDirForURL creates a temporary directory, arranges for its contents
// to be the contents of that URL, and returns the temporary directory's path,
// along with the relative name of a subdirectory which should be used as the
// build context (which may be empty or "."). Removal of the temporary
// directory is the responsibility of the caller. If the string doesn't look
// like a URL or "-", TempDirForURL returns empty strings and a nil error code.
func TempDirForURL(dir, prefix, url string) (name string, subdir string, err error) {
if !strings.HasPrefix(url, "http://") &&
!strings.HasPrefix(url, "https://") &&
!strings.HasPrefix(url, "git://") &&
!strings.HasPrefix(url, "github.com/") &&
url != "-" {
return "", "", nil
}
name, err = os.MkdirTemp(dir, prefix)
if err != nil {
return "", "", fmt.Errorf("creating temporary directory for %q: %w", url, err)
}
downloadDir := filepath.Join(name, "download")
if err = os.MkdirAll(downloadDir, 0o700); err != nil {
return "", "", fmt.Errorf("creating directory %q for %q: %w", downloadDir, url, err)
}
urlParsed, err := urlpkg.Parse(url)
if err != nil {
return "", "", fmt.Errorf("parsing url %q: %w", url, err)
}
if strings.HasPrefix(url, "git://") || strings.HasSuffix(urlParsed.Path, ".git") {
combinedOutput, gitSubDir, err := cloneToDirectory(url, downloadDir)
if err != nil {
if err2 := os.RemoveAll(name); err2 != nil {
logrus.Debugf("error removing temporary directory %q: %v", name, err2)
}
return "", "", fmt.Errorf("cloning %q to %q:\n%s: %w", url, name, string(combinedOutput), err)
}
logrus.Debugf("Build context is at %q", filepath.Join(downloadDir, gitSubDir))
return name, filepath.Join(filepath.Base(downloadDir), gitSubDir), nil
}
if strings.HasPrefix(url, "github.com/") {
ghurl := url
url = fmt.Sprintf("https://%s/archive/master.tar.gz", ghurl)
logrus.Debugf("resolving url %q to %q", ghurl, url)
subdir = path.Base(ghurl) + "-master"
}
if strings.HasPrefix(url, "http://") || strings.HasPrefix(url, "https://") {
err = downloadToDirectory(url, downloadDir)
if err != nil {
if err2 := os.RemoveAll(name); err2 != nil {
logrus.Debugf("error removing temporary directory %q: %v", name, err2)
}
return "", "", err
}
logrus.Debugf("Build context is at %q", filepath.Join(downloadDir, subdir))
return name, filepath.Join(filepath.Base(downloadDir), subdir), nil
}
if url == "-" {
err = stdinToDirectory(downloadDir)
if err != nil {
if err2 := os.RemoveAll(name); err2 != nil {
logrus.Debugf("error removing temporary directory %q: %v", name, err2)
}
return "", "", err
}
logrus.Debugf("Build context is at %q", filepath.Join(downloadDir, subdir))
return name, filepath.Join(filepath.Base(downloadDir), subdir), nil
}
logrus.Debugf("don't know how to retrieve %q", url)
if err2 := os.RemoveAll(name); err2 != nil {
logrus.Debugf("error removing temporary directory %q: %v", name, err2)
}
return "", "", errors.New("unreachable code reached")
}

// parseGitBuildContext parses git build context to `repo`, `sub-dir`
// `branch/commit`, accepts GitBuildContext in the format of
// `repourl.git[#[branch-or-commit]:subdir]`.
func parseGitBuildContext(url string) (string, string, string) {
gitSubdir := ""
gitBranch := ""
gitBranchPart := strings.Split(url, "#")
if len(gitBranchPart) > 1 {
// check if string contains path to a subdir
gitSubDirPart := strings.Split(gitBranchPart[1], ":")
if len(gitSubDirPart) > 1 {
gitSubdir = gitSubDirPart[1]
}
gitBranch = gitSubDirPart[0]
}
return gitBranchPart[0], gitSubdir, gitBranch
}

func cloneToDirectory(url, dir string) ([]byte, string, error) {
var cmd *exec.Cmd
gitRepo, gitSubdir, gitRef := parseGitBuildContext(url)
// init repo
cmd = exec.Command("git", "init", dir)
combinedOutput, err := cmd.CombinedOutput()
if err != nil {
// Return err.Error() instead of err as we want buildah to override error code with more predictable
// value.
return combinedOutput, gitSubdir, fmt.Errorf("failed while performing `git init`: %s", err.Error())
}
// add origin
cmd = exec.Command("git", "remote", "add", "origin", gitRepo)
cmd.Dir = dir
combinedOutput, err = cmd.CombinedOutput()
if err != nil {
// Return err.Error() instead of err as we want buildah to override error code with more predictable
// value.
return combinedOutput, gitSubdir, fmt.Errorf("failed while performing `git remote add`: %s", err.Error())
}

logrus.Debugf("fetching repo %q and branch (or commit ID) %q to %q", gitRepo, gitRef, dir)
args := []string{"fetch", "-u", "--depth=1", "origin", "--", gitRef}
cmd = exec.Command("git", args...)
cmd.Dir = dir
combinedOutput, err = cmd.CombinedOutput()
if err != nil {
// Return err.Error() instead of err as we want buildah to override error code with more predictable
// value.
return combinedOutput, gitSubdir, fmt.Errorf("failed while performing `git fetch`: %s", err.Error())
}

cmd = exec.Command("git", "checkout", "FETCH_HEAD")
cmd.Dir = dir
combinedOutput, err = cmd.CombinedOutput()
if err != nil {
// Return err.Error() instead of err as we want buildah to override error code with more predictable
// value.
return combinedOutput, gitSubdir, fmt.Errorf("failed while performing `git checkout`: %s", err.Error())
}
return combinedOutput, gitSubdir, nil
}

func downloadToDirectory(url, dir string) error {
logrus.Debugf("extracting %q to %q", url, dir)
resp, err := http.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusBadRequest {
return fmt.Errorf("invalid response status %d", resp.StatusCode)
}
if resp.ContentLength == 0 {
return fmt.Errorf("no contents in %q", url)
}
if err := chrootarchive.Untar(resp.Body, dir, nil); err != nil {
resp1, err := http.Get(url)
if err != nil {
return err
}
defer resp1.Body.Close()
body, err := io.ReadAll(resp1.Body)
if err != nil {
return err
}
dockerfile := filepath.Join(dir, "Dockerfile")
// Assume this is a Dockerfile
if err := ioutils.AtomicWriteFile(dockerfile, body, 0o600); err != nil {
return fmt.Errorf("failed to write %q to %q: %w", url, dockerfile, err)
}
}
return nil
}

func stdinToDirectory(dir string) error {
logrus.Debugf("extracting stdin to %q", dir)
r := bufio.NewReader(os.Stdin)
b, err := io.ReadAll(r)
if err != nil {
return fmt.Errorf("failed to read from stdin: %w", err)
}
reader := bytes.NewReader(b)
if err := chrootarchive.Untar(reader, dir, nil); err != nil {
dockerfile := filepath.Join(dir, "Dockerfile")
// Assume this is a Dockerfile
if err := ioutils.AtomicWriteFile(dockerfile, b, 0o600); err != nil {
return fmt.Errorf("failed to write bytes to %q: %w", dockerfile, err)
}
}
return nil
// build context (which may be empty or ".").
// Removal of the temporary directory is the responsibility of the caller.
// If the string doesn't look like a URL or "-", TempDirForURL returns empty
// strings and a nil error code.
func TempDirForURL(dir, prefix, url string) (name, subdir string, err error) {
return tmpdir.ForURL(dir, prefix, url, nil)
}
2 changes: 1 addition & 1 deletion docs/buildah-build.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -1102,7 +1102,7 @@ not affect the timestamps of layer contents.

**--tls-verify** *bool-value*

Require HTTPS and verification of certificates when talking to container registries (defaults to true) and retrieving content from HTTPS locations for ADD instructions. TLS verification cannot be used when talking to an insecure registry.
Require HTTPS and verification of certificates when talking to container registries (defaults to true) and retrieving build contexts and content from HTTPS locations for ADD instructions. TLS verification cannot be used when talking to an insecure registry.

**--ulimit** *type*=*soft-limit*[:*hard-limit*]

Expand Down
4 changes: 4 additions & 0 deletions imagebuildah/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import (
"errors"
"fmt"
"io"
"net/http"
"net/url"
"os"
"slices"
"strconv"
Expand Down Expand Up @@ -175,6 +177,7 @@ type executor struct {
rewriteTimestamp bool
createdAnnotation types.OptionalBool
metadataFile string
proxy func(req *http.Request) (*url.URL, error)
}

type imageTypeAndHistoryAndDiffIDs struct {
Expand Down Expand Up @@ -350,6 +353,7 @@ func newExecutor(logger *logrus.Logger, logPrefix string, store storage.Store, o
rewriteTimestamp: options.RewriteTimestamp,
createdAnnotation: options.CreatedAnnotation,
metadataFile: options.MetadataFile,
proxy: options.Proxy,
}
// sort unsetAnnotations because we will later write these
// values to the history of the image therefore we want to
Expand Down
Loading