mirror of
https://github.com/yusing/godoxy.git
synced 2025-06-09 04:52:35 +02:00
Update default wake timeout to 30 seconds, fixed port selection, improved idlewatcher
This commit is contained in:
parent
dc3575c8fd
commit
d10d0e49fa
14 changed files with 103 additions and 70 deletions
|
@ -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)**<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_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 |
|
||||
|
|
|
@ -34,7 +34,7 @@ const DockerHostFromEnv = "$DOCKER_HOST"
|
|||
|
||||
const (
|
||||
IdleTimeoutDefault = "0"
|
||||
WakeTimeoutDefault = "10s"
|
||||
WakeTimeoutDefault = "30s"
|
||||
StopTimeoutDefault = "10s"
|
||||
StopMethodDefault = "stop"
|
||||
)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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, ":")
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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")
|
||||
|
|
Loading…
Add table
Reference in a new issue