podman/pkg/bindings/images/build.go

231 lines
5.3 KiB
Go

package images
import (
"archive/tar"
"context"
"encoding/json"
"io"
"io/ioutil"
"net/http"
"net/url"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"github.com/containers/buildah"
"github.com/containers/podman/v2/pkg/bindings"
"github.com/containers/podman/v2/pkg/domain/entities"
"github.com/docker/go-units"
"github.com/hashicorp/go-multierror"
jsoniter "github.com/json-iterator/go"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
// Build creates an image using a containerfile reference
func Build(ctx context.Context, containerFiles []string, options entities.BuildOptions) (*entities.BuildReport, error) {
params := url.Values{}
if t := options.Output; len(t) > 0 {
params.Set("t", t)
}
for _, tag := range options.AdditionalTags {
params.Add("t", tag)
}
if options.Quiet {
params.Set("q", "1")
}
if options.NoCache {
params.Set("nocache", "1")
}
// TODO cachefrom
if options.PullPolicy == buildah.PullAlways {
params.Set("pull", "1")
}
if options.RemoveIntermediateCtrs {
params.Set("rm", "1")
}
if options.ForceRmIntermediateCtrs {
params.Set("forcerm", "1")
}
if mem := options.CommonBuildOpts.Memory; mem > 0 {
params.Set("memory", strconv.Itoa(int(mem)))
}
if memSwap := options.CommonBuildOpts.MemorySwap; memSwap > 0 {
params.Set("memswap", strconv.Itoa(int(memSwap)))
}
if cpuShares := options.CommonBuildOpts.CPUShares; cpuShares > 0 {
params.Set("cpushares", strconv.Itoa(int(cpuShares)))
}
if cpuSetCpus := options.CommonBuildOpts.CPUSetCPUs; len(cpuSetCpus) > 0 {
params.Set("cpusetcpus", cpuSetCpus)
}
if cpuPeriod := options.CommonBuildOpts.CPUPeriod; cpuPeriod > 0 {
params.Set("cpuperiod", strconv.Itoa(int(cpuPeriod)))
}
if cpuQuota := options.CommonBuildOpts.CPUQuota; cpuQuota > 0 {
params.Set("cpuquota", strconv.Itoa(int(cpuQuota)))
}
if buildArgs := options.Args; len(buildArgs) > 0 {
bArgs, err := jsoniter.MarshalToString(buildArgs)
if err != nil {
return nil, err
}
params.Set("buildargs", bArgs)
}
if shmSize := options.CommonBuildOpts.ShmSize; len(shmSize) > 0 {
shmBytes, err := units.RAMInBytes(shmSize)
if err != nil {
return nil, err
}
params.Set("shmsize", strconv.Itoa(int(shmBytes)))
}
if options.Squash {
params.Set("squash", "1")
}
if labels := options.Labels; len(labels) > 0 {
l, err := jsoniter.MarshalToString(labels)
if err != nil {
return nil, err
}
params.Set("labels", l)
}
if options.CommonBuildOpts.HTTPProxy {
params.Set("httpproxy", "1")
}
stdout := io.Writer(os.Stdout)
if options.Out != nil {
stdout = options.Out
}
// TODO network?
var platform string
if OS := options.OS; len(OS) > 0 {
platform += OS
}
if arch := options.Architecture; len(arch) > 0 {
platform += "/" + arch
}
if len(platform) > 0 {
params.Set("platform", platform)
}
entries := make([]string, len(containerFiles))
copy(entries, containerFiles)
entries = append(entries, options.ContextDirectory)
tarfile, err := nTar(entries...)
if err != nil {
return nil, err
}
defer tarfile.Close()
params.Set("dockerfile", filepath.Base(containerFiles[0]))
conn, err := bindings.GetClient(ctx)
if err != nil {
return nil, err
}
response, err := conn.DoRequest(tarfile, http.MethodPost, "/build", params, nil)
if err != nil {
return nil, err
}
defer response.Body.Close()
if !response.IsSuccess() {
return nil, response.Process(err)
}
body := response.Body.(io.Reader)
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_*_client")
defer t.Close()
body = io.TeeReader(response.Body, t)
}
}
}
dec := json.NewDecoder(body)
re := regexp.MustCompile(`[0-9a-f]{12}`)
var id string
for {
var s struct {
Stream string `json:"stream,omitempty"`
Error string `json:"error,omitempty"`
}
if err := dec.Decode(&s); err != nil {
if errors.Is(err, io.EOF) {
return &entities.BuildReport{ID: id}, nil
}
s.Error = err.Error() + "\n"
}
switch {
case s.Stream != "":
stdout.Write([]byte(s.Stream))
if re.Match([]byte(s.Stream)) {
id = s.Stream
}
case s.Error != "":
return nil, errors.New(s.Error)
default:
return &entities.BuildReport{ID: id}, errors.New("failed to parse build results stream, unexpected input")
}
}
}
func nTar(sources ...string) (io.ReadCloser, error) {
if len(sources) == 0 {
return nil, errors.New("No source(s) provided for build")
}
pr, pw := io.Pipe()
tw := tar.NewWriter(pw)
var merr error
go func() {
defer pw.Close()
defer tw.Close()
for _, src := range sources {
s := src
err := filepath.Walk(s, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.Mode().IsRegular() || path == s {
return nil
}
f, lerr := os.Open(path)
if lerr != nil {
return lerr
}
name := strings.TrimPrefix(path, s+string(filepath.Separator))
hdr, lerr := tar.FileInfoHeader(info, name)
if lerr != nil {
f.Close()
return lerr
}
hdr.Name = name
if lerr := tw.WriteHeader(hdr); lerr != nil {
f.Close()
return lerr
}
_, cerr := io.Copy(tw, f)
f.Close()
return cerr
})
merr = multierror.Append(merr, err)
}
}()
return pr, merr
}