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