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  }