Update default wake timeout to 30 seconds, fixed port selection, improved idlewatcher

This commit is contained in:
yusing 2024-09-25 05:27:12 +08:00
parent dc3575c8fd
commit d10d0e49fa
14 changed files with 103 additions and 70 deletions

View file

@ -68,7 +68,7 @@
| `proxy.aliases` | comma separated aliases for subdomain and label matching | `gitlab,gitlab-reg,gitlab-ssh` | `container_name` | any | | `proxy.aliases` | comma separated aliases for subdomain and label matching | `gitlab,gitlab-reg,gitlab-ssh` | `container_name` | any |
| `proxy.exclude` | to be excluded from `go-proxy` | | false | boolean | | `proxy.exclude` | to be excluded from `go-proxy` | | false | boolean |
| `proxy.idle_timeout` | time for idle (no traffic) before put it into sleep **(http/s only)**<br> _**NOTE: idlewatcher will only be enabled containers that has non-empty `idle_timeout`**_ | `1h` | empty or `0` **(disabled)** | `number[unit]...`, e.g. `1m30s` | | `proxy.idle_timeout` | time for idle (no traffic) before put it into sleep **(http/s only)**<br> _**NOTE: idlewatcher will only be enabled containers that has non-empty `idle_timeout`**_ | `1h` | empty or `0` **(disabled)** | `number[unit]...`, e.g. `1m30s` |
| `proxy.wake_timeout` | time to wait for target site to be ready | | `10s` | `number[unit]...` | | `proxy.wake_timeout` | time to wait for target site to be ready | | `30s` | `number[unit]...` |
| `proxy.stop_method` | method to stop after `idle_timeout` | | `stop` | `stop`, `pause`, `kill` | | `proxy.stop_method` | method to stop after `idle_timeout` | | `stop` | `stop`, `pause`, `kill` |
| `proxy.stop_timeout` | time to wait for stop command | | `10s` | `number[unit]...` | | `proxy.stop_timeout` | time to wait for stop command | | `10s` | `number[unit]...` |
| `proxy.stop_signal` | signal sent to container for `stop` and `kill` methods | | docker's default | `SIGINT`, `SIGTERM`, `SIGHUP`, `SIGQUIT` and those without **SIG** prefix | | `proxy.stop_signal` | signal sent to container for `stop` and `kill` methods | | docker's default | `SIGINT`, `SIGTERM`, `SIGHUP`, `SIGQUIT` and those without **SIG** prefix |

View file

@ -34,7 +34,7 @@ const DockerHostFromEnv = "$DOCKER_HOST"
const ( const (
IdleTimeoutDefault = "0" IdleTimeoutDefault = "0"
WakeTimeoutDefault = "10s" WakeTimeoutDefault = "30s"
StopTimeoutDefault = "10s" StopTimeoutDefault = "10s"
StopMethodDefault = "stop" StopMethodDefault = "stop"
) )

View file

@ -10,14 +10,15 @@ var (
} }
ServiceNamePortMapTCP = map[string]int{ ServiceNamePortMapTCP = map[string]int{
"mssql": 1433, "mssql": 1433,
"mysql": 3306, "mysql": 3306,
"mariadb": 3306, "mariadb": 3306,
"postgres": 5432, "postgres": 5432,
"rabbitmq": 5672, "rabbitmq": 5672,
"redis": 6379, "redis": 6379,
"memcached": 11211, "memcached": 11211,
"mongo": 27017, "mongo": 27017,
"minecraft-server": 25565,
"ssh": 22, "ssh": 22,
"ftp": 21, "ftp": 21,
@ -53,7 +54,7 @@ var (
"immich": 3001, "immich": 3001,
"jellyfin": 8096, "jellyfin": 8096,
"lidarr": 8686, "lidarr": 8686,
"minecraft-server": 25565, "microbin": 8080,
"nginx": 80, "nginx": 80,
"nginx-proxy-manager": 81, "nginx-proxy-manager": 81,
"open-webui": 8080, "open-webui": 8080,

View file

@ -3,7 +3,6 @@ package idlewatcher
import ( import (
"context" "context"
"net/http" "net/http"
"time"
) )
type ( type (
@ -18,14 +17,14 @@ func (rt roundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
} }
func (w *watcher) roundTrip(origRoundTrip roundTripFunc, req *http.Request) (*http.Response, error) { func (w *watcher) roundTrip(origRoundTrip roundTripFunc, req *http.Request) (*http.Response, error) {
// wake the container
w.wakeCh <- struct{}{}
// target site is ready, passthrough // target site is ready, passthrough
if w.ready.Load() { if w.ready.Load() {
return origRoundTrip(req) return origRoundTrip(req)
} }
// wake the container
w.wakeCh <- struct{}{}
// initial request // initial request
targetUrl := req.Header.Get(headerGoProxyTargetURL) targetUrl := req.Header.Get(headerGoProxyTargetURL)
if targetUrl == "" { if targetUrl == "" {
@ -57,7 +56,6 @@ func (w *watcher) roundTrip(origRoundTrip roundTripFunc, req *http.Request) (*ht
rtDone <- resp rtDone <- resp
return return
} }
time.Sleep(time.Millisecond * 200)
} }
} }
}() }()
@ -66,6 +64,10 @@ func (w *watcher) roundTrip(origRoundTrip roundTripFunc, req *http.Request) (*ht
select { select {
case resp := <-rtDone: case resp := <-rtDone:
return w.makeSuccResp(targetUrl, resp) return w.makeSuccResp(targetUrl, resp)
case err := <-w.wakeDone:
if err != nil {
return w.makeErrResp("error waking up %s\n%s", w.ContainerName, err.Error())
}
case <-ctx.Done(): case <-ctx.Done():
if ctx.Err() == context.DeadlineExceeded { if ctx.Err() == context.DeadlineExceeded {
return w.makeErrResp("Timed out waiting for %s to fully wake", w.ContainerName) return w.makeErrResp("Timed out waiting for %s to fully wake", w.ContainerName)

View file

@ -78,8 +78,8 @@ func Register(entry *P.ReverseProxyEntry) (*watcher, E.NestedError) {
ReverseProxyEntry: entry, ReverseProxyEntry: entry,
client: client, client: client,
refCount: &sync.WaitGroup{}, refCount: &sync.WaitGroup{},
wakeCh: make(chan struct{}, 1), wakeCh: make(chan struct{}),
wakeDone: make(chan E.NestedError, 1), wakeDone: make(chan E.NestedError),
l: logger.WithField("container", entry.ContainerName), l: logger.WithField("container", entry.ContainerName),
} }
w.refCount.Add(1) w.refCount.Add(1)

View file

@ -133,13 +133,19 @@ func (ne NestedError) Subject(s any) NestedError {
if ne == nil { if ne == nil {
return ne return ne
} }
var subject string
switch ss := s.(type) { switch ss := s.(type) {
case string: case string:
ne.subject = ss subject = ss
case fmt.Stringer: case fmt.Stringer:
ne.subject = ss.String() subject = ss.String()
default: default:
ne.subject = fmt.Sprint(s) subject = fmt.Sprint(s)
}
if ne.subject == "" {
ne.subject = subject
} else {
ne.subject = fmt.Sprintf("%s > %s", subject, ne.subject)
} }
return ne return ne
} }

View file

@ -34,29 +34,41 @@ var NewProxyEntries = F.NewMapOf[string, *RawEntry]
func (e *RawEntry) FillMissingFields() bool { func (e *RawEntry) FillMissingFields() bool {
isDocker := e.ProxyProperties != nil isDocker := e.ProxyProperties != nil
if !isDocker { if !isDocker {
e.ProxyProperties = &D.ProxyProperties{} e.ProxyProperties = &D.ProxyProperties{}
} }
lp, pp, extra := e.splitPorts()
if port, ok := ServiceNamePortMapTCP[e.ImageName]; ok { if port, ok := ServiceNamePortMapTCP[e.ImageName]; ok {
e.Port = strconv.Itoa(port) if pp == "" {
pp = strconv.Itoa(port)
}
e.Scheme = "tcp" e.Scheme = "tcp"
} else if port, ok := ImageNamePortMap[e.ImageName]; ok { } else if port, ok := ImageNamePortMap[e.ImageName]; ok {
e.Port = strconv.Itoa(port) if pp == "" {
pp = strconv.Itoa(port)
}
e.Scheme = "http" e.Scheme = "http"
} else if e.Port == "" && e.Scheme == "https" { } else if pp == "" && e.Scheme == "https" {
e.Port = "443" pp = "443"
} else if e.Port == "" { } else if pp == "" {
e.Port = "80" if p, ok := F.FirstValueOf(e.PrivatePortMapping); ok {
pp = fmt.Sprint(p.PrivatePort)
} else {
pp = "80"
}
} }
// replace private port with public port (if any) // replace private port with public port (if any)
if isDocker && e.NetworkMode != "host" { if isDocker && e.NetworkMode != "host" {
if _, ok := e.PublicPortMapping[e.Port]; !ok { // port is not exposed, but specified if p, ok := e.PrivatePortMapping[pp]; ok {
pp = fmt.Sprint(p.PublicPort)
}
if _, ok := e.PublicPortMapping[pp]; !ok { // port is not exposed, but specified
// try to fallback to first public port // try to fallback to first public port
if p, ok := F.FirstValueOf(e.PublicPortMapping); ok { if p, ok := F.FirstValueOf(e.PublicPortMapping); ok {
e.Port = fmt.Sprint(p.PublicPort) pp = fmt.Sprint(p.PublicPort)
} }
// ignore only if it is NOT RUNNING // ignore only if it is NOT RUNNING
// because stopped containers // because stopped containers
@ -68,21 +80,17 @@ func (e *RawEntry) FillMissingFields() bool {
} }
if e.Scheme == "" && isDocker { if e.Scheme == "" && isDocker {
if p, ok := e.PublicPortMapping[e.Port]; ok { if p, ok := e.PublicPortMapping[pp]; ok && p.Type == "udp" {
if p.Type == "udp" { e.Scheme = "udp"
e.Scheme = "udp"
} else {
e.Scheme = "http"
}
} }
} }
if e.Scheme == "" { if e.Scheme == "" {
if strings.ContainsRune(e.Port, ':') { if lp != "" {
e.Scheme = "tcp" e.Scheme = "tcp"
} else if strings.HasSuffix(e.Port, "443") { } else if strings.HasSuffix(pp, "443") {
e.Scheme = "https" e.Scheme = "https"
} else if _, ok := WellKnownHTTPPorts[e.Port]; ok { } else if _, ok := WellKnownHTTPPorts[pp]; ok {
e.Scheme = "http" e.Scheme = "http"
} else { } else {
// assume its http // assume its http
@ -106,5 +114,35 @@ func (e *RawEntry) FillMissingFields() bool {
e.StopMethod = StopMethodDefault e.StopMethod = StopMethodDefault
} }
e.Port = joinPorts(lp, pp, extra)
return true return true
} }
func (e *RawEntry) splitPorts() (lp string, pp string, extra string) {
portSplit := strings.Split(e.Port, ":")
if len(portSplit) == 1 {
pp = portSplit[0]
} else {
lp = portSplit[0]
pp = portSplit[1]
}
if len(portSplit) > 2 {
extra = strings.Join(portSplit[2:], ":")
}
return
}
func joinPorts(lp string, pp string, extra string) string {
s := make([]string, 0, 3)
if lp != "" {
s = append(s, lp)
}
if pp != "" {
s = append(s, pp)
}
if extra != "" {
s = append(s, extra)
}
return strings.Join(s, ":")
}

View file

@ -54,7 +54,7 @@ func ValidateEntry(m *M.RawEntry) (any, E.NestedError) {
} }
var entry any var entry any
e := E.NewBuilder("error validating proxy entry") e := E.NewBuilder("error validating entry")
if scheme.IsStream() { if scheme.IsStream() {
entry = validateStreamEntry(m, e) entry = validateStreamEntry(m, e)
} else { } else {

View file

@ -7,6 +7,6 @@ import (
type Host string type Host string
type Subdomain = Alias type Subdomain = Alias
func ValidateHost(s string) (Host, E.NestedError) { func ValidateHost[String ~string](s String) (Host, E.NestedError) {
return Host(s), nil return Host(s), nil
} }

View file

@ -8,8 +8,8 @@ import (
type Port int type Port int
func ValidatePort(v string) (Port, E.NestedError) { func ValidatePort[String ~string](v String) (Port, E.NestedError) {
p, err := strconv.Atoi(v) p, err := strconv.Atoi(string(v))
if err != nil { if err != nil {
return ErrPort, E.Invalid("port number", v).With(err) return ErrPort, E.Invalid("port number", v).With(err)
} }

View file

@ -6,7 +6,7 @@ import (
type Scheme string type Scheme string
func NewScheme(s string) (Scheme, E.NestedError) { func NewScheme[String ~string](s String) (Scheme, E.NestedError) {
switch s { switch s {
case "http", "https", "tcp", "udp": case "http", "https", "tcp", "udp":
return Scheme(s), nil return Scheme(s), nil

View file

@ -26,18 +26,19 @@ func ValidateStreamPort(p string) (StreamPort, E.NestedError) {
listeningPort, err := ValidatePort(split[0]) listeningPort, err := ValidatePort(split[0])
if err != nil { if err != nil {
return ErrStreamPort, err return ErrStreamPort, err.Subject("listening port")
} }
proxyPort, err := ValidatePort(split[1]) proxyPort, err := ValidatePort(split[1])
if err.Is(E.ErrOutOfRange) { if err.Is(E.ErrOutOfRange) {
return ErrStreamPort, err return ErrStreamPort, err.Subject("proxy port")
} else if proxyPort == 0 { } else if proxyPort == 0 {
return ErrStreamPort, E.Invalid("stream port", p).With("proxy port cannot be 0") return ErrStreamPort, E.Invalid("proxy port", p)
} else if err != nil { } else if err != nil {
proxyPort, err = parseNameToPort(split[1]) proxyPort, err = parseNameToPort(split[1])
if err != nil { if err != nil {
return ErrStreamPort, E.Invalid("stream port", p).With(proxyPort) return ErrStreamPort, E.Invalid("proxy port", proxyPort)
} }
} }

View file

@ -4,7 +4,6 @@ import (
"fmt" "fmt"
"regexp" "regexp"
"strconv" "strconv"
"strings"
D "github.com/yusing/go-proxy/docker" D "github.com/yusing/go-proxy/docker"
E "github.com/yusing/go-proxy/error" E "github.com/yusing/go-proxy/error"
@ -123,6 +122,10 @@ func (p *DockerProvider) OnEvent(event W.Event, routes R.Routes) (res EventResul
func (p *DockerProvider) entriesFromContainerLabels(container D.Container) (M.RawEntries, E.NestedError) { func (p *DockerProvider) entriesFromContainerLabels(container D.Container) (M.RawEntries, E.NestedError) {
entries := M.NewProxyEntries() entries := M.NewProxyEntries()
if container.IsExcluded {
return entries, nil
}
// init entries map for all aliases // init entries map for all aliases
for _, a := range container.Aliases { for _, a := range container.Aliases {
entries.Store(a, &M.RawEntry{ entries.Store(a, &M.RawEntry{
@ -137,31 +140,11 @@ func (p *DockerProvider) entriesFromContainerLabels(container D.Container) (M.Ra
errors.Add(p.applyLabel(container, entries, key, val)) errors.Add(p.applyLabel(container, entries, key, val))
} }
// selecting correct host port
replacePrivPorts := func() {
if container.HostConfig.NetworkMode == "host" {
return
}
entries.RangeAll(func(_ string, entry *M.RawEntry) {
entryPortSplit := strings.Split(entry.Port, ":")
n := len(entryPortSplit)
// if the port matches the proxy port, replace it with the public port
if p, ok := container.PrivatePortMapping[entryPortSplit[n-1]]; ok {
entryPortSplit[n-1] = fmt.Sprint(p.PublicPort)
entry.Port = strings.Join(entryPortSplit, ":")
}
})
}
replacePrivPorts()
// remove all entries that failed to fill in missing fields // remove all entries that failed to fill in missing fields
entries.RemoveAll(func(re *M.RawEntry) bool { entries.RemoveAll(func(re *M.RawEntry) bool {
return !re.FillMissingFields() return !re.FillMissingFields()
}) })
// do it again since the port may got filled in
replacePrivPorts()
return entries, errors.Build().Subject(container.ContainerName) return entries, errors.Build().Subject(container.ContainerName)
} }

View file

@ -249,7 +249,9 @@ func TestImplicitExclude(t *testing.T) {
Labels: map[string]string{ Labels: map[string]string{
D.LabelAliases: "a", D.LabelAliases: "a",
"proxy.a.no_tls_verify": "true", "proxy.a.no_tls_verify": "true",
}}, "")) },
State: "running",
}, ""))
ExpectNoError(t, err.Error()) ExpectNoError(t, err.Error())
_, ok := entries.Load("a") _, ok := entries.Load("a")