github.com/abayer/test-infra@v0.0.5/prow/entrypoint/run.go (about) 1 /* 2 Copyright 2018 The Kubernetes 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 entrypoint 18 19 import ( 20 "errors" 21 "fmt" 22 "io" 23 "io/ioutil" 24 "os" 25 "os/exec" 26 "os/signal" 27 "path/filepath" 28 "strconv" 29 "syscall" 30 "time" 31 32 "github.com/sirupsen/logrus" 33 ) 34 35 const ( 36 // InternalErrorCode is what we write to the marker file to 37 // indicate that we failed to start the wrapped command 38 InternalErrorCode = 127 39 // AbortedErrorCode is what we write to the marker file to 40 // indicate that we were terminated via a signal. 41 AbortedErrorCode = 130 42 43 // DefaultTimeout is the default timeout for the test 44 // process before SIGINT is sent 45 DefaultTimeout = 120 * time.Minute 46 47 // DefaultGracePeriod is the default timeout for the test 48 // process after SIGINT is sent before SIGKILL is sent 49 DefaultGracePeriod = 15 * time.Second 50 ) 51 52 var ( 53 // errTimedOut is used as the command's error when the command 54 // is terminated after the timeout is reached 55 errTimedOut = errors.New("process timed out") 56 // errAborted is used as the command's error when the command 57 // is shut down by an external signal 58 errAborted = errors.New("process aborted") 59 ) 60 61 // Run executes the test process then writes the exit code to the marker file. 62 // This function returns the status code that should be passed to os.Exit(). 63 func (o Options) Run() int { 64 code, err := o.ExecuteProcess() 65 if err != nil { 66 logrus.WithError(err).Error("Error executing test process: %v.", err) 67 } 68 if err := o.mark(code); err != nil { 69 logrus.WithError(err).Error("Error writing exit code to marker file: %v.", err) 70 return InternalErrorCode 71 } 72 return code 73 } 74 75 // ExecuteProcess creates the artifact directory then executes the process as 76 // configured, writing the output to the process log. 77 func (o Options) ExecuteProcess() (int, error) { 78 if o.ArtifactDir != "" { 79 if err := os.MkdirAll(o.ArtifactDir, os.ModePerm); err != nil { 80 return InternalErrorCode, fmt.Errorf("could not create artifact directory(%s): %v", o.ArtifactDir, err) 81 } 82 } 83 processLogFile, err := os.Create(o.ProcessLog) 84 if err != nil { 85 return InternalErrorCode, fmt.Errorf("could not create process logfile(%s): %v", o.ProcessLog, err) 86 } 87 defer processLogFile.Close() 88 89 output := io.MultiWriter(os.Stdout, processLogFile) 90 logrus.SetOutput(output) 91 defer logrus.SetOutput(os.Stdout) 92 93 executable := o.Args[0] 94 var arguments []string 95 if len(o.Args) > 1 { 96 arguments = o.Args[1:] 97 } 98 command := exec.Command(executable, arguments...) 99 command.Stderr = output 100 command.Stdout = output 101 if err := command.Start(); err != nil { 102 return InternalErrorCode, fmt.Errorf("could not start the process: %v", err) 103 } 104 105 // if we get asked to terminate we need to forward 106 // that to the wrapped process as if it timed out 107 interrupt := make(chan os.Signal, 1) 108 signal.Notify(interrupt, os.Interrupt, syscall.SIGTERM) 109 110 timeout := optionOrDefault(o.Timeout, DefaultTimeout) 111 gracePeriod := optionOrDefault(o.GracePeriod, DefaultGracePeriod) 112 var commandErr error 113 cancelled, aborted := false, false 114 done := make(chan error) 115 go func() { 116 done <- command.Wait() 117 }() 118 select { 119 case err := <-done: 120 commandErr = err 121 case <-time.After(timeout): 122 logrus.Errorf("Process did not finish before %s timeout", timeout) 123 cancelled = true 124 gracefullyTerminate(command, done, gracePeriod) 125 case s := <-interrupt: 126 logrus.Errorf("Entrypoint received interrupt: %v", s) 127 cancelled = true 128 aborted = true 129 gracefullyTerminate(command, done, gracePeriod) 130 } 131 132 var returnCode int 133 if cancelled { 134 if aborted { 135 commandErr = errAborted 136 returnCode = AbortedErrorCode 137 } else { 138 commandErr = errTimedOut 139 returnCode = InternalErrorCode 140 } 141 } else { 142 if status, ok := command.ProcessState.Sys().(syscall.WaitStatus); ok { 143 returnCode = status.ExitStatus() 144 } else if commandErr == nil { 145 returnCode = 0 146 } else { 147 returnCode = 1 148 } 149 150 if returnCode != 0 { 151 commandErr = fmt.Errorf("wrapped process failed: %v", commandErr) 152 } 153 } 154 return returnCode, commandErr 155 } 156 157 func (o *Options) mark(exitCode int) error { 158 content := []byte(strconv.Itoa(exitCode)) 159 160 // create temp file in the same directory as the desired marker file 161 dir := filepath.Dir(o.MarkerFile) 162 tempFile, err := ioutil.TempFile(dir, "temp-marker") 163 if err != nil { 164 return fmt.Errorf("could not create temp marker file in %s: %v", dir, err) 165 } 166 // write the exit code to the tempfile, sync to disk and close 167 if _, err = tempFile.Write(content); err != nil { 168 return fmt.Errorf("could not write to temp marker file (%s): %v", tempFile.Name(), err) 169 } 170 if err = tempFile.Sync(); err != nil { 171 return fmt.Errorf("could not sync temp marker file (%s): %v", tempFile.Name(), err) 172 } 173 tempFile.Close() 174 // set desired permission bits, then rename to the desired file name 175 if err = os.Chmod(tempFile.Name(), os.ModePerm); err != nil { 176 return fmt.Errorf("could not chmod (%x) temp marker file (%s): %v", os.ModePerm, tempFile.Name(), err) 177 } 178 if err := os.Rename(tempFile.Name(), o.MarkerFile); err != nil { 179 return fmt.Errorf("could not move marker file to destination path (%s): %v", o.MarkerFile, err) 180 } 181 return nil 182 } 183 184 // optionOrDefault defaults to a value if option 185 // is the zero value 186 func optionOrDefault(option, defaultValue time.Duration) time.Duration { 187 if option == 0 { 188 return defaultValue 189 } 190 191 return option 192 } 193 194 func gracefullyTerminate(command *exec.Cmd, done <-chan error, gracePeriod time.Duration) { 195 if err := command.Process.Signal(os.Interrupt); err != nil { 196 logrus.WithError(err).Error("Could not interrupt process after timeout") 197 } 198 select { 199 case <-done: 200 logrus.Errorf("Process gracefully exited before %s grace period", gracePeriod) 201 // but we ignore the output error as we will want errTimedOut 202 case <-time.After(gracePeriod): 203 logrus.Errorf("Process did not exit before %s grace period", gracePeriod) 204 if err := command.Process.Kill(); err != nil { 205 logrus.WithError(err).Error("Could not kill process after grace period") 206 } 207 } 208 }