Files
podman/pkg/api/handlers/compat/images_build.go
openshift-merge-bot[bot] 8daac5220c Merge pull request #25647 from aguidirh/fix/issue-23915
fix: #23915 podman build is not parsing sbom command line arguments
2025-10-06 10:16:24 +00:00

1123 lines
39 KiB
Go

//go:build !remote
package compat
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"mime"
"net/http"
"net/url"
"os"
"path/filepath"
"slices"
"strconv"
"strings"
"syscall"
"time"
"github.com/containers/buildah"
buildahDefine "github.com/containers/buildah/define"
"github.com/containers/buildah/pkg/parse"
"github.com/containers/podman/v5/libpod"
"github.com/containers/podman/v5/pkg/api/handlers/utils"
api "github.com/containers/podman/v5/pkg/api/types"
"github.com/containers/podman/v5/pkg/auth"
"github.com/containers/podman/v5/pkg/channel"
"github.com/containers/podman/v5/pkg/rootless"
"github.com/containers/podman/v5/pkg/util"
"github.com/opencontainers/runtime-spec/specs-go"
"github.com/sirupsen/logrus"
"go.podman.io/common/pkg/config"
"go.podman.io/image/v5/docker/reference"
"go.podman.io/image/v5/types"
"go.podman.io/storage/pkg/archive"
"go.podman.io/storage/pkg/chrootarchive"
"go.podman.io/storage/pkg/fileutils"
)
type cleanUpFunc func()
// BuildQuery represents query parameters for the container image build API endpoint.
// Uses struct tags to map HTTP query parameters to Go fields for automatic parsing.
type BuildQuery struct {
AddHosts string `schema:"extrahosts"`
AdditionalCapabilities string `schema:"addcaps"`
AdditionalBuildContexts string `schema:"additionalbuildcontexts"`
AllPlatforms bool `schema:"allplatforms"`
Annotations string `schema:"annotations"`
AppArmor string `schema:"apparmor"`
BuildArgs string `schema:"buildargs"`
CacheFrom string `schema:"cachefrom"`
CacheTo string `schema:"cacheto"`
CacheTTL string `schema:"cachettl"`
CgroupParent string `schema:"cgroupparent"`
CompatVolumes bool `schema:"compatvolumes"`
Compression uint64 `schema:"compression"`
ConfigureNetwork string `schema:"networkmode"`
CPPFlags string `schema:"cppflags"`
CpuPeriod uint64 `schema:"cpuperiod"`
CpuQuota int64 `schema:"cpuquota"`
CpuSetCpus string `schema:"cpusetcpus"`
CpuSetMems string `schema:"cpusetmems"`
CpuShares uint64 `schema:"cpushares"`
CreatedAnnotation types.OptionalBool `schema:"createdannotation"`
DNSOptions string `schema:"dnsoptions"`
DNSSearch string `schema:"dnssearch"`
DNSServers string `schema:"dnsservers"`
Devices string `schema:"devices"`
Dockerfile string `schema:"dockerfile"`
DropCapabilities string `schema:"dropcaps"`
Envs []string `schema:"setenv"`
Excludes string `schema:"excludes"`
ForceRm bool `schema:"forcerm"`
From string `schema:"from"`
GroupAdd []string `schema:"groupadd"`
HTTPProxy bool `schema:"httpproxy"`
IDMappingOptions string `schema:"idmappingoptions"`
IdentityLabel bool `schema:"identitylabel"`
Ignore bool `schema:"ignore"`
InheritLabels types.OptionalBool `schema:"inheritlabels"`
InheritAnnotations types.OptionalBool `schema:"inheritannotations"`
Isolation string `schema:"isolation"`
Jobs int `schema:"jobs"`
LabelOpts string `schema:"labelopts"`
Labels string `schema:"labels"`
LayerLabels []string `schema:"layerLabel"`
Layers bool `schema:"layers"`
LogRusage bool `schema:"rusage"`
Manifest string `schema:"manifest"`
MemSwap int64 `schema:"memswap"`
Memory int64 `schema:"memory"`
NamespaceOptions string `schema:"nsoptions"`
NoCache bool `schema:"nocache"`
NoHosts bool `schema:"nohosts"`
OmitHistory bool `schema:"omithistory"`
OSFeatures []string `schema:"osfeature"`
OSVersion string `schema:"osversion"`
OutputFormat string `schema:"outputformat"`
Platform []string `schema:"platform"`
Pull bool `schema:"pull"`
PullPolicy string `schema:"pullpolicy"`
Quiet bool `schema:"q"`
Registry string `schema:"registry"`
Rm bool `schema:"rm"`
RusageLogFile string `schema:"rusagelogfile"`
Remote string `schema:"remote"`
RewriteTimestamp bool `schema:"rewritetimestamp"`
Retry int `schema:"retry"`
RetryDelay string `schema:"retry-delay"`
Seccomp string `schema:"seccomp"`
Secrets string `schema:"secrets"`
SecurityOpt string `schema:"securityopt"`
ShmSize int `schema:"shmsize"`
SkipUnusedStages bool `schema:"skipunusedstages"`
SourceDateEpoch int64 `schema:"sourcedateepoch"`
Squash bool `schema:"squash"`
TLSVerify bool `schema:"tlsVerify"`
Tags []string `schema:"t"`
Target string `schema:"target"`
Timestamp int64 `schema:"timestamp"`
Ulimits string `schema:"ulimits"`
UnsetEnvs []string `schema:"unsetenv"`
UnsetLabels []string `schema:"unsetlabel"`
UnsetAnnotations []string `schema:"unsetannotation"`
Volumes []string `schema:"volume"`
SBOMOutput string `schema:"sbom-output"`
SBOMPURLOutput string `schema:"sbom-purl-output"`
ImageSBOMOutput string `schema:"sbom-image-output"`
ImageSBOMPURLOutput string `schema:"sbom-image-purl-output"`
ImageSBOM string `schema:"sbom-scanner-image"`
SBOMCommands string `schema:"sbom-scanner-command"`
SBOMMergeStrategy string `schema:"sbom-merge-strategy"`
}
// BuildContext represents processed build context and metadata for container image builds.
type BuildContext struct {
ContextDirectory string
AdditionalBuildContexts map[string]*buildahDefine.AdditionalBuildContext
ContainerFiles []string
IgnoreFile string
}
// genSpaceErr wraps filesystem errors to provide more context for disk space issues.
func genSpaceErr(err error) error {
if errors.Is(err, syscall.ENOSPC) {
return fmt.Errorf("context directory may be too large: %w", err)
}
return err
}
// processCacheReferences processes JSON-encoded lists of repository references for cache operations.
func processCacheReferences(jsonValue, fieldName string, queryValues url.Values) ([]reference.Named, error) {
var result []reference.Named
if _, found := queryValues[fieldName]; found {
var stringList []string
if err := json.Unmarshal([]byte(jsonValue), &stringList); err != nil {
return nil, err
}
var err error
result, err = parse.RepoNamesToNamedReferences(stringList)
if err != nil {
return nil, err
}
}
return result, nil
}
// processCacheFrom processes the cachefrom query parameter for build cache lookup.
func processCacheFrom(query *BuildQuery, queryValues url.Values) ([]reference.Named, error) {
return processCacheReferences(query.CacheFrom, "cachefrom", queryValues)
}
// processCacheTo processes the cacheto query parameter for build cache export.
func processCacheTo(query *BuildQuery, queryValues url.Values) ([]reference.Named, error) {
return processCacheReferences(query.CacheTo, "cacheto", queryValues)
}
// validateContentType validates the Content-Type header and determines if multipart processing is needed.
func validateContentType(r *http.Request) (bool, error) {
multipart := false
if hdr, found := r.Header["Content-Type"]; found && len(hdr) > 0 {
contentType, _, err := mime.ParseMediaType(hdr[0])
if err != nil {
return false, utils.GetBadRequestError("Content-Type", hdr[0], err)
}
switch contentType {
case "application/tar":
logrus.Infof("tar file content type is %s, should use \"application/x-tar\" content type", contentType)
case "application/x-tar":
break
case "multipart/form-data":
logrus.Infof("Received %s", hdr[0])
multipart = true
default:
if utils.IsLibpodRequest(r) {
return false, utils.GetBadRequestError("Content-Type", hdr[0],
fmt.Errorf("Content-Type: %s is not supported. Should be \"application/x-tar\"", hdr[0]))
}
logrus.Infof("tar file content type is %s, should use \"application/x-tar\" content type", contentType)
}
}
return multipart, nil
}
// parseBuildQuery parses HTTP query parameters into a BuildQuery struct with defaults.
func parseBuildQuery(r *http.Request, conf *config.Config, queryValues url.Values) (*BuildQuery, error) {
query := &BuildQuery{
Dockerfile: "Dockerfile",
Registry: "docker.io",
Rm: true,
ShmSize: 64 * 1024 * 1024,
TLSVerify: true,
Retry: int(conf.Engine.Retry),
RetryDelay: conf.Engine.RetryDelay,
}
decoder := utils.GetDecoder(r)
if err := decoder.Decode(query, queryValues); err != nil {
return nil, utils.GetGenericBadRequestError(err)
}
// if layers field not set assume its not from a valid podman-client
// could be a docker client, set `layers=true` since that is the default
// expected behaviour
if !utils.IsLibpodRequest(r) {
if _, found := queryValues["layers"]; !found {
query.Layers = true
}
}
return query, nil
}
// processBuildContext processes build context directory and container files based on request parameters.
func processBuildContext(query url.Values, r *http.Request, buildContext *BuildContext, anchorDir string) (*BuildContext, error) {
dockerFileSet := false
remote := query.Get("remote")
if utils.IsLibpodRequest(r) && remote != "" {
tempDir, subDir, err := buildahDefine.TempDirForURL(anchorDir, "buildah", remote)
if err != nil {
return nil, utils.GetInternalServerError(genSpaceErr(err))
}
if tempDir != "" {
buildContext.ContextDirectory = filepath.Join(tempDir, subDir)
} else {
// Nope, it was local. Use it as is.
absDir, err := filepath.Abs(remote)
if err != nil {
return nil, utils.GetBadRequestError("remote", remote, err)
}
buildContext.ContextDirectory = absDir
}
} else {
if dockerFile := query.Get("dockerfile"); dockerFile != "" {
var m = []string{}
if err := json.Unmarshal([]byte(dockerFile), &m); err != nil {
// it's not json, assume just a string
m = []string{dockerFile}
}
for _, containerfile := range m {
// Add path to containerfile iff it is not URL
if !strings.HasPrefix(containerfile, "http://") && !strings.HasPrefix(containerfile, "https://") {
containerfile = filepath.Join(buildContext.ContextDirectory,
filepath.Clean(filepath.FromSlash(containerfile)))
}
buildContext.ContainerFiles = append(buildContext.ContainerFiles, containerfile)
}
dockerFileSet = true
}
}
if !dockerFileSet {
buildContext.ContainerFiles = []string{filepath.Join(buildContext.ContextDirectory, "Dockerfile")}
if utils.IsLibpodRequest(r) {
buildContext.ContainerFiles = []string{filepath.Join(buildContext.ContextDirectory, "Containerfile")}
if err := fileutils.Exists(buildContext.ContainerFiles[0]); err != nil {
buildContext.ContainerFiles = []string{filepath.Join(buildContext.ContextDirectory, "Dockerfile")}
if err1 := fileutils.Exists(buildContext.ContainerFiles[0]); err1 != nil {
return nil, utils.GetBadRequestError("dockerfile", query.Get("dockerfile"), err1)
}
}
}
}
return buildContext, nil
}
// processSecrets processes build secrets for podman-remote operations.
// Moves secrets outside build context to prevent accidental inclusion in images.
func processSecrets(query *BuildQuery, contextDirectory string, queryValues url.Values) ([]string, error) {
var secrets = []string{}
var m = []string{}
if err := utils.ParseOptionalJSONField(query.Secrets, "secrets", queryValues, &m); err != nil {
return nil, err
}
// for podman-remote all secrets must be picked from context director
// hence modify src so contextdir is added as prefix
for _, secret := range m {
secretOpt := strings.Split(secret, ",")
if len(secretOpt) > 0 {
modifiedOpt := []string{}
for _, token := range secretOpt {
key, val, hasVal := strings.Cut(token, "=")
if hasVal {
if key == "src" {
/* move secret away from contextDir */
/* to make sure we dont accidentally commit temporary secrets to image*/
builderDirectory, _ := filepath.Split(contextDirectory)
// following path is outside build context
newSecretPath := filepath.Join(builderDirectory, val)
oldSecretPath := filepath.Join(contextDirectory, val)
err := os.Rename(oldSecretPath, newSecretPath)
if err != nil {
return nil, err
}
modifiedSrc := fmt.Sprintf("src=%s", newSecretPath)
modifiedOpt = append(modifiedOpt, modifiedSrc)
} else {
modifiedOpt = append(modifiedOpt, token)
}
}
}
secrets = append(secrets, strings.Join(modifiedOpt, ","))
}
}
return secrets, nil
}
// createBuildOptions creates a buildah BuildOptions struct from query parameters and build context.
// WARNING: caller must call the cleanup function if not nil.
func createBuildOptions(query *BuildQuery, buildCtx *BuildContext, queryValues url.Values, r *http.Request) (*buildahDefine.BuildOptions, cleanUpFunc, error) {
identityLabel, _ := utils.ParseOptionalBool(query.IdentityLabel, "identitylabel", queryValues)
// Process various query parameters
addCaps, err := utils.ParseJSONOptionalSlice(query.AdditionalCapabilities, queryValues, "addcaps")
if err != nil {
return nil, nil, utils.GetBadRequestError("addcaps", query.AdditionalCapabilities, err)
}
dropCaps, err := utils.ParseJSONOptionalSlice(query.DropCapabilities, queryValues, "dropcaps")
if err != nil {
return nil, nil, utils.GetBadRequestError("dropcaps", query.DropCapabilities, err)
}
devices, err := utils.ParseJSONOptionalSlice(query.Devices, queryValues, "devices")
if err != nil {
return nil, nil, utils.GetBadRequestError("devices", query.Devices, err)
}
dnsservers, err := utils.ParseJSONOptionalSlice(query.DNSServers, queryValues, "dnsservers")
if err != nil {
return nil, nil, utils.GetBadRequestError("dnsservers", query.DNSServers, err)
}
dnsoptions, err := utils.ParseJSONOptionalSlice(query.DNSOptions, queryValues, "dnsoptions")
if err != nil {
return nil, nil, utils.GetBadRequestError("dnsoptions", query.DNSOptions, err)
}
dnssearch, err := utils.ParseJSONOptionalSlice(query.DNSSearch, queryValues, "dnssearch")
if err != nil {
return nil, nil, utils.GetBadRequestError("dnssearch", query.DNSSearch, err)
}
secrets, err := processSecrets(query, buildCtx.ContextDirectory, queryValues)
if err != nil {
return nil, nil, utils.GetBadRequestError("secrets", query.Secrets, err)
}
addhosts, err := utils.ParseJSONOptionalSlice(query.AddHosts, queryValues, "extrahosts")
if err != nil {
return nil, nil, utils.GetBadRequestError("extrahosts", query.AddHosts, err)
}
compatVolumes, _ := utils.ParseOptionalBool(query.CompatVolumes, "compatvolumes", queryValues)
compression := archive.Compression(query.Compression)
// Process tags
tags := query.Tags
var output string
var additionalTags []string
if len(tags) > 0 {
possiblyNormalizedName, err := utils.NormalizeToDockerHub(r, tags[0])
if err != nil {
return nil, nil, utils.GetInternalServerError(fmt.Errorf("normalizing image: %w", err))
}
output = possiblyNormalizedName
for i := 1; i < len(tags); i++ {
possiblyNormalizedTag, err := utils.NormalizeToDockerHub(r, tags[i])
if err != nil {
return nil, nil, utils.GetInternalServerError(fmt.Errorf("normalizing image: %w", err))
}
additionalTags = append(additionalTags, possiblyNormalizedTag)
}
}
// Process build format and isolation
format := buildah.Dockerv2ImageManifest
registry := query.Registry
isolation := buildah.IsolationDefault
if utils.IsLibpodRequest(r) {
var err error
isolation, err = parseLibPodIsolation(query.Isolation)
if err != nil {
return nil, nil, utils.GetInternalServerError(fmt.Errorf("failed to parse isolation: %w", err))
}
// Make sure to force rootless as rootless otherwise buildah runs code which is intended to be run only as root.
// Same the other way around: https://github.com/containers/podman/issues/22109
switch isolation {
case buildah.IsolationOCI:
if rootless.IsRootless() {
isolation = buildah.IsolationOCIRootless
}
case buildah.IsolationOCIRootless:
if !rootless.IsRootless() {
isolation = buildah.IsolationOCI
}
}
registry = ""
format = query.OutputFormat
} else {
if _, found := queryValues["isolation"]; found {
if query.Isolation != "" && query.Isolation != "default" {
logrus.Debugf("invalid `isolation` parameter: %q", query.Isolation)
}
}
}
// Process IDMapping
var idMappingOptions buildahDefine.IDMappingOptions
if err := utils.ParseOptionalJSONField(query.IDMappingOptions, "idmappingoptions", queryValues, &idMappingOptions); err != nil {
return nil, nil, utils.GetBadRequestError("idmappingoptions", query.IDMappingOptions, err)
}
// Process cache options
cacheFrom, err := processCacheFrom(query, queryValues)
if err != nil {
return nil, nil, utils.GetBadRequestError("cachefrom", query.CacheFrom, err)
}
cacheTo, err := processCacheTo(query, queryValues)
if err != nil {
return nil, nil, utils.GetBadRequestError("cacheTo", query.CacheTo, err)
}
var cacheTTL time.Duration
if _, found := queryValues["cachettl"]; found {
cacheTTL, err = time.ParseDuration(query.CacheTTL)
if err != nil {
return nil, nil, utils.GetBadRequestError("cachettl", query.CacheTTL, err)
}
}
// Process build args
var buildArgs = map[string]string{}
if err := utils.ParseOptionalJSONField(query.BuildArgs, "buildargs", queryValues, &buildArgs); err != nil {
return nil, nil, utils.GetBadRequestError("buildargs", query.BuildArgs, err)
}
// Process excludes
var excludes = []string{}
if err := utils.ParseOptionalJSONField(query.Excludes, "excludes", queryValues, &excludes); err != nil {
return nil, nil, utils.GetBadRequestError("excludes", query.Excludes, err)
}
// Process annotations
var annotations = []string{}
if err := utils.ParseOptionalJSONField(query.Annotations, "annotations", queryValues, &annotations); err != nil {
return nil, nil, utils.GetBadRequestError("annotations", query.Annotations, err)
}
// Process CPP flags
var cppflags = []string{}
if err := utils.ParseOptionalJSONField(query.CPPFlags, "cppflags", queryValues, &cppflags); err != nil {
return nil, nil, utils.GetBadRequestError("cppflags", query.CPPFlags, err)
}
// Process namespace options
nsoptions := buildah.NamespaceOptions{}
if _, found := queryValues["nsoptions"]; found {
if err := utils.ParseOptionalJSONField(query.NamespaceOptions, "nsoptions", queryValues, &nsoptions); err != nil {
return nil, nil, utils.GetBadRequestError("nsoptions", query.NamespaceOptions, err)
}
} else {
nsoptions = append(nsoptions, buildah.NamespaceOption{
Name: string(specs.NetworkNamespace),
Host: true,
})
}
// Process labels
var labels = []string{}
if _, found := queryValues["labels"]; found {
makeLabels := make(map[string]string)
err := json.Unmarshal([]byte(query.Labels), &makeLabels)
if err == nil {
for k, v := range makeLabels {
labels = append(labels, k+"="+v)
}
} else {
if err := json.Unmarshal([]byte(query.Labels), &labels); err != nil {
return nil, nil, utils.GetBadRequestError("labels", query.Labels, err)
}
}
}
jobs := 1
if _, found := queryValues["jobs"]; found {
jobs = query.Jobs
}
// Process security options
var (
labelOpts = []string{}
seccomp string
apparmor string
)
if utils.IsLibpodRequest(r) {
seccomp = query.Seccomp
apparmor = query.AppArmor
// convert labelopts formats
if err := utils.ParseOptionalJSONField(query.LabelOpts, "labelopts", queryValues, &labelOpts); err != nil {
return nil, nil, utils.GetBadRequestError("labelopts", query.LabelOpts, err)
}
} else {
// handle security-opt
var securityOpts = []string{}
if err := utils.ParseOptionalJSONField(query.SecurityOpt, "securityopt", queryValues, &securityOpts); err != nil {
return nil, nil, utils.GetBadRequestError("securityopt", query.SecurityOpt, err)
}
for _, opt := range securityOpts {
if opt == "no-new-privileges" {
return nil, nil, utils.GetBadRequestError("securityopt", opt, fmt.Errorf("no-new-privileges is not supported"))
}
name, value, hasValue := strings.Cut(opt, "=")
if !hasValue {
return nil, nil, utils.GetBadRequestError("securityopt", opt, fmt.Errorf("invalid --security-opt name=value pair: %q", opt))
}
switch name {
case "label":
labelOpts = append(labelOpts, value)
case "apparmor":
apparmor = value
case "seccomp":
seccomp = value
default:
return nil, nil, utils.GetBadRequestError("securityopt", opt, fmt.Errorf("invalid --security-opt 2: %q", opt))
}
}
}
// Process ulimits
var ulimits = []string{}
if err := utils.ParseOptionalJSONField(query.Ulimits, "ulimits", queryValues, &ulimits); err != nil {
return nil, nil, utils.GetBadRequestError("ulimits", query.Ulimits, err)
}
// Process pull policy
pullPolicy := buildahDefine.PullIfMissing
if utils.IsLibpodRequest(r) {
pullPolicy = buildahDefine.PolicyMap[query.PullPolicy]
} else {
if _, found := queryValues["pull"]; found {
if query.Pull {
pullPolicy = buildahDefine.PullAlways
}
}
}
// Get authentication
creds, authfile, err := auth.GetCredentials(r)
if err != nil {
// Credential value(s) not returned as their value is not human readable
return nil, nil, utils.GetGenericBadRequestError(err)
}
// this smells
cleanup := func() {
auth.RemoveAuthfile(authfile)
}
// Process from image
fromImage := query.From
if fromImage != "" {
possiblyNormalizedName, err := utils.NormalizeToDockerHub(r, fromImage)
if err != nil {
return nil, cleanup, utils.GetInternalServerError(fmt.Errorf("normalizing image: %w", err))
}
fromImage = possiblyNormalizedName
}
// Create system context
systemContext := &types.SystemContext{
AuthFilePath: authfile,
DockerAuthConfig: creds,
}
if err := utils.PossiblyEnforceDockerHub(r, systemContext); err != nil {
return nil, cleanup, utils.GetInternalServerError(fmt.Errorf("checking to enforce DockerHub: %w", err))
}
skipUnusedStages, _ := utils.ParseOptionalBool(query.SkipUnusedStages, "skipunusedstages", queryValues)
if _, found := queryValues["tlsVerify"]; found {
systemContext.DockerInsecureSkipTLSVerify = types.NewOptionalBool(!query.TLSVerify)
systemContext.OCIInsecureSkipTLSVerify = !query.TLSVerify
systemContext.DockerDaemonInsecureSkipTLSVerify = !query.TLSVerify
}
// Process retry delay
retryDelay := 2 * time.Second
if query.RetryDelay != "" {
retryDelay, err = time.ParseDuration(query.RetryDelay)
if err != nil {
return nil, cleanup, utils.GetBadRequestError("retry-delay", query.RetryDelay, err)
}
}
var sbomScanOptions []buildahDefine.SBOMScanOptions
if query.ImageSBOM != "" ||
query.SBOMOutput != "" ||
query.ImageSBOMOutput != "" ||
query.SBOMPURLOutput != "" ||
query.ImageSBOMPURLOutput != "" ||
query.SBOMCommands != "" ||
query.SBOMMergeStrategy != "" {
sbomScanOption := &buildahDefine.SBOMScanOptions{
SBOMOutput: query.SBOMOutput,
PURLOutput: query.SBOMPURLOutput,
ImageSBOMOutput: query.ImageSBOMOutput,
ImagePURLOutput: query.ImageSBOMPURLOutput,
Image: query.ImageSBOM,
MergeStrategy: buildahDefine.SBOMMergeStrategy(query.SBOMMergeStrategy),
PullPolicy: pullPolicy,
}
if _, found := r.URL.Query()["sbom-scanner-command"]; found {
var m = []string{}
if err := json.Unmarshal([]byte(query.SBOMCommands), &m); err != nil {
return nil, cleanup, utils.GetBadRequestError("sbom-scanner-command", query.SBOMCommands, err)
}
sbomScanOption.Commands = m
}
if !slices.Contains(sbomScanOption.ContextDir, buildCtx.ContextDirectory) {
sbomScanOption.ContextDir = append(sbomScanOption.ContextDir, buildCtx.ContextDirectory)
}
for _, abc := range buildCtx.AdditionalBuildContexts {
if !abc.IsURL && !abc.IsImage {
sbomScanOption.ContextDir = append(sbomScanOption.ContextDir, abc.Value)
}
}
sbomScanOptions = append(sbomScanOptions, *sbomScanOption)
}
// Create build options
buildOptions := &buildahDefine.BuildOptions{
AddCapabilities: addCaps,
AdditionalBuildContexts: buildCtx.AdditionalBuildContexts,
AdditionalTags: additionalTags,
Annotations: annotations,
CPPFlags: cppflags,
CacheFrom: cacheFrom,
CacheTo: cacheTo,
CacheTTL: cacheTTL,
Args: buildArgs,
AllPlatforms: query.AllPlatforms,
CommonBuildOpts: &buildah.CommonBuildOptions{
AddHost: addhosts,
ApparmorProfile: apparmor,
CPUPeriod: query.CpuPeriod,
CPUQuota: query.CpuQuota,
CPUSetCPUs: query.CpuSetCpus,
CPUSetMems: query.CpuSetMems,
CPUShares: query.CpuShares,
CgroupParent: query.CgroupParent,
DNSOptions: dnsoptions,
DNSSearch: dnssearch,
DNSServers: dnsservers,
HTTPProxy: query.HTTPProxy,
IdentityLabel: identityLabel,
LabelOpts: labelOpts,
Memory: query.Memory,
MemorySwap: query.MemSwap,
NoHosts: query.NoHosts,
OmitHistory: query.OmitHistory,
SeccompProfilePath: seccomp,
ShmSize: strconv.Itoa(query.ShmSize),
Ulimit: ulimits,
Secrets: secrets,
Volumes: query.Volumes,
},
CompatVolumes: compatVolumes,
CreatedAnnotation: query.CreatedAnnotation,
Compression: compression,
ConfigureNetwork: parseNetworkConfigurationPolicy(query.ConfigureNetwork),
ContextDirectory: buildCtx.ContextDirectory,
Devices: devices,
DropCapabilities: dropCaps,
Envs: query.Envs,
Excludes: excludes,
ForceRmIntermediateCtrs: query.ForceRm,
GroupAdd: query.GroupAdd,
From: fromImage,
IDMappingOptions: &idMappingOptions,
IgnoreUnrecognizedInstructions: query.Ignore,
IgnoreFile: buildCtx.IgnoreFile,
InheritLabels: query.InheritLabels,
InheritAnnotations: query.InheritAnnotations,
Isolation: isolation,
Jobs: &jobs,
Labels: labels,
LayerLabels: query.LayerLabels,
Layers: query.Layers,
LogRusage: query.LogRusage,
Manifest: query.Manifest,
MaxPullPushRetries: query.Retry,
NamespaceOptions: nsoptions,
NoCache: query.NoCache,
OSFeatures: query.OSFeatures,
OSVersion: query.OSVersion,
Output: output,
OutputFormat: format,
PullPolicy: pullPolicy,
PullPushRetryDelay: retryDelay,
Quiet: query.Quiet,
Registry: registry,
RemoveIntermediateCtrs: query.Rm,
RewriteTimestamp: query.RewriteTimestamp,
RusageLogFile: query.RusageLogFile,
SkipUnusedStages: skipUnusedStages,
Squash: query.Squash,
SystemContext: systemContext,
Target: query.Target,
UnsetEnvs: query.UnsetEnvs,
UnsetLabels: query.UnsetLabels,
UnsetAnnotations: query.UnsetAnnotations,
SBOMScanOptions: sbomScanOptions,
}
// Process platforms
platforms := query.Platform
if len(platforms) == 1 {
// Docker API uses comma separated platform arg so match this here
platforms = strings.Split(query.Platform[0], ",")
}
for _, platformSpec := range platforms {
os, arch, variant, err := parse.Platform(platformSpec)
if err != nil {
return nil, cleanup, utils.GetBadRequestError("platform", platformSpec, err)
}
buildOptions.Platforms = append(buildOptions.Platforms, struct{ OS, Arch, Variant string }{
OS: os,
Arch: arch,
Variant: variant,
})
}
// Process timestamps
if _, found := queryValues["sourcedateepoch"]; found {
ts := time.Unix(query.SourceDateEpoch, 0)
buildOptions.SourceDateEpoch = &ts
}
if _, found := queryValues["timestamp"]; found {
ts := time.Unix(query.Timestamp, 0)
buildOptions.Timestamp = &ts
}
return buildOptions, cleanup, nil
}
// executeBuild performs the container build operation and streams results to the client.
func executeBuild(runtime *libpod.Runtime, w http.ResponseWriter, r *http.Request, buildOptions *buildahDefine.BuildOptions, containerFiles []string, query *BuildQuery) {
// Channels all mux'ed in select{} below to follow API build protocol
stdout := channel.NewWriter(make(chan []byte))
defer stdout.Close()
auxout := channel.NewWriter(make(chan []byte))
defer auxout.Close()
stderr := channel.NewWriter(make(chan []byte))
defer stderr.Close()
reporter := channel.NewWriter(make(chan []byte))
defer reporter.Close()
// Set output channels
buildOptions.Err = auxout
buildOptions.Out = stdout
buildOptions.ReportWriter = reporter
var (
imageID string
success bool
)
runCtx, cancel := context.WithCancel(r.Context())
go func() {
defer cancel()
var err error
imageID, _, err = runtime.Build(r.Context(), *buildOptions, containerFiles...)
if err == nil {
success = true
} else {
stderr.Write([]byte(err.Error() + "\n"))
}
}()
// Send headers and prime client for stream to come
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
sender := utils.NewBuildResponseSender(w)
var stepErrors []string
for {
select {
case e := <-stdout.Chan():
sender.SendBuildStream(string(e))
case e := <-reporter.Chan():
sender.SendBuildStream(string(e))
case e := <-auxout.Chan():
if !query.Quiet {
sender.SendBuildStream(string(e))
} else {
stepErrors = append(stepErrors, string(e))
}
case e := <-stderr.Chan():
// Docker-API Compat parity : Build failed so
// output all step errors irrespective of quiet
// flag.
for _, stepError := range stepErrors {
sender.SendBuildStream(stepError)
}
sender.SendBuildError(string(e))
return
case <-runCtx.Done():
if success {
if !utils.IsLibpodRequest(r) && !query.Quiet {
sender.SendBuildAux(fmt.Appendf(nil, `{"ID":"sha256:%s"}`, imageID))
sender.SendBuildStream(fmt.Sprintf("Successfully built %12.12s\n", imageID))
for _, tag := range query.Tags {
sender.SendBuildStream(fmt.Sprintf("Successfully tagged %s\n", tag))
}
}
}
return
case <-r.Context().Done():
cancel()
logrus.Infof("Client disconnect reported for build %q / %q.", buildOptions.Registry, query.Dockerfile)
return
}
}
}
func BuildImage(w http.ResponseWriter, r *http.Request) {
// Create temporary directory for build context
anchorDir, err := os.MkdirTemp(parse.GetTempDir(), "libpod_builder")
if err != nil {
utils.InternalServerError(w, err)
return
}
defer func() {
if logrus.IsLevelEnabled(logrus.DebugLevel) {
if v, found := os.LookupEnv("PODMAN_RETAIN_BUILD_ARTIFACT"); found {
if keep, _ := strconv.ParseBool(v); keep {
return
}
}
}
err := os.RemoveAll(anchorDir)
if err != nil {
logrus.Warn(fmt.Errorf("failed to remove build scratch directory %q: %w", anchorDir, err))
}
}()
// If we have a multipart we use the operations, if not default extraction for main context
// Validate content type
multipart, err := validateContentType(r)
if err != nil {
utils.ProcessBuildError(w, err)
return
}
queryValues := r.URL.Query()
buildContext, err := getBuildContext(r, queryValues, anchorDir, multipart)
if err != nil {
utils.ProcessBuildError(w, err)
return
}
runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime)
conf, err := runtime.GetConfigNoCopy()
if err != nil {
utils.InternalServerError(w, err)
return
}
query, err := parseBuildQuery(r, conf, queryValues)
if err != nil {
utils.ProcessBuildError(w, err)
return
}
// Create build options
buildOptions, cleanup, err := createBuildOptions(query, buildContext, queryValues, r)
if cleanup != nil {
defer cleanup()
}
if err != nil {
utils.ProcessBuildError(w, err)
return
}
// Execute build
executeBuild(runtime, w, r, buildOptions, buildContext.ContainerFiles, query)
}
// getBuildContext processes build contexts from HTTP request to a BuildContext struct.
func getBuildContext(r *http.Request, query url.Values, anchorDir string, multipart bool) (*BuildContext, error) {
// Handle build contexts (extract from tar/multipart)
buildContext, err := handleBuildContexts(r, query, anchorDir, multipart)
if err != nil {
return nil, utils.GetInternalServerError(genSpaceErr(err))
}
// Process build context and container files
buildContext, err = processBuildContext(query, r, buildContext, anchorDir)
if err != nil {
return nil, err
}
// Process dockerignore
_, ignoreFile, err := util.ParseDockerignore(buildContext.ContainerFiles, buildContext.ContextDirectory)
if err != nil {
return nil, utils.GetInternalServerError(fmt.Errorf("processing ignore file: %w", err))
}
buildContext.IgnoreFile = ignoreFile
return buildContext, nil
}
// handleBuildContexts extracts and processes build contexts from the HTTP request body.
// Supports both single-context builds and multi-context builds with named references.
func handleBuildContexts(r *http.Request, query url.Values, anchorDir string, multipart bool) (*BuildContext, error) {
var err error
out := &BuildContext{
AdditionalBuildContexts: make(map[string]*buildahDefine.AdditionalBuildContext),
}
for _, url := range query["additionalbuildcontexts"] {
name, value, found := strings.Cut(url, "=")
if !found {
return nil, fmt.Errorf("invalid additional build context format: %q", url)
}
logrus.Debugf("name: %q, context: %q", name, value)
if urlValue, ok := strings.CutPrefix(value, "url:"); ok {
tempDir, subdir, err := buildahDefine.TempDirForURL(anchorDir, "buildah", urlValue)
if err != nil {
return nil, fmt.Errorf("downloading URL %q: %w", name, err)
}
contextPath := filepath.Join(tempDir, subdir)
out.AdditionalBuildContexts[name] = &buildahDefine.AdditionalBuildContext{
IsURL: true,
IsImage: false,
Value: contextPath,
DownloadedCache: contextPath,
}
logrus.Debugf("Downloaded URL context %q to %q", name, contextPath)
} else if imageValue, ok := strings.CutPrefix(value, "image:"); ok {
out.AdditionalBuildContexts[name] = &buildahDefine.AdditionalBuildContext{
IsURL: false,
IsImage: true,
Value: imageValue,
}
logrus.Debugf("Using image context %q: %q", name, imageValue)
}
}
if !multipart {
logrus.Debug("No multipart needed")
out.ContextDirectory, err = extractTarFile(anchorDir, r.Body)
if err != nil {
return nil, err
}
return out, nil
}
logrus.Debug("Multipart is needed")
reader, err := r.MultipartReader()
if err != nil {
return nil, fmt.Errorf("failed to create multipart reader: %w", err)
}
for {
part, err := reader.NextPart()
if err == io.EOF {
break
}
if err != nil {
return nil, fmt.Errorf("failed to read multipart: %w", err)
}
defer part.Close()
fieldName := part.FormName()
if fieldName == "MainContext" {
mainDir, err := extractTarFile(anchorDir, part)
if err != nil {
return nil, fmt.Errorf("extracting main context in multipart: %w", err)
}
if mainDir == "" {
return nil, fmt.Errorf("main context directory is empty")
}
out.ContextDirectory = mainDir
} else if contextName, ok := strings.CutPrefix(fieldName, "build-context-"); ok {
// Create temp directory directly under anchorDir
additionalAnchor, err := os.MkdirTemp(anchorDir, contextName+"-*")
if err != nil {
return nil, fmt.Errorf("creating temp directory for additional context %q: %w", contextName, err)
}
if err := chrootarchive.Untar(part, additionalAnchor, nil); err != nil {
return nil, fmt.Errorf("extracting additional context %q: %w", contextName, err)
}
var latestModTime time.Time
fileCount := 0
walkErr := filepath.Walk(additionalAnchor, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
// Skip the root directory itself since it's always going to have the latest timestamp
if path == additionalAnchor {
return nil
}
if !info.IsDir() {
fileCount++
}
// Use any extracted content timestamp (files or subdirectories)
if info.ModTime().After(latestModTime) {
latestModTime = info.ModTime()
}
return nil
})
if walkErr != nil {
return nil, fmt.Errorf("error walking additional context: %w", walkErr)
}
// If we found any files, set the timestamp on the additional context directory
// to the latest modified time found in the files.
if !latestModTime.IsZero() {
if err := os.Chtimes(additionalAnchor, latestModTime, latestModTime); err != nil {
logrus.Warnf("Failed to set timestamp on additional context directory: %v", err)
}
}
out.AdditionalBuildContexts[contextName] = &buildahDefine.AdditionalBuildContext{
IsURL: false,
IsImage: false,
Value: additionalAnchor,
}
} else {
logrus.Debugf("Ignoring unknown multipart field: %s", fieldName)
}
}
return out, nil
}
func parseNetworkConfigurationPolicy(network string) buildah.NetworkConfigurationPolicy {
if val, err := strconv.Atoi(network); err == nil {
return buildah.NetworkConfigurationPolicy(val)
}
switch network {
case "NetworkDefault":
return buildah.NetworkDefault
case "NetworkDisabled":
return buildah.NetworkDisabled
case "NetworkEnabled":
return buildah.NetworkEnabled
default:
return buildah.NetworkDefault
}
}
func parseLibPodIsolation(isolation string) (buildah.Isolation, error) {
if val, err := strconv.Atoi(isolation); err == nil {
return buildah.Isolation(val), nil
}
return parse.IsolationOption(isolation)
}
func extractTarFile(anchorDir string, r io.ReadCloser) (string, error) {
buildDir := filepath.Join(anchorDir, "build")
err := os.Mkdir(buildDir, 0o700)
if err != nil {
return "", err
}
err = archive.Untar(r, buildDir, nil)
return buildDir, err
}