package healthcheck import ( "context" "encoding/json" "errors" "net/http" "time" "github.com/moby/moby/api/types/container" "github.com/yusing/godoxy/internal/docker" "github.com/yusing/godoxy/internal/types" httputils "github.com/yusing/goutils/http" ) type DockerHealthcheckState struct { client *docker.SharedClient containerId string numDockerFailures int } const dockerFailuresThreshold = 3 var ErrDockerHealthCheckFailedTooManyTimes = errors.New("docker health check failed too many times") var ErrDockerHealthCheckNotAvailable = errors.New("docker health check not available") func NewDockerHealthcheckState(client *docker.SharedClient, containerId string) *DockerHealthcheckState { client.InterceptHTTPClient(interceptDockerInspectResponse) return &DockerHealthcheckState{ client: client, containerId: containerId, numDockerFailures: 0, } } func Docker(ctx context.Context, state *DockerHealthcheckState, timeout time.Duration) (types.HealthCheckResult, error) { if state.numDockerFailures > dockerFailuresThreshold { return types.HealthCheckResult{}, ErrDockerHealthCheckFailedTooManyTimes } ctx, cancel := context.WithTimeout(ctx, timeout) defer cancel() // the actual inspect response is intercepted and returned as RequestInterceptedError _, err := state.client.ContainerInspect(ctx, state.containerId) var interceptedErr *httputils.RequestInterceptedError if !httputils.AsRequestInterceptedError(err, &interceptedErr) { state.numDockerFailures++ return types.HealthCheckResult{}, err } if interceptedErr == nil || interceptedErr.Data == nil { // should not happen state.numDockerFailures++ return types.HealthCheckResult{}, errors.New("intercepted error is nil or data is nil") } containerState := interceptedErr.Data.(container.State) status := containerState.Status switch status { case "dead", "exited", "paused", "restarting", "removing": state.numDockerFailures = 0 return types.HealthCheckResult{ Healthy: false, Detail: "container is " + string(status), }, nil case "created": state.numDockerFailures = 0 return types.HealthCheckResult{ Healthy: false, Detail: "container is not started", }, nil } health := containerState.Health if health == nil { // no health check from docker, return error to trigger fallback state.numDockerFailures = dockerFailuresThreshold + 1 return types.HealthCheckResult{}, ErrDockerHealthCheckNotAvailable } state.numDockerFailures = 0 result := types.HealthCheckResult{ Healthy: health.Status == container.Healthy, } if len(health.Log) > 0 { lastLog := health.Log[len(health.Log)-1] result.Detail = lastLog.Output result.Latency = lastLog.End.Sub(lastLog.Start) } return result, nil } func interceptDockerInspectResponse(resp *http.Response) (intercepted bool, err error) { if resp.StatusCode != http.StatusOK { return false, nil } body, release, err := httputils.ReadAllBody(resp) resp.Body.Close() if err != nil { return false, err } var state container.State err = json.Unmarshal(body, &state) release(body) if err != nil { return false, err } return true, httputils.NewRequestInterceptedError(resp, state) }