Skip to content

Commit

Permalink
Update default wake timeout to 30 seconds, fixed port selection, impr…
Browse files Browse the repository at this point in the history
…oved idlewatcher
  • Loading branch information
yusing committed Sep 24, 2024
1 parent dc3575c commit d10d0e4
Show file tree
Hide file tree
Showing 14 changed files with 103 additions and 70 deletions.
2 changes: 1 addition & 1 deletion docs/docker.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
2 changes: 1 addition & 1 deletion src/common/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ const DockerHostFromEnv = "$DOCKER_HOST"

const (
IdleTimeoutDefault = "0"
WakeTimeoutDefault = "10s"
WakeTimeoutDefault = "30s"
StopTimeoutDefault = "10s"
StopMethodDefault = "stop"
)
19 changes: 10 additions & 9 deletions src/common/ports.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
12 changes: 7 additions & 5 deletions src/docker/idlewatcher/round_trip.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package idlewatcher
import (
"context"
"net/http"
"time"
)

type (
Expand All @@ -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 == "" {
Expand Down Expand Up @@ -57,7 +56,6 @@ func (w *watcher) roundTrip(origRoundTrip roundTripFunc, req *http.Request) (*ht
rtDone <- resp
return
}
time.Sleep(time.Millisecond * 200)
}
}
}()
Expand All @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions src/docker/idlewatcher/watcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
12 changes: 9 additions & 3 deletions src/error/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
74 changes: 56 additions & 18 deletions src/models/raw_entry.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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, ":")
}
2 changes: 1 addition & 1 deletion src/proxy/entry.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion src/proxy/fields/host.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
4 changes: 2 additions & 2 deletions src/proxy/fields/port.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
2 changes: 1 addition & 1 deletion src/proxy/fields/scheme.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 5 additions & 4 deletions src/proxy/fields/stream_port.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

Expand Down
25 changes: 4 additions & 21 deletions src/proxy/provider/docker_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"fmt"
"regexp"
"strconv"
"strings"

D "github.com/yusing/go-proxy/docker"
E "github.com/yusing/go-proxy/error"
Expand Down Expand Up @@ -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{
Expand All @@ -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)
}

Expand Down
4 changes: 3 additions & 1 deletion src/proxy/provider/docker_provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down

0 comments on commit d10d0e4

Please sign in to comment.