github.com/hanks177/podman/v4@v4.1.3-0.20220613032544-16d90015bc83/libpod/healthcheck.go (about)

     1  package libpod
     2  
     3  import (
     4  	"bufio"
     5  	"io/ioutil"
     6  	"os"
     7  	"path/filepath"
     8  	"strings"
     9  	"time"
    10  
    11  	"github.com/hanks177/podman/v4/libpod/define"
    12  	"github.com/pkg/errors"
    13  	"github.com/sirupsen/logrus"
    14  )
    15  
    16  const (
    17  	// MaxHealthCheckNumberLogs is the maximum number of attempts we keep
    18  	// in the healthcheck history file
    19  	MaxHealthCheckNumberLogs int = 5
    20  	// MaxHealthCheckLogLength in characters
    21  	MaxHealthCheckLogLength = 500
    22  )
    23  
    24  // HealthCheck verifies the state and validity of the healthcheck configuration
    25  // on the container and then executes the healthcheck
    26  func (r *Runtime) HealthCheck(name string) (define.HealthCheckStatus, error) {
    27  	container, err := r.LookupContainer(name)
    28  	if err != nil {
    29  		return define.HealthCheckContainerNotFound, errors.Wrapf(err, "unable to lookup %s to perform a health check", name)
    30  	}
    31  	hcStatus, err := checkHealthCheckCanBeRun(container)
    32  	if err == nil {
    33  		return container.runHealthCheck()
    34  	}
    35  	return hcStatus, err
    36  }
    37  
    38  // runHealthCheck runs the health check as defined by the container
    39  func (c *Container) runHealthCheck() (define.HealthCheckStatus, error) {
    40  	var (
    41  		newCommand    []string
    42  		returnCode    int
    43  		inStartPeriod bool
    44  	)
    45  	hcCommand := c.HealthCheckConfig().Test
    46  	if len(hcCommand) < 1 {
    47  		return define.HealthCheckNotDefined, errors.Errorf("container %s has no defined healthcheck", c.ID())
    48  	}
    49  	switch hcCommand[0] {
    50  	case "", "NONE":
    51  		return define.HealthCheckNotDefined, errors.Errorf("container %s has no defined healthcheck", c.ID())
    52  	case "CMD":
    53  		newCommand = hcCommand[1:]
    54  	case "CMD-SHELL":
    55  		// TODO: SHELL command from image not available in Container - use Docker default
    56  		newCommand = []string{"/bin/sh", "-c", strings.Join(hcCommand[1:], " ")}
    57  	default:
    58  		// command supplied on command line - pass as-is
    59  		newCommand = hcCommand
    60  	}
    61  	if len(newCommand) < 1 || newCommand[0] == "" {
    62  		return define.HealthCheckNotDefined, errors.Errorf("container %s has no defined healthcheck", c.ID())
    63  	}
    64  	rPipe, wPipe, err := os.Pipe()
    65  	if err != nil {
    66  		return define.HealthCheckInternalError, errors.Wrapf(err, "unable to create pipe for healthcheck session")
    67  	}
    68  	defer wPipe.Close()
    69  	defer rPipe.Close()
    70  
    71  	streams := new(define.AttachStreams)
    72  
    73  	streams.InputStream = bufio.NewReader(os.Stdin)
    74  	streams.OutputStream = wPipe
    75  	streams.ErrorStream = wPipe
    76  	streams.AttachOutput = true
    77  	streams.AttachError = true
    78  	streams.AttachInput = true
    79  
    80  	stdout := []string{}
    81  	go func() {
    82  		scanner := bufio.NewScanner(rPipe)
    83  		for scanner.Scan() {
    84  			stdout = append(stdout, scanner.Text())
    85  		}
    86  	}()
    87  
    88  	logrus.Debugf("executing health check command %s for %s", strings.Join(newCommand, " "), c.ID())
    89  	timeStart := time.Now()
    90  	hcResult := define.HealthCheckSuccess
    91  	config := new(ExecConfig)
    92  	config.Command = newCommand
    93  	exitCode, hcErr := c.Exec(config, streams, nil)
    94  	if hcErr != nil {
    95  		errCause := errors.Cause(hcErr)
    96  		hcResult = define.HealthCheckFailure
    97  		if errCause == define.ErrOCIRuntimeNotFound ||
    98  			errCause == define.ErrOCIRuntimePermissionDenied ||
    99  			errCause == define.ErrOCIRuntime {
   100  			returnCode = 1
   101  			hcErr = nil
   102  		} else {
   103  			returnCode = 125
   104  		}
   105  	} else if exitCode != 0 {
   106  		hcResult = define.HealthCheckFailure
   107  		returnCode = 1
   108  	}
   109  	timeEnd := time.Now()
   110  	if c.HealthCheckConfig().StartPeriod > 0 {
   111  		// there is a start-period we need to honor; we add startPeriod to container start time
   112  		startPeriodTime := c.state.StartedTime.Add(c.HealthCheckConfig().StartPeriod)
   113  		if timeStart.Before(startPeriodTime) {
   114  			// we are still in the start period, flip the inStartPeriod bool
   115  			inStartPeriod = true
   116  			logrus.Debugf("healthcheck for %s being run in start-period", c.ID())
   117  		}
   118  	}
   119  
   120  	eventLog := strings.Join(stdout, "\n")
   121  	if len(eventLog) > MaxHealthCheckLogLength {
   122  		eventLog = eventLog[:MaxHealthCheckLogLength]
   123  	}
   124  
   125  	if timeEnd.Sub(timeStart) > c.HealthCheckConfig().Timeout {
   126  		returnCode = -1
   127  		hcResult = define.HealthCheckFailure
   128  		hcErr = errors.Errorf("healthcheck command exceeded timeout of %s", c.HealthCheckConfig().Timeout.String())
   129  	}
   130  	hcl := newHealthCheckLog(timeStart, timeEnd, returnCode, eventLog)
   131  	if err := c.updateHealthCheckLog(hcl, inStartPeriod); err != nil {
   132  		return hcResult, errors.Wrapf(err, "unable to update health check log %s for %s", c.healthCheckLogPath(), c.ID())
   133  	}
   134  	return hcResult, hcErr
   135  }
   136  
   137  func checkHealthCheckCanBeRun(c *Container) (define.HealthCheckStatus, error) {
   138  	cstate, err := c.State()
   139  	if err != nil {
   140  		return define.HealthCheckInternalError, err
   141  	}
   142  	if cstate != define.ContainerStateRunning {
   143  		return define.HealthCheckContainerStopped, errors.Errorf("container %s is not running", c.ID())
   144  	}
   145  	if !c.HasHealthCheck() {
   146  		return define.HealthCheckNotDefined, errors.Errorf("container %s has no defined healthcheck", c.ID())
   147  	}
   148  	return define.HealthCheckDefined, nil
   149  }
   150  
   151  func newHealthCheckLog(start, end time.Time, exitCode int, log string) define.HealthCheckLog {
   152  	return define.HealthCheckLog{
   153  		Start:    start.Format(time.RFC3339Nano),
   154  		End:      end.Format(time.RFC3339Nano),
   155  		ExitCode: exitCode,
   156  		Output:   log,
   157  	}
   158  }
   159  
   160  // updatedHealthCheckStatus updates the health status of the container
   161  // in the healthcheck log
   162  func (c *Container) updateHealthStatus(status string) error {
   163  	healthCheck, err := c.getHealthCheckLog()
   164  	if err != nil {
   165  		return err
   166  	}
   167  	healthCheck.Status = status
   168  	newResults, err := json.Marshal(healthCheck)
   169  	if err != nil {
   170  		return errors.Wrapf(err, "unable to marshall healthchecks for writing status")
   171  	}
   172  	return ioutil.WriteFile(c.healthCheckLogPath(), newResults, 0700)
   173  }
   174  
   175  // UpdateHealthCheckLog parses the health check results and writes the log
   176  func (c *Container) updateHealthCheckLog(hcl define.HealthCheckLog, inStartPeriod bool) error {
   177  	healthCheck, err := c.getHealthCheckLog()
   178  	if err != nil {
   179  		return err
   180  	}
   181  	if hcl.ExitCode == 0 {
   182  		//	set status to healthy, reset failing state to 0
   183  		healthCheck.Status = define.HealthCheckHealthy
   184  		healthCheck.FailingStreak = 0
   185  	} else {
   186  		if len(healthCheck.Status) < 1 {
   187  			healthCheck.Status = define.HealthCheckHealthy
   188  		}
   189  		if !inStartPeriod {
   190  			// increment failing streak
   191  			healthCheck.FailingStreak++
   192  			// if failing streak > retries, then status to unhealthy
   193  			if healthCheck.FailingStreak >= c.HealthCheckConfig().Retries {
   194  				healthCheck.Status = define.HealthCheckUnhealthy
   195  			}
   196  		}
   197  	}
   198  	healthCheck.Log = append(healthCheck.Log, hcl)
   199  	if len(healthCheck.Log) > MaxHealthCheckNumberLogs {
   200  		healthCheck.Log = healthCheck.Log[1:]
   201  	}
   202  	newResults, err := json.Marshal(healthCheck)
   203  	if err != nil {
   204  		return errors.Wrapf(err, "unable to marshall healthchecks for writing")
   205  	}
   206  	return ioutil.WriteFile(c.healthCheckLogPath(), newResults, 0700)
   207  }
   208  
   209  // HealthCheckLogPath returns the path for where the health check log is
   210  func (c *Container) healthCheckLogPath() string {
   211  	return filepath.Join(filepath.Dir(c.state.RunDir), "healthcheck.log")
   212  }
   213  
   214  // getHealthCheckLog returns HealthCheck results by reading the container's
   215  // health check log file.  If the health check log file does not exist, then
   216  // an empty healthcheck struct is returned
   217  // The caller should lock the container before this function is called.
   218  func (c *Container) getHealthCheckLog() (define.HealthCheckResults, error) {
   219  	var healthCheck define.HealthCheckResults
   220  	if _, err := os.Stat(c.healthCheckLogPath()); os.IsNotExist(err) {
   221  		return healthCheck, nil
   222  	}
   223  	b, err := ioutil.ReadFile(c.healthCheckLogPath())
   224  	if err != nil {
   225  		return healthCheck, errors.Wrap(err, "failed to read health check log file")
   226  	}
   227  	if err := json.Unmarshal(b, &healthCheck); err != nil {
   228  		return healthCheck, errors.Wrapf(err, "failed to unmarshal existing healthcheck results in %s", c.healthCheckLogPath())
   229  	}
   230  	return healthCheck, nil
   231  }
   232  
   233  // HealthCheckStatus returns the current state of a container with a healthcheck
   234  func (c *Container) HealthCheckStatus() (string, error) {
   235  	if !c.HasHealthCheck() {
   236  		return "", errors.Errorf("container %s has no defined healthcheck", c.ID())
   237  	}
   238  	c.lock.Lock()
   239  	defer c.lock.Unlock()
   240  	if err := c.syncContainer(); err != nil {
   241  		return "", err
   242  	}
   243  	results, err := c.getHealthCheckLog()
   244  	if err != nil {
   245  		return "", errors.Wrapf(err, "unable to get healthcheck log for %s", c.ID())
   246  	}
   247  	return results.Status, nil
   248  }
   249  
   250  func (c *Container) disableHealthCheckSystemd() bool {
   251  	if os.Getenv("DISABLE_HC_SYSTEMD") == "true" {
   252  		return true
   253  	}
   254  	if c.config.HealthCheckConfig.Interval == 0 {
   255  		return true
   256  	}
   257  	return false
   258  }