You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

208 lines
6.3 KiB

/*
Copyright 2018 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package entrypoint
import (
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"os/exec"
"os/signal"
"path/filepath"
"strconv"
"syscall"
"time"
"github.com/sirupsen/logrus"
)
const (
// InternalErrorCode is what we write to the marker file to
// indicate that we failed to start the wrapped command
InternalErrorCode = 127
// AbortedErrorCode is what we write to the marker file to
// indicate that we were terminated via a signal.
AbortedErrorCode = 130
// DefaultTimeout is the default timeout for the test
// process before SIGINT is sent
DefaultTimeout = 120 * time.Minute
// DefaultGracePeriod is the default timeout for the test
// process after SIGINT is sent before SIGKILL is sent
DefaultGracePeriod = 15 * time.Second
)
var (
// errTimedOut is used as the command's error when the command
// is terminated after the timeout is reached
errTimedOut = errors.New("process timed out")
// errAborted is used as the command's error when the command
// is shut down by an external signal
errAborted = errors.New("process aborted")
)
// Run executes the test process then writes the exit code to the marker file.
// This function returns the status code that should be passed to os.Exit().
func (o Options) Run() int {
code, err := o.ExecuteProcess()
if err != nil {
logrus.WithError(err).Error("Error executing test process")
}
if err := o.mark(code); err != nil {
logrus.WithError(err).Error("Error writing exit code to marker file")
return InternalErrorCode
}
return code
}
// ExecuteProcess creates the artifact directory then executes the process as
// configured, writing the output to the process log.
func (o Options) ExecuteProcess() (int, error) {
if o.ArtifactDir != "" {
if err := os.MkdirAll(o.ArtifactDir, os.ModePerm); err != nil {
return InternalErrorCode, fmt.Errorf("could not create artifact directory(%s): %v", o.ArtifactDir, err)
}
}
processLogFile, err := os.Create(o.ProcessLog)
if err != nil {
return InternalErrorCode, fmt.Errorf("could not create process logfile(%s): %v", o.ProcessLog, err)
}
defer processLogFile.Close()
output := io.MultiWriter(os.Stdout, processLogFile)
logrus.SetOutput(output)
defer logrus.SetOutput(os.Stdout)
executable := o.Args[0]
var arguments []string
if len(o.Args) > 1 {
arguments = o.Args[1:]
}
command := exec.Command(executable, arguments...)
command.Stderr = output
command.Stdout = output
if err := command.Start(); err != nil {
return InternalErrorCode, fmt.Errorf("could not start the process: %v", err)
}
// if we get asked to terminate we need to forward
// that to the wrapped process as if it timed out
interrupt := make(chan os.Signal, 1)
signal.Notify(interrupt, os.Interrupt, syscall.SIGTERM)
timeout := optionOrDefault(o.Timeout, DefaultTimeout)
gracePeriod := optionOrDefault(o.GracePeriod, DefaultGracePeriod)
var commandErr error
cancelled, aborted := false, false
done := make(chan error)
go func() {
done <- command.Wait()
}()
select {
case err := <-done:
commandErr = err
case <-time.After(timeout):
logrus.Errorf("Process did not finish before %s timeout", timeout)
cancelled = true
gracefullyTerminate(command, done, gracePeriod)
case s := <-interrupt:
logrus.Errorf("Entrypoint received interrupt: %v", s)
cancelled = true
aborted = true
gracefullyTerminate(command, done, gracePeriod)
}
var returnCode int
if cancelled {
if aborted {
commandErr = errAborted
returnCode = AbortedErrorCode
} else {
commandErr = errTimedOut
returnCode = InternalErrorCode
}
} else {
if status, ok := command.ProcessState.Sys().(syscall.WaitStatus); ok {
returnCode = status.ExitStatus()
} else if commandErr == nil {
returnCode = 0
} else {
returnCode = 1
}
if returnCode != 0 {
commandErr = fmt.Errorf("wrapped process failed: %v", commandErr)
}
}
return returnCode, commandErr
}
func (o *Options) mark(exitCode int) error {
content := []byte(strconv.Itoa(exitCode))
// create temp file in the same directory as the desired marker file
dir := filepath.Dir(o.MarkerFile)
tempFile, err := ioutil.TempFile(dir, "temp-marker")
if err != nil {
return fmt.Errorf("could not create temp marker file in %s: %v", dir, err)
}
// write the exit code to the tempfile, sync to disk and close
if _, err = tempFile.Write(content); err != nil {
return fmt.Errorf("could not write to temp marker file (%s): %v", tempFile.Name(), err)
}
if err = tempFile.Sync(); err != nil {
return fmt.Errorf("could not sync temp marker file (%s): %v", tempFile.Name(), err)
}
tempFile.Close()
// set desired permission bits, then rename to the desired file name
if err = os.Chmod(tempFile.Name(), os.ModePerm); err != nil {
return fmt.Errorf("could not chmod (%x) temp marker file (%s): %v", os.ModePerm, tempFile.Name(), err)
}
if err := os.Rename(tempFile.Name(), o.MarkerFile); err != nil {
return fmt.Errorf("could not move marker file to destination path (%s): %v", o.MarkerFile, err)
}
return nil
}
// optionOrDefault defaults to a value if option
// is the zero value
func optionOrDefault(option, defaultValue time.Duration) time.Duration {
if option == 0 {
return defaultValue
}
return option
}
func gracefullyTerminate(command *exec.Cmd, done <-chan error, gracePeriod time.Duration) {
if err := command.Process.Signal(os.Interrupt); err != nil {
logrus.WithError(err).Error("Could not interrupt process after timeout")
}
select {
case <-done:
logrus.Errorf("Process gracefully exited before %s grace period", gracePeriod)
// but we ignore the output error as we will want errTimedOut
case <-time.After(gracePeriod):
logrus.Errorf("Process did not exit before %s grace period", gracePeriod)
if err := command.Process.Kill(); err != nil {
logrus.WithError(err).Error("Could not kill process after grace period")
}
}
}