mirror of
https://github.com/yusing/godoxy.git
synced 2025-06-09 13:02:33 +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.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 |
|
||||||
|
|
|
@ -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"
|
||||||
)
|
)
|
||||||
|
|
|
@ -18,6 +18,7 @@ var (
|
||||||
"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,
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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, ":")
|
||||||
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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")
|
||||||
|
|
Loading…
Add table
Reference in a new issue