vitess.io/vitess@v0.16.2/go/vt/hook/hook.go (about) 1 /* 2 Copyright 2019 The Vitess Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package hook 18 19 import ( 20 "bytes" 21 "context" 22 "errors" 23 "fmt" 24 "io" 25 "os" 26 "os/exec" 27 "path" 28 "strings" 29 "syscall" 30 "time" 31 32 vtenv "vitess.io/vitess/go/vt/env" 33 "vitess.io/vitess/go/vt/log" 34 ) 35 36 // Hook is the input structure for this library. 37 type Hook struct { 38 Name string 39 Parameters []string 40 ExtraEnv map[string]string 41 } 42 43 // HookResult is returned by the Execute method. 44 type HookResult struct { 45 ExitStatus int // HOOK_SUCCESS if it succeeded 46 Stdout string 47 Stderr string 48 } 49 50 // The hook will return a value between 0 and 255. 0 if it succeeds. 51 // So we have these additional values here for more information. 52 const ( 53 // HOOK_SUCCESS is returned when the hook worked. 54 HOOK_SUCCESS = 0 55 56 // HOOK_DOES_NOT_EXIST is returned when the hook cannot be found. 57 HOOK_DOES_NOT_EXIST = -1 58 59 // HOOK_STAT_FAILED is returned when the hook exists, but stat 60 // on it fails. 61 HOOK_STAT_FAILED = -2 62 63 // HOOK_CANNOT_GET_EXIT_STATUS is returned when after 64 // execution, we fail to get the exit code for the hook. 65 HOOK_CANNOT_GET_EXIT_STATUS = -3 66 67 // HOOK_INVALID_NAME is returned if a hook has an invalid name. 68 HOOK_INVALID_NAME = -4 69 70 // HOOK_VTROOT_ERROR is returned if VTROOT is not set properly. 71 HOOK_VTROOT_ERROR = -5 72 73 // HOOK_GENERIC_ERROR is returned for unknown errors. 74 HOOK_GENERIC_ERROR = -6 75 76 // HOOK_TIMEOUT_ERROR is returned when a CommandContext has its context 77 // become done before the command terminates. 78 HOOK_TIMEOUT_ERROR = -7 79 ) 80 81 // WaitFunc is a return type for the Pipe methods. 82 // It returns the process stderr and an error, if any. 83 type WaitFunc func() (string, error) 84 85 // NewHook returns a Hook object with the provided name and params. 86 func NewHook(name string, params []string) *Hook { 87 return &Hook{Name: name, Parameters: params} 88 } 89 90 // NewSimpleHook returns a Hook object with just a name. 91 func NewSimpleHook(name string) *Hook { 92 return &Hook{Name: name} 93 } 94 95 // NewHookWithEnv returns a Hook object with the provided name, params and ExtraEnv. 96 func NewHookWithEnv(name string, params []string, env map[string]string) *Hook { 97 return &Hook{Name: name, Parameters: params, ExtraEnv: env} 98 } 99 100 // findHook tries to locate the hook, and returns the exec.Cmd for it. 101 func (hook *Hook) findHook(ctx context.Context) (*exec.Cmd, int, error) { 102 // Check the hook path. 103 if strings.Contains(hook.Name, "/") { 104 return nil, HOOK_INVALID_NAME, fmt.Errorf("hook cannot contain '/'") 105 } 106 107 // Find our root. 108 root, err := vtenv.VtRoot() 109 if err != nil { 110 return nil, HOOK_VTROOT_ERROR, fmt.Errorf("cannot get VTROOT: %v", err) 111 } 112 113 // See if the hook exists. 114 vthook := path.Join(root, "vthook", hook.Name) 115 _, err = os.Stat(vthook) 116 if err != nil { 117 if os.IsNotExist(err) { 118 return nil, HOOK_DOES_NOT_EXIST, fmt.Errorf("missing hook %v", vthook) 119 } 120 121 return nil, HOOK_STAT_FAILED, fmt.Errorf("cannot stat hook %v: %v", vthook, err) 122 } 123 124 // Configure the command. 125 log.Infof("hook: executing hook: %v %v", vthook, strings.Join(hook.Parameters, " ")) 126 cmd := exec.CommandContext(ctx, vthook, hook.Parameters...) 127 if len(hook.ExtraEnv) > 0 { 128 cmd.Env = os.Environ() 129 for key, value := range hook.ExtraEnv { 130 cmd.Env = append(cmd.Env, key+"="+value) 131 } 132 } 133 134 return cmd, HOOK_SUCCESS, nil 135 } 136 137 // ExecuteContext tries to execute the Hook with the given context and returns a HookResult. 138 func (hook *Hook) ExecuteContext(ctx context.Context) (result *HookResult) { 139 result = &HookResult{} 140 141 // Find the hook. 142 cmd, status, err := hook.findHook(ctx) 143 if err != nil { 144 result.ExitStatus = status 145 result.Stderr = err.Error() + "\n" 146 return result 147 } 148 149 // Run it. 150 var stdout, stderr bytes.Buffer 151 cmd.Stdout = &stdout 152 cmd.Stderr = &stderr 153 154 start := time.Now() 155 err = cmd.Run() 156 duration := time.Since(start) 157 158 result.Stdout = stdout.String() 159 result.Stderr = stderr.String() 160 161 defer func() { 162 log.Infof("hook: result is %v", result.String()) 163 }() 164 165 if err == nil { 166 result.ExitStatus = HOOK_SUCCESS 167 return result 168 } 169 170 if ctx.Err() != nil && errors.Is(ctx.Err(), context.DeadlineExceeded) { 171 // When (exec.Cmd).Run hits a context cancelled, the process is killed via SIGTERM. 172 // This means: 173 // 1. cmd.ProcessState.Exited() is false. 174 // 2. cmd.ProcessState.ExitCode() is -1. 175 // [ref]: https://golang.org/pkg/os/#ProcessState.ExitCode 176 // 177 // Therefore, we need to catch this error specifically, and set result.ExitStatus to 178 // HOOK_TIMEOUT_ERROR, because just using ExitStatus will result in HOOK_DOES_NOT_EXIST, 179 // which would be wrong. Since we're already doing some custom handling, we'll also include 180 // the amount of time the command was running in the error string, in case that is helpful. 181 result.ExitStatus = HOOK_TIMEOUT_ERROR 182 result.Stderr += fmt.Sprintf("ERROR: (after %s) %s\n", duration, err) 183 return result 184 } 185 186 if cmd.ProcessState != nil && cmd.ProcessState.Sys() != nil { 187 result.ExitStatus = cmd.ProcessState.Sys().(syscall.WaitStatus).ExitStatus() 188 } else { 189 result.ExitStatus = HOOK_CANNOT_GET_EXIT_STATUS 190 } 191 result.Stderr += "ERROR: " + err.Error() + "\n" 192 193 return result 194 } 195 196 // Execute tries to execute the Hook and returns a HookResult. 197 func (hook *Hook) Execute() (result *HookResult) { 198 return hook.ExecuteContext(context.Background()) 199 } 200 201 // ExecuteOptional executes an optional hook, logs if it doesn't 202 // exist, and returns a printable error. 203 func (hook *Hook) ExecuteOptional() error { 204 hr := hook.Execute() 205 switch hr.ExitStatus { 206 case HOOK_DOES_NOT_EXIST: 207 log.Infof("%v hook doesn't exist", hook.Name) 208 case HOOK_VTROOT_ERROR: 209 log.Infof("VTROOT not set, so %v hook doesn't exist", hook.Name) 210 case HOOK_SUCCESS: 211 // nothing to do here 212 default: 213 return fmt.Errorf("%v hook failed(%v): %v", hook.Name, hr.ExitStatus, hr.Stderr) 214 } 215 return nil 216 } 217 218 // ExecuteAsWritePipe will execute the hook as in a Unix pipe, 219 // directing output to the provided writer. It will return: 220 // - an io.WriteCloser to write data to. 221 // - a WaitFunc method to call to wait for the process to exit, 222 // that returns stderr and the cmd.Wait() error. 223 // - an error code and an error if anything fails. 224 func (hook *Hook) ExecuteAsWritePipe(out io.Writer) (io.WriteCloser, WaitFunc, int, error) { 225 // Find the hook. 226 cmd, status, err := hook.findHook(context.Background()) 227 if err != nil { 228 return nil, nil, status, err 229 } 230 231 // Configure the process's stdin, stdout, and stderr. 232 in, err := cmd.StdinPipe() 233 if err != nil { 234 return nil, nil, HOOK_GENERIC_ERROR, fmt.Errorf("failed to configure stdin: %v", err) 235 } 236 cmd.Stdout = out 237 var stderr bytes.Buffer 238 cmd.Stderr = &stderr 239 240 // Start the process. 241 err = cmd.Start() 242 if err != nil { 243 status = HOOK_CANNOT_GET_EXIT_STATUS 244 if cmd.ProcessState != nil && cmd.ProcessState.Sys() != nil { 245 status = cmd.ProcessState.Sys().(syscall.WaitStatus).ExitStatus() 246 } 247 return nil, nil, status, err 248 } 249 250 // And return 251 return in, func() (string, error) { 252 err := cmd.Wait() 253 return stderr.String(), err 254 }, HOOK_SUCCESS, nil 255 } 256 257 // ExecuteAsReadPipe will execute the hook as in a Unix pipe, reading 258 // from the provided reader. It will return: 259 // - an io.Reader to read piped data from. 260 // - a WaitFunc method to call to wait for the process to exit, that 261 // returns stderr and the Wait() error. 262 // - an error code and an error if anything fails. 263 func (hook *Hook) ExecuteAsReadPipe(in io.Reader) (io.Reader, WaitFunc, int, error) { 264 // Find the hook. 265 cmd, status, err := hook.findHook(context.Background()) 266 if err != nil { 267 return nil, nil, status, err 268 } 269 270 // Configure the process's stdin, stdout, and stderr. 271 out, err := cmd.StdoutPipe() 272 if err != nil { 273 return nil, nil, HOOK_GENERIC_ERROR, fmt.Errorf("failed to configure stdout: %v", err) 274 } 275 cmd.Stdin = in 276 var stderr bytes.Buffer 277 cmd.Stderr = &stderr 278 279 // Start the process. 280 err = cmd.Start() 281 if err != nil { 282 status = HOOK_CANNOT_GET_EXIT_STATUS 283 if cmd.ProcessState != nil && cmd.ProcessState.Sys() != nil { 284 status = cmd.ProcessState.Sys().(syscall.WaitStatus).ExitStatus() 285 } 286 return nil, nil, status, err 287 } 288 289 // And return 290 return out, func() (string, error) { 291 err := cmd.Wait() 292 return stderr.String(), err 293 }, HOOK_SUCCESS, nil 294 } 295 296 // String returns a printable version of the HookResult 297 func (hr *HookResult) String() string { 298 result := "result: " 299 switch hr.ExitStatus { 300 case HOOK_SUCCESS: 301 result += "HOOK_SUCCESS" 302 case HOOK_DOES_NOT_EXIST: 303 result += "HOOK_DOES_NOT_EXIST" 304 case HOOK_STAT_FAILED: 305 result += "HOOK_STAT_FAILED" 306 case HOOK_CANNOT_GET_EXIT_STATUS: 307 result += "HOOK_CANNOT_GET_EXIT_STATUS" 308 case HOOK_INVALID_NAME: 309 result += "HOOK_INVALID_NAME" 310 case HOOK_VTROOT_ERROR: 311 result += "HOOK_VTROOT_ERROR" 312 default: 313 result += fmt.Sprintf("exit(%v)", hr.ExitStatus) 314 } 315 if hr.Stdout != "" { 316 result += "\nstdout:\n" + hr.Stdout 317 } 318 if hr.Stderr != "" { 319 result += "\nstderr:\n" + hr.Stderr 320 } 321 return result 322 }