github.com/bshelton229/agent@v3.5.4+incompatible/bootstrap/bootstrap.go (about) 1 package bootstrap 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "io" 7 "io/ioutil" 8 "os" 9 "path/filepath" 10 "regexp" 11 "runtime" 12 "strconv" 13 "strings" 14 "time" 15 16 "github.com/buildkite/agent/agent/plugin" 17 "github.com/buildkite/agent/bootstrap/shell" 18 "github.com/buildkite/agent/env" 19 "github.com/buildkite/agent/process" 20 "github.com/buildkite/agent/retry" 21 "github.com/buildkite/shellwords" 22 "github.com/pkg/errors" 23 ) 24 25 // Bootstrap represents the phases of execution in a Buildkite Job. It's run 26 // as a sub-process of the buildkite-agent and finishes at the conclusion of a job. 27 // Historically (prior to v3) the bootstrap was a shell script, but was ported to 28 // Golang for portability and testability 29 type Bootstrap struct { 30 // Config provides the bootstrap configuration 31 Config 32 33 // Phases to execute, defaults to all phases 34 Phases []string 35 36 // Shell is the shell environment for the bootstrap 37 shell *shell.Shell 38 39 // Plugins are checkout out in the PluginPhase 40 plugins []*pluginCheckout 41 42 // Whether the checkout dir was created as part of checkout 43 createdCheckoutDir bool 44 } 45 46 // Start runs the bootstrap and returns the exit code 47 func (b *Bootstrap) Start() (exitCode int) { 48 // Check if not nil to allow for tests to overwrite shell 49 if b.shell == nil { 50 var err error 51 b.shell, err = shell.New() 52 if err != nil { 53 fmt.Printf("Error creating shell: %v", err) 54 return 1 55 } 56 57 b.shell.PTY = b.Config.RunInPty 58 b.shell.Debug = b.Config.Debug 59 } 60 61 // Tear down the environment (and fire pre-exit hook) before we exit 62 defer func() { 63 if err := b.tearDown(); err != nil { 64 b.shell.Errorf("Error tearing down bootstrap: %v", err) 65 66 // this gets passed back via the named return 67 exitCode = shell.GetExitCode(err) 68 } 69 }() 70 71 // Initialize the environment, a failure here will still call the tearDown 72 if err := b.setUp(); err != nil { 73 b.shell.Errorf("Error setting up bootstrap: %v", err) 74 return shell.GetExitCode(err) 75 } 76 77 var includePhase = func(phase string) bool { 78 if len(b.Phases) == 0 { 79 return true 80 } 81 for _, include := range b.Phases { 82 if include == phase { 83 return true 84 } 85 } 86 return false 87 } 88 89 // Execute the bootstrap phases in order 90 var phaseErr error 91 92 if includePhase(`plugin`) { 93 phaseErr = b.PluginPhase() 94 } 95 96 if phaseErr == nil && includePhase(`checkout`) { 97 phaseErr = b.CheckoutPhase() 98 } else { 99 checkoutDir, exists := b.shell.Env.Get(`BUILDKITE_BUILD_CHECKOUT_PATH`) 100 if exists { 101 _ = b.shell.Chdir(checkoutDir) 102 } 103 } 104 105 if phaseErr == nil && includePhase(`command`) { 106 phaseErr = b.CommandPhase() 107 108 // Only upload artifacts as part of the command phase 109 if err := b.uploadArtifacts(); err != nil { 110 b.shell.Errorf("%v", err) 111 return shell.GetExitCode(err) 112 } 113 } 114 115 // Phase errors are where something of ours broke that merits a big red error 116 // this won't include command failures, as we view that as more in the user space 117 if phaseErr != nil { 118 b.shell.Errorf("%v", phaseErr) 119 return shell.GetExitCode(phaseErr) 120 } 121 122 // Use the exit code from the command phase 123 exitStatus, _ := b.shell.Env.Get(`BUILDKITE_COMMAND_EXIT_STATUS`) 124 exitStatusCode, _ := strconv.Atoi(exitStatus) 125 126 return exitStatusCode 127 } 128 129 // executeHook runs a hook script with the hookRunner 130 func (b *Bootstrap) executeHook(name string, hookPath string, extraEnviron *env.Environment) error { 131 if !fileExists(hookPath) { 132 if b.Debug { 133 b.shell.Commentf("Skipping %s hook, no script at \"%s\"", name, hookPath) 134 } 135 return nil 136 } 137 138 b.shell.Headerf("Running %s hook", name) 139 140 // We need a script to wrap the hook script so that we can snaffle the changed 141 // environment variables 142 script, err := newHookScriptWrapper(hookPath) 143 if err != nil { 144 b.shell.Errorf("Error creating hook script: %v", err) 145 return err 146 } 147 defer script.Close() 148 149 cleanHookPath := hookPath 150 151 // Show a relative path if we can 152 if strings.HasPrefix(hookPath, b.shell.Getwd()) { 153 var err error 154 if cleanHookPath, err = filepath.Rel(b.shell.Getwd(), hookPath); err != nil { 155 cleanHookPath = hookPath 156 } 157 } 158 159 // Show the hook runner in debug, but the thing being run otherwise 💅🏻 160 if b.Debug { 161 b.shell.Commentf("A hook runner was written to \"%s\" with the following:", script.Path()) 162 b.shell.Promptf("%s", process.FormatCommand(script.Path(), nil)) 163 } else { 164 b.shell.Promptf("%s", process.FormatCommand(cleanHookPath, []string{})) 165 } 166 167 // Run the wrapper script 168 if err := b.shell.RunScript(script.Path(), extraEnviron); err != nil { 169 exitCode := shell.GetExitCode(err) 170 b.shell.Env.Set("BUILDKITE_LAST_HOOK_EXIT_STATUS", fmt.Sprintf("%d", exitCode)) 171 172 // Give a simpler error if it's just a shell exit error 173 if shell.IsExitError(err) { 174 return &shell.ExitError{ 175 Code: exitCode, 176 Message: fmt.Sprintf("The %s hook exited with status %d", name, exitCode), 177 } 178 } 179 return err 180 } 181 182 // Store the last hook exit code for subsequent steps 183 b.shell.Env.Set("BUILDKITE_LAST_HOOK_EXIT_STATUS", "0") 184 185 // Get changed environment 186 changes, err := script.Changes() 187 if err != nil { 188 return errors.Wrapf(err, "Failed to get environment") 189 } 190 191 // Finally, apply changes to the current shell and config 192 b.applyEnvironmentChanges(changes.Env, changes.Dir) 193 return nil 194 } 195 196 func (b *Bootstrap) applyEnvironmentChanges(environ *env.Environment, dir string) { 197 if dir != b.shell.Getwd() { 198 _ = b.shell.Chdir(dir) 199 } 200 201 // Do we even have any environment variables to change? 202 if environ != nil && environ.Length() > 0 { 203 // First, let see any of the environment variables are supposed 204 // to change the bootstrap configuration at run time. 205 bootstrapConfigEnvChanges := b.Config.ReadFromEnvironment(environ) 206 207 // Print out the env vars that changed. As we go through each 208 // one, we'll determine if it was a special "bootstrap" 209 // environment variable that has changed the bootstrap 210 // configuration at runtime. 211 // 212 // If it's "special", we'll show the value it was changed to - 213 // otherwise we'll hide it. Since we don't know if an 214 // environment variable contains sensitive information (i.e. 215 // THIRD_PARTY_API_KEY) we'll just not show any values for 216 // anything not controlled by us. 217 for k, v := range environ.ToMap() { 218 _, ok := bootstrapConfigEnvChanges[k] 219 if ok { 220 b.shell.Commentf("%s is now %q", k, v) 221 } else { 222 b.shell.Commentf("%s changed", k) 223 } 224 } 225 226 // Now that we've finished telling the user what's changed, 227 // let's mutate the current shell environment to include all 228 // the new values. 229 b.shell.Env = b.shell.Env.Merge(environ) 230 } 231 } 232 233 // Returns the absolute path to the best matching hook file in a path, or os.ErrNotExist if none is found 234 func (b *Bootstrap) findHookFile(hookDir string, name string) (string, error) { 235 if runtime.GOOS == "windows" { 236 // check for windows types first 237 if p, err := shell.LookPath(name, hookDir, ".BAT;.CMD"); err == nil { 238 return p, nil 239 } 240 } 241 // otherwise chech for th default shell script 242 if p := filepath.Join(hookDir, name); fileExists(p) { 243 return p, nil 244 } 245 return "", os.ErrNotExist 246 } 247 248 func (b *Bootstrap) hasGlobalHook(name string) bool { 249 _, err := b.globalHookPath(name) 250 return err == nil 251 } 252 253 // Returns the absolute path to a global hook, or os.ErrNotExist if none is found 254 func (b *Bootstrap) globalHookPath(name string) (string, error) { 255 return b.findHookFile(b.HooksPath, name) 256 } 257 258 // Executes a global hook if one exists 259 func (b *Bootstrap) executeGlobalHook(name string) error { 260 if !b.hasGlobalHook(name) { 261 return nil 262 } 263 p, err := b.globalHookPath(name) 264 if err != nil { 265 return err 266 } 267 return b.executeHook("global "+name, p, nil) 268 } 269 270 // Returns the absolute path to a local hook, or os.ErrNotExist if none is found 271 func (b *Bootstrap) localHookPath(name string) (string, error) { 272 return b.findHookFile(filepath.Join(b.shell.Getwd(), ".buildkite", "hooks"), name) 273 } 274 275 func (b *Bootstrap) hasLocalHook(name string) bool { 276 _, err := b.localHookPath(name) 277 return err == nil 278 } 279 280 // Executes a local hook 281 func (b *Bootstrap) executeLocalHook(name string) error { 282 if !b.hasLocalHook(name) { 283 return nil 284 } 285 286 localHookPath, err := b.localHookPath(name) 287 if err != nil { 288 return nil 289 } 290 291 // For high-security configs, we allow the disabling of local hooks. 292 localHooksEnabled := b.Config.LocalHooksEnabled 293 294 // Allow hooks to disable local hooks by setting BUILDKITE_NO_LOCAL_HOOKS=true 295 noLocalHooks, _ := b.shell.Env.Get(`BUILDKITE_NO_LOCAL_HOOKS`) 296 if noLocalHooks == "true" || noLocalHooks == "1" { 297 localHooksEnabled = false 298 } 299 300 if !localHooksEnabled { 301 return fmt.Errorf("Refusing to run %s, local hooks are disabled", localHookPath) 302 } 303 304 return b.executeHook("local "+name, localHookPath, nil) 305 } 306 307 // Returns whether or not a file exists on the filesystem. We consider any 308 // error returned by os.Stat to indicate that the file doesn't exist. We could 309 // be specific and use os.IsNotExist(err), but most other errors also indicate 310 // that the file isn't there (or isn't available) so we'll just catch them all. 311 func fileExists(filename string) bool { 312 _, err := os.Stat(filename) 313 return err == nil 314 } 315 316 func dirForAgentName(agentName string) string { 317 badCharsPattern := regexp.MustCompile("[[:^alnum:]]") 318 return badCharsPattern.ReplaceAllString(agentName, "-") 319 } 320 321 // Given a repository, it will add the host to the set of SSH known_hosts on the machine 322 func addRepositoryHostToSSHKnownHosts(sh *shell.Shell, repository string) { 323 if fileExists(repository) { 324 return 325 } 326 327 knownHosts, err := findKnownHosts(sh) 328 if err != nil { 329 sh.Warningf("Failed to find SSH known_hosts file: %v", err) 330 return 331 } 332 333 if err = knownHosts.AddFromRepository(repository); err != nil { 334 sh.Warningf("Error adding to known_hosts: %v", err) 335 return 336 } 337 } 338 339 // Makes sure a file is executable 340 func addExecutePermissionToFile(filename string) error { 341 s, err := os.Stat(filename) 342 if err != nil { 343 return fmt.Errorf("Failed to retrieve file information of \"%s\" (%s)", filename, err) 344 } 345 346 if s.Mode()&0100 == 0 { 347 err = os.Chmod(filename, s.Mode()|0100) 348 if err != nil { 349 return fmt.Errorf("Failed to mark \"%s\" as executable (%s)", filename, err) 350 } 351 } 352 353 return nil 354 } 355 356 // setUp is run before all the phases run. It's responsible for initializing the 357 // bootstrap environment 358 func (b *Bootstrap) setUp() error { 359 // Create an empty env for us to keep track of our env changes in 360 b.shell.Env = env.FromSlice(os.Environ()) 361 362 // Add the $BUILDKITE_BIN_PATH to the $PATH if we've been given one 363 if b.BinPath != "" { 364 path, _ := b.shell.Env.Get("PATH") 365 b.shell.Env.Set("PATH", fmt.Sprintf("%s%s%s", b.BinPath, string(os.PathListSeparator), path)) 366 } 367 368 // Set a BUILDKITE_BUILD_CHECKOUT_PATH unless one exists already. We do this here 369 // so that the environment will have a checkout path to work with 370 if _, exists := b.shell.Env.Get("BUILDKITE_BUILD_CHECKOUT_PATH"); !exists { 371 if b.BuildPath == "" { 372 return fmt.Errorf("Must set either a BUILDKITE_BUILD_PATH or a BUILDKITE_BUILD_CHECKOUT_PATH") 373 } 374 b.shell.Env.Set("BUILDKITE_BUILD_CHECKOUT_PATH", 375 filepath.Join(b.BuildPath, dirForAgentName(b.AgentName), b.OrganizationSlug, b.PipelineSlug)) 376 } 377 378 // The job runner sets BUILDKITE_IGNORED_ENV with any keys that were ignored 379 // or overwritten. This shows a warning to the user so they don't get confused 380 // when their environment changes don't seem to do anything 381 if ignored, exists := b.shell.Env.Get("BUILDKITE_IGNORED_ENV"); exists { 382 b.shell.Headerf("Detected protected environment variables") 383 b.shell.Commentf("Your pipeline environment has protected environment variables set. " + 384 "These can only be set via hooks, plugins or the agent configuration.") 385 386 for _, env := range strings.Split(ignored, ",") { 387 b.shell.Warningf("Ignored %s", env) 388 } 389 390 b.shell.Printf("^^^ +++") 391 } 392 393 if b.Debug { 394 b.shell.Headerf("Buildkite environment variables") 395 for _, e := range b.shell.Env.ToSlice() { 396 if strings.HasPrefix(e, "BUILDKITE_AGENT_ACCESS_TOKEN=") { 397 b.shell.Printf("BUILDKITE_AGENT_ACCESS_TOKEN=******************") 398 } else if strings.HasPrefix(e, "BUILDKITE") || strings.HasPrefix(e, "CI") || strings.HasPrefix(e, "PATH") { 399 b.shell.Printf("%s", strings.Replace(e, "\n", "\\n", -1)) 400 } 401 } 402 } 403 404 // Disable any interactive Git/SSH prompting 405 b.shell.Env.Set("GIT_TERMINAL_PROMPT", "0") 406 407 // It's important to do this before checking out plugins, in case you want 408 // to use the global environment hook to whitelist the plugins that are 409 // allowed to be used. 410 return b.executeGlobalHook("environment") 411 } 412 413 // tearDown is called before the bootstrap exits, even on error 414 func (b *Bootstrap) tearDown() error { 415 if err := b.executeGlobalHook("pre-exit"); err != nil { 416 return err 417 } 418 419 if err := b.executeLocalHook("pre-exit"); err != nil { 420 return err 421 } 422 423 if err := b.executePluginHook("pre-exit"); err != nil { 424 return err 425 } 426 427 // Support deprecated BUILDKITE_DOCKER* env vars 428 if hasDeprecatedDockerIntegration(b.shell) { 429 return tearDownDeprecatedDockerIntegration(b.shell) 430 } 431 432 return nil 433 } 434 435 // PluginPhase is where plugins that weren't filtered in the Environment phase are 436 // checked out and made available to later phases 437 func (b *Bootstrap) PluginPhase() error { 438 if b.Plugins == "" { 439 return nil 440 } 441 442 b.shell.Headerf("Setting up plugins") 443 444 // Make sure we have a plugin path before trying to do anything 445 if b.PluginsPath == "" { 446 return fmt.Errorf("Can't checkout plugins without a `plugins-path`") 447 } 448 449 if b.Debug { 450 b.shell.Commentf("Plugin JSON is %s", b.Plugins) 451 } 452 453 // Check if we can run plugins (disabled via --no-plugins) 454 if b.Plugins != "" && !b.Config.PluginsEnabled { 455 if !b.Config.LocalHooksEnabled { 456 return fmt.Errorf("Plugins have been disabled on this agent with `--no-local-hooks`") 457 } else if !b.Config.CommandEval { 458 return fmt.Errorf("Plugins have been disabled on this agent with `--no-command-eval`") 459 } else { 460 return fmt.Errorf("Plugins have been disabled on this agent with `--no-plugins`") 461 } 462 } 463 464 plugins, err := plugin.CreateFromJSON(b.Plugins) 465 if err != nil { 466 return errors.Wrap(err, "Failed to parse plugin definition") 467 } 468 469 b.plugins = []*pluginCheckout{} 470 471 for _, p := range plugins { 472 checkout, err := b.checkoutPlugin(p) 473 if err != nil { 474 return errors.Wrapf(err, "Failed to checkout plugin %s", p.Name()) 475 } 476 if b.Config.PluginValidation { 477 if b.Debug { 478 b.shell.Commentf("Parsing plugin definition for %s from %s", p.Name(), checkout.CheckoutDir) 479 } 480 // parse the plugin definition from the plugin checkout dir 481 checkout.Definition, err = plugin.LoadDefinitionFromDir(checkout.CheckoutDir) 482 if err == plugin.ErrDefinitionNotFound { 483 b.shell.Warningf("Failed to find plugin definition for plugin %s", p.Name()) 484 } else if err != nil { 485 return err 486 } 487 } 488 b.plugins = append(b.plugins, checkout) 489 } 490 491 if b.Config.PluginValidation { 492 for _, checkout := range b.plugins { 493 // This is nil if the definition failed to parse or is missing 494 if checkout.Definition == nil { 495 continue 496 } 497 498 val := &plugin.Validator{} 499 result := val.Validate(checkout.Definition, checkout.Plugin.Configuration) 500 501 if !result.Valid() { 502 b.shell.Headerf("Plugin validation failed for %q", checkout.Plugin.Name()) 503 json, _ := json.Marshal(checkout.Plugin.Configuration) 504 b.shell.Commentf("Plugin configuration JSON is %s", json) 505 return result 506 } else { 507 b.shell.Commentf("Valid plugin configuration for %q", checkout.Plugin.Name()) 508 } 509 } 510 } 511 512 // Now we can run plugin environment hooks too 513 return b.executePluginHook("environment") 514 } 515 516 // Executes a named hook on all plugins that have it 517 func (b *Bootstrap) executePluginHook(name string) error { 518 for _, p := range b.plugins { 519 hookPath, err := b.findHookFile(p.HooksDir, name) 520 if err != nil { 521 continue 522 } 523 524 env, _ := p.ConfigurationToEnvironment() 525 if err := b.executeHook("plugin "+p.Label()+" "+name, hookPath, env); err != nil { 526 return err 527 } 528 } 529 return nil 530 } 531 532 // If any plugin has a hook by this name 533 func (b *Bootstrap) hasPluginHook(name string) bool { 534 for _, p := range b.plugins { 535 if _, err := b.findHookFile(p.HooksDir, name); err == nil { 536 return true 537 } 538 } 539 return false 540 } 541 542 // Checkout a given plugin to the plugins directory and return that directory 543 func (b *Bootstrap) checkoutPlugin(p *plugin.Plugin) (*pluginCheckout, error) { 544 // Get the identifer for the plugin 545 id, err := p.Identifier() 546 if err != nil { 547 return nil, err 548 } 549 550 // Ensure the plugin directory exists, otherwise we can't create the lock 551 err = os.MkdirAll(b.PluginsPath, 0777) 552 if err != nil { 553 return nil, err 554 } 555 556 // Try and lock this particular plugin while we check it out (we create 557 // the file outside of the plugin directory so git clone doesn't have 558 // a cry about the directory not being empty) 559 pluginCheckoutHook, err := b.shell.LockFile(filepath.Join(b.PluginsPath, id+".lock"), time.Minute*5) 560 if err != nil { 561 return nil, err 562 } 563 defer pluginCheckoutHook.Unlock() 564 565 // Create a path to the plugin 566 directory := filepath.Join(b.PluginsPath, id) 567 pluginGitDirectory := filepath.Join(directory, ".git") 568 checkout := &pluginCheckout{ 569 Plugin: p, 570 CheckoutDir: directory, 571 HooksDir: filepath.Join(directory, "hooks"), 572 } 573 574 // Has it already been checked out? 575 if fileExists(pluginGitDirectory) { 576 // It'd be nice to show the current commit of the plugin, so 577 // let's figure that out. 578 headCommit, err := gitRevParseInWorkingDirectory(b.shell, directory, "--short=7", "HEAD") 579 if err != nil { 580 b.shell.Commentf("Plugin %q already checked out (can't `git rev-parse HEAD` plugin git directory)", p.Label()) 581 } else { 582 b.shell.Commentf("Plugin %q already checked out (%s)", p.Label(), strings.TrimSpace(headCommit)) 583 } 584 585 return checkout, nil 586 } 587 588 // Make the directory 589 err = os.MkdirAll(directory, 0777) 590 if err != nil { 591 return nil, err 592 } 593 594 // Once we've got the lock, we need to make sure another process didn't already 595 // checkout the plugin 596 if fileExists(pluginGitDirectory) { 597 b.shell.Commentf("Plugin \"%s\" already checked out", p.Label()) 598 return checkout, nil 599 } 600 601 repo, err := p.Repository() 602 if err != nil { 603 return nil, err 604 } 605 606 b.shell.Commentf("Plugin \"%s\" will be checked out to \"%s\"", p.Location, directory) 607 608 if b.Debug { 609 b.shell.Commentf("Checking if \"%s\" is a local repository", repo) 610 } 611 612 // Switch to the plugin directory 613 previousWd := b.shell.Getwd() 614 if err = b.shell.Chdir(directory); err != nil { 615 return nil, err 616 } 617 618 // Switch back to the previous working directory 619 defer b.shell.Chdir(previousWd) 620 621 b.shell.Commentf("Switching to the plugin directory") 622 623 if b.SSHKeyscan { 624 addRepositoryHostToSSHKnownHosts(b.shell, repo) 625 } 626 627 // Plugin clones shouldn't use custom GitCloneFlags 628 if err = b.shell.Run("git", "clone", "-v", "--", repo, "."); err != nil { 629 return nil, err 630 } 631 632 // Switch to the version if we need to 633 if p.Version != "" { 634 b.shell.Commentf("Checking out `%s`", p.Version) 635 if err = b.shell.Run("git", "checkout", "-f", p.Version); err != nil { 636 return nil, err 637 } 638 } 639 640 return checkout, nil 641 } 642 643 func (b *Bootstrap) removeCheckoutDir() error { 644 checkoutPath, _ := b.shell.Env.Get("BUILDKITE_BUILD_CHECKOUT_PATH") 645 646 b.shell.Commentf("Removing %s", checkoutPath) 647 if err := os.RemoveAll(checkoutPath); err != nil { 648 return fmt.Errorf("Failed to remove \"%s\" (%s)", checkoutPath, err) 649 } 650 return nil 651 } 652 653 func (b *Bootstrap) createCheckoutDir() error { 654 checkoutPath, _ := b.shell.Env.Get("BUILDKITE_BUILD_CHECKOUT_PATH") 655 656 if !fileExists(checkoutPath) { 657 b.shell.Commentf("Creating \"%s\"", checkoutPath) 658 if err := os.MkdirAll(checkoutPath, 0777); err != nil { 659 return err 660 } 661 } 662 663 if b.shell.Getwd() != checkoutPath { 664 if err := b.shell.Chdir(checkoutPath); err != nil { 665 return err 666 } 667 } 668 669 return nil 670 } 671 672 // CheckoutPhase creates the build directory and makes sure we're running the 673 // build at the right commit. 674 func (b *Bootstrap) CheckoutPhase() error { 675 if err := b.executeGlobalHook("pre-checkout"); err != nil { 676 return err 677 } 678 679 if err := b.executePluginHook("pre-checkout"); err != nil { 680 return err 681 } 682 683 // Remove the checkout directory if BUILDKITE_CLEAN_CHECKOUT is present 684 if b.CleanCheckout { 685 b.shell.Headerf("Cleaning pipeline checkout") 686 if err := b.removeCheckoutDir(); err != nil { 687 return err 688 } 689 } 690 691 b.shell.Headerf("Preparing working directory") 692 693 // Make sure the build directory exists 694 if err := b.createCheckoutDir(); err != nil { 695 return err 696 } 697 698 // There can only be one checkout hook, either plugin or global, in that order 699 switch { 700 case b.hasPluginHook("checkout"): 701 if err := b.executePluginHook("checkout"); err != nil { 702 return err 703 } 704 case b.hasGlobalHook("checkout"): 705 if err := b.executeGlobalHook("checkout"); err != nil { 706 return err 707 } 708 default: 709 err := retry.Do(func(s *retry.Stats) error { 710 err := b.defaultCheckoutPhase() 711 if err != nil { 712 b.shell.Warningf("Checkout failed! %s (%s)", err, s) 713 } 714 return err 715 }, &retry.Config{Maximum: 3, Interval: 2 * time.Second}) 716 if err != nil { 717 return err 718 } 719 } 720 721 // Store the current value of BUILDKITE_BUILD_CHECKOUT_PATH, so we can detect if 722 // one of the post-checkout hooks changed it. 723 previousCheckoutPath, _ := b.shell.Env.Get("BUILDKITE_BUILD_CHECKOUT_PATH") 724 725 // Run post-checkout hooks 726 if err := b.executeGlobalHook("post-checkout"); err != nil { 727 return err 728 } 729 730 if err := b.executeLocalHook("post-checkout"); err != nil { 731 return err 732 } 733 734 if err := b.executePluginHook("post-checkout"); err != nil { 735 return err 736 } 737 738 // Capture the new checkout path so we can see if it's changed. 739 newCheckoutPath, _ := b.shell.Env.Get("BUILDKITE_BUILD_CHECKOUT_PATH") 740 741 // If the working directory has been changed by a hook, log and switch to it 742 if previousCheckoutPath != "" && previousCheckoutPath != newCheckoutPath { 743 b.shell.Headerf("A post-checkout hook has changed the working directory to \"%s\"", newCheckoutPath) 744 745 if err := b.shell.Chdir(newCheckoutPath); err != nil { 746 return err 747 } 748 } 749 750 return nil 751 } 752 753 func hasGitSubmodules(sh *shell.Shell) bool { 754 return fileExists(filepath.Join(sh.Getwd(), ".gitmodules")) 755 } 756 757 // defaultCheckoutPhase is called by the CheckoutPhase if no global or plugin checkout 758 // hook exists. It performs the default checkout on the Repository provided in the config 759 func (b *Bootstrap) defaultCheckoutPhase() error { 760 // Make sure the build directory exists 761 if err := b.createCheckoutDir(); err != nil { 762 return err 763 } 764 765 if b.SSHKeyscan { 766 addRepositoryHostToSSHKnownHosts(b.shell, b.Repository) 767 } 768 769 // Does the git directory exist? 770 existingGitDir := filepath.Join(b.shell.Getwd(), ".git") 771 if fileExists(existingGitDir) { 772 // Update the the origin of the repository so we can gracefully handle repository renames 773 if err := b.shell.Run("git", "remote", "set-url", "origin", b.Repository); err != nil { 774 // Remove the checkout as often this is due to a corrupt git repo 775 _ = b.removeCheckoutDir() 776 return err 777 } 778 } else { 779 if err := gitClone(b.shell, b.GitCloneFlags, b.Repository, "."); err != nil { 780 // Remove the checkout as often this is due to files left in the dir 781 _ = b.removeCheckoutDir() 782 return err 783 } 784 } 785 786 // Git clean prior to checkout 787 if hasGitSubmodules(b.shell) { 788 if err := gitCleanSubmodules(b.shell, b.GitCleanFlags); err != nil { 789 // Remove the checkout as often this is due to a corrupt git submodules 790 _ = b.removeCheckoutDir() 791 return err 792 } 793 } 794 795 if err := gitClean(b.shell, b.GitCleanFlags); err != nil { 796 // Remove the checkout as often this is due to a corrupt git submodules 797 _ = b.removeCheckoutDir() 798 return err 799 } 800 801 // If a refspec is provided then use it instead. 802 // i.e. `refs/not/a/head` 803 if b.RefSpec != "" { 804 b.shell.Commentf("Fetch and checkout custom refspec") 805 if err := gitFetch(b.shell, "-v --prune", "origin", b.RefSpec); err != nil { 806 return err 807 } 808 809 if err := b.shell.Run("git", "checkout", "-f", b.Commit); err != nil { 810 return err 811 } 812 813 // GitHub has a special ref which lets us fetch a pull request head, whether 814 // or not there is a current head in this repository or another which 815 // references the commit. We presume a commit sha is provided. See: 816 // https://help.github.com/articles/checking-out-pull-requests-locally/#modifying-an-inactive-pull-request-locally 817 } else if b.PullRequest != "false" && strings.Contains(b.PipelineProvider, "github") { 818 b.shell.Commentf("Fetch and checkout pull request head from GitHub") 819 refspec := fmt.Sprintf("refs/pull/%s/head", b.PullRequest) 820 821 if err := gitFetch(b.shell, "-v", "origin", refspec); err != nil { 822 return err 823 } 824 825 gitFetchHead, _ := b.shell.RunAndCapture("git", "rev-parse", "FETCH_HEAD") 826 b.shell.Commentf("FETCH_HEAD is now `%s`", gitFetchHead) 827 828 if err := b.shell.Run("git", "checkout", "-f", b.Commit); err != nil { 829 return err 830 } 831 832 // If the commit is "HEAD" then we can't do a commit-specific fetch and will 833 // need to fetch the remote head and checkout the fetched head explicitly. 834 } else if b.Commit == "HEAD" { 835 b.shell.Commentf("Fetch and checkout remote branch HEAD commit") 836 if err := gitFetch(b.shell, "-v --prune", "origin", b.Branch); err != nil { 837 return err 838 } 839 840 if err := b.shell.Run("git", "checkout", "-f", "FETCH_HEAD"); err != nil { 841 return err 842 } 843 844 // Otherwise fetch and checkout the commit directly. Some repositories don't 845 // support fetching a specific commit so we fall back to fetching all heads 846 // and tags, hoping that the commit is included. 847 } else { 848 if err := gitFetch(b.shell, "-v", "origin", b.Commit); err != nil { 849 // By default `git fetch origin` will only fetch tags which are 850 // reachable from a fetches branch. git 1.9.0+ changed `--tags` to 851 // fetch all tags in addition to the default refspec, but pre 1.9.0 it 852 // excludes the default refspec. 853 gitFetchRefspec, _ := b.shell.RunAndCapture("git", "config", "remote.origin.fetch") 854 if err := gitFetch(b.shell, "-v --prune", "origin", gitFetchRefspec, "+refs/tags/*:refs/tags/*"); err != nil { 855 return err 856 } 857 } 858 if err := b.shell.Run("git", "checkout", "-f", b.Commit); err != nil { 859 return err 860 } 861 } 862 863 var gitSubmodules bool 864 if !b.GitSubmodules && hasGitSubmodules(b.shell) { 865 b.shell.Warningf("This repository has submodules, but submodules are disabled at an agent level") 866 } else if b.GitSubmodules && hasGitSubmodules(b.shell) { 867 b.shell.Commentf("Git submodules detected") 868 gitSubmodules = true 869 } 870 871 if gitSubmodules { 872 // submodules might need their fingerprints verified too 873 if b.SSHKeyscan { 874 b.shell.Commentf("Checking to see if submodule urls need to be added to known_hosts") 875 submoduleRepos, err := gitEnumerateSubmoduleURLs(b.shell) 876 if err != nil { 877 b.shell.Warningf("Failed to enumerate git submodules: %v", err) 878 } else { 879 for _, repository := range submoduleRepos { 880 addRepositoryHostToSSHKnownHosts(b.shell, repository) 881 } 882 } 883 } 884 885 // `submodule sync` will ensure the .git/config 886 // matches the .gitmodules file. The command 887 // is only available in git version 1.8.1, so 888 // if the call fails, continue the bootstrap 889 // script, and show an informative error. 890 if err := b.shell.Run("git", "submodule", "sync", "--recursive"); err != nil { 891 gitVersionOutput, _ := b.shell.RunAndCapture("git", "--version") 892 b.shell.Warningf("Failed to recursively sync git submodules. This is most likely because you have an older version of git installed (" + gitVersionOutput + ") and you need version 1.8.1 and above. If you're using submodules, it's highly recommended you upgrade if you can.") 893 } 894 895 if err := b.shell.Run("git", "submodule", "update", "--init", "--recursive", "--force"); err != nil { 896 return err 897 } 898 if err := b.shell.Run("git", "submodule", "foreach", "--recursive", "git", "reset", "--hard"); err != nil { 899 return err 900 } 901 } 902 903 // Git clean after checkout. We need to do this because submodules could have 904 // changed in between the last checkout and this one. A double clean is the only 905 // good solution to this problem that we've found 906 b.shell.Commentf("Cleaning again to catch any post-checkout changes") 907 908 if err := gitClean(b.shell, b.GitCleanFlags); err != nil { 909 return err 910 } 911 912 if gitSubmodules { 913 if err := gitCleanSubmodules(b.shell, b.GitCleanFlags); err != nil { 914 return err 915 } 916 } 917 918 if _, hasToken := b.shell.Env.Get("BUILDKITE_AGENT_ACCESS_TOKEN"); !hasToken { 919 b.shell.Warningf("Skipping sending Git information to Buildkite as $BUILDKITE_AGENT_ACCESS_TOKEN is missing") 920 return nil 921 } 922 923 // Grab author and commit information and send 924 // it back to Buildkite. But before we do, 925 // we'll check to see if someone else has done 926 // it first. 927 b.shell.Commentf("Checking to see if Git data needs to be sent to Buildkite") 928 if err := b.shell.Run("buildkite-agent", "meta-data", "exists", "buildkite:git:commit"); err != nil { 929 b.shell.Commentf("Sending Git commit information back to Buildkite") 930 931 gitCommitOutput, err := b.shell.RunAndCapture("git", "--no-pager", "show", "HEAD", "-s", "--format=fuller", "--no-color") 932 if err != nil { 933 return err 934 } 935 936 if err = b.shell.Run("buildkite-agent", "meta-data", "set", "buildkite:git:commit", gitCommitOutput); err != nil { 937 return err 938 } 939 } 940 941 return nil 942 } 943 944 // CommandPhase determines how to run the build, and then runs it 945 func (b *Bootstrap) CommandPhase() error { 946 if err := b.executeGlobalHook("pre-command"); err != nil { 947 return err 948 } 949 950 if err := b.executeLocalHook("pre-command"); err != nil { 951 return err 952 } 953 954 if err := b.executePluginHook("pre-command"); err != nil { 955 return err 956 } 957 958 var commandExitError error 959 960 // There can only be one command hook, so we check them in order of plugin, local 961 switch { 962 case b.hasPluginHook("command"): 963 commandExitError = b.executePluginHook("command") 964 case b.hasLocalHook("command"): 965 commandExitError = b.executeLocalHook("command") 966 case b.hasGlobalHook("command"): 967 commandExitError = b.executeGlobalHook("command") 968 default: 969 commandExitError = b.defaultCommandPhase() 970 } 971 972 // If the command returned an exit that wasn't a `exec.ExitError` 973 // (which is returned when the command is actually run, but fails), 974 // then we'll show it in the log. 975 if shell.IsExitError(commandExitError) { 976 b.shell.Errorf("The command exited with status %d", shell.GetExitCode(commandExitError)) 977 } else if commandExitError != nil { 978 b.shell.Errorf(commandExitError.Error()) 979 } 980 981 // Expand the command header if the command fails for any reason 982 if commandExitError != nil { 983 b.shell.Printf("^^^ +++") 984 } 985 986 // Save the command exit status to the env so hooks + plugins can access it. If there is no error 987 // this will be zero. It's used to set the exit code later, so it's important 988 b.shell.Env.Set("BUILDKITE_COMMAND_EXIT_STATUS", fmt.Sprintf("%d", shell.GetExitCode(commandExitError))) 989 990 // Run post-command hooks 991 if err := b.executeGlobalHook("post-command"); err != nil { 992 return err 993 } 994 995 if err := b.executeLocalHook("post-command"); err != nil { 996 return err 997 } 998 999 if err := b.executePluginHook("post-command"); err != nil { 1000 return err 1001 } 1002 1003 return nil 1004 } 1005 1006 // defaultCommandPhase is executed if there is no global or plugin command hook 1007 func (b *Bootstrap) defaultCommandPhase() error { 1008 // Make sure we actually have a command to run 1009 if strings.TrimSpace(b.Command) == "" { 1010 return fmt.Errorf("No command has been provided") 1011 } 1012 1013 scriptFileName := strings.Replace(b.Command, "\n", "", -1) 1014 pathToCommand, err := filepath.Abs(filepath.Join(b.shell.Getwd(), scriptFileName)) 1015 commandIsScript := err == nil && fileExists(pathToCommand) 1016 1017 // If the command isn't a script, then it's something we need 1018 // to eval. But before we even try running it, we should double 1019 // check that the agent is allowed to eval commands. 1020 if !commandIsScript && !b.CommandEval { 1021 b.shell.Commentf("No such file: \"%s\"", scriptFileName) 1022 return fmt.Errorf("This agent is not allowed to evaluate console commands. To allow this, re-run this agent without the `--no-command-eval` option, or specify a script within your repository to run instead (such as scripts/test.sh).") 1023 } 1024 1025 // Also make sure that the script we've resolved is definitely within this 1026 // repository checkout and isn't elsewhere on the system. 1027 if commandIsScript && !b.CommandEval && !strings.HasPrefix(pathToCommand, b.shell.Getwd()+string(os.PathSeparator)) { 1028 b.shell.Commentf("No such file: \"%s\"", scriptFileName) 1029 return fmt.Errorf("This agent is only allowed to run scripts within your repository. To allow this, re-run this agent without the `--no-command-eval` option, or specify a script within your repository to run instead (such as scripts/test.sh).") 1030 } 1031 1032 var cmdToExec string 1033 1034 // The shell gets parsed based on the operating system 1035 shell, err := shellwords.Split(b.Shell) 1036 if err != nil { 1037 return fmt.Errorf("Failed to split shell (%q) into tokens: %v", b.Shell, err) 1038 } 1039 1040 if len(shell) == 0 { 1041 return fmt.Errorf("No shell set for bootstrap") 1042 } 1043 1044 // Windows CMD.EXE is horrible and can't handle newline delimited commands. We write 1045 // a batch script so that it works, but we don't like it 1046 if strings.ToUpper(filepath.Base(shell[0])) == `CMD.EXE` { 1047 batchScript, err := b.writeBatchScript(b.Command) 1048 if err != nil { 1049 return err 1050 } 1051 defer os.Remove(batchScript) 1052 1053 b.shell.Headerf("Running batch script") 1054 if b.Debug { 1055 contents, err := ioutil.ReadFile(batchScript) 1056 if err != nil { 1057 return err 1058 } 1059 b.shell.Commentf("Wrote batch script %s\n%s", batchScript, contents) 1060 } 1061 1062 cmdToExec = batchScript 1063 } else if commandIsScript { 1064 // Make script executable 1065 if err = addExecutePermissionToFile(pathToCommand); err != nil { 1066 b.shell.Warningf("Error marking script %q as executable: %v", pathToCommand, err) 1067 return err 1068 } 1069 1070 // Make the path relative to the shell working dir 1071 scriptPath, err := filepath.Rel(b.shell.Getwd(), pathToCommand) 1072 if err != nil { 1073 return err 1074 } 1075 1076 b.shell.Headerf("Running script") 1077 cmdToExec = fmt.Sprintf(".%c%s", os.PathSeparator, scriptPath) 1078 } else { 1079 b.shell.Headerf("Running commands") 1080 cmdToExec = b.Command 1081 } 1082 1083 // Support deprecated BUILDKITE_DOCKER* env vars 1084 if hasDeprecatedDockerIntegration(b.shell) { 1085 if b.Debug { 1086 b.shell.Commentf("Detected deprecated docker environment variables") 1087 } 1088 return runDeprecatedDockerIntegration(b.shell, []string{cmdToExec}) 1089 } 1090 1091 var cmd []string 1092 cmd = append(cmd, shell...) 1093 cmd = append(cmd, cmdToExec) 1094 1095 if b.Debug { 1096 b.shell.Promptf("%s", process.FormatCommand(cmd[0], cmd[1:])) 1097 } else { 1098 b.shell.Promptf("%s", cmdToExec) 1099 } 1100 1101 return b.shell.RunWithoutPrompt(cmd[0], cmd[1:]...) 1102 } 1103 1104 func (b *Bootstrap) writeBatchScript(cmd string) (string, error) { 1105 scriptFile, err := shell.TempFileWithExtension( 1106 `buildkite-script.bat`, 1107 ) 1108 if err != nil { 1109 return "", err 1110 } 1111 defer scriptFile.Close() 1112 1113 var scriptContents = "@echo off\n" 1114 1115 for _, line := range strings.Split(cmd, "\n") { 1116 if line != "" { 1117 scriptContents += line + "\n" + "if %errorlevel% neq 0 exit /b %errorlevel%\n" 1118 } 1119 } 1120 1121 _, err = io.WriteString(scriptFile, scriptContents) 1122 if err != nil { 1123 return "", err 1124 } 1125 1126 return scriptFile.Name(), nil 1127 1128 } 1129 1130 func (b *Bootstrap) uploadArtifacts() error { 1131 if b.AutomaticArtifactUploadPaths == "" { 1132 return nil 1133 } 1134 1135 // Run pre-artifact hooks 1136 if err := b.executeGlobalHook("pre-artifact"); err != nil { 1137 return err 1138 } 1139 1140 if err := b.executeLocalHook("pre-artifact"); err != nil { 1141 return err 1142 } 1143 1144 if err := b.executePluginHook("pre-artifact"); err != nil { 1145 return err 1146 } 1147 1148 // Run the artifact upload command 1149 b.shell.Headerf("Uploading artifacts") 1150 args := []string{"artifact", "upload", b.AutomaticArtifactUploadPaths} 1151 1152 // If blank, the upload destination is buildkite 1153 if b.ArtifactUploadDestination != "" { 1154 b.shell.Commentf("Using default artifact upload destination") 1155 args = append(args, b.ArtifactUploadDestination) 1156 } 1157 1158 if err := b.shell.Run("buildkite-agent", args...); err != nil { 1159 return err 1160 } 1161 1162 // Run post-artifact hooks 1163 if err := b.executeGlobalHook("post-artifact"); err != nil { 1164 return err 1165 } 1166 1167 if err := b.executeLocalHook("post-artifact"); err != nil { 1168 return err 1169 } 1170 1171 if err := b.executePluginHook("post-artifact"); err != nil { 1172 return err 1173 } 1174 1175 return nil 1176 } 1177 1178 // Check for ignored env variables from the job runner. Some 1179 // env (e.g BUILDKITE_BUILD_PATH) can only be set from config or by hooks. 1180 // If these env are set at a pipeline level, we rewrite them to BUILDKITE_X_BUILD_PATH 1181 // and warn on them here so that users know what is going on 1182 func (b *Bootstrap) ignoredEnv() []string { 1183 var ignored []string 1184 for _, env := range os.Environ() { 1185 if strings.HasPrefix(env, `BUILDKITE_X_`) { 1186 ignored = append(ignored, fmt.Sprintf("BUILDKITE_%s", 1187 strings.TrimPrefix(env, `BUILDKITE_X_`))) 1188 } 1189 } 1190 return ignored 1191 } 1192 1193 type pluginCheckout struct { 1194 *plugin.Plugin 1195 *plugin.Definition 1196 CheckoutDir string 1197 HooksDir string 1198 }