Files
podman/pkg/bindings/images/build.go
Kazım SARIKAYA 3ae91aff9e podman-remote fix sending tar content
1.) podman cannot send proper dockerfile when it is not inside root
folder.
2.) support for sending symlinks and folders inside context dir
3.) when sending context dir as tar to remote, prevent sending items
inside .dockerignore

Signed-off-by: Kazım SARIKAYA <kazimsarikaya@sanaldiyar.com>
2021-01-03 19:26:02 +03:00

344 lines
8.6 KiB
Go

package images
import (
"archive/tar"
"compress/gzip"
"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/auth"
"github.com/containers/podman/v2/pkg/bindings"
"github.com/containers/podman/v2/pkg/domain/entities"
"github.com/containers/storage/pkg/fileutils"
"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")
}
if options.Layers {
params.Set("layers", "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")
}
var (
headers map[string]string
err error
)
if options.SystemContext == nil {
headers, err = auth.Header(options.SystemContext, auth.XRegistryConfigHeader, "", "", "")
} else {
if options.SystemContext.DockerAuthConfig != nil {
headers, err = auth.Header(options.SystemContext, auth.XRegistryAuthHeader, options.SystemContext.AuthFilePath, options.SystemContext.DockerAuthConfig.Username, options.SystemContext.DockerAuthConfig.Password)
} else {
headers, err = auth.Header(options.SystemContext, auth.XRegistryConfigHeader, options.SystemContext.AuthFilePath, "", "")
}
}
if err != nil {
return nil, err
}
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)
excludes := options.Excludes
if len(excludes) == 0 {
excludes, err = parseDockerignore(options.ContextDirectory)
if err != nil {
return nil, err
}
}
tarfile, err := nTar(excludes, entries...)
if err != nil {
logrus.Errorf("cannot tar container entries %v error: %v", entries, err)
return nil, err
}
defer tarfile.Close()
containerFile, err := filepath.Abs(entries[0])
if err != nil {
logrus.Errorf("cannot find absolute path of %v: %v", entries[0], err)
return nil, err
}
contextDir, err := filepath.Abs(entries[1])
if err != nil {
logrus.Errorf("cannot find absolute path of %v: %v", entries[1], err)
return nil, err
}
if strings.HasPrefix(containerFile, contextDir+string(filepath.Separator)) {
containerFile = strings.TrimPrefix(containerFile, contextDir+string(filepath.Separator))
}
params.Set("dockerfile", containerFile)
conn, err := bindings.GetClient(ctx)
if err != nil {
return nil, err
}
response, err := conn.DoRequest(tarfile, http.MethodPost, "/build", params, headers)
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 = strings.TrimSuffix(s.Stream, "\n")
}
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(excludes []string, sources ...string) (io.ReadCloser, error) {
pm, err := fileutils.NewPatternMatcher(excludes)
if err != nil {
return nil, errors.Wrapf(err, "error processing excludes list %v", excludes)
}
if len(sources) == 0 {
return nil, errors.New("No source(s) provided for build")
}
pr, pw := io.Pipe()
gw := gzip.NewWriter(pw)
tw := tar.NewWriter(gw)
var merr error
go func() {
defer pw.Close()
defer gw.Close()
defer tw.Close()
for _, src := range sources {
s, err := filepath.Abs(src)
if err != nil {
logrus.Errorf("cannot stat one of source context: %v", err)
merr = multierror.Append(merr, err)
return
}
err = filepath.Walk(s, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if path == s {
return nil // skip root dir
}
name := strings.TrimPrefix(path, s+string(filepath.Separator))
excluded, err := pm.Matches(filepath.ToSlash(name)) // nolint:staticcheck
if err != nil {
return errors.Wrapf(err, "error checking if %q is excluded", name)
}
if excluded {
return nil
}
if info.Mode().IsRegular() { // add file item
f, lerr := os.Open(path)
if lerr != nil {
return lerr
}
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
} else if info.Mode().IsDir() { // add folders
hdr, lerr := tar.FileInfoHeader(info, name)
if lerr != nil {
return lerr
}
hdr.Name = name
if lerr := tw.WriteHeader(hdr); lerr != nil {
return lerr
}
} else if info.Mode()&os.ModeSymlink != 0 { // add symlinks as it, not content
link, err := os.Readlink(path)
if err != nil {
return err
}
hdr, lerr := tar.FileInfoHeader(info, link)
if lerr != nil {
return lerr
}
hdr.Name = name
if lerr := tw.WriteHeader(hdr); lerr != nil {
return lerr
}
} //skip other than file,folder and symlinks
return nil
})
merr = multierror.Append(merr, err)
}
}()
return pr, merr
}
func parseDockerignore(root string) ([]string, error) {
ignore, err := ioutil.ReadFile(filepath.Join(root, ".dockerignore"))
if err != nil && !os.IsNotExist(err) {
return nil, errors.Wrapf(err, "error reading .dockerignore: '%s'", root)
}
rawexcludes := strings.Split(string(ignore), "\n")
excludes := make([]string, 0, len(rawexcludes))
for _, e := range rawexcludes {
if len(e) == 0 || e[0] == '#' {
continue
}
excludes = append(excludes, e)
}
return excludes, nil
}