From d10d0e49fad9d6d79565a47cb50b9724e9035b0f Mon Sep 17 00:00:00 2001 From: yusing Date: Wed, 25 Sep 2024 05:27:12 +0800 Subject: [PATCH] Update default wake timeout to 30 seconds, fixed port selection, improved idlewatcher --- docs/docker.md | 2 +- src/common/constants.go | 2 +- src/common/ports.go | 19 +++--- src/docker/idlewatcher/round_trip.go | 12 ++-- src/docker/idlewatcher/watcher.go | 4 +- src/error/error.go | 12 +++- src/models/raw_entry.go | 74 ++++++++++++++++------ src/proxy/entry.go | 2 +- src/proxy/fields/host.go | 2 +- src/proxy/fields/port.go | 4 +- src/proxy/fields/scheme.go | 2 +- src/proxy/fields/stream_port.go | 9 +-- src/proxy/provider/docker_provider.go | 25 ++------ src/proxy/provider/docker_provider_test.go | 4 +- 14 files changed, 103 insertions(+), 70 deletions(-) diff --git a/docs/docker.md b/docs/docker.md index 29ec5d7..cee0a87 100644 --- a/docs/docker.md +++ b/docs/docker.md @@ -68,7 +68,7 @@ | `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.idle_timeout` | time for idle (no traffic) before put it into sleep **(http/s only)**
_**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_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 | diff --git a/src/common/constants.go b/src/common/constants.go index 41f06dc..156f545 100644 --- a/src/common/constants.go +++ b/src/common/constants.go @@ -34,7 +34,7 @@ const DockerHostFromEnv = "$DOCKER_HOST" const ( IdleTimeoutDefault = "0" - WakeTimeoutDefault = "10s" + WakeTimeoutDefault = "30s" StopTimeoutDefault = "10s" StopMethodDefault = "stop" ) diff --git a/src/common/ports.go b/src/common/ports.go index 0eec93c..a4f3fc1 100644 --- a/src/common/ports.go +++ b/src/common/ports.go @@ -10,14 +10,15 @@ var ( } ServiceNamePortMapTCP = map[string]int{ - "mssql": 1433, - "mysql": 3306, - "mariadb": 3306, - "postgres": 5432, - "rabbitmq": 5672, - "redis": 6379, - "memcached": 11211, - "mongo": 27017, + "mssql": 1433, + "mysql": 3306, + "mariadb": 3306, + "postgres": 5432, + "rabbitmq": 5672, + "redis": 6379, + "memcached": 11211, + "mongo": 27017, + "minecraft-server": 25565, "ssh": 22, "ftp": 21, @@ -53,7 +54,7 @@ var ( "immich": 3001, "jellyfin": 8096, "lidarr": 8686, - "minecraft-server": 25565, + "microbin": 8080, "nginx": 80, "nginx-proxy-manager": 81, "open-webui": 8080, diff --git a/src/docker/idlewatcher/round_trip.go b/src/docker/idlewatcher/round_trip.go index f4444fa..b1d8c2a 100644 --- a/src/docker/idlewatcher/round_trip.go +++ b/src/docker/idlewatcher/round_trip.go @@ -3,7 +3,6 @@ package idlewatcher import ( "context" "net/http" - "time" ) 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) { + // wake the container + w.wakeCh <- struct{}{} + // target site is ready, passthrough if w.ready.Load() { return origRoundTrip(req) } - // wake the container - w.wakeCh <- struct{}{} - // initial request targetUrl := req.Header.Get(headerGoProxyTargetURL) if targetUrl == "" { @@ -57,7 +56,6 @@ func (w *watcher) roundTrip(origRoundTrip roundTripFunc, req *http.Request) (*ht rtDone <- resp return } - time.Sleep(time.Millisecond * 200) } } }() @@ -66,6 +64,10 @@ func (w *watcher) roundTrip(origRoundTrip roundTripFunc, req *http.Request) (*ht select { case resp := <-rtDone: 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(): if ctx.Err() == context.DeadlineExceeded { return w.makeErrResp("Timed out waiting for %s to fully wake", w.ContainerName) diff --git a/src/docker/idlewatcher/watcher.go b/src/docker/idlewatcher/watcher.go index cdb158e..af17f00 100644 --- a/src/docker/idlewatcher/watcher.go +++ b/src/docker/idlewatcher/watcher.go @@ -78,8 +78,8 @@ func Register(entry *P.ReverseProxyEntry) (*watcher, E.NestedError) { ReverseProxyEntry: entry, client: client, refCount: &sync.WaitGroup{}, - wakeCh: make(chan struct{}, 1), - wakeDone: make(chan E.NestedError, 1), + wakeCh: make(chan struct{}), + wakeDone: make(chan E.NestedError), l: logger.WithField("container", entry.ContainerName), } w.refCount.Add(1) diff --git a/src/error/error.go b/src/error/error.go index f5206e1..46f4bc8 100644 --- a/src/error/error.go +++ b/src/error/error.go @@ -133,13 +133,19 @@ func (ne NestedError) Subject(s any) NestedError { if ne == nil { return ne } + var subject string switch ss := s.(type) { case string: - ne.subject = ss + subject = ss case fmt.Stringer: - ne.subject = ss.String() + subject = ss.String() 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 } diff --git a/src/models/raw_entry.go b/src/models/raw_entry.go index a2e38aa..2fa54be 100644 --- a/src/models/raw_entry.go +++ b/src/models/raw_entry.go @@ -34,29 +34,41 @@ var NewProxyEntries = F.NewMapOf[string, *RawEntry] func (e *RawEntry) FillMissingFields() bool { isDocker := e.ProxyProperties != nil - if !isDocker { e.ProxyProperties = &D.ProxyProperties{} } + lp, pp, extra := e.splitPorts() + if port, ok := ServiceNamePortMapTCP[e.ImageName]; ok { - e.Port = strconv.Itoa(port) + if pp == "" { + pp = strconv.Itoa(port) + } e.Scheme = "tcp" } else if port, ok := ImageNamePortMap[e.ImageName]; ok { - e.Port = strconv.Itoa(port) + if pp == "" { + pp = strconv.Itoa(port) + } e.Scheme = "http" - } else if e.Port == "" && e.Scheme == "https" { - e.Port = "443" - } else if e.Port == "" { - e.Port = "80" + } else if pp == "" && e.Scheme == "https" { + pp = "443" + } else if pp == "" { + if p, ok := F.FirstValueOf(e.PrivatePortMapping); ok { + pp = fmt.Sprint(p.PrivatePort) + } else { + pp = "80" + } } // replace private port with public port (if any) 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 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 // because stopped containers @@ -68,21 +80,17 @@ func (e *RawEntry) FillMissingFields() bool { } if e.Scheme == "" && isDocker { - if p, ok := e.PublicPortMapping[e.Port]; ok { - if p.Type == "udp" { - e.Scheme = "udp" - } else { - e.Scheme = "http" - } + if p, ok := e.PublicPortMapping[pp]; ok && p.Type == "udp" { + e.Scheme = "udp" } } if e.Scheme == "" { - if strings.ContainsRune(e.Port, ':') { + if lp != "" { e.Scheme = "tcp" - } else if strings.HasSuffix(e.Port, "443") { + } else if strings.HasSuffix(pp, "443") { e.Scheme = "https" - } else if _, ok := WellKnownHTTPPorts[e.Port]; ok { + } else if _, ok := WellKnownHTTPPorts[pp]; ok { e.Scheme = "http" } else { // assume its http @@ -106,5 +114,35 @@ func (e *RawEntry) FillMissingFields() bool { e.StopMethod = StopMethodDefault } + e.Port = joinPorts(lp, pp, extra) + 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, ":") +} diff --git a/src/proxy/entry.go b/src/proxy/entry.go index 7217b7e..80873b4 100644 --- a/src/proxy/entry.go +++ b/src/proxy/entry.go @@ -54,7 +54,7 @@ func ValidateEntry(m *M.RawEntry) (any, E.NestedError) { } var entry any - e := E.NewBuilder("error validating proxy entry") + e := E.NewBuilder("error validating entry") if scheme.IsStream() { entry = validateStreamEntry(m, e) } else { diff --git a/src/proxy/fields/host.go b/src/proxy/fields/host.go index 39b7b7b..0839971 100644 --- a/src/proxy/fields/host.go +++ b/src/proxy/fields/host.go @@ -7,6 +7,6 @@ import ( type Host string type Subdomain = Alias -func ValidateHost(s string) (Host, E.NestedError) { +func ValidateHost[String ~string](s String) (Host, E.NestedError) { return Host(s), nil } diff --git a/src/proxy/fields/port.go b/src/proxy/fields/port.go index 7756492..1087237 100644 --- a/src/proxy/fields/port.go +++ b/src/proxy/fields/port.go @@ -8,8 +8,8 @@ import ( type Port int -func ValidatePort(v string) (Port, E.NestedError) { - p, err := strconv.Atoi(v) +func ValidatePort[String ~string](v String) (Port, E.NestedError) { + p, err := strconv.Atoi(string(v)) if err != nil { return ErrPort, E.Invalid("port number", v).With(err) } diff --git a/src/proxy/fields/scheme.go b/src/proxy/fields/scheme.go index 2c60178..44e0a8e 100644 --- a/src/proxy/fields/scheme.go +++ b/src/proxy/fields/scheme.go @@ -6,7 +6,7 @@ import ( type Scheme string -func NewScheme(s string) (Scheme, E.NestedError) { +func NewScheme[String ~string](s String) (Scheme, E.NestedError) { switch s { case "http", "https", "tcp", "udp": return Scheme(s), nil diff --git a/src/proxy/fields/stream_port.go b/src/proxy/fields/stream_port.go index 0c8d674..a1418fd 100644 --- a/src/proxy/fields/stream_port.go +++ b/src/proxy/fields/stream_port.go @@ -26,18 +26,19 @@ func ValidateStreamPort(p string) (StreamPort, E.NestedError) { listeningPort, err := ValidatePort(split[0]) if err != nil { - return ErrStreamPort, err + return ErrStreamPort, err.Subject("listening port") } proxyPort, err := ValidatePort(split[1]) + if err.Is(E.ErrOutOfRange) { - return ErrStreamPort, err + return ErrStreamPort, err.Subject("proxy port") } 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 { proxyPort, err = parseNameToPort(split[1]) if err != nil { - return ErrStreamPort, E.Invalid("stream port", p).With(proxyPort) + return ErrStreamPort, E.Invalid("proxy port", proxyPort) } } diff --git a/src/proxy/provider/docker_provider.go b/src/proxy/provider/docker_provider.go index ad3ac6c..111c8a5 100755 --- a/src/proxy/provider/docker_provider.go +++ b/src/proxy/provider/docker_provider.go @@ -4,7 +4,6 @@ import ( "fmt" "regexp" "strconv" - "strings" D "github.com/yusing/go-proxy/docker" 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) { entries := M.NewProxyEntries() + if container.IsExcluded { + return entries, nil + } + // init entries map for all aliases for _, a := range container.Aliases { 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)) } - // 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 entries.RemoveAll(func(re *M.RawEntry) bool { return !re.FillMissingFields() }) - // do it again since the port may got filled in - replacePrivPorts() - return entries, errors.Build().Subject(container.ContainerName) } diff --git a/src/proxy/provider/docker_provider_test.go b/src/proxy/provider/docker_provider_test.go index 7bd9a52..2c91b90 100644 --- a/src/proxy/provider/docker_provider_test.go +++ b/src/proxy/provider/docker_provider_test.go @@ -249,7 +249,9 @@ func TestImplicitExclude(t *testing.T) { Labels: map[string]string{ D.LabelAliases: "a", "proxy.a.no_tls_verify": "true", - }}, "")) + }, + State: "running", + }, "")) ExpectNoError(t, err.Error()) _, ok := entries.Load("a")