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 }