package healthcheck import ( "context" "errors" "net/http" "time" "github.com/bytedance/sonic" "github.com/moby/moby/api/types/container" "github.com/moby/moby/client" "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") 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, client.ContainerInspectOptions{}) 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 = sonic.Unmarshal(body, &state) release(body) if err != nil { return false, err } return true, httputils.NewRequestInterceptedError(resp, state) }