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 }