github.com/bshelton229/agent@v3.5.4+incompatible/bootstrap/hook.go (about)

     1  package bootstrap
     2  
     3  import (
     4  	"fmt"
     5  	"io/ioutil"
     6  	"os"
     7  	"path/filepath"
     8  	"runtime"
     9  
    10  	"github.com/buildkite/agent/bootstrap/shell"
    11  	"github.com/buildkite/agent/env"
    12  )
    13  
    14  const (
    15  	hookExitStatusEnv = `BUILDKITE_HOOK_EXIT_STATUS`
    16  	hookWorkingDirEnv = `BUILDKITE_HOOK_WORKING_DIR`
    17  )
    18  
    19  // Hooks get "sourced" into the bootstrap in the sense that they get the
    20  // environment set for them and then we capture any extra environment variables
    21  // that are exported in the script.
    22  
    23  // The tricky thing is that it's impossible to grab the ENV of a child process
    24  // before it finishes, so we've got an awesome (ugly) hack to get around this.
    25  // We write the ENV to file, run the hook and then write the ENV back to another file.
    26  // Then we can use the diff of the two to figure out what changes to make to the
    27  // bootstrap. Horrible, but effective.
    28  
    29  // hookScriptWrapper wraps a hook script with env collection and then provides
    30  // a way to get the difference between the environment before the hook is run and
    31  // after it
    32  type hookScriptWrapper struct {
    33  	hookPath      string
    34  	scriptFile    *os.File
    35  	beforeEnvFile *os.File
    36  	afterEnvFile  *os.File
    37  	beforeWd      string
    38  }
    39  
    40  type hookScriptChanges struct {
    41  	Env *env.Environment
    42  	Dir string
    43  }
    44  
    45  func newHookScriptWrapper(hookPath string) (*hookScriptWrapper, error) {
    46  	var h = &hookScriptWrapper{
    47  		hookPath: hookPath,
    48  	}
    49  
    50  	var err error
    51  	var scriptFileName string = `buildkite-agent-bootstrap-hook-runner`
    52  	var isBashHook bool
    53  
    54  	// we use bash hooks for scripts with no extension, otherwise on windows
    55  	// we probably need a .bat extension
    56  	if filepath.Ext(hookPath) == "" {
    57  		isBashHook = true
    58  	} else if runtime.GOOS == "windows" {
    59  		scriptFileName += ".bat"
    60  	}
    61  
    62  	// Create a temporary file that we'll put the hook runner code in
    63  	h.scriptFile, err = shell.TempFileWithExtension(scriptFileName)
    64  	if err != nil {
    65  		return nil, err
    66  	}
    67  	defer h.scriptFile.Close()
    68  
    69  	// We'll pump the ENV before the hook into this temp file
    70  	h.beforeEnvFile, err = shell.TempFileWithExtension(
    71  		`buildkite-agent-bootstrap-hook-env-before`,
    72  	)
    73  	if err != nil {
    74  		return nil, err
    75  	}
    76  	h.beforeEnvFile.Close()
    77  
    78  	// We'll then pump the ENV _after_ the hook into this temp file
    79  	h.afterEnvFile, err = shell.TempFileWithExtension(
    80  		`buildkite-agent-bootstrap-hook-env-after`,
    81  	)
    82  	if err != nil {
    83  		return nil, err
    84  	}
    85  	h.afterEnvFile.Close()
    86  
    87  	absolutePathToHook, err := filepath.Abs(h.hookPath)
    88  	if err != nil {
    89  		return nil, fmt.Errorf("Failed to find absolute path to \"%s\" (%s)", h.hookPath, err)
    90  	}
    91  
    92  	h.beforeWd, err = os.Getwd()
    93  	if err != nil {
    94  		return nil, err
    95  	}
    96  
    97  	// Create the hook runner code
    98  	var script string
    99  	if runtime.GOOS == "windows" && !isBashHook {
   100  		script = "@echo off\n" +
   101  			"SETLOCAL ENABLEDELAYEDEXPANSION\n" +
   102  			"SET > \"" + h.beforeEnvFile.Name() + "\"\n" +
   103  			"CALL \"" + absolutePathToHook + "\"\n" +
   104  			"SET " + hookExitStatusEnv + "=!ERRORLEVEL!\n" +
   105  			"SET " + hookWorkingDirEnv + "=%CD%\n" +
   106  			"SET > \"" + h.afterEnvFile.Name() + "\"\n" +
   107  			"EXIT %" + hookExitStatusEnv + "%"
   108  	} else {
   109  		script = "export -p > \"" + filepath.ToSlash(h.beforeEnvFile.Name()) + "\"\n" +
   110  			". \"" + filepath.ToSlash(absolutePathToHook) + "\"\n" +
   111  			"export " + hookExitStatusEnv + "=$?\n" +
   112  			"export " + hookWorkingDirEnv + "=$PWD\n" +
   113  			"export -p > \"" + filepath.ToSlash(h.afterEnvFile.Name()) + "\"\n" +
   114  			"exit $" + hookExitStatusEnv
   115  	}
   116  
   117  	// Write the hook script to the runner then close the file so we can run it
   118  	_, err = h.scriptFile.WriteString(script)
   119  	if err != nil {
   120  		return nil, err
   121  	}
   122  
   123  	// Make script executable
   124  	err = addExecutePermissionToFile(h.scriptFile.Name())
   125  	if err != nil {
   126  		return h, err
   127  	}
   128  
   129  	return h, nil
   130  }
   131  
   132  // Path returns the path to the wrapper script, this is the one that should be executed
   133  func (h *hookScriptWrapper) Path() string {
   134  	return h.scriptFile.Name()
   135  }
   136  
   137  // Close cleans up the wrapper script and the environment files
   138  func (h *hookScriptWrapper) Close() {
   139  	os.Remove(h.scriptFile.Name())
   140  	os.Remove(h.beforeEnvFile.Name())
   141  	os.Remove(h.afterEnvFile.Name())
   142  }
   143  
   144  // Changes returns the changes in the environment and working dir after the hook script runs
   145  func (h *hookScriptWrapper) Changes() (hookScriptChanges, error) {
   146  	beforeEnvContents, err := ioutil.ReadFile(h.beforeEnvFile.Name())
   147  	if err != nil {
   148  		return hookScriptChanges{}, fmt.Errorf("Failed to read \"%s\" (%s)", h.beforeEnvFile.Name(), err)
   149  	}
   150  
   151  	afterEnvContents, err := ioutil.ReadFile(h.afterEnvFile.Name())
   152  	if err != nil {
   153  		return hookScriptChanges{}, fmt.Errorf("Failed to read \"%s\" (%s)", h.afterEnvFile.Name(), err)
   154  	}
   155  
   156  	beforeEnv := env.FromExport(string(beforeEnvContents))
   157  	afterEnv := env.FromExport(string(afterEnvContents))
   158  	diff := afterEnv.Diff(beforeEnv)
   159  	wd, _ := diff.Get(hookWorkingDirEnv)
   160  
   161  	diff.Remove(hookExitStatusEnv)
   162  	diff.Remove(hookWorkingDirEnv)
   163  
   164  	return hookScriptChanges{Env: diff, Dir: wd}, nil
   165  }