github.com/containers/podman/v4@v4.9.4/pkg/bindings/images/build.go (about)

     1  package images
     2  
     3  import (
     4  	"archive/tar"
     5  	"compress/gzip"
     6  	"context"
     7  	"encoding/json"
     8  	"errors"
     9  	"fmt"
    10  	"io"
    11  	"io/fs"
    12  	"net/http"
    13  	"net/url"
    14  	"os"
    15  	"path/filepath"
    16  	"runtime"
    17  	"strconv"
    18  	"strings"
    19  
    20  	"github.com/containers/buildah/define"
    21  	"github.com/containers/image/v5/types"
    22  	ldefine "github.com/containers/podman/v4/libpod/define"
    23  	"github.com/containers/podman/v4/pkg/auth"
    24  	"github.com/containers/podman/v4/pkg/bindings"
    25  	"github.com/containers/podman/v4/pkg/domain/entities"
    26  	"github.com/containers/podman/v4/pkg/util"
    27  	"github.com/containers/storage/pkg/fileutils"
    28  	"github.com/containers/storage/pkg/ioutils"
    29  	"github.com/containers/storage/pkg/regexp"
    30  	"github.com/docker/docker/pkg/jsonmessage"
    31  	"github.com/docker/go-units"
    32  	"github.com/hashicorp/go-multierror"
    33  	jsoniter "github.com/json-iterator/go"
    34  	"github.com/sirupsen/logrus"
    35  )
    36  
    37  type devino struct {
    38  	Dev uint64
    39  	Ino uint64
    40  }
    41  
    42  var iidRegex = regexp.Delayed(`^[0-9a-f]{12}`)
    43  
    44  type BuildResponse struct {
    45  	Stream string                 `json:"stream,omitempty"`
    46  	Error  *jsonmessage.JSONError `json:"errorDetail,omitempty"`
    47  	// NOTE: `error` is being deprecated check https://github.com/moby/moby/blob/master/pkg/jsonmessage/jsonmessage.go#L148
    48  	ErrorMessage string          `json:"error,omitempty"` // deprecate this slowly
    49  	Aux          json.RawMessage `json:"aux,omitempty"`
    50  }
    51  
    52  // Build creates an image using a containerfile reference
    53  func Build(ctx context.Context, containerFiles []string, options entities.BuildOptions) (*entities.BuildReport, error) {
    54  	if options.CommonBuildOpts == nil {
    55  		options.CommonBuildOpts = new(define.CommonBuildOptions)
    56  	}
    57  
    58  	params := url.Values{}
    59  
    60  	if caps := options.AddCapabilities; len(caps) > 0 {
    61  		c, err := jsoniter.MarshalToString(caps)
    62  		if err != nil {
    63  			return nil, err
    64  		}
    65  		params.Add("addcaps", c)
    66  	}
    67  
    68  	if annotations := options.Annotations; len(annotations) > 0 {
    69  		l, err := jsoniter.MarshalToString(annotations)
    70  		if err != nil {
    71  			return nil, err
    72  		}
    73  		params.Set("annotations", l)
    74  	}
    75  
    76  	if cppflags := options.CPPFlags; len(cppflags) > 0 {
    77  		l, err := jsoniter.MarshalToString(cppflags)
    78  		if err != nil {
    79  			return nil, err
    80  		}
    81  		params.Set("cppflags", l)
    82  	}
    83  
    84  	if options.AllPlatforms {
    85  		params.Add("allplatforms", "1")
    86  	}
    87  
    88  	params.Add("t", options.Output)
    89  	for _, tag := range options.AdditionalTags {
    90  		params.Add("t", tag)
    91  	}
    92  	if additionalBuildContexts := options.AdditionalBuildContexts; len(additionalBuildContexts) > 0 {
    93  		additionalBuildContextMap, err := jsoniter.Marshal(additionalBuildContexts)
    94  		if err != nil {
    95  			return nil, err
    96  		}
    97  		params.Set("additionalbuildcontexts", string(additionalBuildContextMap))
    98  	}
    99  	if options.IDMappingOptions != nil {
   100  		idmappingsOptions, err := jsoniter.Marshal(options.IDMappingOptions)
   101  		if err != nil {
   102  			return nil, err
   103  		}
   104  		params.Set("idmappingoptions", string(idmappingsOptions))
   105  	}
   106  	if buildArgs := options.Args; len(buildArgs) > 0 {
   107  		bArgs, err := jsoniter.MarshalToString(buildArgs)
   108  		if err != nil {
   109  			return nil, err
   110  		}
   111  		params.Set("buildargs", bArgs)
   112  	}
   113  	if excludes := options.Excludes; len(excludes) > 0 {
   114  		bArgs, err := jsoniter.MarshalToString(excludes)
   115  		if err != nil {
   116  			return nil, err
   117  		}
   118  		params.Set("excludes", bArgs)
   119  	}
   120  	if cpuPeriod := options.CommonBuildOpts.CPUPeriod; cpuPeriod > 0 {
   121  		params.Set("cpuperiod", strconv.Itoa(int(cpuPeriod)))
   122  	}
   123  	if cpuQuota := options.CommonBuildOpts.CPUQuota; cpuQuota > 0 {
   124  		params.Set("cpuquota", strconv.Itoa(int(cpuQuota)))
   125  	}
   126  	if cpuSetCpus := options.CommonBuildOpts.CPUSetCPUs; len(cpuSetCpus) > 0 {
   127  		params.Set("cpusetcpus", cpuSetCpus)
   128  	}
   129  	if cpuSetMems := options.CommonBuildOpts.CPUSetMems; len(cpuSetMems) > 0 {
   130  		params.Set("cpusetmems", cpuSetMems)
   131  	}
   132  	if cpuShares := options.CommonBuildOpts.CPUShares; cpuShares > 0 {
   133  		params.Set("cpushares", strconv.Itoa(int(cpuShares)))
   134  	}
   135  	if len(options.CommonBuildOpts.CgroupParent) > 0 {
   136  		params.Set("cgroupparent", options.CommonBuildOpts.CgroupParent)
   137  	}
   138  
   139  	params.Set("networkmode", strconv.Itoa(int(options.ConfigureNetwork)))
   140  	params.Set("outputformat", options.OutputFormat)
   141  
   142  	if devices := options.Devices; len(devices) > 0 {
   143  		d, err := jsoniter.MarshalToString(devices)
   144  		if err != nil {
   145  			return nil, err
   146  		}
   147  		params.Add("devices", d)
   148  	}
   149  
   150  	if dnsservers := options.CommonBuildOpts.DNSServers; len(dnsservers) > 0 {
   151  		c, err := jsoniter.MarshalToString(dnsservers)
   152  		if err != nil {
   153  			return nil, err
   154  		}
   155  		params.Add("dnsservers", c)
   156  	}
   157  	if dnsoptions := options.CommonBuildOpts.DNSOptions; len(dnsoptions) > 0 {
   158  		c, err := jsoniter.MarshalToString(dnsoptions)
   159  		if err != nil {
   160  			return nil, err
   161  		}
   162  		params.Add("dnsoptions", c)
   163  	}
   164  	if dnssearch := options.CommonBuildOpts.DNSSearch; len(dnssearch) > 0 {
   165  		c, err := jsoniter.MarshalToString(dnssearch)
   166  		if err != nil {
   167  			return nil, err
   168  		}
   169  		params.Add("dnssearch", c)
   170  	}
   171  
   172  	if caps := options.DropCapabilities; len(caps) > 0 {
   173  		c, err := jsoniter.MarshalToString(caps)
   174  		if err != nil {
   175  			return nil, err
   176  		}
   177  		params.Add("dropcaps", c)
   178  	}
   179  
   180  	if options.ForceRmIntermediateCtrs {
   181  		params.Set("forcerm", "1")
   182  	}
   183  	if options.RemoveIntermediateCtrs {
   184  		params.Set("rm", "1")
   185  	} else {
   186  		params.Set("rm", "0")
   187  	}
   188  	if options.CommonBuildOpts.OmitHistory {
   189  		params.Set("omithistory", "1")
   190  	} else {
   191  		params.Set("omithistory", "0")
   192  	}
   193  	if len(options.From) > 0 {
   194  		params.Set("from", options.From)
   195  	}
   196  	if options.IgnoreUnrecognizedInstructions {
   197  		params.Set("ignore", "1")
   198  	}
   199  	params.Set("isolation", strconv.Itoa(int(options.Isolation)))
   200  	if options.CommonBuildOpts.HTTPProxy {
   201  		params.Set("httpproxy", "1")
   202  	}
   203  	if options.Jobs != nil {
   204  		params.Set("jobs", strconv.FormatUint(uint64(*options.Jobs), 10))
   205  	}
   206  	if labels := options.Labels; len(labels) > 0 {
   207  		l, err := jsoniter.MarshalToString(labels)
   208  		if err != nil {
   209  			return nil, err
   210  		}
   211  		params.Set("labels", l)
   212  	}
   213  
   214  	if opt := options.CommonBuildOpts.LabelOpts; len(opt) > 0 {
   215  		o, err := jsoniter.MarshalToString(opt)
   216  		if err != nil {
   217  			return nil, err
   218  		}
   219  		params.Set("labelopts", o)
   220  	}
   221  
   222  	if len(options.CommonBuildOpts.SeccompProfilePath) > 0 {
   223  		params.Set("seccomp", options.CommonBuildOpts.SeccompProfilePath)
   224  	}
   225  
   226  	if len(options.CommonBuildOpts.ApparmorProfile) > 0 {
   227  		params.Set("apparmor", options.CommonBuildOpts.ApparmorProfile)
   228  	}
   229  
   230  	for _, layerLabel := range options.LayerLabels {
   231  		params.Add("layerLabel", layerLabel)
   232  	}
   233  	if options.Layers {
   234  		params.Set("layers", "1")
   235  	}
   236  	if options.LogRusage {
   237  		params.Set("rusage", "1")
   238  	}
   239  	if len(options.RusageLogFile) > 0 {
   240  		params.Set("rusagelogfile", options.RusageLogFile)
   241  	}
   242  	if len(options.Manifest) > 0 {
   243  		params.Set("manifest", options.Manifest)
   244  	}
   245  	if options.CacheFrom != nil {
   246  		cacheFrom := []string{}
   247  		for _, cacheSrc := range options.CacheFrom {
   248  			cacheFrom = append(cacheFrom, cacheSrc.String())
   249  		}
   250  		cacheFromJSON, err := jsoniter.MarshalToString(cacheFrom)
   251  		if err != nil {
   252  			return nil, err
   253  		}
   254  		params.Set("cachefrom", cacheFromJSON)
   255  	}
   256  
   257  	switch options.SkipUnusedStages {
   258  	case types.OptionalBoolTrue:
   259  		params.Set("skipunusedstages", "1")
   260  	case types.OptionalBoolFalse:
   261  		params.Set("skipunusedstages", "0")
   262  	}
   263  
   264  	if options.CacheTo != nil {
   265  		cacheTo := []string{}
   266  		for _, cacheSrc := range options.CacheTo {
   267  			cacheTo = append(cacheTo, cacheSrc.String())
   268  		}
   269  		cacheToJSON, err := jsoniter.MarshalToString(cacheTo)
   270  		if err != nil {
   271  			return nil, err
   272  		}
   273  		params.Set("cacheto", cacheToJSON)
   274  	}
   275  	if int64(options.CacheTTL) != 0 {
   276  		params.Set("cachettl", options.CacheTTL.String())
   277  	}
   278  	if memSwap := options.CommonBuildOpts.MemorySwap; memSwap > 0 {
   279  		params.Set("memswap", strconv.Itoa(int(memSwap)))
   280  	}
   281  	if mem := options.CommonBuildOpts.Memory; mem > 0 {
   282  		params.Set("memory", strconv.Itoa(int(mem)))
   283  	}
   284  	if options.NoCache {
   285  		params.Set("nocache", "1")
   286  	}
   287  	if t := options.Output; len(t) > 0 {
   288  		params.Set("output", t)
   289  	}
   290  	if t := options.OSVersion; len(t) > 0 {
   291  		params.Set("osversion", t)
   292  	}
   293  	for _, t := range options.OSFeatures {
   294  		params.Set("osfeature", t)
   295  	}
   296  	var platform string
   297  	if len(options.OS) > 0 {
   298  		platform = options.OS
   299  	}
   300  	if len(options.Architecture) > 0 {
   301  		if len(platform) == 0 {
   302  			platform = "linux"
   303  		}
   304  		platform += "/" + options.Architecture
   305  	} else if len(platform) > 0 {
   306  		platform += "/" + runtime.GOARCH
   307  	}
   308  	if len(platform) > 0 {
   309  		params.Set("platform", platform)
   310  	}
   311  	if len(options.Platforms) > 0 {
   312  		params.Del("platform")
   313  		for _, platformSpec := range options.Platforms {
   314  			// podman-cli will send empty struct, in such
   315  			// case don't add platform to param and let the
   316  			// build backend decide the default platform.
   317  			if platformSpec.OS == "" && platformSpec.Arch == "" && platformSpec.Variant == "" {
   318  				continue
   319  			}
   320  			platform = platformSpec.OS + "/" + platformSpec.Arch
   321  			if platformSpec.Variant != "" {
   322  				platform += "/" + platformSpec.Variant
   323  			}
   324  			params.Add("platform", platform)
   325  		}
   326  	}
   327  
   328  	for _, volume := range options.CommonBuildOpts.Volumes {
   329  		params.Add("volume", volume)
   330  	}
   331  
   332  	for _, group := range options.GroupAdd {
   333  		params.Add("groupadd", group)
   334  	}
   335  
   336  	var err error
   337  	var contextDir string
   338  	if contextDir, err = filepath.EvalSymlinks(options.ContextDirectory); err == nil {
   339  		options.ContextDirectory = contextDir
   340  	}
   341  
   342  	params.Set("pullpolicy", options.PullPolicy.String())
   343  
   344  	switch options.CommonBuildOpts.IdentityLabel {
   345  	case types.OptionalBoolTrue:
   346  		params.Set("identitylabel", "1")
   347  	case types.OptionalBoolFalse:
   348  		params.Set("identitylabel", "0")
   349  	}
   350  	if options.Quiet {
   351  		params.Set("q", "1")
   352  	}
   353  	if options.RemoveIntermediateCtrs {
   354  		params.Set("rm", "1")
   355  	}
   356  	if len(options.Target) > 0 {
   357  		params.Set("target", options.Target)
   358  	}
   359  
   360  	if hosts := options.CommonBuildOpts.AddHost; len(hosts) > 0 {
   361  		h, err := jsoniter.MarshalToString(hosts)
   362  		if err != nil {
   363  			return nil, err
   364  		}
   365  		params.Set("extrahosts", h)
   366  	}
   367  	if nsoptions := options.NamespaceOptions; len(nsoptions) > 0 {
   368  		ns, err := jsoniter.MarshalToString(nsoptions)
   369  		if err != nil {
   370  			return nil, err
   371  		}
   372  		params.Set("nsoptions", ns)
   373  	}
   374  	if shmSize := options.CommonBuildOpts.ShmSize; len(shmSize) > 0 {
   375  		shmBytes, err := units.RAMInBytes(shmSize)
   376  		if err != nil {
   377  			return nil, err
   378  		}
   379  		params.Set("shmsize", strconv.Itoa(int(shmBytes)))
   380  	}
   381  	if options.Squash {
   382  		params.Set("squash", "1")
   383  	}
   384  
   385  	if options.Timestamp != nil {
   386  		t := *options.Timestamp
   387  		params.Set("timestamp", strconv.FormatInt(t.Unix(), 10))
   388  	}
   389  
   390  	if len(options.CommonBuildOpts.Ulimit) > 0 {
   391  		ulimitsJSON, err := json.Marshal(options.CommonBuildOpts.Ulimit)
   392  		if err != nil {
   393  			return nil, err
   394  		}
   395  		params.Set("ulimits", string(ulimitsJSON))
   396  	}
   397  
   398  	for _, env := range options.Envs {
   399  		params.Add("setenv", env)
   400  	}
   401  
   402  	for _, uenv := range options.UnsetEnvs {
   403  		params.Add("unsetenv", uenv)
   404  	}
   405  
   406  	for _, ulabel := range options.UnsetLabels {
   407  		params.Add("unsetlabel", ulabel)
   408  	}
   409  
   410  	var (
   411  		headers http.Header
   412  	)
   413  	if options.SystemContext != nil {
   414  		if options.SystemContext.DockerAuthConfig != nil {
   415  			headers, err = auth.MakeXRegistryAuthHeader(options.SystemContext, options.SystemContext.DockerAuthConfig.Username, options.SystemContext.DockerAuthConfig.Password)
   416  		} else {
   417  			headers, err = auth.MakeXRegistryConfigHeader(options.SystemContext, "", "")
   418  		}
   419  		if options.SystemContext.DockerInsecureSkipTLSVerify == types.OptionalBoolTrue {
   420  			params.Set("tlsVerify", "false")
   421  		}
   422  	}
   423  	if err != nil {
   424  		return nil, err
   425  	}
   426  
   427  	stdout := io.Writer(os.Stdout)
   428  	if options.Out != nil {
   429  		stdout = options.Out
   430  	}
   431  
   432  	contextDir, err = filepath.Abs(options.ContextDirectory)
   433  	if err != nil {
   434  		logrus.Errorf("Cannot find absolute path of %v: %v", options.ContextDirectory, err)
   435  		return nil, err
   436  	}
   437  
   438  	tarContent := []string{options.ContextDirectory}
   439  	newContainerFiles := []string{} // dockerfile paths, relative to context dir, ToSlash()ed
   440  
   441  	dontexcludes := []string{"!Dockerfile", "!Containerfile", "!.dockerignore", "!.containerignore"}
   442  	for _, c := range containerFiles {
   443  		// Don not add path to containerfile if it is a URL
   444  		if strings.HasPrefix(c, "http://") || strings.HasPrefix(c, "https://") {
   445  			newContainerFiles = append(newContainerFiles, c)
   446  			continue
   447  		}
   448  		if c == "/dev/stdin" {
   449  			content, err := io.ReadAll(os.Stdin)
   450  			if err != nil {
   451  				return nil, err
   452  			}
   453  			tmpFile, err := os.CreateTemp("", "build")
   454  			if err != nil {
   455  				return nil, err
   456  			}
   457  			defer os.Remove(tmpFile.Name()) // clean up
   458  			defer tmpFile.Close()
   459  			if _, err := tmpFile.Write(content); err != nil {
   460  				return nil, err
   461  			}
   462  			c = tmpFile.Name()
   463  		}
   464  		c = filepath.Clean(c)
   465  		cfDir := filepath.Dir(c)
   466  		if absDir, err := filepath.EvalSymlinks(cfDir); err == nil {
   467  			name := filepath.ToSlash(strings.TrimPrefix(c, cfDir+string(filepath.Separator)))
   468  			c = filepath.Join(absDir, name)
   469  		}
   470  
   471  		containerfile, err := filepath.Abs(c)
   472  		if err != nil {
   473  			logrus.Errorf("Cannot find absolute path of %v: %v", c, err)
   474  			return nil, err
   475  		}
   476  
   477  		// Check if Containerfile is in the context directory, if so truncate the context directory off path
   478  		// Do NOT add to tarfile
   479  		if strings.HasPrefix(containerfile, contextDir+string(filepath.Separator)) {
   480  			containerfile = strings.TrimPrefix(containerfile, contextDir+string(filepath.Separator))
   481  			dontexcludes = append(dontexcludes, "!"+containerfile)
   482  			dontexcludes = append(dontexcludes, "!"+containerfile+".dockerignore")
   483  			dontexcludes = append(dontexcludes, "!"+containerfile+".containerignore")
   484  		} else {
   485  			// If Containerfile does not exist, assume it is in context directory and do Not add to tarfile
   486  			if _, err := os.Lstat(containerfile); err != nil {
   487  				if !os.IsNotExist(err) {
   488  					return nil, err
   489  				}
   490  				containerfile = c
   491  				dontexcludes = append(dontexcludes, "!"+containerfile)
   492  				dontexcludes = append(dontexcludes, "!"+containerfile+".dockerignore")
   493  				dontexcludes = append(dontexcludes, "!"+containerfile+".containerignore")
   494  			} else {
   495  				// If Containerfile does exist and not in the context directory, add it to the tarfile
   496  				tarContent = append(tarContent, containerfile)
   497  			}
   498  		}
   499  		newContainerFiles = append(newContainerFiles, filepath.ToSlash(containerfile))
   500  	}
   501  
   502  	if len(newContainerFiles) > 0 {
   503  		cFileJSON, err := json.Marshal(newContainerFiles)
   504  		if err != nil {
   505  			return nil, err
   506  		}
   507  		params.Set("dockerfile", string(cFileJSON))
   508  	}
   509  
   510  	excludes := options.Excludes
   511  	if len(excludes) == 0 {
   512  		excludes, _, err = util.ParseDockerignore(newContainerFiles, options.ContextDirectory)
   513  		if err != nil {
   514  			return nil, err
   515  		}
   516  	}
   517  
   518  	saveFormat := ldefine.OCIArchive
   519  	if options.OutputFormat == define.Dockerv2ImageManifest {
   520  		saveFormat = ldefine.V2s2Archive
   521  	}
   522  
   523  	// build secrets are usually absolute host path or relative to context dir on host
   524  	// in any case move secret to current context and ship the tar.
   525  	if secrets := options.CommonBuildOpts.Secrets; len(secrets) > 0 {
   526  		secretsForRemote := []string{}
   527  
   528  		for _, secret := range secrets {
   529  			secretOpt := strings.Split(secret, ",")
   530  			if len(secretOpt) > 0 {
   531  				modifiedOpt := []string{}
   532  				for _, token := range secretOpt {
   533  					arr := strings.SplitN(token, "=", 2)
   534  					if len(arr) > 1 {
   535  						if arr[0] == "src" {
   536  							// read specified secret into a tmp file
   537  							// move tmp file to tar and change secret source to relative tmp file
   538  							tmpSecretFile, err := os.CreateTemp(options.ContextDirectory, "podman-build-secret")
   539  							if err != nil {
   540  								return nil, err
   541  							}
   542  							defer os.Remove(tmpSecretFile.Name()) // clean up
   543  							defer tmpSecretFile.Close()
   544  							srcSecretFile, err := os.Open(arr[1])
   545  							if err != nil {
   546  								return nil, err
   547  							}
   548  							defer srcSecretFile.Close()
   549  							_, err = io.Copy(tmpSecretFile, srcSecretFile)
   550  							if err != nil {
   551  								return nil, err
   552  							}
   553  
   554  							// add tmp file to context dir
   555  							tarContent = append(tarContent, tmpSecretFile.Name())
   556  
   557  							modifiedSrc := fmt.Sprintf("src=%s", filepath.Base(tmpSecretFile.Name()))
   558  							modifiedOpt = append(modifiedOpt, modifiedSrc)
   559  						} else {
   560  							modifiedOpt = append(modifiedOpt, token)
   561  						}
   562  					}
   563  				}
   564  				secretsForRemote = append(secretsForRemote, strings.Join(modifiedOpt, ","))
   565  			}
   566  		}
   567  
   568  		c, err := jsoniter.MarshalToString(secretsForRemote)
   569  		if err != nil {
   570  			return nil, err
   571  		}
   572  		params.Add("secrets", c)
   573  	}
   574  
   575  	tarfile, err := nTar(append(excludes, dontexcludes...), tarContent...)
   576  	if err != nil {
   577  		logrus.Errorf("Cannot tar container entries %v error: %v", tarContent, err)
   578  		return nil, err
   579  	}
   580  	defer func() {
   581  		if err := tarfile.Close(); err != nil {
   582  			logrus.Errorf("%v\n", err)
   583  		}
   584  	}()
   585  
   586  	conn, err := bindings.GetClient(ctx)
   587  	if err != nil {
   588  		return nil, err
   589  	}
   590  	response, err := conn.DoRequest(ctx, tarfile, http.MethodPost, "/build", params, headers)
   591  	if err != nil {
   592  		return nil, err
   593  	}
   594  	defer response.Body.Close()
   595  
   596  	if !response.IsSuccess() {
   597  		return nil, response.Process(err)
   598  	}
   599  
   600  	body := response.Body.(io.Reader)
   601  	if logrus.IsLevelEnabled(logrus.DebugLevel) {
   602  		if v, found := os.LookupEnv("PODMAN_RETAIN_BUILD_ARTIFACT"); found {
   603  			if keep, _ := strconv.ParseBool(v); keep {
   604  				t, _ := os.CreateTemp("", "build_*_client")
   605  				defer t.Close()
   606  				body = io.TeeReader(response.Body, t)
   607  			}
   608  		}
   609  	}
   610  
   611  	dec := json.NewDecoder(body)
   612  
   613  	var id string
   614  	for {
   615  		var s BuildResponse
   616  		select {
   617  		// FIXME(vrothberg): it seems we always hit the EOF case below,
   618  		// even when the server quit but it seems desirable to
   619  		// distinguish a proper build from a transient EOF.
   620  		case <-response.Request.Context().Done():
   621  			return &entities.BuildReport{ID: id, SaveFormat: saveFormat}, nil
   622  		default:
   623  			// non-blocking select
   624  		}
   625  
   626  		if err := dec.Decode(&s); err != nil {
   627  			if errors.Is(err, io.ErrUnexpectedEOF) {
   628  				return nil, fmt.Errorf("server probably quit: %w", err)
   629  			}
   630  			// EOF means the stream is over in which case we need
   631  			// to have read the id.
   632  			if errors.Is(err, io.EOF) && id != "" {
   633  				break
   634  			}
   635  			return &entities.BuildReport{ID: id, SaveFormat: saveFormat}, fmt.Errorf("decoding stream: %w", err)
   636  		}
   637  
   638  		switch {
   639  		case s.Stream != "":
   640  			raw := []byte(s.Stream)
   641  			stdout.Write(raw)
   642  			if iidRegex.Match(raw) {
   643  				id = strings.TrimSuffix(s.Stream, "\n")
   644  			}
   645  		case s.Error != nil:
   646  			// If there's an error, return directly.  The stream
   647  			// will be closed on return.
   648  			return &entities.BuildReport{ID: id, SaveFormat: saveFormat}, errors.New(s.Error.Message)
   649  		default:
   650  			return &entities.BuildReport{ID: id, SaveFormat: saveFormat}, errors.New("failed to parse build results stream, unexpected input")
   651  		}
   652  	}
   653  	return &entities.BuildReport{ID: id, SaveFormat: saveFormat}, nil
   654  }
   655  
   656  func nTar(excludes []string, sources ...string) (io.ReadCloser, error) {
   657  	pm, err := fileutils.NewPatternMatcher(excludes)
   658  	if err != nil {
   659  		return nil, fmt.Errorf("processing excludes list %v: %w", excludes, err)
   660  	}
   661  
   662  	if len(sources) == 0 {
   663  		return nil, errors.New("no source(s) provided for build")
   664  	}
   665  
   666  	pr, pw := io.Pipe()
   667  	gw := gzip.NewWriter(pw)
   668  	tw := tar.NewWriter(gw)
   669  
   670  	var merr *multierror.Error
   671  	go func() {
   672  		defer pw.Close()
   673  		defer gw.Close()
   674  		defer tw.Close()
   675  		seen := make(map[devino]string)
   676  		for i, src := range sources {
   677  			source, err := filepath.Abs(src)
   678  			if err != nil {
   679  				logrus.Errorf("Cannot stat one of source context: %v", err)
   680  				merr = multierror.Append(merr, err)
   681  				return
   682  			}
   683  			err = filepath.WalkDir(source, func(path string, dentry fs.DirEntry, err error) error {
   684  				if err != nil {
   685  					return err
   686  				}
   687  
   688  				separator := string(filepath.Separator)
   689  				// check if what we are given is an empty dir, if so then continue w/ it. Else return.
   690  				// if we are given a file or a symlink, we do not want to exclude it.
   691  				if source == path {
   692  					separator = ""
   693  					if dentry.IsDir() {
   694  						var p *os.File
   695  						p, err = os.Open(path)
   696  						if err != nil {
   697  							return err
   698  						}
   699  						defer p.Close()
   700  						_, err = p.Readdir(1)
   701  						if err == nil {
   702  							return nil // non empty root dir, need to return
   703  						}
   704  						if err != io.EOF {
   705  							logrus.Errorf("While reading directory %v: %v", path, err)
   706  						}
   707  					}
   708  				}
   709  				var name string
   710  				if i == 0 {
   711  					name = filepath.ToSlash(strings.TrimPrefix(path, source+separator))
   712  				} else {
   713  					if !dentry.Type().IsRegular() {
   714  						return fmt.Errorf("path %s must be a regular file", path)
   715  					}
   716  					name = filepath.ToSlash(path)
   717  				}
   718  				// If name is absolute path, then it has to be containerfile outside of build context.
   719  				// If not, we should check it for being excluded via pattern matcher.
   720  				if !filepath.IsAbs(name) {
   721  					excluded, err := pm.Matches(name) //nolint:staticcheck
   722  					if err != nil {
   723  						return fmt.Errorf("checking if %q is excluded: %w", name, err)
   724  					}
   725  					if excluded {
   726  						// Note: filepath.SkipDir is not possible to use given .dockerignore semantics.
   727  						// An exception to exclusions may include an excluded directory, therefore we
   728  						// are required to visit all files. :(
   729  						return nil
   730  					}
   731  				}
   732  				switch {
   733  				case dentry.Type().IsRegular(): // add file item
   734  					info, err := dentry.Info()
   735  					if err != nil {
   736  						return err
   737  					}
   738  					di, isHardLink := checkHardLink(info)
   739  					if err != nil {
   740  						return err
   741  					}
   742  
   743  					hdr, err := tar.FileInfoHeader(info, "")
   744  					if err != nil {
   745  						return err
   746  					}
   747  					hdr.Uid, hdr.Gid = 0, 0
   748  					orig, ok := seen[di]
   749  					if ok {
   750  						hdr.Typeflag = tar.TypeLink
   751  						hdr.Linkname = orig
   752  						hdr.Size = 0
   753  						hdr.Name = name
   754  						return tw.WriteHeader(hdr)
   755  					}
   756  					f, err := os.Open(path)
   757  					if err != nil {
   758  						return err
   759  					}
   760  
   761  					hdr.Name = name
   762  					if err := tw.WriteHeader(hdr); err != nil {
   763  						f.Close()
   764  						return err
   765  					}
   766  
   767  					_, err = io.Copy(tw, f)
   768  					f.Close()
   769  					if err == nil && isHardLink {
   770  						seen[di] = name
   771  					}
   772  					return err
   773  				case dentry.IsDir(): // add folders
   774  					info, err := dentry.Info()
   775  					if err != nil {
   776  						return err
   777  					}
   778  					hdr, lerr := tar.FileInfoHeader(info, name)
   779  					if lerr != nil {
   780  						return lerr
   781  					}
   782  					hdr.Name = name
   783  					hdr.Uid, hdr.Gid = 0, 0
   784  					if lerr := tw.WriteHeader(hdr); lerr != nil {
   785  						return lerr
   786  					}
   787  				case dentry.Type()&os.ModeSymlink != 0: // add symlinks as it, not content
   788  					link, err := os.Readlink(path)
   789  					if err != nil {
   790  						return err
   791  					}
   792  					info, err := dentry.Info()
   793  					if err != nil {
   794  						return err
   795  					}
   796  					hdr, lerr := tar.FileInfoHeader(info, link)
   797  					if lerr != nil {
   798  						return lerr
   799  					}
   800  					hdr.Name = name
   801  					hdr.Uid, hdr.Gid = 0, 0
   802  					if lerr := tw.WriteHeader(hdr); lerr != nil {
   803  						return lerr
   804  					}
   805  				} // skip other than file,folder and symlinks
   806  				return nil
   807  			})
   808  			merr = multierror.Append(merr, err)
   809  		}
   810  	}()
   811  	rc := ioutils.NewReadCloserWrapper(pr, func() error {
   812  		if merr != nil {
   813  			merr = multierror.Append(merr, pr.Close())
   814  			return merr.ErrorOrNil()
   815  		}
   816  		return pr.Close()
   817  	})
   818  	return rc, nil
   819  }