mirror of
https://github.com/yusing/godoxy.git
synced 2025-05-21 13:02:34 +02:00

Optionally allow a user to specify a “warm-up” endpoint to start the container, returning a 403 if the endpoint isn’t hit and the container has been stopped. This can help prevent bots from starting random containers, or allow health check systems to run some probes.
114 lines
2.9 KiB
Go
114 lines
2.9 KiB
Go
package idlewatcher
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net/http"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/yusing/go-proxy/internal/common"
|
|
gphttp "github.com/yusing/go-proxy/internal/net/http"
|
|
"github.com/yusing/go-proxy/internal/watcher/health"
|
|
)
|
|
|
|
// ServeHTTP implements http.Handler.
|
|
func (w *Watcher) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
|
|
shouldNext := w.wakeFromHTTP(rw, r)
|
|
if !shouldNext {
|
|
return
|
|
}
|
|
select {
|
|
case <-r.Context().Done():
|
|
return
|
|
default:
|
|
w.rp.ServeHTTP(rw, r)
|
|
}
|
|
}
|
|
|
|
func (w *Watcher) wakeFromHTTP(rw http.ResponseWriter, r *http.Request) (shouldNext bool) {
|
|
w.resetIdleTimer()
|
|
|
|
// pass through if container is already ready
|
|
if w.ready.Load() {
|
|
return true
|
|
}
|
|
|
|
// Check if start endpoint is configured and request path matches
|
|
if w.StartEndpoint != "" && r.URL.Path != w.StartEndpoint {
|
|
http.Error(rw, "Forbidden: Container can only be started via configured start endpoint", http.StatusForbidden)
|
|
return false
|
|
}
|
|
|
|
if r.Body != nil {
|
|
defer r.Body.Close()
|
|
}
|
|
|
|
accept := gphttp.GetAccept(r.Header)
|
|
acceptHTML := (r.Method == http.MethodGet && accept.AcceptHTML() || r.RequestURI == "/" && accept.IsEmpty())
|
|
|
|
isCheckRedirect := r.Header.Get(common.HeaderCheckRedirect) != ""
|
|
if !isCheckRedirect && acceptHTML {
|
|
// Send a loading response to the client
|
|
body := w.makeLoadingPageBody()
|
|
rw.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
rw.Header().Set("Content-Length", strconv.Itoa(len(body)))
|
|
rw.Header().Add("Cache-Control", "no-cache")
|
|
rw.Header().Add("Cache-Control", "no-store")
|
|
rw.Header().Add("Cache-Control", "must-revalidate")
|
|
rw.Header().Add("Connection", "close")
|
|
if _, err := rw.Write(body); err != nil {
|
|
w.Err(err).Msg("error writing http response")
|
|
}
|
|
return false
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeoutCause(r.Context(), w.WakeTimeout, errors.New("wake timeout"))
|
|
defer cancel()
|
|
|
|
checkCanceled := func() (canceled bool) {
|
|
select {
|
|
case <-ctx.Done():
|
|
w.WakeDebug().Str("cause", context.Cause(ctx).Error()).Msg("canceled")
|
|
return true
|
|
case <-w.task.Context().Done():
|
|
w.WakeDebug().Str("cause", w.task.FinishCause().Error()).Msg("canceled")
|
|
http.Error(rw, "Service unavailable", http.StatusServiceUnavailable)
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
if checkCanceled() {
|
|
return false
|
|
}
|
|
|
|
w.WakeTrace().Msg("signal received")
|
|
err := w.wakeIfStopped()
|
|
if err != nil {
|
|
w.WakeError(err)
|
|
http.Error(rw, "Error waking container", http.StatusInternalServerError)
|
|
return false
|
|
}
|
|
|
|
for {
|
|
if checkCanceled() {
|
|
return false
|
|
}
|
|
|
|
if w.Status() == health.StatusHealthy {
|
|
w.resetIdleTimer()
|
|
if isCheckRedirect {
|
|
w.Debug().Msgf("redirecting to %s ...", w.hc.URL())
|
|
rw.WriteHeader(http.StatusOK)
|
|
return false
|
|
}
|
|
w.Debug().Msgf("passing through to %s ...", w.hc.URL())
|
|
return true
|
|
}
|
|
|
|
// retry until the container is ready or timeout
|
|
time.Sleep(idleWakerCheckInterval)
|
|
}
|
|
}
|