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 }