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  }