mirror of
https://github.com/containers/podman.git
synced 2026-07-21 04:31:57 -04:00
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>
344 lines
8.6 KiB
Go
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
|
|
}
|