Files
podman/pkg/api/handlers/compat/images_build.go
Paul Holzinger dbfe79757b remote build: set rootless oci isolation correctly
When we run rootless buildah needs to have IsolationOCIRootless set
otherwise it will run code which cannot be used as rootless user.
Podman should use the buildah default if possible and change it to
rootless mode if needed.

[NO NEW TESTS NEEDED] Should be covered by existing tests once we have
podman-remote rootless tests.

Fixes #12989

Signed-off-by: Paul Holzinger <pholzing@redhat.com>
2022-01-24 19:51:59 +01:00

750 lines
23 KiB
Go

package compat
import (
"context"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/containers/buildah"
buildahDefine "github.com/containers/buildah/define"
"github.com/containers/buildah/pkg/parse"
"github.com/containers/image/v5/types"
"github.com/containers/podman/v4/libpod"
"github.com/containers/podman/v4/pkg/api/handlers/utils"
api "github.com/containers/podman/v4/pkg/api/types"
"github.com/containers/podman/v4/pkg/auth"
"github.com/containers/podman/v4/pkg/channel"
"github.com/containers/podman/v4/pkg/rootless"
"github.com/containers/storage/pkg/archive"
"github.com/docker/docker/pkg/jsonmessage"
"github.com/gorilla/schema"
"github.com/opencontainers/runtime-spec/specs-go"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
func BuildImage(w http.ResponseWriter, r *http.Request) {
if hdr, found := r.Header["Content-Type"]; found && len(hdr) > 0 {
contentType := hdr[0]
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
default:
if utils.IsLibpodRequest(r) {
utils.BadRequest(w, "Content-Type", hdr[0],
fmt.Errorf("Content-Type: %s is not supported. Should be \"application/x-tar\"", hdr[0]))
return
}
logrus.Infof("tar file content type is %s, should use \"application/x-tar\" content type", contentType)
}
}
contextDirectory, err := extractTarFile(r)
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(filepath.Dir(contextDirectory))
if err != nil {
logrus.Warn(errors.Wrapf(err, "failed to remove build scratch directory %q", filepath.Dir(contextDirectory)))
}
}()
query := struct {
AddHosts string `schema:"extrahosts"`
AdditionalCapabilities string `schema:"addcaps"`
Annotations string `schema:"annotations"`
AppArmor string `schema:"apparmor"`
AllPlatforms bool `schema:"allplatforms"`
BuildArgs string `schema:"buildargs"`
CacheFrom string `schema:"cachefrom"`
Compression uint64 `schema:"compression"`
ConfigureNetwork string `schema:"networkmode"`
CpuPeriod uint64 `schema:"cpuperiod"` // nolint
CpuQuota int64 `schema:"cpuquota"` // nolint
CpuSetCpus string `schema:"cpusetcpus"` // nolint
CpuSetMems string `schema:"cpusetmems"` // nolint
CpuShares uint64 `schema:"cpushares"` // nolint
CgroupParent string `schema:"cgroupparent"` // nolint
DNSOptions string `schema:"dnsoptions"`
DNSSearch string `schema:"dnssearch"`
DNSServers string `schema:"dnsservers"`
Devices string `schema:"devices"`
Dockerfile string `schema:"dockerfile"`
DropCapabilities string `schema:"dropcaps"`
Excludes string `schema:"excludes"`
ForceRm bool `schema:"forcerm"`
From string `schema:"from"`
HTTPProxy bool `schema:"httpproxy"`
Ignore bool `schema:"ignore"`
Isolation string `schema:"isolation"`
Jobs int `schema:"jobs"` // nolint
LabelOpts string `schema:"labelopts"`
Labels string `schema:"labels"`
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"`
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"`
Seccomp string `schema:"seccomp"`
SecurityOpt string `schema:"securityopt"`
ShmSize int `schema:"shmsize"`
Squash bool `schema:"squash"`
Tags []string `schema:"t"`
Target string `schema:"target"`
Timestamp int64 `schema:"timestamp"`
Ulimits string `schema:"ulimits"`
UnsetEnvs []string `schema:"unsetenv"`
Secrets string `schema:"secrets"`
}{
Dockerfile: "Dockerfile",
Registry: "docker.io",
Rm: true,
ShmSize: 64 * 1024 * 1024,
}
decoder := r.Context().Value(api.DecoderKey).(*schema.Decoder)
if err := decoder.Decode(&query, r.URL.Query()); err != nil {
utils.Error(w, http.StatusBadRequest, err)
return
}
// 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 := r.URL.Query()["layers"]; !found {
query.Layers = true
}
}
// convert tag formats
tags := query.Tags
// convert addcaps formats
var addCaps = []string{}
if _, found := r.URL.Query()["addcaps"]; found {
var m = []string{}
if err := json.Unmarshal([]byte(query.AdditionalCapabilities), &m); err != nil {
utils.BadRequest(w, "addcaps", query.AdditionalCapabilities, err)
return
}
addCaps = m
}
// convert addcaps formats
containerFiles := []string{}
if _, found := r.URL.Query()["dockerfile"]; found {
var m = []string{}
if err := json.Unmarshal([]byte(query.Dockerfile), &m); err != nil {
// it's not json, assume just a string
m = []string{filepath.Join(contextDirectory, query.Dockerfile)}
}
containerFiles = m
} else {
containerFiles = []string{filepath.Join(contextDirectory, "Dockerfile")}
if utils.IsLibpodRequest(r) {
containerFiles = []string{filepath.Join(contextDirectory, "Containerfile")}
if _, err = os.Stat(containerFiles[0]); err != nil {
containerFiles = []string{filepath.Join(contextDirectory, "Dockerfile")}
if _, err1 := os.Stat(containerFiles[0]); err1 != nil {
utils.BadRequest(w, "dockerfile", query.Dockerfile, err)
}
}
}
}
addhosts := []string{}
if _, found := r.URL.Query()["extrahosts"]; found {
if err := json.Unmarshal([]byte(query.AddHosts), &addhosts); err != nil {
utils.BadRequest(w, "extrahosts", query.AddHosts, err)
return
}
}
compression := archive.Compression(query.Compression)
// convert dropcaps formats
var dropCaps = []string{}
if _, found := r.URL.Query()["dropcaps"]; found {
var m = []string{}
if err := json.Unmarshal([]byte(query.DropCapabilities), &m); err != nil {
utils.BadRequest(w, "dropcaps", query.DropCapabilities, err)
return
}
dropCaps = m
}
// convert devices formats
var devices = []string{}
if _, found := r.URL.Query()["devices"]; found {
var m = []string{}
if err := json.Unmarshal([]byte(query.Devices), &m); err != nil {
utils.BadRequest(w, "devices", query.Devices, err)
return
}
devices = m
}
var dnsservers = []string{}
if _, found := r.URL.Query()["dnsservers"]; found {
var m = []string{}
if err := json.Unmarshal([]byte(query.DNSServers), &m); err != nil {
utils.BadRequest(w, "dnsservers", query.DNSServers, err)
return
}
dnsservers = m
}
var dnsoptions = []string{}
if _, found := r.URL.Query()["dnsoptions"]; found {
var m = []string{}
if err := json.Unmarshal([]byte(query.DNSOptions), &m); err != nil {
utils.BadRequest(w, "dnsoptions", query.DNSOptions, err)
return
}
dnsoptions = m
}
var dnssearch = []string{}
if _, found := r.URL.Query()["dnssearch"]; found {
var m = []string{}
if err := json.Unmarshal([]byte(query.DNSSearch), &m); err != nil {
utils.BadRequest(w, "dnssearches", query.DNSSearch, err)
return
}
dnssearch = m
}
var secrets = []string{}
if _, found := r.URL.Query()["secrets"]; found {
var m = []string{}
if err := json.Unmarshal([]byte(query.Secrets), &m); err != nil {
utils.BadRequest(w, "secrets", query.Secrets, err)
return
}
// 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 {
arr := strings.SplitN(token, "=", 2)
if len(arr) > 1 {
if arr[0] == "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, arr[1])
oldSecretPath := filepath.Join(contextDirectory, arr[1])
err := os.Rename(oldSecretPath, newSecretPath)
if err != nil {
utils.BadRequest(w, "secrets", query.Secrets, err)
return
}
modifiedSrc := fmt.Sprintf("src=%s", newSecretPath)
modifiedOpt = append(modifiedOpt, modifiedSrc)
} else {
modifiedOpt = append(modifiedOpt, token)
}
}
}
secrets = append(secrets, strings.Join(modifiedOpt[:], ","))
}
}
}
var output string
if len(tags) > 0 {
possiblyNormalizedName, err := utils.NormalizeToDockerHub(r, tags[0])
if err != nil {
utils.Error(w, http.StatusInternalServerError, errors.Wrap(err, "error normalizing image"))
return
}
output = possiblyNormalizedName
}
format := buildah.Dockerv2ImageManifest
registry := query.Registry
isolation := buildah.IsolationDefault
if utils.IsLibpodRequest(r) {
var err error
isolation, err = parseLibPodIsolation(query.Isolation)
if err != nil {
utils.Error(w, http.StatusInternalServerError, errors.Wrap(err, "failed to parse isolation"))
return
}
// make sure to force rootless as rootless otherwise buildah runs code which is intended to be run only as root.
if isolation == buildah.IsolationOCI && rootless.IsRootless() {
isolation = buildah.IsolationOCIRootless
}
registry = ""
format = query.OutputFormat
} else {
if _, found := r.URL.Query()["isolation"]; found {
if query.Isolation != "" && query.Isolation != "default" {
logrus.Debugf("invalid `isolation` parameter: %q", query.Isolation)
}
}
}
var additionalTags []string // nolint
for i := 1; i < len(tags); i++ {
possiblyNormalizedTag, err := utils.NormalizeToDockerHub(r, tags[i])
if err != nil {
utils.Error(w, http.StatusInternalServerError, errors.Wrap(err, "error normalizing image"))
return
}
additionalTags = append(additionalTags, possiblyNormalizedTag)
}
var buildArgs = map[string]string{}
if _, found := r.URL.Query()["buildargs"]; found {
if err := json.Unmarshal([]byte(query.BuildArgs), &buildArgs); err != nil {
utils.BadRequest(w, "buildargs", query.BuildArgs, err)
return
}
}
var excludes = []string{}
if _, found := r.URL.Query()["excludes"]; found {
if err := json.Unmarshal([]byte(query.Excludes), &excludes); err != nil {
utils.BadRequest(w, "excludes", query.Excludes, err)
return
}
}
// convert annotations formats
var annotations = []string{}
if _, found := r.URL.Query()["annotations"]; found {
if err := json.Unmarshal([]byte(query.Annotations), &annotations); err != nil {
utils.BadRequest(w, "annotations", query.Annotations, err)
return
}
}
// convert nsoptions formats
nsoptions := buildah.NamespaceOptions{}
if _, found := r.URL.Query()["nsoptions"]; found {
if err := json.Unmarshal([]byte(query.NamespaceOptions), &nsoptions); err != nil {
utils.BadRequest(w, "nsoptions", query.NamespaceOptions, err)
return
}
} else {
nsoptions = append(nsoptions, buildah.NamespaceOption{
Name: string(specs.NetworkNamespace),
Host: true,
})
}
// convert label formats
var labels = []string{}
if _, found := r.URL.Query()["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 {
utils.BadRequest(w, "labels", query.Labels, err)
return
}
}
}
jobs := 1
if _, found := r.URL.Query()["jobs"]; found {
jobs = query.Jobs
}
var (
labelOpts = []string{}
seccomp string
apparmor string
)
if utils.IsLibpodRequest(r) {
seccomp = query.Seccomp
apparmor = query.AppArmor
// convert labelopts formats
if _, found := r.URL.Query()["labelopts"]; found {
var m = []string{}
if err := json.Unmarshal([]byte(query.LabelOpts), &m); err != nil {
utils.BadRequest(w, "labelopts", query.LabelOpts, err)
return
}
labelOpts = m
}
} else {
// handle security-opt
if _, found := r.URL.Query()["securityopt"]; found {
var securityOpts = []string{}
if err := json.Unmarshal([]byte(query.SecurityOpt), &securityOpts); err != nil {
utils.BadRequest(w, "securityopt", query.SecurityOpt, err)
return
}
for _, opt := range securityOpts {
if opt == "no-new-privileges" {
utils.BadRequest(w, "securityopt", query.SecurityOpt, errors.New("no-new-privileges is not supported"))
return
}
con := strings.SplitN(opt, "=", 2)
if len(con) != 2 {
utils.BadRequest(w, "securityopt", query.SecurityOpt, errors.Errorf("Invalid --security-opt name=value pair: %q", opt))
return
}
switch con[0] {
case "label":
labelOpts = append(labelOpts, con[1])
case "apparmor":
apparmor = con[1]
case "seccomp":
seccomp = con[1]
default:
utils.BadRequest(w, "securityopt", query.SecurityOpt, errors.Errorf("Invalid --security-opt 2: %q", opt))
return
}
}
}
}
// convert ulimits formats
var ulimits = []string{}
if _, found := r.URL.Query()["ulimits"]; found {
var m = []string{}
if err := json.Unmarshal([]byte(query.Ulimits), &m); err != nil {
utils.BadRequest(w, "ulimits", query.Ulimits, err)
return
}
ulimits = m
}
pullPolicy := buildahDefine.PullIfMissing
if utils.IsLibpodRequest(r) {
pullPolicy = buildahDefine.PolicyMap[query.PullPolicy]
} else {
if _, found := r.URL.Query()["pull"]; found {
if query.Pull {
pullPolicy = buildahDefine.PullAlways
}
}
}
creds, authfile, err := auth.GetCredentials(r)
if err != nil {
// Credential value(s) not returned as their value is not human readable
utils.Error(w, http.StatusBadRequest, err)
return
}
defer auth.RemoveAuthfile(authfile)
fromImage := query.From
if fromImage != "" {
possiblyNormalizedName, err := utils.NormalizeToDockerHub(r, fromImage)
if err != nil {
utils.Error(w, http.StatusInternalServerError, errors.Wrap(err, "error normalizing image"))
return
}
fromImage = possiblyNormalizedName
}
systemContext := &types.SystemContext{
AuthFilePath: authfile,
DockerAuthConfig: creds,
}
utils.PossiblyEnforceDockerHub(r, systemContext)
// 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()
runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime)
buildOptions := buildahDefine.BuildOptions{
AddCapabilities: addCaps,
AdditionalTags: additionalTags,
Annotations: annotations,
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,
LabelOpts: labelOpts,
Memory: query.Memory,
MemorySwap: query.MemSwap,
SeccompProfilePath: seccomp,
ShmSize: strconv.Itoa(query.ShmSize),
Ulimit: ulimits,
Secrets: secrets,
},
Compression: compression,
ConfigureNetwork: parseNetworkConfigurationPolicy(query.ConfigureNetwork),
ContextDirectory: contextDirectory,
Devices: devices,
DropCapabilities: dropCaps,
Err: auxout,
Excludes: excludes,
ForceRmIntermediateCtrs: query.ForceRm,
From: fromImage,
IgnoreUnrecognizedInstructions: query.Ignore,
Isolation: isolation,
Jobs: &jobs,
Labels: labels,
Layers: query.Layers,
LogRusage: query.LogRusage,
Manifest: query.Manifest,
MaxPullPushRetries: 3,
NamespaceOptions: nsoptions,
NoCache: query.NoCache,
Out: stdout,
Output: output,
OutputFormat: format,
PullPolicy: pullPolicy,
PullPushRetryDelay: time.Duration(2 * time.Second),
Quiet: query.Quiet,
Registry: registry,
RemoveIntermediateCtrs: query.Rm,
ReportWriter: reporter,
RusageLogFile: query.RusageLogFile,
Squash: query.Squash,
Target: query.Target,
SystemContext: systemContext,
UnsetEnvs: query.UnsetEnvs,
}
for _, platformSpec := range query.Platform {
os, arch, variant, err := parse.Platform(platformSpec)
if err != nil {
utils.BadRequest(w, "platform", platformSpec, err)
return
}
buildOptions.Platforms = append(buildOptions.Platforms, struct{ OS, Arch, Variant string }{
OS: os,
Arch: arch,
Variant: variant,
})
}
if _, found := r.URL.Query()["timestamp"]; found {
ts := time.Unix(query.Timestamp, 0)
buildOptions.Timestamp = &ts
}
var (
imageID string
success bool
)
runCtx, cancel := context.WithCancel(context.Background())
go func() {
defer cancel()
imageID, _, err = runtime.Build(r.Context(), buildOptions, containerFiles...)
if err == nil {
success = true
} else {
stderr.Write([]byte(err.Error() + "\n"))
}
}()
flush := func() {
if flusher, ok := w.(http.Flusher); ok {
flusher.Flush()
}
}
// Send headers and prime client for stream to come
w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", "application/json")
flush()
body := w.(io.Writer)
if logrus.IsLevelEnabled(logrus.DebugLevel) {
if v, found := os.LookupEnv("PODMAN_RETAIN_BUILD_ARTIFACT"); found {
if keep, _ := strconv.ParseBool(v); keep {
t, _ := ioutil.TempFile("", "build_*_server")
defer t.Close()
body = io.MultiWriter(t, w)
}
}
}
enc := json.NewEncoder(body)
enc.SetEscapeHTML(true)
for {
m := struct {
Stream string `json:"stream,omitempty"`
Error *jsonmessage.JSONError `json:"errorDetail,omitempty"`
// NOTE: `error` is being deprecated check https://github.com/moby/moby/blob/master/pkg/jsonmessage/jsonmessage.go#L148
ErrorMessage string `json:"error,omitempty"` // deprecate this slowly
Aux json.RawMessage `json:"aux,omitempty"`
}{}
select {
case e := <-stdout.Chan():
m.Stream = string(e)
if err := enc.Encode(m); err != nil {
stderr.Write([]byte(err.Error()))
}
flush()
case e := <-reporter.Chan():
m.Stream = string(e)
if err := enc.Encode(m); err != nil {
stderr.Write([]byte(err.Error()))
}
flush()
case e := <-auxout.Chan():
m.Stream = string(e)
if err := enc.Encode(m); err != nil {
stderr.Write([]byte(err.Error()))
}
flush()
case e := <-stderr.Chan():
m.ErrorMessage = string(e)
m.Error = &jsonmessage.JSONError{
Message: m.ErrorMessage,
}
if err := enc.Encode(m); err != nil {
logrus.Warnf("Failed to json encode error %v", err)
}
flush()
return
case <-runCtx.Done():
if success {
if !utils.IsLibpodRequest(r) && !query.Quiet {
m.Aux = []byte(fmt.Sprintf(`{"ID":"sha256:%s"}`, imageID))
if err := enc.Encode(m); err != nil {
logrus.Warnf("failed to json encode error %v", err)
}
m.Aux = nil
m.Stream = fmt.Sprintf("Successfully built %12.12s\n", imageID)
if err := enc.Encode(m); err != nil {
logrus.Warnf("Failed to json encode error %v", err)
}
flush()
for _, tag := range tags {
m.Stream = fmt.Sprintf("Successfully tagged %s\n", tag)
if err := enc.Encode(m); err != nil {
logrus.Warnf("Failed to json encode error %v", err)
}
flush()
}
}
}
flush()
return
case <-r.Context().Done():
cancel()
logrus.Infof("Client disconnect reported for build %q / %q.", registry, query.Dockerfile)
return
}
}
}
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) { // nolint
if val, err := strconv.Atoi(isolation); err == nil {
return buildah.Isolation(val), nil
}
return parse.IsolationOption(isolation)
}
func extractTarFile(r *http.Request) (string, error) {
// build a home for the request body
anchorDir, err := ioutil.TempDir("", "libpod_builder")
if err != nil {
return "", err
}
path := filepath.Join(anchorDir, "tarBall")
tarBall, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
return "", err
}
defer tarBall.Close()
// Content-Length not used as too many existing API clients didn't honor it
_, err = io.Copy(tarBall, r.Body)
r.Body.Close()
if err != nil {
return "", fmt.Errorf("failed Request: Unable to copy tar file from request body %s", r.RequestURI)
}
buildDir := filepath.Join(anchorDir, "build")
err = os.Mkdir(buildDir, 0700)
if err != nil {
return "", err
}
_, _ = tarBall.Seek(0, 0)
err = archive.Untar(tarBall, buildDir, nil)
return buildDir, err
}