run.go 6.3 KB

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