feat(healthcheck): allow health checking for excluded routes

This commit is contained in:
yusing 2025-06-02 23:19:30 +08:00
parent 4705989f4b
commit 9087c4f195
9 changed files with 138 additions and 57 deletions

23
internal/route/common.go Normal file
View file

@ -0,0 +1,23 @@
package route
import (
"github.com/yusing/go-proxy/internal/gperr"
"github.com/yusing/go-proxy/internal/route/routes"
)
func checkExists(r routes.Route) gperr.Error {
var (
existing routes.Route
ok bool
)
switch r := r.(type) {
case routes.HTTPRoute:
existing, ok = routes.HTTP.Get(r.Key())
case routes.StreamRoute:
existing, ok = routes.Stream.Get(r.Key())
}
if ok {
return gperr.Errorf("route already exists: from provider %s and %s", existing.ProviderName(), r.ProviderName())
}
return nil
}

View file

@ -96,8 +96,16 @@ func (s *FileServer) Start(parent task.Parent) gperr.Error {
} }
} }
if s.ShouldExclude() {
return nil
}
if err := checkExists(s); err != nil {
return err
}
routes.HTTP.Add(s) routes.HTTP.Add(s)
s.task.OnCancel("entrypoint_remove_route", func() { s.task.OnFinished("remove_route_from_http", func() {
routes.HTTP.Del(s) routes.HTTP.Del(s)
}) })
return nil return nil

View file

@ -71,9 +71,6 @@ func (p *DockerProvider) loadRoutesImpl() (route.Routes, gperr.Error) {
for _, c := range containers { for _, c := range containers {
container := docker.FromDocker(&c, p.dockerHost) container := docker.FromDocker(&c, p.dockerHost)
if container.IsExcluded {
continue
}
if container.IsHostNetworkMode { if container.IsHostNetworkMode {
err := container.UpdatePorts() err := container.UpdatePorts()
@ -89,10 +86,15 @@ func (p *DockerProvider) loadRoutesImpl() (route.Routes, gperr.Error) {
} }
for k, v := range newEntries { for k, v := range newEntries {
if conflict, ok := routes[k]; ok { if conflict, ok := routes[k]; ok {
errs.Add(gperr.Multiline(). err := gperr.Multiline().
Addf("route with alias %s already exists", k). Addf("route with alias %s already exists", k).
Addf("container %s", container.ContainerName). Addf("container %s", container.ContainerName).
Addf("conflicting container %s", conflict.Container.ContainerName)) Addf("conflicting container %s", conflict.Container.ContainerName)
if conflict.ShouldExclude() || v.ShouldExclude() {
gperr.LogWarn("skipping conflicting route", err)
} else {
errs.Add(err)
}
} else { } else {
routes[k] = v routes[k] = v
} }

View file

@ -4,6 +4,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"path" "path"
"slices"
"time" "time"
"github.com/rs/zerolog" "github.com/rs/zerolog"
@ -11,6 +12,7 @@ import (
"github.com/yusing/go-proxy/internal/gperr" "github.com/yusing/go-proxy/internal/gperr"
"github.com/yusing/go-proxy/internal/route" "github.com/yusing/go-proxy/internal/route"
"github.com/yusing/go-proxy/internal/route/provider/types" "github.com/yusing/go-proxy/internal/route/provider/types"
"github.com/yusing/go-proxy/internal/route/routes"
"github.com/yusing/go-proxy/internal/task" "github.com/yusing/go-proxy/internal/task"
W "github.com/yusing/go-proxy/internal/watcher" W "github.com/yusing/go-proxy/internal/watcher"
"github.com/yusing/go-proxy/internal/watcher/events" "github.com/yusing/go-proxy/internal/watcher/events"
@ -90,9 +92,17 @@ func (p *Provider) startRoute(parent task.Parent, r *route.Route) gperr.Error {
err := r.Start(parent) err := r.Start(parent)
if err != nil { if err != nil {
delete(p.routes, r.Alias) delete(p.routes, r.Alias)
routes.All.Del(r)
return err.Subject(r.Alias) return err.Subject(r.Alias)
} }
p.routes[r.Alias] = r if conflict, added := routes.All.AddIfNotExists(r); !added {
delete(p.routes, r.Alias)
return gperr.Errorf("route %s already exists: from %s and %s", r.Alias, r.ProviderName(), conflict.ProviderName())
} else {
r.Task().OnCancel("remove_routes_from_all", func() {
routes.All.Del(r)
})
}
return nil return nil
} }
@ -155,10 +165,6 @@ func (p *Provider) loadRoutes() (routes route.Routes, err gperr.Error) {
delete(routes, alias) delete(routes, alias)
continue continue
} }
if r.ShouldExclude() {
delete(routes, alias)
continue
}
r.FinalizeHomepageConfig() r.FinalizeHomepageConfig()
} }
return routes, errs.Error() return routes, errs.Error()

View file

@ -50,7 +50,7 @@ func NewReverseProxyRoute(base *Route) (*ReveseProxyRoute, gperr.Error) {
} else { } else {
trans = gphttp.NewTransport() trans = gphttp.NewTransport()
if httpConfig.NoTLSVerify { if httpConfig.NoTLSVerify {
trans.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} trans.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} //nolint:gosec
} }
if httpConfig.ResponseHeaderTimeout > 0 { if httpConfig.ResponseHeaderTimeout > 0 {
trans.ResponseHeaderTimeout = httpConfig.ResponseHeaderTimeout trans.ResponseHeaderTimeout = httpConfig.ResponseHeaderTimeout
@ -98,9 +98,6 @@ func (r *ReveseProxyRoute) ReverseProxy() *reverseproxy.ReverseProxy {
// Start implements task.TaskStarter. // Start implements task.TaskStarter.
func (r *ReveseProxyRoute) Start(parent task.Parent) gperr.Error { func (r *ReveseProxyRoute) Start(parent task.Parent) gperr.Error {
if existing, ok := routes.HTTP.Get(r.Key()); ok && !r.UseLoadBalance() {
return gperr.Errorf("route already exists: from provider %s and %s", existing.ProviderName(), r.ProviderName())
}
r.task = parent.Subtask("http."+r.Name(), false) r.task = parent.Subtask("http."+r.Name(), false)
switch { switch {
@ -139,11 +136,19 @@ func (r *ReveseProxyRoute) Start(parent task.Parent) gperr.Error {
} }
} }
if r.ShouldExclude() {
return nil
}
if err := checkExists(r); err != nil {
return err
}
if r.UseLoadBalance() { if r.UseLoadBalance() {
r.addToLoadBalancer(parent) r.addToLoadBalancer(parent)
} else { } else {
routes.HTTP.Add(r) routes.HTTP.Add(r)
r.task.OnFinished("entrypoint_remove_route", func() { r.task.OnCancel("remove_route_from_http", func() {
routes.HTTP.Del(r) routes.HTTP.Del(r)
}) })
} }

View file

@ -2,6 +2,7 @@ package route
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"strings" "strings"
"time" "time"
@ -215,6 +216,13 @@ func (r *Route) Validate() gperr.Error {
return nil return nil
} }
func (r *Route) Task() *task.Task {
if r.impl == nil { // should not happen
panic(errors.New("route not initialized"))
}
return r.impl.Task()
}
func (r *Route) Start(parent task.Parent) (err gperr.Error) { func (r *Route) Start(parent task.Parent) (err gperr.Error) {
if r.impl == nil { // should not happen if r.impl == nil { // should not happen
return gperr.New("route not initialized") return gperr.New("route not initialized")
@ -354,10 +362,20 @@ func (r *Route) UseLoadBalance() bool {
} }
func (r *Route) UseIdleWatcher() bool { func (r *Route) UseIdleWatcher() bool {
return r.Idlewatcher != nil && r.Idlewatcher.IdleTimeout > 0 return r.Idlewatcher != nil && r.Idlewatcher.IdleTimeout != 0
} }
func (r *Route) UseHealthCheck() bool { func (r *Route) UseHealthCheck() bool {
if r.Container != nil {
switch {
case r.Container.Image.Name == "godoxy-agent":
return false
case !r.Container.Running && !r.UseIdleWatcher():
return false
case strings.HasPrefix(r.Container.ContainerName, "buildx_"):
return false
}
}
return !r.HealthCheck.Disable return !r.HealthCheck.Disable
} }
@ -482,6 +500,12 @@ func (r *Route) FinalizeHomepageConfig() {
} }
r.Homepage = r.Homepage.GetOverride(r.Alias) r.Homepage = r.Homepage.GetOverride(r.Alias)
if r.ShouldExclude() && isDocker {
r.Homepage.Show = false
r.Homepage.Name = r.Container.ContainerName // still show container name in metrics page
return
}
hp := r.Homepage hp := r.Homepage
refs := r.References() refs := r.References()
for _, ref := range refs { for _, ref := range refs {

View file

@ -7,15 +7,16 @@ import (
var ( var (
HTTP = pool.New[HTTPRoute]("http_routes") HTTP = pool.New[HTTPRoute]("http_routes")
Stream = pool.New[StreamRoute]("stream_routes") Stream = pool.New[StreamRoute]("stream_routes")
// All is a pool of all routes, including HTTP, Stream routes and also excluded routes.
All = pool.New[Route]("all_routes")
) )
func init() {
All.DisableLog()
}
func Iter(yield func(r Route) bool) { func Iter(yield func(r Route) bool) {
for _, r := range HTTP.Iter { for _, r := range All.Iter {
if !yield(r) {
break
}
}
for _, r := range Stream.Iter {
if !yield(r) { if !yield(r) {
break break
} }
@ -23,12 +24,7 @@ func Iter(yield func(r Route) bool) {
} }
func IterKV(yield func(alias string, r Route) bool) { func IterKV(yield func(alias string, r Route) bool) {
for k, r := range HTTP.Iter { for k, r := range All.Iter {
if !yield(k, r) {
break
}
}
for k, r := range Stream.Iter {
if !yield(k, r) { if !yield(k, r) {
break break
} }
@ -36,12 +32,13 @@ func IterKV(yield func(alias string, r Route) bool) {
} }
func NumRoutes() int { func NumRoutes() int {
return HTTP.Size() + Stream.Size() return All.Size()
} }
func Clear() { func Clear() {
HTTP.Clear() HTTP.Clear()
Stream.Clear() Stream.Clear()
All.Clear()
} }
func GetHTTPRouteOrExact(alias, host string) (HTTPRoute, bool) { func GetHTTPRouteOrExact(alias, host string) (HTTPRoute, bool) {
@ -54,9 +51,5 @@ func GetHTTPRouteOrExact(alias, host string) (HTTPRoute, bool) {
} }
func Get(alias string) (Route, bool) { func Get(alias string) (Route, bool) {
r, ok := HTTP.Get(alias) return All.Get(alias)
if ok {
return r, true
}
return Stream.Get(alias)
} }

View file

@ -41,9 +41,6 @@ func NewStreamRoute(base *Route) (routes.Route, gperr.Error) {
// Start implements task.TaskStarter. // Start implements task.TaskStarter.
func (r *StreamRoute) Start(parent task.Parent) gperr.Error { func (r *StreamRoute) Start(parent task.Parent) gperr.Error {
if existing, ok := routes.Stream.Get(r.Key()); ok {
return gperr.Errorf("route already exists: from provider %s and %s", existing.ProviderName(), r.ProviderName())
}
r.task = parent.Subtask("stream."+r.Name(), true) r.task = parent.Subtask("stream."+r.Name(), true)
r.Stream = NewStream(r) r.Stream = NewStream(r)
@ -60,12 +57,13 @@ func (r *StreamRoute) Start(parent task.Parent) gperr.Error {
r.HealthMon = monitor.NewMonitor(r) r.HealthMon = monitor.NewMonitor(r)
} }
if !r.ShouldExclude() {
if err := r.Setup(); err != nil { if err := r.Setup(); err != nil {
r.task.Finish(err) r.task.Finish(err)
return gperr.Wrap(err) return gperr.Wrap(err)
} }
r.l.Info().Int("port", r.Port.Listening).Msg("listening") r.l.Info().Int("port", r.Port.Listening).Msg("listening")
}
if r.HealthMon != nil { if r.HealthMon != nil {
if err := r.HealthMon.Start(r.task); err != nil { if err := r.HealthMon.Start(r.task); err != nil {
@ -73,10 +71,18 @@ func (r *StreamRoute) Start(parent task.Parent) gperr.Error {
} }
} }
if r.ShouldExclude() {
return nil
}
if err := checkExists(r); err != nil {
return err
}
go r.acceptConnections() go r.acceptConnections()
routes.Stream.Add(r) routes.Stream.Add(r)
r.task.OnFinished("entrypoint_remove_route", func() { r.task.OnCancel("remove_route_from_stream", func() {
routes.Stream.Del(r) routes.Stream.Del(r)
}) })
return nil return nil

View file

@ -11,6 +11,7 @@ type (
Pool[T Object] struct { Pool[T Object] struct {
m *xsync.Map[string, T] m *xsync.Map[string, T]
name string name string
disableLog bool
} }
Object interface { Object interface {
Key() string Key() string
@ -19,41 +20,54 @@ type (
) )
func New[T Object](name string) Pool[T] { func New[T Object](name string) Pool[T] {
return Pool[T]{xsync.NewMap[string, T](), name} return Pool[T]{xsync.NewMap[string, T](), name, false}
} }
func (p Pool[T]) Name() string { func (p *Pool[T]) DisableLog() {
p.disableLog = true
}
func (p *Pool[T]) Name() string {
return p.name return p.name
} }
func (p Pool[T]) Add(obj T) { func (p *Pool[T]) Add(obj T) {
p.checkExists(obj.Key()) p.checkExists(obj.Key())
p.m.Store(obj.Key(), obj) p.m.Store(obj.Key(), obj)
if !p.disableLog {
log.Info().Msgf("%s: added %s", p.name, obj.Name()) log.Info().Msgf("%s: added %s", p.name, obj.Name())
} }
func (p Pool[T]) Del(obj T) {
p.m.Delete(obj.Key())
log.Info().Msgf("%s: removed %s", p.name, obj.Name())
} }
func (p Pool[T]) Get(key string) (T, bool) { func (p *Pool[T]) AddIfNotExists(obj T) (actual T, added bool) {
actual, loaded := p.m.LoadOrStore(obj.Key(), obj)
return actual, !loaded
}
func (p *Pool[T]) Del(obj T) {
p.m.Delete(obj.Key())
if !p.disableLog {
log.Info().Msgf("%s: removed %s", p.name, obj.Name())
}
}
func (p *Pool[T]) Get(key string) (T, bool) {
return p.m.Load(key) return p.m.Load(key)
} }
func (p Pool[T]) Size() int { func (p *Pool[T]) Size() int {
return p.m.Size() return p.m.Size()
} }
func (p Pool[T]) Clear() { func (p *Pool[T]) Clear() {
p.m.Clear() p.m.Clear()
} }
func (p Pool[T]) Iter(fn func(k string, v T) bool) { func (p *Pool[T]) Iter(fn func(k string, v T) bool) {
p.m.Range(fn) p.m.Range(fn)
} }
func (p Pool[T]) Slice() []T { func (p *Pool[T]) Slice() []T {
slice := make([]T, 0, p.m.Size()) slice := make([]T, 0, p.m.Size())
for _, v := range p.m.Range { for _, v := range p.m.Range {
slice = append(slice, v) slice = append(slice, v)