Improved healthcheck, idlewatcher support for loadbalanced routes, bug fixes

This commit is contained in:
yusing
2024-10-15 15:34:27 +08:00
parent 53fa28ae77
commit f4d532598c
34 changed files with 568 additions and 423 deletions

View File

@@ -16,32 +16,36 @@ import (
type (
ReverseProxyEntry struct { // real model after validation
Alias T.Alias `json:"alias"`
Scheme T.Scheme `json:"scheme"`
URL net.URL `json:"url"`
Raw *types.RawEntry `json:"raw"`
Alias T.Alias `json:"alias,omitempty"`
Scheme T.Scheme `json:"scheme,omitempty"`
URL net.URL `json:"url,omitempty"`
NoTLSVerify bool `json:"no_tls_verify,omitempty"`
PathPatterns T.PathPatterns `json:"path_patterns"`
HealthCheck *health.HealthCheckConfig `json:"healthcheck"`
PathPatterns T.PathPatterns `json:"path_patterns,omitempty"`
HealthCheck *health.HealthCheckConfig `json:"healthcheck,omitempty"`
LoadBalance *loadbalancer.Config `json:"load_balance,omitempty"`
Middlewares D.NestedLabelMap `json:"middlewares,omitempty"`
/* Docker only */
IdleTimeout time.Duration `json:"idle_timeout"`
WakeTimeout time.Duration `json:"wake_timeout"`
StopMethod T.StopMethod `json:"stop_method"`
StopTimeout int `json:"stop_timeout"`
IdleTimeout time.Duration `json:"idle_timeout,omitempty"`
WakeTimeout time.Duration `json:"wake_timeout,omitempty"`
StopMethod T.StopMethod `json:"stop_method,omitempty"`
StopTimeout int `json:"stop_timeout,omitempty"`
StopSignal T.Signal `json:"stop_signal,omitempty"`
DockerHost string `json:"docker_host"`
ContainerName string `json:"container_name"`
ContainerID string `json:"container_id"`
ContainerRunning bool `json:"container_running"`
DockerHost string `json:"docker_host,omitempty"`
ContainerName string `json:"container_name,omitempty"`
ContainerID string `json:"container_id,omitempty"`
ContainerRunning bool `json:"container_running,omitempty"`
}
StreamEntry struct {
Alias T.Alias `json:"alias"`
Scheme T.StreamScheme `json:"scheme"`
Host T.Host `json:"host"`
Port T.StreamPort `json:"port"`
Healthcheck *health.HealthCheckConfig `json:"healthcheck"`
Raw *types.RawEntry `json:"raw"`
Alias T.Alias `json:"alias,omitempty"`
Scheme T.StreamScheme `json:"scheme,omitempty"`
Host T.Host `json:"host,omitempty"`
Port T.StreamPort `json:"port,omitempty"`
Healthcheck *health.HealthCheckConfig `json:"healthcheck,omitempty"`
}
)
@@ -88,6 +92,10 @@ func ValidateEntry(m *types.RawEntry) (any, E.NestedError) {
func validateRPEntry(m *types.RawEntry, s T.Scheme, b E.Builder) *ReverseProxyEntry {
var stopTimeOut time.Duration
cont := m.Container
if cont == nil {
cont = D.DummyContainer
}
host, err := T.ValidateHost(m.Host)
b.Add(err)
@@ -101,21 +109,21 @@ func validateRPEntry(m *types.RawEntry, s T.Scheme, b E.Builder) *ReverseProxyEn
url, err := E.Check(url.Parse(fmt.Sprintf("%s://%s:%d", s, host, port)))
b.Add(err)
idleTimeout, err := T.ValidateDurationPostitive(m.IdleTimeout)
idleTimeout, err := T.ValidateDurationPostitive(cont.IdleTimeout)
b.Add(err)
wakeTimeout, err := T.ValidateDurationPostitive(m.WakeTimeout)
wakeTimeout, err := T.ValidateDurationPostitive(cont.WakeTimeout)
b.Add(err)
stopMethod, err := T.ValidateStopMethod(m.StopMethod)
stopMethod, err := T.ValidateStopMethod(cont.StopMethod)
b.Add(err)
if stopMethod == T.StopMethodStop {
stopTimeOut, err = T.ValidateDurationPostitive(m.StopTimeout)
stopTimeOut, err = T.ValidateDurationPostitive(cont.StopTimeout)
b.Add(err)
}
stopSignal, err := T.ValidateSignal(m.StopSignal)
stopSignal, err := T.ValidateSignal(cont.StopSignal)
b.Add(err)
if err != nil {
@@ -123,6 +131,7 @@ func validateRPEntry(m *types.RawEntry, s T.Scheme, b E.Builder) *ReverseProxyEn
}
return &ReverseProxyEntry{
Raw: m,
Alias: T.NewAlias(m.Alias),
Scheme: s,
URL: net.NewURL(url),
@@ -136,10 +145,10 @@ func validateRPEntry(m *types.RawEntry, s T.Scheme, b E.Builder) *ReverseProxyEn
StopMethod: stopMethod,
StopTimeout: int(stopTimeOut.Seconds()), // docker api takes integer seconds for timeout argument
StopSignal: stopSignal,
DockerHost: m.DockerHost,
ContainerName: m.ContainerName,
ContainerID: m.ContainerID,
ContainerRunning: m.Running,
DockerHost: cont.DockerHost,
ContainerName: cont.ContainerName,
ContainerID: cont.ContainerID,
ContainerRunning: cont.Running,
}
}
@@ -158,6 +167,7 @@ func validateStreamEntry(m *types.RawEntry, b E.Builder) *StreamEntry {
}
return &StreamEntry{
Raw: m,
Alias: T.NewAlias(m.Alias),
Scheme: *scheme,
Host: host,

View File

@@ -32,7 +32,7 @@ func ValidateStreamScheme(s string) (ss *StreamScheme, err E.NestedError) {
}
func (s StreamScheme) String() string {
return fmt.Sprintf("%s:%s", s.ListeningScheme, s.ProxyScheme)
return fmt.Sprintf("%s -> %s", s.ListeningScheme, s.ProxyScheme)
}
// IsCoherent checks if the ListeningScheme and ProxyScheme of the StreamScheme are equal.

View File

@@ -72,7 +72,7 @@ func (p *DockerProvider) LoadRoutesImpl() (routes R.Routes, err E.NestedError) {
}
entries.RangeAll(func(_ string, e *types.RawEntry) {
e.DockerHost = p.dockerHost
e.Container.DockerHost = p.dockerHost
})
routes, err = R.FromEntries(entries)
@@ -88,7 +88,7 @@ func (p *DockerProvider) shouldIgnore(container *D.Container) bool {
strings.HasSuffix(container.ContainerName, "-old")
}
func (p *DockerProvider) OnEvent(event W.Event, routes R.Routes) (res EventResult) {
func (p *DockerProvider) OnEvent(event W.Event, oldRoutes R.Routes) (res EventResult) {
switch event.Action {
case events.ActionContainerStart, events.ActionContainerStop:
break
@@ -98,75 +98,66 @@ func (p *DockerProvider) OnEvent(event W.Event, routes R.Routes) (res EventResul
b := E.NewBuilder("event %s error", event)
defer b.To(&res.err)
routes.RangeAll(func(k string, v *R.Route) {
if v.Entry.ContainerID == event.ActorID ||
v.Entry.ContainerName == event.ActorName {
matches := R.NewRoutes()
oldRoutes.RangeAllParallel(func(k string, v *R.Route) {
if v.Entry.Container.ContainerID == event.ActorID ||
v.Entry.Container.ContainerName == event.ActorName {
matches.Store(k, v)
}
})
var newRoutes R.Routes
var err E.NestedError
if matches.Size() == 0 { // id & container name changed
matches = oldRoutes
newRoutes, err = p.LoadRoutesImpl()
b.Add(err)
} else {
cont, err := D.Inspect(p.dockerHost, event.ActorID)
if err != nil {
b.Add(E.FailWith("inspect container", err))
return
}
if p.shouldIgnore(cont) {
// stop all old routes
matches.RangeAllParallel(func(_ string, v *R.Route) {
b.Add(v.Stop())
})
return
}
entries, err := p.entriesFromContainerLabels(cont)
b.Add(err)
newRoutes, err = R.FromEntries(entries)
b.Add(err)
}
matches.RangeAll(func(k string, v *R.Route) {
if !newRoutes.Has(k) && !oldRoutes.Has(k) {
b.Add(v.Stop())
routes.Delete(k)
matches.Delete(k)
res.nRemoved++
}
})
if res.nRemoved == 0 { // id & container name changed
// load all routes (rescan)
routesNew, err := p.LoadRoutesImpl()
routesOld := routes
if routesNew.Size() == 0 {
b.Add(E.FailWith("rescan routes", err))
return
}
routesNew.Range(func(k string, v *R.Route) bool {
if !routesOld.Has(k) {
routesOld.Store(k, v)
b.Add(v.Start())
res.nAdded++
return false
}
return true
})
routesOld.Range(func(k string, v *R.Route) bool {
if !routesNew.Has(k) {
b.Add(v.Stop())
routesOld.Delete(k)
res.nRemoved++
return false
}
return true
})
return
}
client, err := D.ConnectClient(p.dockerHost)
if err != nil {
b.Add(E.FailWith("connect to docker", err))
return
}
defer client.Close()
cont, err := client.Inspect(event.ActorID)
if err != nil {
b.Add(E.FailWith("inspect container", err))
return
}
if p.shouldIgnore(cont) {
return
}
entries, err := p.entriesFromContainerLabels(cont)
b.Add(err)
entries.RangeAll(func(alias string, entry *types.RawEntry) {
if routes.Has(alias) {
b.Add(E.Duplicated("alias", alias))
} else {
if route, err := R.NewRoute(entry); err != nil {
newRoutes.RangeAll(func(alias string, newRoute *R.Route) {
oldRoute, exists := oldRoutes.Load(alias)
if exists {
if err := oldRoute.Stop(); err != nil {
b.Add(err)
} else {
routes.Store(alias, route)
b.Add(route.Start())
res.nAdded++
}
}
oldRoutes.Store(alias, newRoute)
if err := newRoute.Start(); err != nil {
b.Add(err)
}
if exists {
res.nReloaded++
} else {
res.nAdded++
}
})
return

View File

@@ -88,20 +88,20 @@ func TestApplyLabelWildcard(t *testing.T) {
ExpectDeepEqual(t, a.Middlewares, middlewaresExpect)
ExpectEqual(t, len(b.Middlewares), 0)
ExpectEqual(t, a.IdleTimeout, common.IdleTimeoutDefault)
ExpectEqual(t, b.IdleTimeout, common.IdleTimeoutDefault)
ExpectEqual(t, a.Container.IdleTimeout, common.IdleTimeoutDefault)
ExpectEqual(t, b.Container.IdleTimeout, common.IdleTimeoutDefault)
ExpectEqual(t, a.StopTimeout, common.StopTimeoutDefault)
ExpectEqual(t, b.StopTimeout, common.StopTimeoutDefault)
ExpectEqual(t, a.Container.StopTimeout, common.StopTimeoutDefault)
ExpectEqual(t, b.Container.StopTimeout, common.StopTimeoutDefault)
ExpectEqual(t, a.StopMethod, common.StopMethodDefault)
ExpectEqual(t, b.StopMethod, common.StopMethodDefault)
ExpectEqual(t, a.Container.StopMethod, common.StopMethodDefault)
ExpectEqual(t, b.Container.StopMethod, common.StopMethodDefault)
ExpectEqual(t, a.WakeTimeout, common.WakeTimeoutDefault)
ExpectEqual(t, b.WakeTimeout, common.WakeTimeoutDefault)
ExpectEqual(t, a.Container.WakeTimeout, common.WakeTimeoutDefault)
ExpectEqual(t, b.Container.WakeTimeout, common.WakeTimeoutDefault)
ExpectEqual(t, a.StopSignal, "SIGTERM")
ExpectEqual(t, b.StopSignal, "SIGTERM")
ExpectEqual(t, a.Container.StopSignal, "SIGTERM")
ExpectEqual(t, b.Container.StopSignal, "SIGTERM")
}
func TestApplyLabelWithAlias(t *testing.T) {
@@ -186,16 +186,16 @@ func TestPublicIPLocalhost(t *testing.T) {
c := D.FromDocker(&types.Container{Names: dummyNames}, client.DefaultDockerHost)
raw, ok := Must(p.entriesFromContainerLabels(c)).Load("a")
ExpectTrue(t, ok)
ExpectEqual(t, raw.PublicIP, "127.0.0.1")
ExpectEqual(t, raw.Host, raw.PublicIP)
ExpectEqual(t, raw.Container.PublicIP, "127.0.0.1")
ExpectEqual(t, raw.Host, raw.Container.PublicIP)
}
func TestPublicIPRemote(t *testing.T) {
c := D.FromDocker(&types.Container{Names: dummyNames}, "tcp://1.2.3.4:2375")
raw, ok := Must(p.entriesFromContainerLabels(c)).Load("a")
ExpectTrue(t, ok)
ExpectEqual(t, raw.PublicIP, "1.2.3.4")
ExpectEqual(t, raw.Host, raw.PublicIP)
ExpectEqual(t, raw.Container.PublicIP, "1.2.3.4")
ExpectEqual(t, raw.Host, raw.Container.PublicIP)
}
func TestPrivateIPLocalhost(t *testing.T) {
@@ -211,8 +211,8 @@ func TestPrivateIPLocalhost(t *testing.T) {
}, client.DefaultDockerHost)
raw, ok := Must(p.entriesFromContainerLabels(c)).Load("a")
ExpectTrue(t, ok)
ExpectEqual(t, raw.PrivateIP, "172.17.0.123")
ExpectEqual(t, raw.Host, raw.PrivateIP)
ExpectEqual(t, raw.Container.PrivateIP, "172.17.0.123")
ExpectEqual(t, raw.Host, raw.Container.PrivateIP)
}
func TestPrivateIPRemote(t *testing.T) {
@@ -228,9 +228,9 @@ func TestPrivateIPRemote(t *testing.T) {
}, "tcp://1.2.3.4:2375")
raw, ok := Must(p.entriesFromContainerLabels(c)).Load("a")
ExpectTrue(t, ok)
ExpectEqual(t, raw.PrivateIP, "")
ExpectEqual(t, raw.PublicIP, "1.2.3.4")
ExpectEqual(t, raw.Host, raw.PublicIP)
ExpectEqual(t, raw.Container.PrivateIP, "")
ExpectEqual(t, raw.Container.PublicIP, "1.2.3.4")
ExpectEqual(t, raw.Host, raw.Container.PublicIP)
}
func TestStreamDefaultValues(t *testing.T) {

View File

@@ -5,6 +5,7 @@ import (
"path"
"github.com/sirupsen/logrus"
"github.com/yusing/go-proxy/internal/common"
E "github.com/yusing/go-proxy/internal/error"
R "github.com/yusing/go-proxy/internal/route"
W "github.com/yusing/go-proxy/internal/watcher"
@@ -19,7 +20,7 @@ type (
routes R.Routes
watcher W.Watcher
watcherCtx context.Context
watcherTask common.Task
watcherCancel context.CancelFunc
l *logrus.Entry
@@ -38,9 +39,10 @@ type (
Type ProviderType `json:"type"`
}
EventResult struct {
nRemoved int
nAdded int
err E.NestedError
nAdded int
nRemoved int
nReloaded int
err E.NestedError
}
)
@@ -129,6 +131,7 @@ func (p *Provider) StopAllRoutes() (res E.NestedError) {
p.routes.RangeAllParallel(func(alias string, r *R.Route) {
errors.Add(r.Stop().Subject(r))
})
p.routes.Clear()
return
}
@@ -175,27 +178,21 @@ func (p *Provider) Statistics() ProviderStats {
}
func (p *Provider) watchEvents() {
p.watcherCtx, p.watcherCancel = context.WithCancel(context.Background())
events, errs := p.watcher.Events(p.watcherCtx)
p.watcherTask, p.watcherCancel = common.NewTaskWithCancel("Watcher for provider %s", p.name)
defer p.watcherTask.Finished()
events, errs := p.watcher.Events(p.watcherTask.Context())
l := p.l.WithField("module", "watcher")
for {
select {
case <-p.watcherCtx.Done():
case <-p.watcherTask.Context().Done():
return
case event := <-events:
res := p.OnEvent(event, p.routes)
l.Infof("%s event %q", event.Type, event)
if res.nAdded > 0 || res.nRemoved > 0 {
n := res.nAdded - res.nRemoved
switch {
case n == 0:
l.Infof("%d route(s) reloaded", res.nAdded)
case n > 0:
l.Infof("%d route(s) added", n)
default:
l.Infof("%d route(s) removed", -n)
}
if res.nAdded+res.nRemoved+res.nReloaded > 0 {
l.Infof("%s event %q", event.Type, event)
l.Infof("| %d NEW | %d REMOVED | %d RELOADED |", res.nAdded, res.nRemoved, res.nReloaded)
}
if res.err != nil {
l.Error(res.err)