mirror of
https://github.com/yusing/godoxy.git
synced 2026-04-17 05:59:42 +02:00
Fixed a few issues:
- Incorrect name being shown on dashboard "Proxies page" - Apps being shown when homepage.show is false - Load balanced routes are shown on homepage instead of the load balancer - Route with idlewatcher will now be removed on container destroy - Idlewatcher panic - Performance improvement - Idlewatcher infinitely loading - Reload stucked / not working properly - Streams stuck on shutdown / reload - etc... Added: - support idlewatcher for loadbalanced routes - partial implementation for stream type idlewatcher Issues: - graceful shutdown
This commit is contained in:
@@ -1,177 +0,0 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
D "github.com/yusing/go-proxy/internal/docker"
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
"github.com/yusing/go-proxy/internal/net/http/loadbalancer"
|
||||
net "github.com/yusing/go-proxy/internal/net/types"
|
||||
T "github.com/yusing/go-proxy/internal/proxy/fields"
|
||||
"github.com/yusing/go-proxy/internal/types"
|
||||
"github.com/yusing/go-proxy/internal/watcher/health"
|
||||
)
|
||||
|
||||
type (
|
||||
ReverseProxyEntry struct { // real model after validation
|
||||
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,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,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,omitempty"`
|
||||
ContainerName string `json:"container_name,omitempty"`
|
||||
ContainerID string `json:"container_id,omitempty"`
|
||||
ContainerRunning bool `json:"container_running,omitempty"`
|
||||
}
|
||||
StreamEntry struct {
|
||||
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"`
|
||||
}
|
||||
)
|
||||
|
||||
func (rp *ReverseProxyEntry) UseIdleWatcher() bool {
|
||||
return rp.IdleTimeout > 0 && rp.IsDocker()
|
||||
}
|
||||
|
||||
func (rp *ReverseProxyEntry) UseLoadBalance() bool {
|
||||
return rp.LoadBalance != nil && rp.LoadBalance.Link != ""
|
||||
}
|
||||
|
||||
func (rp *ReverseProxyEntry) IsDocker() bool {
|
||||
return rp.DockerHost != ""
|
||||
}
|
||||
|
||||
func (rp *ReverseProxyEntry) IsZeroPort() bool {
|
||||
return rp.URL.Port() == "0"
|
||||
}
|
||||
|
||||
func (rp *ReverseProxyEntry) ShouldNotServe() bool {
|
||||
return rp.IsZeroPort() && !rp.UseIdleWatcher()
|
||||
}
|
||||
|
||||
func ValidateEntry(m *types.RawEntry) (any, E.NestedError) {
|
||||
m.FillMissingFields()
|
||||
|
||||
scheme, err := T.NewScheme(m.Scheme)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var entry any
|
||||
e := E.NewBuilder("error validating entry")
|
||||
if scheme.IsStream() {
|
||||
entry = validateStreamEntry(m, e)
|
||||
} else {
|
||||
entry = validateRPEntry(m, scheme, e)
|
||||
}
|
||||
if err := e.Build(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return entry, nil
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
port, err := T.ValidatePort(m.Port)
|
||||
b.Add(err)
|
||||
|
||||
pathPatterns, err := T.ValidatePathPatterns(m.PathPatterns)
|
||||
b.Add(err)
|
||||
|
||||
url, err := E.Check(url.Parse(fmt.Sprintf("%s://%s:%d", s, host, port)))
|
||||
b.Add(err)
|
||||
|
||||
idleTimeout, err := T.ValidateDurationPostitive(cont.IdleTimeout)
|
||||
b.Add(err)
|
||||
|
||||
wakeTimeout, err := T.ValidateDurationPostitive(cont.WakeTimeout)
|
||||
b.Add(err)
|
||||
|
||||
stopMethod, err := T.ValidateStopMethod(cont.StopMethod)
|
||||
b.Add(err)
|
||||
|
||||
if stopMethod == T.StopMethodStop {
|
||||
stopTimeOut, err = T.ValidateDurationPostitive(cont.StopTimeout)
|
||||
b.Add(err)
|
||||
}
|
||||
|
||||
stopSignal, err := T.ValidateSignal(cont.StopSignal)
|
||||
b.Add(err)
|
||||
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &ReverseProxyEntry{
|
||||
Raw: m,
|
||||
Alias: T.NewAlias(m.Alias),
|
||||
Scheme: s,
|
||||
URL: net.NewURL(url),
|
||||
NoTLSVerify: m.NoTLSVerify,
|
||||
PathPatterns: pathPatterns,
|
||||
HealthCheck: &m.HealthCheck,
|
||||
LoadBalance: &m.LoadBalance,
|
||||
Middlewares: m.Middlewares,
|
||||
IdleTimeout: idleTimeout,
|
||||
WakeTimeout: wakeTimeout,
|
||||
StopMethod: stopMethod,
|
||||
StopTimeout: int(stopTimeOut.Seconds()), // docker api takes integer seconds for timeout argument
|
||||
StopSignal: stopSignal,
|
||||
DockerHost: cont.DockerHost,
|
||||
ContainerName: cont.ContainerName,
|
||||
ContainerID: cont.ContainerID,
|
||||
ContainerRunning: cont.Running,
|
||||
}
|
||||
}
|
||||
|
||||
func validateStreamEntry(m *types.RawEntry, b E.Builder) *StreamEntry {
|
||||
host, err := T.ValidateHost(m.Host)
|
||||
b.Add(err)
|
||||
|
||||
port, err := T.ValidateStreamPort(m.Port)
|
||||
b.Add(err)
|
||||
|
||||
scheme, err := T.ValidateStreamScheme(m.Scheme)
|
||||
b.Add(err)
|
||||
|
||||
if b.HasError() {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &StreamEntry{
|
||||
Raw: m,
|
||||
Alias: T.NewAlias(m.Alias),
|
||||
Scheme: *scheme,
|
||||
Host: host,
|
||||
Port: port,
|
||||
Healthcheck: &m.HealthCheck,
|
||||
}
|
||||
}
|
||||
68
internal/proxy/entry/entry.go
Normal file
68
internal/proxy/entry/entry.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package entry
|
||||
|
||||
import (
|
||||
idlewatcher "github.com/yusing/go-proxy/internal/docker/idlewatcher/config"
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
"github.com/yusing/go-proxy/internal/net/http/loadbalancer"
|
||||
net "github.com/yusing/go-proxy/internal/net/types"
|
||||
T "github.com/yusing/go-proxy/internal/proxy/fields"
|
||||
"github.com/yusing/go-proxy/internal/watcher/health"
|
||||
)
|
||||
|
||||
type Entry interface {
|
||||
TargetName() string
|
||||
TargetURL() net.URL
|
||||
RawEntry() *RawEntry
|
||||
LoadBalanceConfig() *loadbalancer.Config
|
||||
HealthCheckConfig() *health.HealthCheckConfig
|
||||
IdlewatcherConfig() *idlewatcher.Config
|
||||
}
|
||||
|
||||
func ValidateEntry(m *RawEntry) (Entry, E.NestedError) {
|
||||
m.FillMissingFields()
|
||||
|
||||
scheme, err := T.NewScheme(m.Scheme)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var entry Entry
|
||||
e := E.NewBuilder("error validating entry")
|
||||
if scheme.IsStream() {
|
||||
entry = validateStreamEntry(m, e)
|
||||
} else {
|
||||
entry = validateRPEntry(m, scheme, e)
|
||||
}
|
||||
if err := e.Build(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return entry, nil
|
||||
}
|
||||
|
||||
func IsDocker(entry Entry) bool {
|
||||
iw := entry.IdlewatcherConfig()
|
||||
return iw != nil && iw.ContainerID != ""
|
||||
}
|
||||
|
||||
func IsZeroPort(entry Entry) bool {
|
||||
return entry.TargetURL().Port() == "0"
|
||||
}
|
||||
|
||||
func ShouldNotServe(entry Entry) bool {
|
||||
return IsZeroPort(entry) && !UseIdleWatcher(entry)
|
||||
}
|
||||
|
||||
func UseLoadBalance(entry Entry) bool {
|
||||
lb := entry.LoadBalanceConfig()
|
||||
return lb != nil && lb.Link != ""
|
||||
}
|
||||
|
||||
func UseIdleWatcher(entry Entry) bool {
|
||||
iw := entry.IdlewatcherConfig()
|
||||
return iw != nil && iw.IdleTimeout > 0
|
||||
}
|
||||
|
||||
func UseHealthCheck(entry Entry) bool {
|
||||
hc := entry.HealthCheckConfig()
|
||||
return hc != nil && !hc.Disabled
|
||||
}
|
||||
201
internal/proxy/entry/raw.go
Normal file
201
internal/proxy/entry/raw.go
Normal file
@@ -0,0 +1,201 @@
|
||||
package entry
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
"github.com/yusing/go-proxy/internal/docker"
|
||||
"github.com/yusing/go-proxy/internal/homepage"
|
||||
"github.com/yusing/go-proxy/internal/net/http/loadbalancer"
|
||||
U "github.com/yusing/go-proxy/internal/utils"
|
||||
F "github.com/yusing/go-proxy/internal/utils/functional"
|
||||
"github.com/yusing/go-proxy/internal/watcher/health"
|
||||
)
|
||||
|
||||
type (
|
||||
RawEntry struct {
|
||||
_ U.NoCopy
|
||||
|
||||
// raw entry object before validation
|
||||
// loaded from docker labels or yaml file
|
||||
Alias string `json:"-" yaml:"-"`
|
||||
Scheme string `json:"scheme,omitempty" yaml:"scheme"`
|
||||
Host string `json:"host,omitempty" yaml:"host"`
|
||||
Port string `json:"port,omitempty" yaml:"port"`
|
||||
NoTLSVerify bool `json:"no_tls_verify,omitempty" yaml:"no_tls_verify"` // https proxy only
|
||||
PathPatterns []string `json:"path_patterns,omitempty" yaml:"path_patterns"` // http(s) proxy only
|
||||
HealthCheck *health.HealthCheckConfig `json:"healthcheck,omitempty" yaml:"healthcheck"`
|
||||
LoadBalance *loadbalancer.Config `json:"load_balance,omitempty" yaml:"load_balance"`
|
||||
Middlewares docker.NestedLabelMap `json:"middlewares,omitempty" yaml:"middlewares"`
|
||||
Homepage *homepage.Item `json:"homepage,omitempty" yaml:"homepage"`
|
||||
|
||||
/* Docker only */
|
||||
Container *docker.Container `json:"container,omitempty" yaml:"-"`
|
||||
}
|
||||
|
||||
RawEntries = F.Map[string, *RawEntry]
|
||||
)
|
||||
|
||||
var NewProxyEntries = F.NewMapOf[string, *RawEntry]
|
||||
|
||||
func (e *RawEntry) FillMissingFields() {
|
||||
isDocker := e.Container != nil
|
||||
cont := e.Container
|
||||
if !isDocker {
|
||||
cont = docker.DummyContainer
|
||||
}
|
||||
|
||||
if e.Host == "" {
|
||||
switch {
|
||||
case cont.PrivateIP != "":
|
||||
e.Host = cont.PrivateIP
|
||||
case cont.PublicIP != "":
|
||||
e.Host = cont.PublicIP
|
||||
case !isDocker:
|
||||
e.Host = "localhost"
|
||||
}
|
||||
}
|
||||
|
||||
lp, pp, extra := e.splitPorts()
|
||||
|
||||
if port, ok := common.ServiceNamePortMapTCP[cont.ImageName]; ok {
|
||||
if pp == "" {
|
||||
pp = strconv.Itoa(port)
|
||||
}
|
||||
if e.Scheme == "" {
|
||||
e.Scheme = "tcp"
|
||||
}
|
||||
} else if port, ok := common.ImageNamePortMap[cont.ImageName]; ok {
|
||||
if pp == "" {
|
||||
pp = strconv.Itoa(port)
|
||||
}
|
||||
if e.Scheme == "" {
|
||||
e.Scheme = "http"
|
||||
}
|
||||
} else if pp == "" && e.Scheme == "https" {
|
||||
pp = "443"
|
||||
} else if pp == "" {
|
||||
if p := lowestPort(cont.PrivatePortMapping); p != "" {
|
||||
pp = p
|
||||
} else if p := lowestPort(cont.PublicPortMapping); p != "" {
|
||||
pp = p
|
||||
} else if !isDocker {
|
||||
pp = "80"
|
||||
} else {
|
||||
logrus.Debugf("no port found for %s", e.Alias)
|
||||
}
|
||||
}
|
||||
|
||||
// replace private port with public port if using public IP.
|
||||
if e.Host == cont.PublicIP {
|
||||
if p, ok := cont.PrivatePortMapping[pp]; ok {
|
||||
pp = U.PortString(p.PublicPort)
|
||||
}
|
||||
}
|
||||
// replace public port with private port if using private IP.
|
||||
if e.Host == cont.PrivateIP {
|
||||
if p, ok := cont.PublicPortMapping[pp]; ok {
|
||||
pp = U.PortString(p.PrivatePort)
|
||||
}
|
||||
}
|
||||
|
||||
if e.Scheme == "" && isDocker {
|
||||
switch {
|
||||
case e.Host == cont.PublicIP && cont.PublicPortMapping[pp].Type == "udp":
|
||||
e.Scheme = "udp"
|
||||
case e.Host == cont.PrivateIP && cont.PrivatePortMapping[pp].Type == "udp":
|
||||
e.Scheme = "udp"
|
||||
}
|
||||
}
|
||||
|
||||
if e.Scheme == "" {
|
||||
switch {
|
||||
case lp != "":
|
||||
e.Scheme = "tcp"
|
||||
case strings.HasSuffix(pp, "443"):
|
||||
e.Scheme = "https"
|
||||
default: // assume its http
|
||||
e.Scheme = "http"
|
||||
}
|
||||
}
|
||||
|
||||
if e.HealthCheck == nil {
|
||||
e.HealthCheck = new(health.HealthCheckConfig)
|
||||
}
|
||||
|
||||
if e.HealthCheck.Disabled {
|
||||
e.HealthCheck = nil
|
||||
} else {
|
||||
if e.HealthCheck.Interval == 0 {
|
||||
e.HealthCheck.Interval = common.HealthCheckIntervalDefault
|
||||
}
|
||||
if e.HealthCheck.Timeout == 0 {
|
||||
e.HealthCheck.Timeout = common.HealthCheckTimeoutDefault
|
||||
}
|
||||
}
|
||||
|
||||
if cont.IdleTimeout != "" {
|
||||
if cont.WakeTimeout == "" {
|
||||
cont.WakeTimeout = common.WakeTimeoutDefault
|
||||
}
|
||||
if cont.StopTimeout == "" {
|
||||
cont.StopTimeout = common.StopTimeoutDefault
|
||||
}
|
||||
if cont.StopMethod == "" {
|
||||
cont.StopMethod = common.StopMethodDefault
|
||||
}
|
||||
}
|
||||
|
||||
e.Port = joinPorts(lp, pp, extra)
|
||||
|
||||
if e.Port == "" || e.Host == "" {
|
||||
if lp != "" {
|
||||
e.Port = lp + ":0"
|
||||
} else {
|
||||
e.Port = "0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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, ":")
|
||||
}
|
||||
|
||||
func lowestPort(ports map[string]types.Port) string {
|
||||
var cmp uint16
|
||||
var res string
|
||||
for port, v := range ports {
|
||||
if v.PrivatePort < cmp || cmp == 0 {
|
||||
cmp = v.PrivatePort
|
||||
res = port
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
98
internal/proxy/entry/reverse_proxy.go
Normal file
98
internal/proxy/entry/reverse_proxy.go
Normal file
@@ -0,0 +1,98 @@
|
||||
package entry
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"github.com/yusing/go-proxy/internal/docker"
|
||||
idlewatcher "github.com/yusing/go-proxy/internal/docker/idlewatcher/config"
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
"github.com/yusing/go-proxy/internal/net/http/loadbalancer"
|
||||
net "github.com/yusing/go-proxy/internal/net/types"
|
||||
"github.com/yusing/go-proxy/internal/proxy/fields"
|
||||
"github.com/yusing/go-proxy/internal/watcher/health"
|
||||
)
|
||||
|
||||
type ReverseProxyEntry struct { // real model after validation
|
||||
Raw *RawEntry `json:"raw"`
|
||||
|
||||
Alias fields.Alias `json:"alias,omitempty"`
|
||||
Scheme fields.Scheme `json:"scheme,omitempty"`
|
||||
URL net.URL `json:"url,omitempty"`
|
||||
NoTLSVerify bool `json:"no_tls_verify,omitempty"`
|
||||
PathPatterns fields.PathPatterns `json:"path_patterns,omitempty"`
|
||||
HealthCheck *health.HealthCheckConfig `json:"healthcheck,omitempty"`
|
||||
LoadBalance *loadbalancer.Config `json:"load_balance,omitempty"`
|
||||
Middlewares docker.NestedLabelMap `json:"middlewares,omitempty"`
|
||||
|
||||
/* Docker only */
|
||||
Idlewatcher *idlewatcher.Config `json:"idlewatcher,omitempty"`
|
||||
}
|
||||
|
||||
func (rp *ReverseProxyEntry) TargetName() string {
|
||||
return string(rp.Alias)
|
||||
}
|
||||
|
||||
func (rp *ReverseProxyEntry) TargetURL() net.URL {
|
||||
return rp.URL
|
||||
}
|
||||
|
||||
func (rp *ReverseProxyEntry) RawEntry() *RawEntry {
|
||||
return rp.Raw
|
||||
}
|
||||
|
||||
func (rp *ReverseProxyEntry) LoadBalanceConfig() *loadbalancer.Config {
|
||||
return rp.LoadBalance
|
||||
}
|
||||
|
||||
func (rp *ReverseProxyEntry) HealthCheckConfig() *health.HealthCheckConfig {
|
||||
return rp.HealthCheck
|
||||
}
|
||||
|
||||
func (rp *ReverseProxyEntry) IdlewatcherConfig() *idlewatcher.Config {
|
||||
return rp.Idlewatcher
|
||||
}
|
||||
|
||||
func validateRPEntry(m *RawEntry, s fields.Scheme, b E.Builder) *ReverseProxyEntry {
|
||||
cont := m.Container
|
||||
if cont == nil {
|
||||
cont = docker.DummyContainer
|
||||
}
|
||||
|
||||
lb := m.LoadBalance
|
||||
if lb != nil && lb.Link == "" {
|
||||
lb = nil
|
||||
}
|
||||
|
||||
host, err := fields.ValidateHost(m.Host)
|
||||
b.Add(err)
|
||||
|
||||
port, err := fields.ValidatePort(m.Port)
|
||||
b.Add(err)
|
||||
|
||||
pathPatterns, err := fields.ValidatePathPatterns(m.PathPatterns)
|
||||
b.Add(err)
|
||||
|
||||
url, err := E.Check(url.Parse(fmt.Sprintf("%s://%s:%d", s, host, port)))
|
||||
b.Add(err)
|
||||
|
||||
idleWatcherCfg, err := idlewatcher.ValidateConfig(m.Container)
|
||||
b.Add(err)
|
||||
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &ReverseProxyEntry{
|
||||
Raw: m,
|
||||
Alias: fields.NewAlias(m.Alias),
|
||||
Scheme: s,
|
||||
URL: net.NewURL(url),
|
||||
NoTLSVerify: m.NoTLSVerify,
|
||||
PathPatterns: pathPatterns,
|
||||
HealthCheck: m.HealthCheck,
|
||||
LoadBalance: lb,
|
||||
Middlewares: m.Middlewares,
|
||||
Idlewatcher: idleWatcherCfg,
|
||||
}
|
||||
}
|
||||
89
internal/proxy/entry/stream.go
Normal file
89
internal/proxy/entry/stream.go
Normal file
@@ -0,0 +1,89 @@
|
||||
package entry
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/yusing/go-proxy/internal/docker"
|
||||
idlewatcher "github.com/yusing/go-proxy/internal/docker/idlewatcher/config"
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
"github.com/yusing/go-proxy/internal/net/http/loadbalancer"
|
||||
net "github.com/yusing/go-proxy/internal/net/types"
|
||||
"github.com/yusing/go-proxy/internal/proxy/fields"
|
||||
"github.com/yusing/go-proxy/internal/watcher/health"
|
||||
)
|
||||
|
||||
type StreamEntry struct {
|
||||
Raw *RawEntry `json:"raw"`
|
||||
|
||||
Alias fields.Alias `json:"alias,omitempty"`
|
||||
Scheme fields.StreamScheme `json:"scheme,omitempty"`
|
||||
URL net.URL `json:"url,omitempty"`
|
||||
Host fields.Host `json:"host,omitempty"`
|
||||
Port fields.StreamPort `json:"port,omitempty"`
|
||||
HealthCheck *health.HealthCheckConfig `json:"healthcheck,omitempty"`
|
||||
|
||||
/* Docker only */
|
||||
Idlewatcher *idlewatcher.Config `json:"idlewatcher,omitempty"`
|
||||
}
|
||||
|
||||
func (s *StreamEntry) TargetName() string {
|
||||
return string(s.Alias)
|
||||
}
|
||||
|
||||
func (s *StreamEntry) TargetURL() net.URL {
|
||||
return s.URL
|
||||
}
|
||||
|
||||
func (s *StreamEntry) RawEntry() *RawEntry {
|
||||
return s.Raw
|
||||
}
|
||||
|
||||
func (s *StreamEntry) LoadBalanceConfig() *loadbalancer.Config {
|
||||
// TODO: support stream load balance
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *StreamEntry) HealthCheckConfig() *health.HealthCheckConfig {
|
||||
return s.HealthCheck
|
||||
}
|
||||
|
||||
func (s *StreamEntry) IdlewatcherConfig() *idlewatcher.Config {
|
||||
return s.Idlewatcher
|
||||
}
|
||||
|
||||
func validateStreamEntry(m *RawEntry, b E.Builder) *StreamEntry {
|
||||
cont := m.Container
|
||||
if cont == nil {
|
||||
cont = docker.DummyContainer
|
||||
}
|
||||
|
||||
host, err := fields.ValidateHost(m.Host)
|
||||
b.Add(err)
|
||||
|
||||
port, err := fields.ValidateStreamPort(m.Port)
|
||||
b.Add(err)
|
||||
|
||||
scheme, err := fields.ValidateStreamScheme(m.Scheme)
|
||||
b.Add(err)
|
||||
|
||||
url, err := E.Check(net.ParseURL(fmt.Sprintf("%s://%s:%d", scheme.ProxyScheme, m.Host, port.ProxyPort)))
|
||||
b.Add(err)
|
||||
|
||||
idleWatcherCfg, err := idlewatcher.ValidateConfig(m.Container)
|
||||
b.Add(err)
|
||||
|
||||
if b.HasError() {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &StreamEntry{
|
||||
Raw: m,
|
||||
Alias: fields.NewAlias(m.Alias),
|
||||
Scheme: *scheme,
|
||||
URL: url,
|
||||
Host: host,
|
||||
Port: port,
|
||||
HealthCheck: m.HealthCheck,
|
||||
Idlewatcher: idleWatcherCfg,
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
package fields
|
||||
|
||||
import (
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
)
|
||||
|
||||
type Signal string
|
||||
|
||||
func ValidateSignal(s string) (Signal, E.NestedError) {
|
||||
switch s {
|
||||
case "", "SIGINT", "SIGTERM", "SIGHUP", "SIGQUIT",
|
||||
"INT", "TERM", "HUP", "QUIT":
|
||||
return Signal(s), nil
|
||||
}
|
||||
|
||||
return "", E.Invalid("signal", s)
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
package fields
|
||||
|
||||
import (
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
)
|
||||
|
||||
type StopMethod string
|
||||
|
||||
const (
|
||||
StopMethodPause StopMethod = "pause"
|
||||
StopMethodStop StopMethod = "stop"
|
||||
StopMethodKill StopMethod = "kill"
|
||||
)
|
||||
|
||||
func ValidateStopMethod(s string) (StopMethod, E.NestedError) {
|
||||
sm := StopMethod(s)
|
||||
switch sm {
|
||||
case StopMethodPause, StopMethodStop, StopMethodKill:
|
||||
return sm, nil
|
||||
default:
|
||||
return "", E.Invalid("stop_method", sm)
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
package fields
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
)
|
||||
|
||||
func ValidateDurationPostitive(value string) (time.Duration, E.NestedError) {
|
||||
d, err := time.ParseDuration(value)
|
||||
if err != nil {
|
||||
return 0, E.Invalid("duration", value)
|
||||
}
|
||||
if d < 0 {
|
||||
return 0, E.Invalid("duration", "negative value")
|
||||
}
|
||||
return d, nil
|
||||
}
|
||||
@@ -1,248 +0,0 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
D "github.com/yusing/go-proxy/internal/docker"
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
R "github.com/yusing/go-proxy/internal/route"
|
||||
"github.com/yusing/go-proxy/internal/types"
|
||||
W "github.com/yusing/go-proxy/internal/watcher"
|
||||
"github.com/yusing/go-proxy/internal/watcher/events"
|
||||
)
|
||||
|
||||
type DockerProvider struct {
|
||||
name, dockerHost string
|
||||
ExplicitOnly bool
|
||||
}
|
||||
|
||||
var (
|
||||
AliasRefRegex = regexp.MustCompile(`#\d+`)
|
||||
AliasRefRegexOld = regexp.MustCompile(`\$\d+`)
|
||||
)
|
||||
|
||||
func DockerProviderImpl(name, dockerHost string, explicitOnly bool) (ProviderImpl, E.NestedError) {
|
||||
if dockerHost == common.DockerHostFromEnv {
|
||||
dockerHost = common.GetEnv("DOCKER_HOST", client.DefaultDockerHost)
|
||||
}
|
||||
return &DockerProvider{name, dockerHost, explicitOnly}, nil
|
||||
}
|
||||
|
||||
func (p *DockerProvider) String() string {
|
||||
return "docker: " + p.name
|
||||
}
|
||||
|
||||
func (p *DockerProvider) NewWatcher() W.Watcher {
|
||||
return W.NewDockerWatcher(p.dockerHost)
|
||||
}
|
||||
|
||||
func (p *DockerProvider) LoadRoutesImpl() (routes R.Routes, err E.NestedError) {
|
||||
routes = R.NewRoutes()
|
||||
entries := types.NewProxyEntries()
|
||||
|
||||
info, err := D.GetClientInfo(p.dockerHost, true)
|
||||
if err != nil {
|
||||
return routes, E.FailWith("connect to docker", err)
|
||||
}
|
||||
|
||||
errors := E.NewBuilder("errors in docker labels")
|
||||
|
||||
for _, c := range info.Containers {
|
||||
container := D.FromDocker(&c, p.dockerHost)
|
||||
if container.IsExcluded {
|
||||
continue
|
||||
}
|
||||
|
||||
newEntries, err := p.entriesFromContainerLabels(container)
|
||||
if err != nil {
|
||||
errors.Add(err)
|
||||
}
|
||||
// although err is not nil
|
||||
// there may be some valid entries in `en`
|
||||
dups := entries.MergeFrom(newEntries)
|
||||
// add the duplicate proxy entries to the error
|
||||
dups.RangeAll(func(k string, v *types.RawEntry) {
|
||||
errors.Addf("duplicate alias %s", k)
|
||||
})
|
||||
}
|
||||
|
||||
entries.RangeAll(func(_ string, e *types.RawEntry) {
|
||||
e.Container.DockerHost = p.dockerHost
|
||||
})
|
||||
|
||||
routes, err = R.FromEntries(entries)
|
||||
errors.Add(err)
|
||||
|
||||
return routes, errors.Build()
|
||||
}
|
||||
|
||||
func (p *DockerProvider) shouldIgnore(container *D.Container) bool {
|
||||
return container.IsExcluded ||
|
||||
!container.IsExplicit && p.ExplicitOnly ||
|
||||
!container.IsExplicit && container.IsDatabase ||
|
||||
strings.HasSuffix(container.ContainerName, "-old")
|
||||
}
|
||||
|
||||
func (p *DockerProvider) OnEvent(event W.Event, oldRoutes R.Routes) (res EventResult) {
|
||||
b := E.NewBuilder("event %s error", event)
|
||||
defer b.To(&res.err)
|
||||
|
||||
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)
|
||||
}
|
||||
})
|
||||
//FIXME: docker event die stuck
|
||||
|
||||
var newRoutes R.Routes
|
||||
var err E.NestedError
|
||||
|
||||
switch {
|
||||
// id & container name changed
|
||||
case matches.Size() == 0:
|
||||
matches = oldRoutes
|
||||
newRoutes, err = p.LoadRoutesImpl()
|
||||
b.Add(err)
|
||||
case event.Action == events.ActionContainerDestroy:
|
||||
// stop all old routes
|
||||
matches.RangeAllParallel(func(_ string, v *R.Route) {
|
||||
oldRoutes.Delete(v.Entry.Alias)
|
||||
b.Add(v.Stop())
|
||||
res.nRemoved++
|
||||
})
|
||||
return
|
||||
default:
|
||||
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())
|
||||
res.nRemoved++
|
||||
})
|
||||
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())
|
||||
matches.Delete(k)
|
||||
res.nRemoved++
|
||||
}
|
||||
})
|
||||
|
||||
newRoutes.RangeAll(func(alias string, newRoute *R.Route) {
|
||||
oldRoute, exists := oldRoutes.Load(alias)
|
||||
if exists {
|
||||
b.Add(oldRoute.Stop())
|
||||
res.nReloaded++
|
||||
} else {
|
||||
res.nAdded++
|
||||
}
|
||||
b.Add(newRoute.Start())
|
||||
oldRoutes.Store(alias, newRoute)
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Returns a list of proxy entries for a container.
|
||||
// Always non-nil.
|
||||
func (p *DockerProvider) entriesFromContainerLabels(container *D.Container) (entries types.RawEntries, _ E.NestedError) {
|
||||
entries = types.NewProxyEntries()
|
||||
|
||||
if p.shouldIgnore(container) {
|
||||
return
|
||||
}
|
||||
|
||||
// init entries map for all aliases
|
||||
for _, a := range container.Aliases {
|
||||
entries.Store(a, &types.RawEntry{
|
||||
Alias: a,
|
||||
Container: container,
|
||||
})
|
||||
}
|
||||
|
||||
errors := E.NewBuilder("failed to apply label")
|
||||
for key, val := range container.Labels {
|
||||
errors.Add(p.applyLabel(container, entries, key, val))
|
||||
}
|
||||
|
||||
// remove all entries that failed to fill in missing fields
|
||||
entries.RangeAll(func(_ string, re *types.RawEntry) {
|
||||
re.FillMissingFields()
|
||||
})
|
||||
|
||||
return entries, errors.Build().Subject(container.ContainerName)
|
||||
}
|
||||
|
||||
func (p *DockerProvider) applyLabel(container *D.Container, entries types.RawEntries, key, val string) (res E.NestedError) {
|
||||
b := E.NewBuilder("errors in label %s", key)
|
||||
defer b.To(&res)
|
||||
|
||||
refErr := E.NewBuilder("errors in alias references")
|
||||
replaceIndexRef := func(ref string) string {
|
||||
index, err := strconv.Atoi(ref[1:])
|
||||
if err != nil {
|
||||
refErr.Add(E.Invalid("integer", ref))
|
||||
return ref
|
||||
}
|
||||
if index < 1 || index > len(container.Aliases) {
|
||||
refErr.Add(E.OutOfRange("index", ref))
|
||||
return ref
|
||||
}
|
||||
return container.Aliases[index-1]
|
||||
}
|
||||
|
||||
lbl, err := D.ParseLabel(key, val)
|
||||
if err != nil {
|
||||
b.Add(err.Subject(key))
|
||||
}
|
||||
if lbl.Namespace != D.NSProxy {
|
||||
return
|
||||
}
|
||||
if lbl.Target == D.WildcardAlias {
|
||||
// apply label for all aliases
|
||||
entries.RangeAll(func(a string, e *types.RawEntry) {
|
||||
if err = D.ApplyLabel(e, lbl); err != nil {
|
||||
b.Add(err)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
lbl.Target = AliasRefRegex.ReplaceAllStringFunc(lbl.Target, replaceIndexRef)
|
||||
lbl.Target = AliasRefRegexOld.ReplaceAllStringFunc(lbl.Target, func(s string) string {
|
||||
logrus.Warnf("%q should now be %q, old syntax will be removed in a future version", lbl, strings.ReplaceAll(lbl.String(), "$", "#"))
|
||||
return replaceIndexRef(s)
|
||||
})
|
||||
if refErr.HasError() {
|
||||
b.Add(refErr.Build())
|
||||
return
|
||||
}
|
||||
config, ok := entries.Load(lbl.Target)
|
||||
if !ok {
|
||||
b.Add(E.NotExist("alias", lbl.Target))
|
||||
return
|
||||
}
|
||||
if err = D.ApplyLabel(config, lbl); err != nil {
|
||||
b.Add(err)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -1,370 +0,0 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/network"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
D "github.com/yusing/go-proxy/internal/docker"
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
P "github.com/yusing/go-proxy/internal/proxy"
|
||||
T "github.com/yusing/go-proxy/internal/proxy/fields"
|
||||
. "github.com/yusing/go-proxy/internal/utils/testing"
|
||||
)
|
||||
|
||||
var (
|
||||
dummyNames = []string{"/a"}
|
||||
p DockerProvider
|
||||
)
|
||||
|
||||
func TestApplyLabelWildcard(t *testing.T) {
|
||||
pathPatterns := `
|
||||
- /
|
||||
- POST /upload/{$}
|
||||
- GET /static
|
||||
`[1:]
|
||||
pathPatternsExpect := []string{
|
||||
"/",
|
||||
"POST /upload/{$}",
|
||||
"GET /static",
|
||||
}
|
||||
middlewaresExpect := D.NestedLabelMap{
|
||||
"middleware1": {
|
||||
"prop1": "value1",
|
||||
"prop2": "value2",
|
||||
},
|
||||
"middleware2": {
|
||||
"prop3": "value3",
|
||||
"prop4": "value4",
|
||||
},
|
||||
}
|
||||
var p DockerProvider
|
||||
entries, err := p.entriesFromContainerLabels(D.FromDocker(&types.Container{
|
||||
Names: dummyNames,
|
||||
Labels: map[string]string{
|
||||
D.LabelAliases: "a,b",
|
||||
D.LabelIdleTimeout: common.IdleTimeoutDefault,
|
||||
D.LabelStopMethod: common.StopMethodDefault,
|
||||
D.LabelStopSignal: "SIGTERM",
|
||||
D.LabelStopTimeout: common.StopTimeoutDefault,
|
||||
D.LabelWakeTimeout: common.WakeTimeoutDefault,
|
||||
"proxy.*.no_tls_verify": "true",
|
||||
"proxy.*.scheme": "https",
|
||||
"proxy.*.host": "app",
|
||||
"proxy.*.port": "4567",
|
||||
"proxy.a.no_tls_verify": "true",
|
||||
"proxy.a.path_patterns": pathPatterns,
|
||||
"proxy.a.middlewares.middleware1.prop1": "value1",
|
||||
"proxy.a.middlewares.middleware1.prop2": "value2",
|
||||
"proxy.a.middlewares.middleware2.prop3": "value3",
|
||||
"proxy.a.middlewares.middleware2.prop4": "value4",
|
||||
},
|
||||
}, ""))
|
||||
ExpectNoError(t, err.Error())
|
||||
|
||||
a, ok := entries.Load("a")
|
||||
ExpectTrue(t, ok)
|
||||
b, ok := entries.Load("b")
|
||||
ExpectTrue(t, ok)
|
||||
|
||||
ExpectEqual(t, a.Scheme, "https")
|
||||
ExpectEqual(t, b.Scheme, "https")
|
||||
|
||||
ExpectEqual(t, a.Host, "app")
|
||||
ExpectEqual(t, b.Host, "app")
|
||||
|
||||
ExpectEqual(t, a.Port, "4567")
|
||||
ExpectEqual(t, b.Port, "4567")
|
||||
|
||||
ExpectTrue(t, a.NoTLSVerify)
|
||||
ExpectTrue(t, b.NoTLSVerify)
|
||||
|
||||
ExpectDeepEqual(t, a.PathPatterns, pathPatternsExpect)
|
||||
ExpectEqual(t, len(b.PathPatterns), 0)
|
||||
|
||||
ExpectDeepEqual(t, a.Middlewares, middlewaresExpect)
|
||||
ExpectEqual(t, len(b.Middlewares), 0)
|
||||
|
||||
ExpectEqual(t, a.Container.IdleTimeout, common.IdleTimeoutDefault)
|
||||
ExpectEqual(t, b.Container.IdleTimeout, common.IdleTimeoutDefault)
|
||||
|
||||
ExpectEqual(t, a.Container.StopTimeout, common.StopTimeoutDefault)
|
||||
ExpectEqual(t, b.Container.StopTimeout, common.StopTimeoutDefault)
|
||||
|
||||
ExpectEqual(t, a.Container.StopMethod, common.StopMethodDefault)
|
||||
ExpectEqual(t, b.Container.StopMethod, common.StopMethodDefault)
|
||||
|
||||
ExpectEqual(t, a.Container.WakeTimeout, common.WakeTimeoutDefault)
|
||||
ExpectEqual(t, b.Container.WakeTimeout, common.WakeTimeoutDefault)
|
||||
|
||||
ExpectEqual(t, a.Container.StopSignal, "SIGTERM")
|
||||
ExpectEqual(t, b.Container.StopSignal, "SIGTERM")
|
||||
}
|
||||
|
||||
func TestApplyLabelWithAlias(t *testing.T) {
|
||||
entries, err := p.entriesFromContainerLabels(D.FromDocker(&types.Container{
|
||||
Names: dummyNames,
|
||||
Labels: map[string]string{
|
||||
D.LabelAliases: "a,b,c",
|
||||
"proxy.a.no_tls_verify": "true",
|
||||
"proxy.a.port": "3333",
|
||||
"proxy.b.port": "1234",
|
||||
"proxy.c.scheme": "https",
|
||||
},
|
||||
}, ""))
|
||||
a, ok := entries.Load("a")
|
||||
ExpectTrue(t, ok)
|
||||
b, ok := entries.Load("b")
|
||||
ExpectTrue(t, ok)
|
||||
c, ok := entries.Load("c")
|
||||
ExpectTrue(t, ok)
|
||||
|
||||
ExpectNoError(t, err.Error())
|
||||
ExpectEqual(t, a.Scheme, "http")
|
||||
ExpectEqual(t, a.Port, "3333")
|
||||
ExpectEqual(t, a.NoTLSVerify, true)
|
||||
ExpectEqual(t, b.Scheme, "http")
|
||||
ExpectEqual(t, b.Port, "1234")
|
||||
ExpectEqual(t, c.Scheme, "https")
|
||||
}
|
||||
|
||||
func TestApplyLabelWithRef(t *testing.T) {
|
||||
entries := Must(p.entriesFromContainerLabels(D.FromDocker(&types.Container{
|
||||
Names: dummyNames,
|
||||
Labels: map[string]string{
|
||||
D.LabelAliases: "a,b,c",
|
||||
"proxy.#1.host": "localhost",
|
||||
"proxy.#1.port": "4444",
|
||||
"proxy.#2.port": "9999",
|
||||
"proxy.#3.port": "1111",
|
||||
"proxy.#3.scheme": "https",
|
||||
},
|
||||
}, "")))
|
||||
a, ok := entries.Load("a")
|
||||
ExpectTrue(t, ok)
|
||||
b, ok := entries.Load("b")
|
||||
ExpectTrue(t, ok)
|
||||
c, ok := entries.Load("c")
|
||||
ExpectTrue(t, ok)
|
||||
|
||||
ExpectEqual(t, a.Scheme, "http")
|
||||
ExpectEqual(t, a.Host, "localhost")
|
||||
ExpectEqual(t, a.Port, "4444")
|
||||
ExpectEqual(t, b.Port, "9999")
|
||||
ExpectEqual(t, c.Scheme, "https")
|
||||
ExpectEqual(t, c.Port, "1111")
|
||||
}
|
||||
|
||||
func TestApplyLabelWithRefIndexError(t *testing.T) {
|
||||
c := D.FromDocker(&types.Container{
|
||||
Names: dummyNames,
|
||||
Labels: map[string]string{
|
||||
D.LabelAliases: "a,b",
|
||||
"proxy.#1.host": "localhost",
|
||||
"proxy.#4.scheme": "https",
|
||||
},
|
||||
}, "")
|
||||
_, err := p.entriesFromContainerLabels(c)
|
||||
ExpectError(t, E.ErrOutOfRange, err.Error())
|
||||
ExpectTrue(t, strings.Contains(err.String(), "index out of range"))
|
||||
|
||||
_, err = p.entriesFromContainerLabels(D.FromDocker(&types.Container{
|
||||
Names: dummyNames,
|
||||
Labels: map[string]string{
|
||||
D.LabelAliases: "a,b",
|
||||
"proxy.#0.host": "localhost",
|
||||
},
|
||||
}, ""))
|
||||
ExpectError(t, E.ErrOutOfRange, err.Error())
|
||||
ExpectTrue(t, strings.Contains(err.String(), "index out of range"))
|
||||
}
|
||||
|
||||
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.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.Container.PublicIP, "1.2.3.4")
|
||||
ExpectEqual(t, raw.Host, raw.Container.PublicIP)
|
||||
}
|
||||
|
||||
func TestPrivateIPLocalhost(t *testing.T) {
|
||||
c := D.FromDocker(&types.Container{
|
||||
Names: dummyNames,
|
||||
NetworkSettings: &types.SummaryNetworkSettings{
|
||||
Networks: map[string]*network.EndpointSettings{
|
||||
"network": {
|
||||
IPAddress: "172.17.0.123",
|
||||
},
|
||||
},
|
||||
},
|
||||
}, client.DefaultDockerHost)
|
||||
raw, ok := Must(p.entriesFromContainerLabels(c)).Load("a")
|
||||
ExpectTrue(t, ok)
|
||||
ExpectEqual(t, raw.Container.PrivateIP, "172.17.0.123")
|
||||
ExpectEqual(t, raw.Host, raw.Container.PrivateIP)
|
||||
}
|
||||
|
||||
func TestPrivateIPRemote(t *testing.T) {
|
||||
c := D.FromDocker(&types.Container{
|
||||
Names: dummyNames,
|
||||
NetworkSettings: &types.SummaryNetworkSettings{
|
||||
Networks: map[string]*network.EndpointSettings{
|
||||
"network": {
|
||||
IPAddress: "172.17.0.123",
|
||||
},
|
||||
},
|
||||
},
|
||||
}, "tcp://1.2.3.4:2375")
|
||||
raw, ok := Must(p.entriesFromContainerLabels(c)).Load("a")
|
||||
ExpectTrue(t, ok)
|
||||
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) {
|
||||
privPort := uint16(1234)
|
||||
pubPort := uint16(4567)
|
||||
privIP := "172.17.0.123"
|
||||
cont := &types.Container{
|
||||
Names: []string{"a"},
|
||||
NetworkSettings: &types.SummaryNetworkSettings{
|
||||
Networks: map[string]*network.EndpointSettings{
|
||||
"network": {
|
||||
IPAddress: privIP,
|
||||
},
|
||||
},
|
||||
},
|
||||
Ports: []types.Port{
|
||||
{Type: "udp", PrivatePort: privPort, PublicPort: pubPort},
|
||||
},
|
||||
}
|
||||
|
||||
t.Run("local", func(t *testing.T) {
|
||||
c := D.FromDocker(cont, client.DefaultDockerHost)
|
||||
|
||||
raw, ok := Must(p.entriesFromContainerLabels(c)).Load("a")
|
||||
ExpectTrue(t, ok)
|
||||
entry := Must(P.ValidateEntry(raw))
|
||||
|
||||
a := ExpectType[*P.StreamEntry](t, entry)
|
||||
ExpectEqual(t, a.Scheme.ListeningScheme, T.Scheme("udp"))
|
||||
ExpectEqual(t, a.Scheme.ProxyScheme, T.Scheme("udp"))
|
||||
ExpectEqual(t, a.Host, T.Host(privIP))
|
||||
ExpectEqual(t, a.Port.ListeningPort, 0)
|
||||
ExpectEqual(t, a.Port.ProxyPort, T.Port(privPort))
|
||||
})
|
||||
|
||||
t.Run("remote", func(t *testing.T) {
|
||||
c := D.FromDocker(cont, "tcp://1.2.3.4:2375")
|
||||
raw, ok := Must(p.entriesFromContainerLabels(c)).Load("a")
|
||||
ExpectTrue(t, ok)
|
||||
entry := Must(P.ValidateEntry(raw))
|
||||
|
||||
a := ExpectType[*P.StreamEntry](t, entry)
|
||||
ExpectEqual(t, a.Scheme.ListeningScheme, T.Scheme("udp"))
|
||||
ExpectEqual(t, a.Scheme.ProxyScheme, T.Scheme("udp"))
|
||||
ExpectEqual(t, a.Host, "1.2.3.4")
|
||||
ExpectEqual(t, a.Port.ListeningPort, 0)
|
||||
ExpectEqual(t, a.Port.ProxyPort, T.Port(pubPort))
|
||||
})
|
||||
}
|
||||
|
||||
func TestExplicitExclude(t *testing.T) {
|
||||
_, ok := Must(p.entriesFromContainerLabels(D.FromDocker(&types.Container{
|
||||
Names: dummyNames,
|
||||
Labels: map[string]string{
|
||||
D.LabelAliases: "a",
|
||||
D.LabelExclude: "true",
|
||||
"proxy.a.no_tls_verify": "true",
|
||||
},
|
||||
}, ""))).Load("a")
|
||||
ExpectFalse(t, ok)
|
||||
}
|
||||
|
||||
func TestImplicitExcludeDatabase(t *testing.T) {
|
||||
t.Run("mount path detection", func(t *testing.T) {
|
||||
_, ok := Must(p.entriesFromContainerLabels(D.FromDocker(&types.Container{
|
||||
Names: dummyNames,
|
||||
Mounts: []types.MountPoint{
|
||||
{Source: "/data", Destination: "/var/lib/postgresql/data"},
|
||||
},
|
||||
}, ""))).Load("a")
|
||||
ExpectFalse(t, ok)
|
||||
})
|
||||
t.Run("exposed port detection", func(t *testing.T) {
|
||||
_, ok := Must(p.entriesFromContainerLabels(D.FromDocker(&types.Container{
|
||||
Names: dummyNames,
|
||||
Ports: []types.Port{
|
||||
{Type: "tcp", PrivatePort: 5432, PublicPort: 5432},
|
||||
},
|
||||
}, ""))).Load("a")
|
||||
ExpectFalse(t, ok)
|
||||
})
|
||||
}
|
||||
|
||||
// func TestImplicitExcludeNoExposedPort(t *testing.T) {
|
||||
// var p DockerProvider
|
||||
// entries, err := p.entriesFromContainerLabels(D.FromDocker(&types.Container{
|
||||
// Image: "redis",
|
||||
// Names: []string{"redis"},
|
||||
// Ports: []types.Port{
|
||||
// {Type: "tcp", PrivatePort: 6379, PublicPort: 0}, // not exposed
|
||||
// },
|
||||
// State: "running",
|
||||
// }, ""))
|
||||
// ExpectNoError(t, err.Error())
|
||||
|
||||
// _, ok := entries.Load("redis")
|
||||
// ExpectFalse(t, ok)
|
||||
// }
|
||||
|
||||
// func TestNotExcludeSpecifiedPort(t *testing.T) {
|
||||
// var p DockerProvider
|
||||
// entries, err := p.entriesFromContainerLabels(D.FromDocker(&types.Container{
|
||||
// Image: "redis",
|
||||
// Names: []string{"redis"},
|
||||
// Ports: []types.Port{
|
||||
// {Type: "tcp", PrivatePort: 6379, PublicPort: 0}, // not exposed
|
||||
// },
|
||||
// Labels: map[string]string{
|
||||
// "proxy.redis.port": "6379:6379", // but specified in label
|
||||
// },
|
||||
// }, ""))
|
||||
// ExpectNoError(t, err.Error())
|
||||
|
||||
// _, ok := entries.Load("redis")
|
||||
// ExpectTrue(t, ok)
|
||||
// }
|
||||
|
||||
// func TestNotExcludeNonExposedPortHostNetwork(t *testing.T) {
|
||||
// var p DockerProvider
|
||||
// cont := &types.Container{
|
||||
// Image: "redis",
|
||||
// Names: []string{"redis"},
|
||||
// Ports: []types.Port{
|
||||
// {Type: "tcp", PrivatePort: 6379, PublicPort: 0}, // not exposed
|
||||
// },
|
||||
// Labels: map[string]string{
|
||||
// "proxy.redis.port": "6379:6379",
|
||||
// },
|
||||
// }
|
||||
// cont.HostConfig.NetworkMode = "host"
|
||||
|
||||
// entries, err := p.entriesFromContainerLabels(D.FromDocker(cont, ""))
|
||||
// ExpectNoError(t, err.Error())
|
||||
|
||||
// _, ok := entries.Load("redis")
|
||||
// ExpectTrue(t, ok)
|
||||
// }
|
||||
@@ -1,94 +0,0 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
R "github.com/yusing/go-proxy/internal/route"
|
||||
"github.com/yusing/go-proxy/internal/types"
|
||||
U "github.com/yusing/go-proxy/internal/utils"
|
||||
W "github.com/yusing/go-proxy/internal/watcher"
|
||||
)
|
||||
|
||||
type FileProvider struct {
|
||||
fileName string
|
||||
path string
|
||||
}
|
||||
|
||||
func FileProviderImpl(filename string) (ProviderImpl, E.NestedError) {
|
||||
impl := &FileProvider{
|
||||
fileName: filename,
|
||||
path: path.Join(common.ConfigBasePath, filename),
|
||||
}
|
||||
_, err := os.Stat(impl.path)
|
||||
switch {
|
||||
case err == nil:
|
||||
return impl, nil
|
||||
case errors.Is(err, os.ErrNotExist):
|
||||
return nil, E.NotExist("file", impl.path)
|
||||
default:
|
||||
return nil, E.UnexpectedError(err)
|
||||
}
|
||||
}
|
||||
|
||||
func Validate(data []byte) E.NestedError {
|
||||
return U.ValidateYaml(U.GetSchema(common.FileProviderSchemaPath), data)
|
||||
}
|
||||
|
||||
func (p FileProvider) String() string {
|
||||
return p.fileName
|
||||
}
|
||||
|
||||
func (p FileProvider) OnEvent(event W.Event, routes R.Routes) (res EventResult) {
|
||||
b := E.NewBuilder("event %s error", event)
|
||||
defer b.To(&res.err)
|
||||
|
||||
newRoutes, err := p.LoadRoutesImpl()
|
||||
if err != nil {
|
||||
b.Add(err)
|
||||
return
|
||||
}
|
||||
|
||||
res.nRemoved = newRoutes.Size()
|
||||
routes.RangeAllParallel(func(_ string, v *R.Route) {
|
||||
b.Add(v.Stop())
|
||||
})
|
||||
routes.Clear()
|
||||
|
||||
newRoutes.RangeAllParallel(func(_ string, v *R.Route) {
|
||||
b.Add(v.Start())
|
||||
})
|
||||
res.nAdded = newRoutes.Size()
|
||||
|
||||
routes.MergeFrom(newRoutes)
|
||||
return
|
||||
}
|
||||
|
||||
func (p *FileProvider) LoadRoutesImpl() (routes R.Routes, res E.NestedError) {
|
||||
routes = R.NewRoutes()
|
||||
|
||||
b := E.NewBuilder("file %q validation failure", p.fileName)
|
||||
defer b.To(&res)
|
||||
|
||||
entries := types.NewProxyEntries()
|
||||
|
||||
data, err := E.Check(os.ReadFile(p.path))
|
||||
if err != nil {
|
||||
b.Add(E.FailWith("read file", err))
|
||||
return
|
||||
}
|
||||
|
||||
if err = entries.UnmarshalFromYAML(data); err != nil {
|
||||
b.Add(err)
|
||||
return
|
||||
}
|
||||
|
||||
return R.FromEntries(entries)
|
||||
}
|
||||
|
||||
func (p *FileProvider) NewWatcher() W.Watcher {
|
||||
return W.NewConfigFileWatcher(p.fileName)
|
||||
}
|
||||
@@ -1,209 +0,0 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"context"
|
||||
"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"
|
||||
)
|
||||
|
||||
type (
|
||||
Provider struct {
|
||||
ProviderImpl `json:"-"`
|
||||
|
||||
name string
|
||||
t ProviderType
|
||||
routes R.Routes
|
||||
|
||||
watcher W.Watcher
|
||||
watcherTask common.Task
|
||||
watcherCancel context.CancelFunc
|
||||
|
||||
l *logrus.Entry
|
||||
}
|
||||
ProviderImpl interface {
|
||||
NewWatcher() W.Watcher
|
||||
// even returns error, routes must be non-nil
|
||||
LoadRoutesImpl() (R.Routes, E.NestedError)
|
||||
OnEvent(event W.Event, routes R.Routes) EventResult
|
||||
String() string
|
||||
}
|
||||
ProviderType string
|
||||
ProviderStats struct {
|
||||
NumRPs int `json:"num_reverse_proxies"`
|
||||
NumStreams int `json:"num_streams"`
|
||||
Type ProviderType `json:"type"`
|
||||
}
|
||||
EventResult struct {
|
||||
nAdded int
|
||||
nRemoved int
|
||||
nReloaded int
|
||||
err E.NestedError
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
ProviderTypeDocker ProviderType = "docker"
|
||||
ProviderTypeFile ProviderType = "file"
|
||||
)
|
||||
|
||||
func newProvider(name string, t ProviderType) *Provider {
|
||||
p := &Provider{
|
||||
name: name,
|
||||
t: t,
|
||||
routes: R.NewRoutes(),
|
||||
}
|
||||
p.l = logrus.WithField("provider", p)
|
||||
return p
|
||||
}
|
||||
|
||||
func NewFileProvider(filename string) (p *Provider, err E.NestedError) {
|
||||
name := path.Base(filename)
|
||||
if name == "" {
|
||||
return nil, E.Invalid("file name", "empty")
|
||||
}
|
||||
p = newProvider(name, ProviderTypeFile)
|
||||
p.ProviderImpl, err = FileProviderImpl(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p.watcher = p.NewWatcher()
|
||||
return
|
||||
}
|
||||
|
||||
func NewDockerProvider(name string, dockerHost string) (p *Provider, err E.NestedError) {
|
||||
if name == "" {
|
||||
return nil, E.Invalid("provider name", "empty")
|
||||
}
|
||||
|
||||
p = newProvider(name, ProviderTypeDocker)
|
||||
p.ProviderImpl, err = DockerProviderImpl(name, dockerHost, p.IsExplicitOnly())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p.watcher = p.NewWatcher()
|
||||
return
|
||||
}
|
||||
|
||||
func (p *Provider) IsExplicitOnly() bool {
|
||||
return p.name[len(p.name)-1] == '!'
|
||||
}
|
||||
|
||||
func (p *Provider) GetName() string {
|
||||
return p.name
|
||||
}
|
||||
|
||||
func (p *Provider) GetType() ProviderType {
|
||||
return p.t
|
||||
}
|
||||
|
||||
// to work with json marshaller.
|
||||
func (p *Provider) MarshalText() ([]byte, error) {
|
||||
return []byte(p.String()), nil
|
||||
}
|
||||
|
||||
func (p *Provider) StartAllRoutes() (res E.NestedError) {
|
||||
errors := E.NewBuilder("errors starting routes")
|
||||
defer errors.To(&res)
|
||||
|
||||
// start watcher no matter load success or not
|
||||
go p.watchEvents()
|
||||
|
||||
p.routes.RangeAllParallel(func(alias string, r *R.Route) {
|
||||
errors.Add(r.Start().Subject(r))
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func (p *Provider) StopAllRoutes() (res E.NestedError) {
|
||||
if p.watcherCancel != nil {
|
||||
p.watcherCancel()
|
||||
p.watcherCancel = nil
|
||||
}
|
||||
|
||||
errors := E.NewBuilder("errors stopping routes")
|
||||
defer errors.To(&res)
|
||||
|
||||
p.routes.RangeAllParallel(func(alias string, r *R.Route) {
|
||||
errors.Add(r.Stop().Subject(r))
|
||||
})
|
||||
p.routes.Clear()
|
||||
return
|
||||
}
|
||||
|
||||
func (p *Provider) RangeRoutes(do func(string, *R.Route)) {
|
||||
p.routes.RangeAll(do)
|
||||
}
|
||||
|
||||
func (p *Provider) GetRoute(alias string) (*R.Route, bool) {
|
||||
return p.routes.Load(alias)
|
||||
}
|
||||
|
||||
func (p *Provider) LoadRoutes() E.NestedError {
|
||||
var err E.NestedError
|
||||
p.routes, err = p.LoadRoutesImpl()
|
||||
if p.routes.Size() > 0 {
|
||||
p.l.Infof("loaded %d routes", p.routes.Size())
|
||||
return err
|
||||
}
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
return E.FailWith("loading routes", err)
|
||||
}
|
||||
|
||||
func (p *Provider) Statistics() ProviderStats {
|
||||
numRPs := 0
|
||||
numStreams := 0
|
||||
p.routes.RangeAll(func(_ string, r *R.Route) {
|
||||
if !r.Started() {
|
||||
return
|
||||
}
|
||||
switch r.Type {
|
||||
case R.RouteTypeReverseProxy:
|
||||
numRPs++
|
||||
case R.RouteTypeStream:
|
||||
numStreams++
|
||||
}
|
||||
})
|
||||
return ProviderStats{
|
||||
NumRPs: numRPs,
|
||||
NumStreams: numStreams,
|
||||
Type: p.t,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Provider) watchEvents() {
|
||||
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.watcherTask.Context().Done():
|
||||
return
|
||||
case event := <-events:
|
||||
task := p.watcherTask.Subtask("%s event %s", event.Type, event)
|
||||
l.Infof("%s event %q", event.Type, event)
|
||||
res := p.OnEvent(event, p.routes)
|
||||
task.Finished()
|
||||
if res.nAdded+res.nRemoved+res.nReloaded > 0 {
|
||||
l.Infof("| %d NEW | %d REMOVED | %d RELOADED |", res.nAdded, res.nRemoved, res.nReloaded)
|
||||
}
|
||||
if res.err != nil {
|
||||
l.Error(res.err)
|
||||
}
|
||||
case err := <-errs:
|
||||
if err == nil || err.Is(context.Canceled) {
|
||||
continue
|
||||
}
|
||||
l.Errorf("watcher error: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user