implement godoxy-agent

This commit is contained in:
yusing
2025-02-10 09:36:37 +08:00
parent ecb89f80a0
commit eaf191e350
57 changed files with 1479 additions and 467 deletions

View File

@@ -10,6 +10,7 @@ import (
"github.com/yusing/go-proxy/internal/common"
config "github.com/yusing/go-proxy/internal/config/types"
"github.com/yusing/go-proxy/internal/logging"
"github.com/yusing/go-proxy/internal/logging/memlogger"
"github.com/yusing/go-proxy/internal/utils/strutils"
)
@@ -35,7 +36,7 @@ func NewHandler(cfg config.ConfigInstance) http.Handler {
mux.HandleFunc("GET", "/v1/stats", useCfg(cfg, v1.Stats))
mux.HandleFunc("GET", "/v1/stats/ws", useCfg(cfg, v1.StatsWS))
mux.HandleFunc("GET", "/v1/health/ws", auth.RequireAuth(useCfg(cfg, v1.HealthWS)))
mux.HandleFunc("GET", "/v1/logs/ws", auth.RequireAuth(useCfg(cfg, v1.LogsWS())))
mux.HandleFunc("GET", "/v1/logs/ws", auth.RequireAuth(memlogger.LogsWS(cfg)))
mux.HandleFunc("GET", "/v1/favicon", auth.RequireAuth(favicon.GetFavIcon))
mux.HandleFunc("POST", "/v1/homepage/set", auth.RequireAuth(v1.SetHomePageOverrides))

View File

@@ -9,7 +9,6 @@ import (
func reqLogger(r *http.Request, level zerolog.Level) *zerolog.Event {
return logging.WithLevel(level).
Str("module", "api").
Str("remote", r.RemoteAddr).
Str("host", r.Host).
Str("uri", r.Method+" "+r.RequestURI)

View File

@@ -22,7 +22,7 @@ func InitiateWS(cfg config.ConfigInstance, w http.ResponseWriter, r *http.Reques
localAddresses := []string{"127.0.0.1", "10.0.*.*", "172.16.*.*", "192.168.*.*"}
if len(cfg.Value().MatchDomains) == 0 {
if cfg == nil || len(cfg.Value().MatchDomains) == 0 {
warnNoMatchDomainOnce.Do(warnNoMatchDomains)
originPats = []string{"*"}
} else {

View File

@@ -1,18 +1,9 @@
package common
import (
"flag"
"fmt"
"log"
)
type Args struct {
Command string
}
const (
CommandStart = ""
CommandSetup = "setup"
CommandNewAgent = "new-agent"
CommandValidate = "validate"
CommandListConfigs = "ls-config"
CommandListRoutes = "ls-routes"
@@ -23,34 +14,22 @@ const (
CommandDebugListMTrace = "debug-ls-mtrace"
)
var ValidCommands = []string{
CommandStart,
CommandSetup,
CommandValidate,
CommandListConfigs,
CommandListRoutes,
CommandListIcons,
CommandReload,
CommandDebugListEntries,
CommandDebugListProviders,
CommandDebugListMTrace,
}
type MainServerCommandValidator struct{}
func GetArgs() Args {
var args Args
flag.Parse()
args.Command = flag.Arg(0)
if err := validateArg(args.Command); err != nil {
log.Fatalf("invalid command: %s", err)
func (v MainServerCommandValidator) IsCommandValid(cmd string) bool {
switch cmd {
case CommandStart,
CommandSetup,
CommandNewAgent,
CommandValidate,
CommandListConfigs,
CommandListRoutes,
CommandListIcons,
CommandReload,
CommandDebugListEntries,
CommandDebugListProviders,
CommandDebugListMTrace:
return true
}
return args
}
func validateArg(arg string) error {
for _, v := range ValidCommands {
if arg == v {
return nil
}
}
return fmt.Errorf("invalid command %q", arg)
return false
}

View File

@@ -30,6 +30,8 @@ const (
ComposeExampleFileName = "compose.example.yml"
ErrorPagesBasePath = "error_pages"
AgentCertsBasePath = "certs"
)
var RequiredDirectories = []string{

View File

@@ -86,6 +86,10 @@ func GetEnvBool(key string, defaultValue bool) bool {
return GetEnv(key, defaultValue, strconv.ParseBool)
}
func GetEnvInt(key string, defaultValue int) int {
return GetEnv(key, defaultValue, strconv.Atoi)
}
func GetAddrEnv(key, defaultValue, scheme string) (addr, host, port, fullURL string) {
addr = GetEnvString(key, defaultValue)
if addr == "" {

View File

@@ -9,7 +9,7 @@ var (
"3000": true,
}
ServiceNamePortMapTCP = map[string]int{
ImageNamePortMapTCP = map[string]int{
"mssql": 1433,
"mysql": 3306,
"mariadb": 3306,
@@ -19,27 +19,9 @@ var (
"memcached": 11211,
"mongo": 27017,
"minecraft-server": 25565,
"ssh": 22,
"ftp": 21,
"smtp": 25,
"dns": 53,
"pop3": 110,
"imap": 143,
}
ImageNamePortMap = func() (m map[string]int) {
m = make(map[string]int, len(ServiceNamePortMapTCP)+len(imageNamePortMap))
for k, v := range ServiceNamePortMapTCP {
m[k] = v
}
for k, v := range imageNamePortMap {
m[k] = v
}
return
}()
imageNamePortMap = map[string]int{
ImageNamePortMapHTTP = map[string]int{
"adguardhome": 3000,
"bazarr": 6767,
"calibre-web": 8083,

View File

@@ -287,6 +287,9 @@ func (cfg *Config) loadRouteProviders(providers *types.Providers) E.Error {
lenLongestName = len(p.String())
}
}
for _, agent := range providers.Agents {
cfg.providers.Store(agent.Name(), proxy.NewAgentProvider(&agent))
}
cfg.providers.RangeAllParallel(func(_ string, p *proxy.Provider) {
if err := p.LoadRoutes(); err != nil {
errs.Add(err.Subject(p.String()))

View File

@@ -5,6 +5,7 @@ import (
"regexp"
"github.com/go-playground/validator/v10"
"github.com/yusing/go-proxy/agent/pkg/agent"
"github.com/yusing/go-proxy/internal/autocert"
"github.com/yusing/go-proxy/internal/net/http/accesslog"
"github.com/yusing/go-proxy/internal/notif"
@@ -23,9 +24,10 @@ type (
TimeoutShutdown int `json:"timeout_shutdown" validate:"gte=0"`
}
Providers struct {
Files []string `json:"include" validate:"dive,filepath"`
Docker map[string]string `json:"docker" validate:"dive,unix_addr|url"`
Notification []notif.NotificationConfig `json:"notification"`
Files []string `json:"include" yaml:"include,omitempty" validate:"dive,filepath"`
Docker map[string]string `json:"docker" yaml:"docker,omitempty" validate:"dive,unix_addr|url"`
Agents []agent.AgentConfig `json:"agents" yaml:"agents,omitempty"`
Notification []notif.NotificationConfig `json:"notification" yaml:"notification,omitempty"`
}
Entrypoint struct {
Middlewares []map[string]any `json:"middlewares"`

View File

@@ -2,12 +2,14 @@ package docker
import (
"errors"
"fmt"
"net/http"
"sync"
"github.com/docker/cli/cli/connhelper"
"github.com/docker/docker/client"
"github.com/rs/zerolog"
"github.com/yusing/go-proxy/agent/pkg/agent"
"github.com/yusing/go-proxy/internal/common"
"github.com/yusing/go-proxy/internal/logging"
"github.com/yusing/go-proxy/internal/task"
@@ -81,32 +83,44 @@ func ConnectClient(host string) (*SharedClient, error) {
// create client
var opt []client.Opt
switch host {
case "":
return nil, errors.New("empty docker host")
case common.DockerHostFromEnv:
opt = clientOptEnvHost
default:
helper, err := connhelper.GetConnectionHelper(host)
if err != nil {
logging.Panic().Err(err).Msg("failed to get connection helper")
if agent.IsDockerHostAgent(host) {
cfg, ok := agent.GetAgentFromDockerHost(host)
if !ok {
return nil, fmt.Errorf("agent not found for host: %s", host)
}
if helper != nil {
httpClient := &http.Client{
Transport: &http.Transport{
DialContext: helper.Dialer,
},
opt = []client.Opt{
client.WithHost(agent.DockerHost),
client.WithHTTPClient(cfg.NewHTTPClient()),
client.WithAPIVersionNegotiation(),
}
} else {
switch host {
case "":
return nil, errors.New("empty docker host")
case common.DockerHostFromEnv:
opt = clientOptEnvHost
default:
helper, err := connhelper.GetConnectionHelper(host)
if err != nil {
logging.Panic().Err(err).Msg("failed to get connection helper")
}
opt = []client.Opt{
client.WithHTTPClient(httpClient),
client.WithHost(helper.Host),
client.WithAPIVersionNegotiation(),
client.WithDialContext(helper.Dialer),
}
} else {
opt = []client.Opt{
client.WithHost(host),
client.WithAPIVersionNegotiation(),
if helper != nil {
httpClient := &http.Client{
Transport: &http.Transport{
DialContext: helper.Dialer,
},
}
opt = []client.Opt{
client.WithHTTPClient(httpClient),
client.WithHost(helper.Host),
client.WithAPIVersionNegotiation(),
client.WithDialContext(helper.Dialer),
}
} else {
opt = []client.Opt{
client.WithHost(host),
client.WithAPIVersionNegotiation(),
}
}
}
}

View File

@@ -6,6 +6,7 @@ import (
"strings"
"github.com/docker/docker/api/types"
"github.com/yusing/go-proxy/agent/pkg/agent"
"github.com/yusing/go-proxy/internal/logging"
U "github.com/yusing/go-proxy/internal/utils"
"github.com/yusing/go-proxy/internal/utils/strutils"
@@ -21,13 +22,14 @@ type (
ContainerID string `json:"container_id"`
ImageName string `json:"image_name"`
Agent *agent.AgentConfig `json:"agent"`
Labels map[string]string `json:"-"`
PublicPortMapping PortMapping `json:"public_ports"` // non-zero publicPort:types.Port
PrivatePortMapping PortMapping `json:"private_ports"` // privatePort:types.Port
PublicIP string `json:"public_ip"`
PrivateIP string `json:"private_ip"`
NetworkMode string `json:"network_mode"`
PublicHostname string `json:"public_hostname"`
PrivateHostname string `json:"private_hostname"`
Aliases []string `json:"aliases"`
IsExcluded bool `json:"is_excluded"`
@@ -51,7 +53,8 @@ func FromDocker(c *types.Container, dockerHost string) (res *Container) {
for lbl := range c.Labels {
if strings.HasPrefix(lbl, NSProxy+".") {
isExplicit = true
break
} else {
delete(c.Labels, lbl)
}
}
res = &Container{
@@ -64,7 +67,6 @@ func FromDocker(c *types.Container, dockerHost string) (res *Container) {
PublicPortMapping: helper.getPublicPortMapping(),
PrivatePortMapping: helper.getPrivatePortMapping(),
NetworkMode: c.HostConfig.NetworkMode,
Aliases: helper.getAliases(),
IsExcluded: strutils.ParseBool(helper.getDeleteLabel(LabelExclude)),
@@ -78,8 +80,13 @@ func FromDocker(c *types.Container, dockerHost string) (res *Container) {
StartEndpoint: helper.getDeleteLabel(LabelStartEndpoint),
Running: c.Status == "running" || c.State == "running",
}
res.setPrivateIP(helper)
res.setPublicIP()
if agent.IsDockerHostAgent(dockerHost) {
res.Agent, _ = agent.GetAgentFromDockerHost(dockerHost)
}
res.setPrivateHostname(helper)
res.setPublicHostname()
return
}
@@ -115,29 +122,28 @@ func FromJSON(json types.ContainerJSON, dockerHost string) *Container {
Networks: json.NetworkSettings.Networks,
},
}, dockerHost)
cont.NetworkMode = string(json.HostConfig.NetworkMode)
return cont
}
func (c *Container) setPublicIP() {
func (c *Container) setPublicHostname() {
if !c.Running {
return
}
if strings.HasPrefix(c.DockerHost, "unix://") {
c.PublicIP = "127.0.0.1"
c.PublicHostname = "127.0.0.1"
return
}
url, err := url.Parse(c.DockerHost)
if err != nil {
logging.Err(err).Msgf("invalid docker host %q, falling back to 127.0.0.1", c.DockerHost)
c.PublicIP = "127.0.0.1"
c.PublicHostname = "127.0.0.1"
return
}
c.PublicIP = url.Hostname()
c.PublicHostname = url.Hostname()
}
func (c *Container) setPrivateIP(helper containerHelper) {
if !strings.HasPrefix(c.DockerHost, "unix://") {
func (c *Container) setPrivateHostname(helper containerHelper) {
if !strings.HasPrefix(c.DockerHost, "unix://") && c.Agent == nil {
return
}
if helper.NetworkSettings == nil {
@@ -147,7 +153,7 @@ func (c *Container) setPrivateIP(helper containerHelper) {
if v.IPAddress == "" {
continue
}
c.PrivateIP = v.IPAddress
c.PrivateHostname = v.IPAddress
return
}
}

View File

@@ -2,6 +2,7 @@ package err
import (
"github.com/rs/zerolog"
"github.com/yusing/go-proxy/internal/common"
"github.com/yusing/go-proxy/internal/logging"
)
@@ -14,6 +15,9 @@ func getLogger(logger ...*zerolog.Logger) *zerolog.Logger {
//go:inline
func LogFatal(msg string, err error, logger ...*zerolog.Logger) {
if common.IsDebug {
LogPanic(msg, err, logger...)
}
getLogger(logger...).Fatal().Msg(err.Error())
}

View File

@@ -66,9 +66,3 @@ func Collect[T any, Err error, Arg any, Func func(Arg) (T, Err)](eb *Builder, fn
eb.Add(err)
return result
}
func Collect2[T any, Err error, Arg1 any, Arg2 any, Func func(Arg1, Arg2) (T, Err)](eb *Builder, fn Func, arg1 Arg1, arg2 Arg2) T {
result, err := fn(arg1, arg2)
eb.Add(err)
return result
}

View File

@@ -1,159 +0,0 @@
package logging
import (
"errors"
"fmt"
"time"
"github.com/rs/zerolog"
"github.com/yusing/go-proxy/internal/common"
)
var levelHTMLFormats = [][]byte{
[]byte(` <span class="log-trace">TRC</span> `),
[]byte(` <span class="log-debug">DBG</span> `),
[]byte(` <span class="log-info">INF</span> `),
[]byte(` <span class="log-warn">WRN</span> `),
[]byte(` <span class="log-error">ERR</span> `),
[]byte(` <span class="log-fatal">FTL</span> `),
[]byte(` <span class="log-panic">PAN</span> `),
}
var colorToClass = map[string]string{
"1": "log-bold",
"3": "log-italic",
"4": "log-underline",
"30": "log-black",
"31": "log-red",
"32": "log-green",
"33": "log-yellow",
"34": "log-blue",
"35": "log-magenta",
"36": "log-cyan",
"37": "log-white",
"90": "log-bright-black",
"91": "log-red",
"92": "log-bright-green",
"93": "log-bright-yellow",
"94": "log-bright-blue",
"95": "log-bright-magenta",
"96": "log-bright-cyan",
"97": "log-bright-white",
}
// FormatMessageToHTMLBytes converts text with ANSI color codes to HTML with class names.
// ANSI codes are mapped to classes via a static map, and reset codes ([0m) close all spans.
// Time complexity is O(n) with minimal allocations.
func FormatMessageToHTMLBytes(msg string, buf []byte) ([]byte, error) {
buf = append(buf, "<span class=\"log-message\">"...)
var stack []string
lastPos := 0
for i := 0; i < len(msg); {
if msg[i] == '\x1b' && i+1 < len(msg) && msg[i+1] == '[' {
if lastPos < i {
escapeAndAppend(msg[lastPos:i], &buf)
}
i += 2 // Skip \x1b[
start := i
for ; i < len(msg) && msg[i] != 'm'; i++ {
if !isANSICodeChar(msg[i]) {
return nil, fmt.Errorf("invalid ANSI char: %c", msg[i])
}
}
if i >= len(msg) {
return nil, errors.New("unterminated ANSI sequence")
}
codeStr := msg[start:i]
i++ // Skip 'm'
lastPos = i
startPart := 0
for j := 0; j <= len(codeStr); j++ {
if j == len(codeStr) || codeStr[j] == ';' {
part := codeStr[startPart:j]
if part == "" {
return nil, errors.New("empty code part")
}
if part == "0" {
for range stack {
buf = append(buf, "</span>"...)
}
stack = stack[:0]
} else {
className, ok := colorToClass[part]
if !ok {
return nil, fmt.Errorf("invalid ANSI code: %s", part)
}
stack = append(stack, className)
buf = append(buf, `<span class="`...)
buf = append(buf, className...)
buf = append(buf, `">`...)
}
startPart = j + 1
}
}
} else {
i++
}
}
if lastPos < len(msg) {
escapeAndAppend(msg[lastPos:], &buf)
}
for range stack {
buf = append(buf, "</span>"...)
}
buf = append(buf, "</span>"...)
return buf, nil
}
func isANSICodeChar(c byte) bool {
return (c >= '0' && c <= '9') || c == ';'
}
func escapeAndAppend(s string, buf *[]byte) {
for i, r := range s {
switch r {
case '•':
*buf = append(*buf, "&middot;"...)
case '&':
*buf = append(*buf, "&amp;"...)
case '<':
*buf = append(*buf, "&lt;"...)
case '>':
*buf = append(*buf, "&gt;"...)
case '\t':
*buf = append(*buf, "&#9;"...)
case '\n':
*buf = append(*buf, "<br>"...)
*buf = append(*buf, prefixHTML...)
default:
*buf = append(*buf, s[i])
}
}
}
func timeNowHTML() []byte {
if !common.IsTest {
return []byte(time.Now().Format(timeFmt))
}
return []byte(time.Date(2024, 1, 1, 1, 1, 1, 1, time.UTC).Format(timeFmt))
}
func FormatLogEntryHTML(level zerolog.Level, message string, buf []byte) []byte {
buf = append(buf, []byte(`<pre class="log-entry">`)...)
buf = append(buf, timeNowHTML()...)
if level < zerolog.NoLevel {
buf = append(buf, levelHTMLFormats[level+1]...)
}
buf, _ = FormatMessageToHTMLBytes(message, buf)
buf = append(buf, []byte("</pre>")...)
return buf
}

View File

@@ -1,30 +0,0 @@
package logging
import (
"testing"
"github.com/rs/zerolog"
. "github.com/yusing/go-proxy/internal/utils/testing"
)
func TestFormatHTML(t *testing.T) {
buf := make([]byte, 0, 100)
buf = FormatLogEntryHTML(zerolog.InfoLevel, "This is a test.\nThis is a new line.", buf)
ExpectEqual(t, string(buf), `<pre class="log-entry">01-01 01:01 <span class="log-info">INF</span> <span class="log-message">This is a test.<br>`+prefix+`This is a new line.</span></pre>`)
}
func TestFormatHTMLANSI(t *testing.T) {
buf := make([]byte, 0, 100)
buf = FormatLogEntryHTML(zerolog.InfoLevel, "This is \x1b[91m\x1b[1ma test.\x1b[0mOK!.", buf)
ExpectEqual(t, string(buf), `<pre class="log-entry">01-01 01:01 <span class="log-info">INF</span> <span class="log-message">This is <span class="log-red"><span class="log-bold">a test.</span></span>OK!.</span></pre>`)
buf = buf[:0]
buf = FormatLogEntryHTML(zerolog.InfoLevel, "This is \x1b[91ma \x1b[1mtest.\x1b[0mOK!.", buf)
ExpectEqual(t, string(buf), `<pre class="log-entry">01-01 01:01 <span class="log-info">INF</span> <span class="log-message">This is <span class="log-red">a <span class="log-bold">test.</span></span>OK!.</span></pre>`)
}
func BenchmarkFormatLogEntryHTML(b *testing.B) {
buf := make([]byte, 0, 250)
for range b.N {
FormatLogEntryHTML(zerolog.InfoLevel, "This is \x1b[91ma \x1b[1mtest.\x1b[0mOK!.", buf)
}
}

View File

@@ -1,4 +1,4 @@
package v1
package memlogger
import (
"bytes"
@@ -9,7 +9,6 @@ import (
"time"
"github.com/coder/websocket"
"github.com/rs/zerolog"
"github.com/yusing/go-proxy/internal/api/v1/utils"
"github.com/yusing/go-proxy/internal/common"
config "github.com/yusing/go-proxy/internal/config/types"
@@ -31,11 +30,7 @@ type memLogger struct {
bufPool sync.Pool // used in hook mode
}
type MemLogger interface {
io.Writer
// TODO: hook does not pass in fields, looking for a workaround to do server side log rendering
zerolog.Hook
}
type MemLogger io.Writer
type buffer struct {
data []byte
@@ -85,8 +80,10 @@ func init() {
}
}
func LogsWS() func(config config.ConfigInstance, w http.ResponseWriter, r *http.Request) {
return memLoggerInstance.ServeHTTP
func LogsWS(config config.ConfigInstance) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
memLoggerInstance.ServeHTTP(config, w, r)
}
}
func GetMemLogger() MemLogger {
@@ -138,29 +135,6 @@ func (m *memLogger) writeBuf(b []byte) (pos int, err error) {
return
}
// Run implements zerolog.Hook.
func (m *memLogger) Run(e *zerolog.Event, level zerolog.Level, message string) {
bufStruct := m.bufPool.Get().(*buffer)
buf := bufStruct.data
defer func() {
bufStruct.data = bufStruct.data[:0]
m.bufPool.Put(bufStruct)
}()
buf = logging.FormatLogEntryHTML(level, message, buf)
n := len(buf)
m.truncateIfNeeded(n)
pos, err := m.writeBuf(buf)
if err != nil {
// not logging the error here, it will cause Run to be called again = infinite loop
return
}
m.notifyWS(pos, n)
}
// Write implements io.Writer.
func (m *memLogger) Write(p []byte) (n int, err error) {
n = len(p)

View File

@@ -17,6 +17,8 @@ type customErrorPage struct{}
var CustomErrorPage = NewMiddleware[customErrorPage]()
const StaticFilePathPrefix = "/$gperrorpage/"
// before implements RequestModifier.
func (customErrorPage) before(w http.ResponseWriter, r *http.Request) (proceed bool) {
return !ServeStaticErrorPageFile(w, r)
@@ -49,8 +51,8 @@ func ServeStaticErrorPageFile(w http.ResponseWriter, r *http.Request) (served bo
if path != "" && path[0] != '/' {
path = "/" + path
}
if strings.HasPrefix(path, gphttp.StaticFilePathPrefix) {
filename := path[len(gphttp.StaticFilePathPrefix):]
if strings.HasPrefix(path, StaticFilePathPrefix) {
filename := path[len(StaticFilePathPrefix):]
file, ok := errorpage.GetStaticFile(filename)
if !ok {
logging.Error().Msg("unable to load resource " + filename)

View File

@@ -6,6 +6,7 @@ import (
"errors"
"io"
"log"
"net"
"net/http"
"time"
@@ -45,7 +46,7 @@ func StartServer(parent task.Parent, opt Options) (s *Server) {
func NewServer(opt Options) (s *Server) {
var httpSer, httpsSer *http.Server
logger := logging.With().Str("module", "server").Str("name", opt.Name).Logger()
logger := logging.With().Str("server", opt.Name).Logger()
certAvailable := false
if opt.CertProvider != nil {
@@ -55,7 +56,7 @@ func NewServer(opt Options) (s *Server) {
out := io.Discard
if common.IsDebug {
out = logging.GetLogger()
out = logger
}
if opt.HTTPAddr != "" {
@@ -107,7 +108,13 @@ func (s *Server) Start(parent task.Parent) {
if s.https != nil {
go func() {
s.handleErr("https", s.https.ListenAndServeTLS(s.CertProvider.GetCertPath(), s.CertProvider.GetKeyPath()))
l, err := net.Listen("tcp", s.https.Addr)
if err != nil {
s.handleErr("https", err)
return
}
defer l.Close()
s.handleErr("https", s.https.Serve(tls.NewListener(l, s.https.TLSConfig)))
}()
s.httpsStarted = true
s.l.Info().Str("addr", s.https.Addr).Msgf("server started")

View File

@@ -7,28 +7,28 @@ import (
"time"
)
var (
defaultDialer = net.Dialer{
Timeout: 60 * time.Second,
}
DefaultTransport = &http.Transport{
var DefaultDialer = net.Dialer{
Timeout: 5 * time.Second,
}
func NewTransport() *http.Transport {
return &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: defaultDialer.DialContext,
DialContext: DefaultDialer.DialContext,
ForceAttemptHTTP2: true,
MaxIdleConnsPerHost: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
DisableCompression: true, // Prevent double compression
// DisableCompression: true, // Prevent double compression
ResponseHeaderTimeout: 60 * time.Second,
WriteBufferSize: 16 * 1024, // 16KB
ReadBufferSize: 16 * 1024, // 16KB
}
DefaultTransportNoTLS = func() *http.Transport {
clone := DefaultTransport.Clone()
clone.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
return clone
}()
)
}
const StaticFilePathPrefix = "/$gperrorpage/"
func NewTransportWithTLSConfig(tlsConfig *tls.Config) *http.Transport {
tr := NewTransport()
tr.TLSClientConfig = tlsConfig
return tr
}

View File

@@ -99,7 +99,7 @@ func (s *FileServer) Start(parent task.Parent) E.Error {
}
if s.UseHealthCheck() {
s.Health = monitor.NewFileServerHealthMonitor(s.TargetName(), s.HealthCheck, s.Root)
s.Health = monitor.NewFileServerHealthMonitor(s.HealthCheck, s.Root)
if err := s.Health.Start(s.task); err != nil {
return err
}

View File

@@ -0,0 +1,34 @@
package provider
import (
"github.com/rs/zerolog"
"github.com/yusing/go-proxy/agent/pkg/agent"
E "github.com/yusing/go-proxy/internal/error"
"github.com/yusing/go-proxy/internal/route"
"github.com/yusing/go-proxy/internal/watcher"
)
type AgentProvider struct {
*agent.AgentConfig
docker ProviderImpl
}
func (p *AgentProvider) ShortName() string {
return p.Name()
}
func (p *AgentProvider) NewWatcher() watcher.Watcher {
return p.docker.NewWatcher()
}
func (p *AgentProvider) IsExplicitOnly() bool {
return p.docker.IsExplicitOnly()
}
func (p *AgentProvider) loadRoutesImpl() (route.Routes, E.Error) {
return p.docker.loadRoutesImpl()
}
func (p *AgentProvider) Logger() *zerolog.Logger {
return p.docker.Logger()
}

View File

@@ -29,7 +29,7 @@ const (
var ErrAliasRefIndexOutOfRange = E.New("index out of range")
func DockerProviderImpl(name, dockerHost string) (ProviderImpl, error) {
func DockerProviderImpl(name, dockerHost string) ProviderImpl {
if dockerHost == common.DockerHostFromEnv {
dockerHost = common.GetEnvString("DOCKER_HOST", client.DefaultDockerHost)
}
@@ -37,7 +37,7 @@ func DockerProviderImpl(name, dockerHost string) (ProviderImpl, error) {
name,
dockerHost,
logging.With().Str("type", "docker").Str("name", name).Logger(),
}, nil
}
}
func (p *DockerProvider) String() string {

View File

@@ -258,16 +258,16 @@ func TestPublicIPLocalhost(t *testing.T) {
c := &types.Container{Names: dummyNames, State: "running"}
r, ok := makeRoutes(c)["a"]
ExpectTrue(t, ok)
ExpectEqual(t, r.Container.PublicIP, "127.0.0.1")
ExpectEqual(t, r.Host, r.Container.PublicIP)
ExpectEqual(t, r.Container.PublicHostname, "127.0.0.1")
ExpectEqual(t, r.Host, r.Container.PublicHostname)
}
func TestPublicIPRemote(t *testing.T) {
c := &types.Container{Names: dummyNames, State: "running"}
raw, ok := makeRoutes(c, testIP)["a"]
ExpectTrue(t, ok)
ExpectEqual(t, raw.Container.PublicIP, testIP)
ExpectEqual(t, raw.Host, raw.Container.PublicIP)
ExpectEqual(t, raw.Container.PublicHostname, testIP)
ExpectEqual(t, raw.Host, raw.Container.PublicHostname)
}
func TestPrivateIPLocalhost(t *testing.T) {
@@ -283,8 +283,8 @@ func TestPrivateIPLocalhost(t *testing.T) {
}
r, ok := makeRoutes(c)["a"]
ExpectTrue(t, ok)
ExpectEqual(t, r.Container.PrivateIP, testDockerIP)
ExpectEqual(t, r.Host, r.Container.PrivateIP)
ExpectEqual(t, r.Container.PrivateHostname, testDockerIP)
ExpectEqual(t, r.Host, r.Container.PrivateHostname)
}
func TestPrivateIPRemote(t *testing.T) {
@@ -301,9 +301,9 @@ func TestPrivateIPRemote(t *testing.T) {
}
r, ok := makeRoutes(c, testIP)["a"]
ExpectTrue(t, ok)
ExpectEqual(t, r.Container.PrivateIP, "")
ExpectEqual(t, r.Container.PublicIP, testIP)
ExpectEqual(t, r.Host, r.Container.PublicIP)
ExpectEqual(t, r.Container.PrivateHostname, "")
ExpectEqual(t, r.Container.PublicHostname, testIP)
ExpectEqual(t, r.Host, r.Container.PublicHostname)
}
func TestStreamDefaultValues(t *testing.T) {

View File

@@ -1,7 +1,6 @@
package provider
import (
"github.com/yusing/go-proxy/internal/common"
E "github.com/yusing/go-proxy/internal/error"
"github.com/yusing/go-proxy/internal/route"
"github.com/yusing/go-proxy/internal/route/provider/types"
@@ -38,25 +37,25 @@ func (handler *EventHandler) Handle(parent task.Parent, events []watcher.Event)
}
}
if common.IsDebug {
eventsLog := E.NewBuilder("events")
for _, event := range events {
eventsLog.Addf("event %s, actor: name=%s, id=%s", event.Action, event.ActorName, event.ActorID)
}
E.LogDebug(eventsLog.About(), eventsLog.Error(), handler.provider.Logger())
// if common.IsDebug {
// eventsLog := E.NewBuilder("events")
// for _, event := range events {
// eventsLog.Addf("event %s, actor: name=%s, id=%s", event.Action, event.ActorName, event.ActorID)
// }
// E.LogDebug(eventsLog.About(), eventsLog.Error(), handler.provider.Logger())
oldRoutesLog := E.NewBuilder("old routes")
for k := range oldRoutes {
oldRoutesLog.Adds(k)
}
E.LogDebug(oldRoutesLog.About(), oldRoutesLog.Error(), handler.provider.Logger())
// oldRoutesLog := E.NewBuilder("old routes")
// for k := range oldRoutes {
// oldRoutesLog.Adds(k)
// }
// E.LogDebug(oldRoutesLog.About(), oldRoutesLog.Error(), handler.provider.Logger())
newRoutesLog := E.NewBuilder("new routes")
for k := range newRoutes {
newRoutesLog.Adds(k)
}
E.LogDebug(newRoutesLog.About(), newRoutesLog.Error(), handler.provider.Logger())
}
// newRoutesLog := E.NewBuilder("new routes")
// for k := range newRoutes {
// newRoutesLog.Adds(k)
// }
// E.LogDebug(newRoutesLog.About(), newRoutesLog.Error(), handler.provider.Logger())
// }
for k, oldr := range oldRoutes {
newr, ok := newRoutes[k]
@@ -85,7 +84,7 @@ func (handler *EventHandler) matchAny(events []watcher.Event, route *route.Route
func (handler *EventHandler) match(event watcher.Event, route *route.Route) bool {
switch handler.provider.GetType() {
case types.ProviderTypeDocker:
case types.ProviderTypeDocker, types.ProviderTypeAgent:
return route.Container.ContainerID == event.ActorID ||
route.Container.ContainerName == event.ActorName
case types.ProviderTypeFile:

View File

@@ -7,6 +7,7 @@ import (
"time"
"github.com/rs/zerolog"
"github.com/yusing/go-proxy/agent/pkg/agent"
E "github.com/yusing/go-proxy/internal/error"
"github.com/yusing/go-proxy/internal/route"
"github.com/yusing/go-proxy/internal/route/provider/types"
@@ -64,14 +65,22 @@ func NewDockerProvider(name string, dockerHost string) (p *Provider, err error)
}
p = newProvider(types.ProviderTypeDocker)
p.ProviderImpl, err = DockerProviderImpl(name, dockerHost)
if err != nil {
return nil, err
}
p.ProviderImpl = DockerProviderImpl(name, dockerHost)
p.watcher = p.NewWatcher()
return
}
func NewAgentProvider(cfg *agent.AgentConfig) *Provider {
p := newProvider(types.ProviderTypeAgent)
agent := &AgentProvider{
AgentConfig: cfg,
docker: DockerProviderImpl(cfg.Name(), cfg.FakeDockerHost()),
}
p.ProviderImpl = agent
p.watcher = p.NewWatcher()
return p
}
func (p *Provider) GetType() types.ProviderType {
return p.t
}

View File

@@ -5,4 +5,5 @@ type ProviderType string
const (
ProviderTypeDocker ProviderType = "docker"
ProviderTypeFile ProviderType = "file"
ProviderTypeAgent ProviderType = "agent"
)

View File

@@ -1,8 +1,11 @@
package route
import (
"crypto/tls"
"net/http"
"github.com/yusing/go-proxy/agent/pkg/agent"
"github.com/yusing/go-proxy/agent/pkg/agentproxy"
"github.com/yusing/go-proxy/internal/api/v1/favicon"
"github.com/yusing/go-proxy/internal/common"
"github.com/yusing/go-proxy/internal/docker"
@@ -38,20 +41,27 @@ type (
// var globalMux = http.NewServeMux() // TODO: support regex subdomain matching.
// TODO: fix this for agent
func NewReverseProxyRoute(base *Route) (*ReveseProxyRoute, E.Error) {
trans := gphttp.DefaultTransport
httpConfig := base.HTTPConfig
proxyURL := base.ProxyURL
if httpConfig.NoTLSVerify {
trans = gphttp.DefaultTransportNoTLS
}
if httpConfig.ResponseHeaderTimeout > 0 {
trans = trans.Clone()
trans.ResponseHeaderTimeout = httpConfig.ResponseHeaderTimeout
trans := gphttp.NewTransport()
a := base.Agent()
if a != nil {
trans = a.Transport()
proxyURL = agent.HTTPProxyURL
} else {
if httpConfig.NoTLSVerify {
trans.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
}
if httpConfig.ResponseHeaderTimeout > 0 {
trans.ResponseHeaderTimeout = httpConfig.ResponseHeaderTimeout
}
}
service := base.TargetName()
rp := reverseproxy.NewReverseProxy(service, base.ProxyURL, trans)
rp := reverseproxy.NewReverseProxy(service, proxyURL, trans)
if len(base.Middlewares) > 0 {
err := middleware.PatchReverseProxy(rp, base.Middlewares)
@@ -60,6 +70,20 @@ func NewReverseProxyRoute(base *Route) (*ReveseProxyRoute, E.Error) {
}
}
if a != nil {
headers := &agentproxy.AgentProxyHeaders{
Host: base.ProxyURL.Host,
IsHTTPS: base.ProxyURL.Scheme == "https",
SkipTLSVerify: httpConfig.NoTLSVerify,
ResponseHeaderTimeout: int(httpConfig.ResponseHeaderTimeout.Seconds()),
}
ori := rp.HandlerFunc
rp.HandlerFunc = func(w http.ResponseWriter, r *http.Request) {
agentproxy.SetAgentProxyHeaders(r, headers)
ori(w, r)
}
}
r := &ReveseProxyRoute{
Route: base,
rp: rp,
@@ -88,13 +112,13 @@ func (r *ReveseProxyRoute) Start(parent task.Parent) E.Error {
if r.IsDocker() {
client, err := docker.ConnectClient(r.Idlewatcher.DockerHost)
if err == nil {
fallback := monitor.NewHTTPHealthChecker(r.rp.TargetURL, r.HealthCheck)
fallback := r.newHealthMonitor()
r.HealthMon = monitor.NewDockerHealthMonitor(client, r.Idlewatcher.ContainerID, r.TargetName(), r.HealthCheck, fallback)
r.task.OnCancel("close_docker_client", client.Close)
}
}
if r.HealthMon == nil {
r.HealthMon = monitor.NewHTTPHealthMonitor(r.rp.TargetURL, r.HealthCheck)
r.HealthMon = r.newHealthMonitor()
}
}
@@ -178,6 +202,17 @@ func (r *ReveseProxyRoute) HealthMonitor() health.HealthMonitor {
return r.HealthMon
}
func (r *ReveseProxyRoute) newHealthMonitor() interface {
health.HealthMonitor
health.HealthChecker
} {
if a := r.Agent(); a != nil {
target := monitor.AgentCheckHealthTargetFromURL(r.ProxyURL)
return monitor.NewAgentRouteMonitor(a, r.HealthCheck, target)
}
return monitor.NewHTTPHealthMonitor(r.ProxyURL, r.HealthCheck)
}
func (r *ReveseProxyRoute) addToLoadBalancer(parent task.Parent) {
var lb *loadbalancer.LoadBalancer
cfg := r.LoadBalance

View File

@@ -5,6 +5,7 @@ import (
"strconv"
"strings"
"github.com/yusing/go-proxy/agent/pkg/agent"
"github.com/yusing/go-proxy/internal/docker"
idlewatcher "github.com/yusing/go-proxy/internal/docker/idlewatcher/types"
"github.com/yusing/go-proxy/internal/homepage"
@@ -159,6 +160,17 @@ func (r *Route) Type() types.RouteType {
panic(fmt.Errorf("unexpected scheme %s for alias %s", r.Scheme, r.Alias))
}
func (r *Route) Agent() *agent.AgentConfig {
if r.Container == nil {
return nil
}
return r.Container.Agent
}
func (r *Route) IsAgent() bool {
return r.Container != nil && r.Container.Agent != nil
}
func (r *Route) HealthMonitor() health.HealthMonitor {
return r.impl.HealthMonitor()
}
@@ -240,24 +252,24 @@ func (r *Route) Finalize() {
switch {
case !isDocker:
r.Host = "localhost"
case cont.PrivateIP != "":
r.Host = cont.PrivateIP
case cont.PublicIP != "":
r.Host = cont.PublicIP
case cont.PrivateHostname != "":
r.Host = cont.PrivateHostname
case cont.PublicHostname != "":
r.Host = cont.PublicHostname
}
}
lp, pp := r.Port.Listening, r.Port.Proxy
if isDocker {
if port, ok := common.ServiceNamePortMapTCP[cont.ImageName]; ok {
if port, ok := common.ImageNamePortMapTCP[cont.ImageName]; ok {
if pp == 0 {
pp = port
}
if r.Scheme == "" {
r.Scheme = "tcp"
}
} else if port, ok := common.ImageNamePortMap[cont.ImageName]; ok {
} else if port, ok := common.ImageNamePortMapHTTP[cont.ImageName]; ok {
if pp == 0 {
pp = port
}
@@ -268,39 +280,34 @@ func (r *Route) Finalize() {
}
if pp == 0 {
switch {
case r.Scheme == "https":
pp = 443
case !isDocker:
pp = 80
default:
if isDocker {
pp = lowestPort(cont.PrivatePortMapping)
if pp == 0 {
pp = lowestPort(cont.PublicPortMapping)
}
} else if r.Scheme == "https" {
pp = 443
} else {
pp = 80
}
}
if isDocker {
// replace private port with public port if using public IP.
if r.Host == cont.PublicIP {
if r.Host == cont.PublicHostname {
if p, ok := cont.PrivatePortMapping[pp]; ok {
pp = int(p.PublicPort)
if r.Scheme == "" && p.Type == "udp" {
r.Scheme = "udp"
}
}
}
// replace public port with private port if using private IP.
if r.Host == cont.PrivateIP {
} else {
// replace public port with private port if using private IP.
if p, ok := cont.PublicPortMapping[pp]; ok {
pp = int(p.PrivatePort)
}
}
if r.Scheme == "" {
switch {
case r.Host == cont.PublicIP && cont.PublicPortMapping[pp].Type == "udp":
r.Scheme = "udp"
case r.Host == cont.PrivateIP && cont.PrivatePortMapping[pp].Type == "udp":
r.Scheme = "udp"
if r.Scheme == "" && p.Type == "udp" {
r.Scheme = "udp"
}
}
}
}
@@ -322,13 +329,10 @@ func (r *Route) Finalize() {
r.HealthCheck = health.DefaultHealthConfig
}
// set or keep at least default
if !r.HealthCheck.Disable {
if r.HealthCheck.Interval == 0 {
r.HealthCheck.Interval = common.HealthCheckIntervalDefault
}
if r.HealthCheck.Timeout == 0 {
r.HealthCheck.Timeout = common.HealthCheckTimeoutDefault
}
r.HealthCheck.Interval |= common.HealthCheckIntervalDefault
r.HealthCheck.Timeout |= common.HealthCheckTimeoutDefault
}
if isDocker && cont.IdleTimeout != "" {

View File

@@ -125,7 +125,11 @@ func HomepageConfig(useDefaultCategories bool, categoryFilter, providerFilter st
if item.Category == "" {
item.Category = "Docker"
}
item.SourceType = string(provider.ProviderTypeDocker)
if r.IsAgent() {
item.SourceType = string(provider.ProviderTypeAgent)
} else {
item.SourceType = string(provider.ProviderTypeDocker)
}
case r.UseLoadBalance():
if item.Category == "" {
item.Category = "Load-balanced"

View File

@@ -164,7 +164,7 @@ var commands = map[string]struct {
if target.Scheme == "" {
target.Scheme = "http"
}
rp := reverseproxy.NewReverseProxy("", target, gphttp.DefaultTransport)
rp := reverseproxy.NewReverseProxy("", target, gphttp.NewTransport())
return ReturningCommand(rp.ServeHTTP)
},
},

View File

@@ -234,7 +234,8 @@ func TestOnCorrectness(t *testing.T) {
tests = append(tests, genCorrectnessTestCases("header", func(k, v string) *http.Request {
return &http.Request{
Header: http.Header{k: []string{v}}}
Header: http.Header{k: []string{v}},
}
})...)
tests = append(tests, genCorrectnessTestCases("query", func(k, v string) *http.Request {
return &http.Request{

View File

@@ -3,6 +3,7 @@ package types
import (
"net/http"
"github.com/yusing/go-proxy/agent/pkg/agent"
"github.com/yusing/go-proxy/internal/docker"
idlewatcher "github.com/yusing/go-proxy/internal/docker/idlewatcher/types"
"github.com/yusing/go-proxy/internal/homepage"
@@ -31,7 +32,10 @@ type (
HomepageConfig() *homepage.Item
ContainerInfo() *docker.Container
Agent() *agent.AgentConfig
IsDocker() bool
IsAgent() bool
UseLoadBalance() bool
UseIdleWatcher() bool
UseHealthCheck() bool

View File

@@ -1,127 +0,0 @@
package internal
import (
"io"
"log"
"net/http"
"net/url"
"os"
"path"
"github.com/yusing/go-proxy/internal/common"
)
var (
branch = common.GetEnvString("BRANCH", "v0.9")
baseURL = "https://github.com/yusing/go-proxy/raw/" + branch
requiredConfigs = []Config{
{common.ConfigBasePath, true, false, ""},
{common.DotEnvPath, false, true, common.DotEnvExamplePath},
{common.ComposeFileName, false, true, common.ComposeExampleFileName},
{path.Join(common.ConfigBasePath, common.ConfigFileName), false, true, common.ConfigExampleFileName},
}
)
type Config struct {
Pathname string
IsDir bool
NeedDownload bool
DownloadFileName string
}
func Setup() {
log.Println("setting up go-proxy")
log.Println("branch:", branch)
if err := os.Chdir("/setup"); err != nil {
log.Fatalf("failed: %s\n", err)
}
for _, config := range requiredConfigs {
config.setup()
}
log.Println("setup finished")
}
func (c *Config) setup() {
if c.IsDir {
mkdir(c.Pathname)
return
}
if !c.NeedDownload {
touch(c.Pathname)
return
}
fetch(c.DownloadFileName, c.Pathname)
}
func hasFileOrDir(path string) bool {
_, err := os.Stat(path)
return err == nil
}
func mkdir(pathname string) {
_, err := os.Stat(pathname)
if err != nil && os.IsNotExist(err) {
log.Printf("creating directory %q\n", pathname)
err := os.MkdirAll(pathname, 0o755)
if err != nil {
log.Fatalf("failed: %s\n", err)
}
return
}
if err != nil {
log.Fatalf("failed: %s\n", err)
}
}
func touch(pathname string) {
if hasFileOrDir(pathname) {
return
}
log.Printf("creating file %q\n", pathname)
_, err := os.Create(pathname)
if err != nil {
log.Fatalf("failed: %s\n", err)
}
}
func fetch(remoteFilename string, outFileName string) {
if hasFileOrDir(outFileName) {
if remoteFilename == outFileName {
log.Printf("%q already exists, not overwriting\n", outFileName)
return
}
log.Printf("%q already exists, downloading to %q\n", outFileName, remoteFilename)
outFileName = remoteFilename
}
log.Printf("downloading %q to %q\n", remoteFilename, outFileName)
url, err := url.JoinPath(baseURL, remoteFilename)
if err != nil {
log.Fatalf("unexpected error: %s\n", err)
}
resp, err := http.Get(url)
if err != nil {
log.Fatalf("http request failed: %s\n", err)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
resp.Body.Close()
log.Fatalf("error reading response body: %s\n", err)
}
err = os.WriteFile(outFileName, body, 0o644)
if err != nil {
resp.Body.Close()
log.Fatalf("failed to write to file: %s\n", err)
}
log.Print("done")
resp.Body.Close()
}

View File

@@ -34,3 +34,15 @@ func ListFiles(dir string, maxDepth int, hideHidden ...bool) ([]string, error) {
}
return files, nil
}
// FileExists checks if a file exists.
//
// If the file does not exist, it returns false and nil,
// otherwise it returns true and any error that is not os.ErrNotExist.
func FileExists(file string) (bool, error) {
_, err := os.Stat(file)
if os.IsNotExist(err) {
return false, nil
}
return true, err
}

View File

@@ -0,0 +1,25 @@
package utils
import (
"os"
"os/signal"
"syscall"
"time"
"github.com/yusing/go-proxy/internal/logging"
"github.com/yusing/go-proxy/internal/task"
)
func WaitExit(shutdownTimeout int) {
sig := make(chan os.Signal, 1)
signal.Notify(sig, syscall.SIGINT)
signal.Notify(sig, syscall.SIGTERM)
signal.Notify(sig, syscall.SIGHUP)
// wait for signal
<-sig
// gracefully shutdown
logging.Info().Msg("shutting down")
_ = task.GracefulShutdown(time.Second * time.Duration(shutdownTimeout))
}

View File

@@ -6,17 +6,13 @@ import (
docker_events "github.com/docker/docker/api/types/events"
"github.com/docker/docker/api/types/filters"
"github.com/rs/zerolog"
D "github.com/yusing/go-proxy/internal/docker"
E "github.com/yusing/go-proxy/internal/error"
"github.com/yusing/go-proxy/internal/logging"
"github.com/yusing/go-proxy/internal/watcher/events"
)
type (
DockerWatcher struct {
zerolog.Logger
host string
client *D.SharedClient
clientOwned bool
@@ -56,20 +52,12 @@ func NewDockerWatcher(host string) DockerWatcher {
return DockerWatcher{
host: host,
clientOwned: true,
Logger: logging.With().
Str("type", "docker").
Str("host", host).
Logger(),
}
}
func NewDockerWatcherWithClient(client *D.SharedClient) DockerWatcher {
return DockerWatcher{
client: client,
Logger: logging.With().
Str("type", "docker").
Str("host", client.DaemonHost()).
Logger(),
}
}
@@ -124,7 +112,6 @@ func (w DockerWatcher) EventsWithOptions(ctx context.Context, options DockerList
case msg := <-cEventCh:
action, ok := events.DockerEventMap[msg.Action]
if !ok {
w.Debug().Msgf("ignored unknown docker event: %s for container %s", msg.Action, msg.Actor.Attributes["name"])
continue
}
event := Event{

View File

@@ -0,0 +1,75 @@
package monitor
import (
"encoding/json"
"errors"
"net/http"
"net/url"
agentPkg "github.com/yusing/go-proxy/agent/pkg/agent"
"github.com/yusing/go-proxy/internal/net/types"
"github.com/yusing/go-proxy/internal/watcher/health"
)
type (
AgentRouteMonior struct {
agent *agentPkg.AgentConfig
endpointURL string
*monitor
}
AgentCheckHealthTarget struct {
Scheme string
Host string
Path string
}
)
func AgentCheckHealthTargetFromURL(url *types.URL) *AgentCheckHealthTarget {
return &AgentCheckHealthTarget{
Scheme: url.Scheme,
Host: url.Host,
Path: url.Path,
}
}
func (target *AgentCheckHealthTarget) buildQuery() string {
query := make(url.Values, 3)
query.Set("scheme", target.Scheme)
query.Set("host", target.Host)
query.Set("path", target.Path)
return query.Encode()
}
func (target *AgentCheckHealthTarget) displayURL() *types.URL {
return types.NewURL(&url.URL{
Scheme: target.Scheme,
Host: target.Host,
Path: target.Path,
})
}
func NewAgentRouteMonitor(agent *agentPkg.AgentConfig, config *health.HealthCheckConfig, target *AgentCheckHealthTarget) *AgentRouteMonior {
mon := &AgentRouteMonior{
agent: agent,
endpointURL: agentPkg.EndpointHealth + "?" + target.buildQuery(),
}
mon.monitor = newMonitor(target.displayURL(), config, mon.CheckHealth)
return mon
}
func (mon *AgentRouteMonior) CheckHealth() (result *health.HealthCheckResult, err error) {
result = new(health.HealthCheckResult)
ctx, cancel := mon.ContextWithTimeout("timeout querying agent")
defer cancel()
data, status, err := mon.agent.Fetch(ctx, mon.endpointURL)
if err != nil {
return result, err
}
switch status {
case http.StatusOK:
err = json.Unmarshal(data, result)
default:
err = errors.New(string(data))
}
return
}

View File

@@ -12,10 +12,9 @@ type FileServerHealthMonitor struct {
path string
}
func NewFileServerHealthMonitor(alias string, config *health.HealthCheckConfig, path string) *FileServerHealthMonitor {
func NewFileServerHealthMonitor(config *health.HealthCheckConfig, path string) *FileServerHealthMonitor {
mon := &FileServerHealthMonitor{path: path}
mon.monitor = newMonitor(nil, config, mon.CheckHealth)
mon.service = alias
return mon
}

View File

@@ -11,9 +11,9 @@ import (
type (
HealthCheckResult struct {
Healthy bool
Detail string
Latency time.Duration
Healthy bool `json:"healthy"`
Detail string `json:"detail"`
Latency time.Duration `json:"latency"`
}
WithHealthInfo interface {
Status() Status