refactor(config): restructured with better concurrency and error handling, reduced cross referencing

This commit is contained in:
yusing
2025-10-09 01:02:24 +08:00
parent d08be872a0
commit cab68807ee
25 changed files with 720 additions and 623 deletions

View File

@@ -5,6 +5,7 @@ import (
"sync"
"github.com/rs/zerolog/log"
"github.com/yusing/godoxy/internal/api"
"github.com/yusing/godoxy/internal/auth"
"github.com/yusing/godoxy/internal/common"
"github.com/yusing/godoxy/internal/config"
@@ -17,6 +18,7 @@ import (
"github.com/yusing/godoxy/internal/net/gphttp/middleware"
"github.com/yusing/godoxy/pkg"
gperr "github.com/yusing/goutils/errs"
"github.com/yusing/goutils/server"
"github.com/yusing/goutils/task"
)
@@ -50,26 +52,26 @@ func main() {
prepareDirectory(dir)
}
cfg, err := config.Load()
err := config.Load()
if err != nil {
gperr.LogWarn("errors in config", err)
}
cfg.Start(&config.StartServersOptions{
Proxy: true,
})
config.StartProxyServers()
if err := auth.Initialize(); err != nil {
log.Fatal().Err(err).Msg("failed to initialize authentication")
}
// API Handler needs to start after auth is initialized.
cfg.StartServers(&config.StartServersOptions{
API: true,
server.StartServer(task.RootTask("api_server", false), server.Options{
Name: "api",
HTTPAddr: common.APIHTTPAddr,
Handler: api.NewHandler(),
})
uptime.Poller.Start()
config.WatchChanges()
task.WaitExit(cfg.Value().TimeoutShutdown)
task.WaitExit(config.Value().TimeoutShutdown)
}
func prepareDirectory(dir string) {

Submodule goutils updated: 66b3d4cbeb...b2336ee8a6

View File

@@ -9,7 +9,9 @@ import (
"github.com/yusing/godoxy/agent/pkg/agent"
"github.com/yusing/godoxy/agent/pkg/certs"
config "github.com/yusing/godoxy/internal/config/types"
"github.com/yusing/godoxy/internal/route/provider"
apitypes "github.com/yusing/goutils/apitypes"
gperr "github.com/yusing/goutils/errs"
)
type VerifyNewAgentRequest struct {
@@ -57,7 +59,7 @@ func Verify(c *gin.Context) {
return
}
nRoutesAdded, err := config.GetInstance().VerifyNewAgent(request.Host, ca, client, request.ContainerRuntime)
nRoutesAdded, err := verifyNewAgent(request.Host, ca, client, request.ContainerRuntime)
if err != nil {
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
return
@@ -76,3 +78,37 @@ func Verify(c *gin.Context) {
c.JSON(http.StatusOK, apitypes.Success(fmt.Sprintf("Added %d routes", nRoutesAdded)))
}
func verifyNewAgent(host string, ca agent.PEMPair, client agent.PEMPair, containerRuntime agent.ContainerRuntime) (int, gperr.Error) {
cfgState := config.ActiveState.Load()
for _, a := range cfgState.Value().Providers.Agents {
if a.Addr == host {
return 0, gperr.New("agent already exists")
}
}
var agentCfg agent.AgentConfig
agentCfg.Addr = host
agentCfg.Runtime = containerRuntime
err := agentCfg.StartWithCerts(cfgState.Context(), ca.Cert, client.Cert, client.Key)
if err != nil {
return 0, gperr.Wrap(err, "failed to start agent")
}
provider := provider.NewAgentProvider(&agentCfg)
if _, loaded := cfgState.LoadOrStoreProvider(provider.String(), provider); loaded {
return 0, gperr.Errorf("provider %s already exists", provider.String())
}
// agent must be added before loading routes
agent.AddAgent(&agentCfg)
err = provider.LoadRoutes()
if err != nil {
cfgState.DeleteProvider(provider.String())
agent.RemoveAgent(&agentCfg)
return 0, gperr.Wrap(err, "failed to load routes")
}
return provider.NumRoutes(), nil
}

View File

@@ -8,6 +8,8 @@ import (
apitypes "github.com/yusing/godoxy/internal/api/types"
"github.com/yusing/godoxy/internal/homepage"
"github.com/yusing/godoxy/internal/route/routes"
_ "unsafe"
)
type GetFavIconRequest struct {
@@ -62,6 +64,7 @@ func FavIcon(c *gin.Context) {
c.Data(result.StatusCode, result.ContentType(), result.Icon)
}
//go:linkname GetFavIconFromAlias v1.GetFavIconFromAlias
func GetFavIconFromAlias(ctx context.Context, alias string) (homepage.FetchResult, error) {
// try with route.Icon
r, ok := routes.HTTP.Get(alias)

View File

@@ -5,7 +5,7 @@ import (
"github.com/gin-gonic/gin"
apitypes "github.com/yusing/godoxy/internal/api/types"
config "github.com/yusing/godoxy/internal/config/types"
"github.com/yusing/godoxy/internal/config"
)
// @x-id "reload"
@@ -20,7 +20,7 @@ import (
// @Failure 500 {object} apitypes.ErrorResponse
// @Router /reload [post]
func Reload(c *gin.Context) {
if err := config.GetInstance().Reload(); err != nil {
if err := config.Reload(); err != nil {
c.Error(apitypes.InternalServerError(err, "failed to reload config"))
return
}

View File

@@ -5,7 +5,7 @@ import (
"time"
"github.com/gin-gonic/gin"
config "github.com/yusing/godoxy/internal/config/types"
statequery "github.com/yusing/godoxy/internal/config/query"
"github.com/yusing/goutils/http/httpheaders"
"github.com/yusing/goutils/http/websocket"
)
@@ -22,12 +22,11 @@ import (
// @Failure 500 {object} apitypes.ErrorResponse
// @Router /route/providers [get]
func Providers(c *gin.Context) {
cfg := config.GetInstance()
if httpheaders.IsWebsocket(c.Request.Header) {
websocket.PeriodicWrite(c, 5*time.Second, func() (any, error) {
return config.GetInstance().RouteProviderList(), nil
return statequery.RouteProviderList(), nil
})
} else {
c.JSON(http.StatusOK, cfg.RouteProviderList())
c.JSON(http.StatusOK, statequery.RouteProviderList())
}
}

View File

@@ -5,7 +5,7 @@ import (
"github.com/gin-gonic/gin"
apitypes "github.com/yusing/godoxy/internal/api/types"
config "github.com/yusing/godoxy/internal/config/types"
statequery "github.com/yusing/godoxy/internal/config/query"
"github.com/yusing/godoxy/internal/route/routes"
)
@@ -40,7 +40,7 @@ func Route(c *gin.Context) {
}
// also search for excluded routes
route = config.GetInstance().SearchRoute(request.Which)
route = statequery.SearchRoute(request.Which)
if route != nil {
c.JSON(http.StatusOK, route)
return

View File

@@ -5,7 +5,7 @@ import (
"time"
"github.com/gin-gonic/gin"
config "github.com/yusing/godoxy/internal/config/types"
statequery "github.com/yusing/godoxy/internal/config/query"
"github.com/yusing/godoxy/internal/types"
"github.com/yusing/goutils/http/httpheaders"
"github.com/yusing/goutils/http/websocket"
@@ -35,10 +35,9 @@ type ProxyStats struct {
// @Failure 500 {object} apitypes.ErrorResponse
// @Router /stats [get]
func Stats(c *gin.Context) {
cfg := config.GetInstance()
getStats := func() (any, error) {
return map[string]any{
"proxies": cfg.Statistics(),
"proxies": statequery.GetStatistics(),
"uptime": int64(time.Since(startTime).Round(time.Second).Seconds()),
}, nil
}

View File

@@ -1,40 +0,0 @@
package config
import (
"github.com/yusing/godoxy/agent/pkg/agent"
"github.com/yusing/godoxy/internal/route/provider"
gperr "github.com/yusing/goutils/errs"
)
func (cfg *Config) VerifyNewAgent(host string, ca agent.PEMPair, client agent.PEMPair, containerRuntime agent.ContainerRuntime) (int, gperr.Error) {
for _, a := range cfg.value.Providers.Agents {
if a.Addr == host {
return 0, gperr.New("agent already exists")
}
}
agentCfg := agent.AgentConfig{
Addr: host,
Runtime: containerRuntime,
}
err := agentCfg.StartWithCerts(cfg.Task().Context(), ca.Cert, client.Cert, client.Key)
if err != nil {
return 0, gperr.Wrap(err, "failed to start agent")
}
provider := provider.NewAgentProvider(&agentCfg)
if _, loaded := cfg.providers.LoadOrStore(provider.String(), provider); loaded {
return 0, gperr.Errorf("provider %s already exists", provider.String())
}
// agent must be added before loading routes
agent.AddAgent(&agentCfg)
err = provider.LoadRoutes()
if err != nil {
cfg.providers.Delete(provider.String())
agent.RemoveAgent(&agentCfg)
return 0, gperr.Wrap(err, "failed to load routes")
}
return provider.NumRoutes(), nil
}

View File

@@ -1,409 +0,0 @@
package config
import (
"context"
"errors"
"os"
"strconv"
"strings"
"sync"
"time"
"github.com/puzpuzpuz/xsync/v4"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
agentPkg "github.com/yusing/godoxy/agent/pkg/agent"
"github.com/yusing/godoxy/internal/api"
autocert "github.com/yusing/godoxy/internal/autocert"
"github.com/yusing/godoxy/internal/common"
config "github.com/yusing/godoxy/internal/config/types"
"github.com/yusing/godoxy/internal/entrypoint"
"github.com/yusing/godoxy/internal/maxmind"
"github.com/yusing/godoxy/internal/notif"
"github.com/yusing/godoxy/internal/proxmox"
proxy "github.com/yusing/godoxy/internal/route/provider"
"github.com/yusing/godoxy/internal/serialization"
"github.com/yusing/godoxy/internal/watcher"
"github.com/yusing/godoxy/internal/watcher/events"
gperr "github.com/yusing/goutils/errs"
"github.com/yusing/goutils/server"
"github.com/yusing/goutils/strings/ansi"
"github.com/yusing/goutils/task"
)
type Config struct {
value *config.Config
providers *xsync.Map[string, *proxy.Provider]
autocertProvider *autocert.Provider
entrypoint *entrypoint.Entrypoint
task *task.Task
}
var (
cfgWatcher watcher.Watcher
reloadMu sync.Mutex
)
const configEventFlushInterval = 500 * time.Millisecond
const (
cfgRenameWarn = `Config file renamed, not reloading.
Make sure you rename it back before next time you start.`
cfgDeleteWarn = `Config file deleted, not reloading.
You may run "ls-config" to show or dump the current config.`
)
var Validate = config.Validate
func newConfig() *Config {
return &Config{
value: config.DefaultConfig(),
providers: xsync.NewMap[string, *proxy.Provider](),
entrypoint: entrypoint.NewEntrypoint(),
task: task.RootTask("config", false),
}
}
func Load() (*Config, gperr.Error) {
if config.HasInstance() {
panic(errors.New("config already loaded"))
}
cfg := newConfig()
config.SetInstance(cfg)
cfgWatcher = watcher.NewConfigFileWatcher(common.ConfigFileName)
return cfg, cfg.load()
}
func MatchDomains() []string {
return config.GetInstance().Value().MatchDomains
}
func WatchChanges() {
t := task.RootTask("config_watcher", true)
eventQueue := events.NewEventQueue(
t,
configEventFlushInterval,
OnConfigChange,
func(err gperr.Error) {
gperr.LogError("config reload error", err)
},
)
eventQueue.Start(cfgWatcher.Events(t.Context()))
}
func OnConfigChange(ev []events.Event) {
// no matter how many events during the interval
// just reload once and check the last event
switch ev[len(ev)-1].Action {
case events.ActionFileRenamed:
log.Warn().Msg(cfgRenameWarn)
return
case events.ActionFileDeleted:
log.Warn().Msg(cfgDeleteWarn)
return
}
if err := Reload(); err != nil {
// recovered in event queue
panic(err)
}
}
func Reload() gperr.Error {
// avoid race between config change and API reload request
reloadMu.Lock()
defer reloadMu.Unlock()
newCfg := newConfig()
err := newCfg.load()
if err != nil {
newCfg.task.FinishAndWait(err)
return gperr.New(ansi.Warning("using last config")).With(err)
}
// cancel all current subtasks -> wait
// -> replace config -> start new subtasks
config.GetInstance().(*Config).Task().FinishAndWait("config changed")
newCfg.Start(StartAllServers)
config.SetInstance(newCfg)
return nil
}
func (cfg *Config) Value() *config.Config {
return cfg.value
}
func (cfg *Config) Reload() gperr.Error {
return Reload()
}
// AutoCertProvider returns the autocert provider.
//
// If the autocert provider is not configured, it returns nil.
func (cfg *Config) AutoCertProvider() *autocert.Provider {
return cfg.autocertProvider
}
func (cfg *Config) Task() *task.Task {
return cfg.task
}
func (cfg *Config) Context() context.Context {
return cfg.task.Context()
}
func (cfg *Config) Start(opts ...*StartServersOptions) {
cfg.StartAutoCert()
cfg.StartProxyProviders()
cfg.StartServers(opts...)
}
func (cfg *Config) StartAutoCert() {
autocert := cfg.autocertProvider
if autocert == nil {
log.Info().Msg("autocert not configured")
return
}
if err := autocert.Setup(); err != nil {
gperr.LogFatal("autocert setup error", err)
} else {
autocert.ScheduleRenewal(cfg.task)
}
}
func (cfg *Config) StartProxyProviders() {
var wg sync.WaitGroup
errs := gperr.NewBuilderWithConcurrency()
for _, p := range cfg.providers.Range {
wg.Go(func() {
if err := p.Start(cfg.task); err != nil {
errs.Add(err.Subject(p.String()))
}
})
}
wg.Wait()
if err := errs.Error(); err != nil {
gperr.LogError("route provider errors", err)
}
}
type StartServersOptions struct {
Proxy, API bool
}
var StartAllServers = &StartServersOptions{true, true}
func (cfg *Config) StartServers(opts ...*StartServersOptions) {
if len(opts) == 0 {
opts = append(opts, &StartServersOptions{})
}
opt := opts[0]
if opt.Proxy {
server.StartServer(cfg.task, server.Options{
Name: "proxy",
CertProvider: cfg.AutoCertProvider(),
HTTPAddr: common.ProxyHTTPAddr,
HTTPSAddr: common.ProxyHTTPSAddr,
Handler: cfg.entrypoint,
ACL: cfg.value.ACL,
SupportProxyProtocol: cfg.value.Entrypoint.SupportProxyProtocol,
})
}
if opt.API {
server.StartServer(cfg.task, server.Options{
Name: "api",
CertProvider: cfg.AutoCertProvider(),
HTTPAddr: common.APIHTTPAddr,
Handler: api.NewHandler(),
})
}
}
func (cfg *Config) load() gperr.Error {
const errMsg = "config load error"
data, err := os.ReadFile(common.ConfigPath)
if err != nil {
if os.IsNotExist(err) {
log.Warn().Msg("config file not found, using default config")
cfg.value = config.DefaultConfig()
return nil
} else {
gperr.LogFatal(errMsg, err)
}
}
model := config.DefaultConfig()
if err := serialization.UnmarshalValidateYAML(data, model); err != nil {
gperr.LogFatal(errMsg, err)
}
// errors are non fatal below
errs := gperr.NewBuilder(errMsg)
errs.Add(cfg.entrypoint.SetMiddlewares(model.Entrypoint.Middlewares))
errs.Add(cfg.entrypoint.SetAccessLogger(cfg.task, model.Entrypoint.AccessLog))
errs.Add(cfg.initMaxMind(model.Providers.MaxMind))
cfg.initNotification(model.Providers.Notification)
errs.Add(cfg.initAutoCert(model.AutoCert))
errs.Add(cfg.initProxmox(model.Providers.Proxmox))
errs.Add(cfg.loadRouteProviders(&model.Providers))
cfg.value = model
for i, domain := range model.MatchDomains {
if !strings.HasPrefix(domain, ".") {
model.MatchDomains[i] = "." + domain
}
}
cfg.entrypoint.SetFindRouteDomains(model.MatchDomains)
if model.ACL.Valid() {
err := model.ACL.Start(cfg.task)
if err != nil {
errs.Add(err)
}
}
if errs.HasError() {
notif.Notify(&notif.LogMessage{
Level: zerolog.ErrorLevel,
Title: "Config Reload Error",
Body: notif.ErrorBody{Error: errs.Error()},
})
return errs.Error()
}
return nil
}
func (cfg *Config) initMaxMind(maxmindCfg *maxmind.Config) gperr.Error {
if maxmindCfg != nil {
return maxmind.SetInstance(cfg.task, maxmindCfg)
}
return nil
}
func (cfg *Config) initNotification(notifCfg []notif.NotificationConfig) {
if len(notifCfg) == 0 {
return
}
dispatcher := notif.StartNotifDispatcher(cfg.task)
for _, notifier := range notifCfg {
dispatcher.RegisterProvider(&notifier)
}
}
func (cfg *Config) initAutoCert(autocertCfg *autocert.Config) gperr.Error {
if cfg.autocertProvider != nil {
return nil
}
if autocertCfg == nil {
autocertCfg = new(autocert.Config)
}
user, legoCfg, err := autocertCfg.GetLegoConfig()
if err != nil {
return err
}
cfg.autocertProvider = autocert.NewProvider(autocertCfg, user, legoCfg)
return nil
}
func (cfg *Config) initProxmox(proxmoxCfg []proxmox.Config) gperr.Error {
proxmox.Clients.Clear()
errs := gperr.NewBuilder()
for _, cfg := range proxmoxCfg {
if err := cfg.Init(); err != nil {
errs.Add(err.Subject(cfg.URL))
}
}
return errs.Error()
}
func (cfg *Config) storeProvider(p *proxy.Provider) {
cfg.providers.Store(p.String(), p)
}
func (cfg *Config) loadRouteProviders(providers *config.Providers) gperr.Error {
errs := gperr.NewBuilderWithConcurrency("route provider errors")
results := gperr.NewBuilder("loaded route providers")
agentPkg.RemoveAllAgents()
numProviders := len(providers.Agents) + len(providers.Files) + len(providers.Docker)
providersCh := make(chan *proxy.Provider, numProviders)
// start providers concurrently
var providersConsumer sync.WaitGroup
providersConsumer.Go(func() {
for p := range providersCh {
if actual, loaded := cfg.providers.LoadOrStore(p.String(), p); loaded {
errs.Add(gperr.Errorf("provider %s already exists, first: %s, second: %s", p.String(), actual.GetType(), p.GetType()))
continue
}
cfg.storeProvider(p)
}
})
var providersProducer sync.WaitGroup
for _, agent := range providers.Agents {
providersProducer.Go(func() {
if err := agent.Start(cfg.task.Context()); err != nil {
errs.Add(gperr.PrependSubject(agent.String(), err))
return
}
agentPkg.AddAgent(agent)
p := proxy.NewAgentProvider(agent)
providersCh <- p
})
}
for _, filename := range providers.Files {
providersProducer.Go(func() {
p, err := proxy.NewFileProvider(filename)
if err != nil {
errs.Add(gperr.PrependSubject(filename, err))
} else {
providersCh <- p
}
})
}
for name, dockerHost := range providers.Docker {
providersProducer.Go(func() {
providersCh <- proxy.NewDockerProvider(name, dockerHost)
})
}
providersProducer.Wait()
close(providersCh)
providersConsumer.Wait()
lenLongestName := 0
for k := range cfg.providers.Range {
if len(k) > lenLongestName {
lenLongestName = len(k)
}
}
results.EnableConcurrency()
// load routes concurrently
var providersLoader sync.WaitGroup
for _, p := range cfg.providers.Range {
providersLoader.Go(func() {
if err := p.LoadRoutes(); err != nil {
errs.Add(err.Subject(p.String()))
}
results.Addf("%-"+strconv.Itoa(lenLongestName)+"s %d routes", p.String(), p.NumRoutes())
})
}
providersLoader.Wait()
log.Info().Msg(results.String())
return errs.Error()
}

121
internal/config/events.go Normal file
View File

@@ -0,0 +1,121 @@
package config
import (
"errors"
"fmt"
"sync"
"time"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/yusing/godoxy/internal/common"
"github.com/yusing/godoxy/internal/notif"
"github.com/yusing/godoxy/internal/watcher"
"github.com/yusing/godoxy/internal/watcher/events"
gperr "github.com/yusing/goutils/errs"
"github.com/yusing/goutils/server"
"github.com/yusing/goutils/strings/ansi"
"github.com/yusing/goutils/task"
)
var (
cfgWatcher watcher.Watcher
reloadMu sync.Mutex
)
const configEventFlushInterval = 500 * time.Millisecond
const (
cfgRenameWarn = `Config file renamed, not reloading.
Make sure you rename it back before next time you start.`
cfgDeleteWarn = `Config file deleted, not reloading.
You may run "ls-config" to show or dump the current config.`
)
func Load() error {
if HasState() {
panic(errors.New("config already loaded"))
}
state := NewState()
cfgWatcher = watcher.NewConfigFileWatcher(common.ConfigFileName)
err := state.InitFromFileOrExit(common.ConfigPath)
if err != nil {
notifyError("init", err)
}
SetState(state)
return err
}
func notifyError(action string, err error) {
notif.Notify(&notif.LogMessage{
Level: zerolog.ErrorLevel,
Title: fmt.Sprintf("Config %s Error", action),
Body: notif.ErrorBody(err),
})
}
func Reload() gperr.Error {
// avoid race between config change and API reload request
reloadMu.Lock()
defer reloadMu.Unlock()
newState := NewState()
err := newState.InitFromFileOrExit(common.ConfigPath)
if err != nil {
newState.Task().FinishAndWait(err)
notifyError("reload", err)
return gperr.New(ansi.Warning("using last config")).With(err)
}
// cancel all current subtasks -> wait
// -> replace config -> start new subtasks
GetState().Task().FinishAndWait("config changed")
SetState(newState)
StartProxyServers()
return nil
}
func WatchChanges() {
t := task.RootTask("config_watcher", true)
eventQueue := events.NewEventQueue(
t,
configEventFlushInterval,
OnConfigChange,
func(err gperr.Error) {
gperr.LogError("config reload error", err)
},
)
eventQueue.Start(cfgWatcher.Events(t.Context()))
}
func OnConfigChange(ev []events.Event) {
// no matter how many events during the interval
// just reload once and check the last event
switch ev[len(ev)-1].Action {
case events.ActionFileRenamed:
log.Warn().Msg(cfgRenameWarn)
return
case events.ActionFileDeleted:
log.Warn().Msg(cfgDeleteWarn)
return
}
if err := Reload(); err != nil {
// recovered in event queue
panic(err)
}
}
func StartProxyServers() {
cfg := GetState()
server.StartServer(cfg.Task(), server.Options{
Name: "proxy",
CertProvider: cfg.AutoCertProvider(),
HTTPAddr: common.ProxyHTTPAddr,
HTTPSAddr: common.ProxyHTTPSAddr,
Handler: cfg.EntrypointHandler(),
ACL: cfg.Value().ACL,
SupportProxyProtocol: cfg.Value().Entrypoint.SupportProxyProtocol,
})
}

View File

@@ -1,56 +0,0 @@
package config
import (
config "github.com/yusing/godoxy/internal/config/types"
"github.com/yusing/godoxy/internal/route/provider"
"github.com/yusing/godoxy/internal/types"
)
func (cfg *Config) DumpRouteProviders() map[string]*provider.Provider {
entries := make(map[string]*provider.Provider, cfg.providers.Size())
for _, p := range cfg.providers.Range {
entries[p.ShortName()] = p
}
return entries
}
func (cfg *Config) RouteProviderList() []config.RouteProviderListResponse {
list := make([]config.RouteProviderListResponse, 0, cfg.providers.Size())
for _, p := range cfg.providers.Range {
list = append(list, config.RouteProviderListResponse{
ShortName: p.ShortName(),
FullName: p.String(),
})
}
return list
}
func (cfg *Config) SearchRoute(alias string) types.Route {
for _, p := range cfg.providers.Range {
if r, ok := p.GetRoute(alias); ok {
return r
}
}
return nil
}
func (cfg *Config) Statistics() map[string]any {
var rps, streams types.RouteStats
var total uint16
providerStats := make(map[string]types.ProviderStats)
for _, p := range cfg.providers.Range {
stats := p.Statistics()
providerStats[p.ShortName()] = stats
rps.AddOther(stats.RPs)
streams.AddOther(stats.Streams)
total += stats.RPs.Total + stats.Streams.Total
}
return map[string]any{
"total": total,
"reverse_proxies": rps,
"streams": streams,
"providers": providerStats,
}
}

View File

@@ -0,0 +1,42 @@
package statequery
import (
config "github.com/yusing/godoxy/internal/config/types"
"github.com/yusing/godoxy/internal/types"
)
type RouteProviderListResponse struct {
ShortName string `json:"short_name"`
FullName string `json:"full_name"`
} // @name RouteProvider
func DumpRouteProviders() map[string]types.RouteProvider {
state := config.ActiveState.Load()
entries := make(map[string]types.RouteProvider, state.NumProviders())
for _, p := range state.IterProviders() {
entries[p.ShortName()] = p
}
return entries
}
func RouteProviderList() []RouteProviderListResponse {
state := config.ActiveState.Load()
list := make([]RouteProviderListResponse, 0, state.NumProviders())
for _, p := range state.IterProviders() {
list = append(list, RouteProviderListResponse{
ShortName: p.ShortName(),
FullName: p.String(),
})
}
return list
}
func SearchRoute(alias string) types.Route {
state := config.ActiveState.Load()
for _, p := range state.IterProviders() {
if r, ok := p.GetRoute(alias); ok {
return r
}
}
return nil
}

View File

@@ -0,0 +1,38 @@
package statequery
import (
config "github.com/yusing/godoxy/internal/config/types"
"github.com/yusing/godoxy/internal/types"
)
type Statistics struct {
Total uint16 `json:"total"`
ReverseProxies types.RouteStats `json:"reverse_proxies"`
Streams types.RouteStats `json:"streams"`
Providers map[string]types.ProviderStats `json:"providers"`
}
func GetStatistics() Statistics {
state := config.ActiveState.Load()
var (
rps, streams types.RouteStats
total uint16
providerStats = make(map[string]types.ProviderStats)
)
for _, p := range state.IterProviders() {
stats := p.Statistics()
providerStats[p.ShortName()] = stats
rps.AddOther(stats.RPs)
streams.AddOther(stats.Streams)
total += stats.RPs.Total + stats.Streams.Total
}
return Statistics{
Total: total,
ReverseProxies: rps,
Streams: streams,
Providers: providerStats,
}
}

330
internal/config/state.go Normal file
View File

@@ -0,0 +1,330 @@
package config
import (
"context"
"fmt"
"iter"
"net/http"
"os"
"strconv"
"sync"
"github.com/puzpuzpuz/xsync/v4"
"github.com/rs/zerolog/log"
"github.com/yusing/godoxy/agent/pkg/agent"
"github.com/yusing/godoxy/internal/acl"
"github.com/yusing/godoxy/internal/autocert"
"github.com/yusing/godoxy/internal/common"
config "github.com/yusing/godoxy/internal/config/types"
"github.com/yusing/godoxy/internal/entrypoint"
homepage "github.com/yusing/godoxy/internal/homepage/types"
"github.com/yusing/godoxy/internal/maxmind"
"github.com/yusing/godoxy/internal/notif"
route "github.com/yusing/godoxy/internal/route/provider"
"github.com/yusing/godoxy/internal/serialization"
"github.com/yusing/godoxy/internal/types"
gperr "github.com/yusing/goutils/errs"
"github.com/yusing/goutils/server"
"github.com/yusing/goutils/task"
)
type state struct {
config.Config
providers *xsync.Map[string, types.RouteProvider]
autocertProvider *autocert.Provider
entrypoint entrypoint.Entrypoint
task *task.Task
}
func NewState() config.State {
return &state{
providers: xsync.NewMap[string, types.RouteProvider](),
entrypoint: entrypoint.NewEntrypoint(),
task: task.RootTask("config", false),
}
}
var stateMu sync.RWMutex
func GetState() config.State {
return config.ActiveState.Load()
}
func SetState(state config.State) {
stateMu.Lock()
defer stateMu.Unlock()
cfg := state.Value()
config.ActiveConfig.Store(cfg)
config.ActiveState.Store(state)
acl.ActiveConfig.Store(cfg.ACL)
entrypoint.ActiveConfig.Store(&cfg.Entrypoint)
homepage.ActiveConfig.Store(&cfg.Homepage)
autocert.ActiveProvider.Store(state.AutoCertProvider().(*autocert.Provider))
}
func HasState() bool {
return config.ActiveState.Load() != nil
}
func Value() *config.Config {
return config.ActiveConfig.Load()
}
func (state *state) InitFromFileOrExit(filename string) error {
data, err := os.ReadFile(common.ConfigPath)
if err != nil {
if os.IsNotExist(err) {
log.Warn().Msg("config file not found, using default config")
state.Config = *config.DefaultConfig()
return nil
} else {
gperr.LogFatal("config init error", err)
}
}
return state.Init(data)
}
func (state *state) Init(data []byte) error {
err := serialization.UnmarshalValidateYAML(data, &state.Config)
if err != nil {
return err
}
g := gperr.NewGroup("config load error")
g.Go(state.initMaxMind)
g.Go(state.initProxmox)
g.Go(state.loadRouteProviders)
g.Go(state.initAutoCert)
errs := g.Wait()
// these won't benefit from running on goroutines
errs.Add(state.initNotification())
errs.Add(state.initAccessLogger())
errs.Add(state.initEntrypoint())
// this must be run after loadRouteProviders
errs.Add(state.startRouteProviders())
return errs.Error()
}
func (state *state) Task() *task.Task {
return state.task
}
func (state *state) Context() context.Context {
return state.task.Context()
}
func (state *state) Value() *config.Config {
return &state.Config
}
func (state *state) EntrypointHandler() http.Handler {
return &state.entrypoint
}
// AutoCertProvider returns the autocert provider.
//
// If the autocert provider is not configured, it returns nil.
func (state *state) AutoCertProvider() server.CertProvider {
return state.autocertProvider
}
func (state *state) LoadOrStoreProvider(key string, value types.RouteProvider) (actual types.RouteProvider, loaded bool) {
actual, loaded = state.providers.LoadOrStore(key, value)
return
}
func (state *state) DeleteProvider(key string) {
state.providers.Delete(key)
}
func (state *state) IterProviders() iter.Seq2[string, types.RouteProvider] {
return func(yield func(string, types.RouteProvider) bool) {
for k, v := range state.providers.Range {
if !yield(k, v) {
return
}
}
}
}
func (state *state) NumProviders() int {
return state.providers.Size()
}
// this one is connection level access logger, different from entrypoint access logger
func (state *state) initAccessLogger() error {
if !state.ACL.Valid() {
return nil
}
return state.ACL.Start(state.task)
}
func (state *state) initEntrypoint() error {
epCfg := state.Entrypoint
matchDomains := state.MatchDomains
state.entrypoint.SetFindRouteDomains(matchDomains)
state.entrypoint.SetCatchAllRules(epCfg.Rules.CatchAll)
state.entrypoint.SetNotFoundRules(epCfg.Rules.NotFound)
errs := gperr.NewBuilder("entrypoint error")
errs.Add(state.entrypoint.SetMiddlewares(epCfg.Middlewares))
errs.Add(state.entrypoint.SetAccessLogger(state.task, epCfg.AccessLog))
return errs.Error()
}
func (state *state) initMaxMind() error {
maxmindCfg := state.Providers.MaxMind
if maxmindCfg != nil {
return maxmind.SetInstance(state.task, maxmindCfg)
}
return nil
}
func (state *state) initNotification() error {
notifCfg := state.Providers.Notification
if len(notifCfg) == 0 {
return nil
}
dispatcher := notif.StartNotifDispatcher(state.task)
for _, notifier := range notifCfg {
dispatcher.RegisterProvider(&notifier)
}
return nil
}
func (state *state) initAutoCert() error {
autocertCfg := state.AutoCert
if autocertCfg == nil {
autocertCfg = new(autocert.Config)
}
user, legoCfg, err := autocertCfg.GetLegoConfig()
if err != nil {
return err
}
state.autocertProvider = autocert.NewProvider(autocertCfg, user, legoCfg)
if err := state.autocertProvider.Setup(); err != nil {
return fmt.Errorf("autocert error: %w", err)
} else {
state.autocertProvider.ScheduleRenewal(state.task)
}
return nil
}
func (state *state) initProxmox() error {
proxmoxCfg := state.Providers.Proxmox
if len(proxmoxCfg) == 0 {
return nil
}
errs := gperr.NewBuilder()
for _, cfg := range proxmoxCfg {
if err := cfg.Init(); err != nil {
errs.Add(err.Subject(cfg.URL))
}
}
return errs.Error()
}
func (state *state) storeProvider(p types.RouteProvider) {
state.providers.Store(p.String(), p)
}
func (state *state) loadRouteProviders() error {
providers := &state.Providers
errs := gperr.NewBuilderWithConcurrency("route provider errors")
results := gperr.NewBuilder("loaded route providers")
agent.RemoveAllAgents()
numProviders := len(providers.Agents) + len(providers.Files) + len(providers.Docker)
providersCh := make(chan types.RouteProvider, numProviders)
// start providers concurrently
var providersConsumer sync.WaitGroup
providersConsumer.Go(func() {
for p := range providersCh {
if actual, loaded := state.providers.LoadOrStore(p.String(), p); loaded {
errs.Add(gperr.Errorf("provider %s already exists, first: %s, second: %s", p.String(), actual.GetType(), p.GetType()))
continue
}
state.storeProvider(p)
}
})
var providersProducer sync.WaitGroup
for _, a := range providers.Agents {
providersProducer.Go(func() {
if err := a.Start(state.task.Context()); err != nil {
errs.Add(gperr.PrependSubject(a.String(), err))
return
}
agent.AddAgent(&a)
p := route.NewAgentProvider(&a)
providersCh <- p
})
}
for _, filename := range providers.Files {
providersProducer.Go(func() {
p, err := route.NewFileProvider(filename)
if err != nil {
errs.Add(gperr.PrependSubject(filename, err))
} else {
providersCh <- p
}
})
}
for name, dockerHost := range providers.Docker {
providersProducer.Go(func() {
providersCh <- route.NewDockerProvider(name, dockerHost)
})
}
providersProducer.Wait()
close(providersCh)
providersConsumer.Wait()
lenLongestName := 0
for k := range state.providers.Range {
if len(k) > lenLongestName {
lenLongestName = len(k)
}
}
results.EnableConcurrency()
// load routes concurrently
var providersLoader sync.WaitGroup
for _, p := range state.providers.Range {
providersLoader.Go(func() {
if err := p.LoadRoutes(); err != nil {
errs.Add(err.Subject(p.String()))
}
results.Addf("%-"+strconv.Itoa(lenLongestName)+"s %d routes", p.String(), p.NumRoutes())
})
}
providersLoader.Wait()
log.Info().Msg(results.String())
return errs.Error()
}
func (state *state) startRouteProviders() error {
errs := gperr.NewGroup("provider errors")
for _, p := range state.providers.Range {
errs.Go(func() error {
return p.Start(state.Task())
})
}
return errs.Wait().Error()
}

View File

@@ -1,95 +1,47 @@
package config
import (
"context"
"regexp"
"sync"
"sync/atomic"
"github.com/go-playground/validator/v10"
"github.com/yusing/godoxy/agent/pkg/agent"
"github.com/yusing/godoxy/internal/acl"
"github.com/yusing/godoxy/internal/autocert"
"github.com/yusing/godoxy/internal/logging/accesslog"
entrypoint "github.com/yusing/godoxy/internal/entrypoint/types"
homepage "github.com/yusing/godoxy/internal/homepage/types"
maxmind "github.com/yusing/godoxy/internal/maxmind/types"
"github.com/yusing/godoxy/internal/notif"
"github.com/yusing/godoxy/internal/proxmox"
"github.com/yusing/godoxy/internal/serialization"
"github.com/yusing/godoxy/internal/types"
gperr "github.com/yusing/goutils/errs"
)
type (
Config struct {
ACL *acl.Config `json:"acl"`
AutoCert *autocert.Config `json:"autocert"`
Entrypoint Entrypoint `json:"entrypoint"`
Providers Providers `json:"providers"`
MatchDomains []string `json:"match_domains" validate:"domain_name"`
Homepage HomepageConfig `json:"homepage"`
TimeoutShutdown int `json:"timeout_shutdown" validate:"gte=0"`
ACL *acl.Config `json:"acl"`
AutoCert *autocert.Config `json:"autocert"`
Entrypoint entrypoint.Config `json:"entrypoint"`
Providers Providers `json:"providers"`
MatchDomains []string `json:"match_domains" validate:"domain_name"`
Homepage homepage.Config `json:"homepage"`
TimeoutShutdown int `json:"timeout_shutdown" validate:"gte=0"`
}
Providers struct {
Files []string `json:"include" yaml:"include,omitempty" validate:"dive,filepath"`
Docker map[string]string `json:"docker" yaml:"docker,omitempty" validate:"non_empty_docker_keys,dive,unix_addr|url"`
Agents []*agent.AgentConfig `json:"agents" yaml:"agents,omitempty"`
Agents []agent.AgentConfig `json:"agents" yaml:"agents,omitempty"`
Notification []notif.NotificationConfig `json:"notification" yaml:"notification,omitempty"`
Proxmox []proxmox.Config `json:"proxmox" yaml:"proxmox,omitempty"`
MaxMind *maxmind.Config `json:"maxmind" yaml:"maxmind,omitempty"`
}
Entrypoint struct {
SupportProxyProtocol bool `json:"support_proxy_protocol"`
Middlewares []map[string]any `json:"middlewares"`
AccessLog *accesslog.RequestLoggerConfig `json:"access_log" validate:"omitempty"`
}
HomepageConfig struct {
UseDefaultCategories bool `json:"use_default_categories"`
}
RouteProviderListResponse struct {
ShortName string `json:"short_name"`
FullName string `json:"full_name"`
} // @name RouteProvider
ConfigInstance interface {
Value() *Config
Reload() gperr.Error
Statistics() map[string]any
RouteProviderList() []RouteProviderListResponse
SearchRoute(alias string) types.Route
Context() context.Context
VerifyNewAgent(host string, ca agent.PEMPair, client agent.PEMPair, containerRuntime agent.ContainerRuntime) (int, gperr.Error)
AutoCertProvider() *autocert.Provider
}
)
var (
instance ConfigInstance
instanceMu sync.RWMutex
)
// nil-safe
var ActiveConfig atomic.Pointer[Config]
func DefaultConfig() *Config {
return &Config{
TimeoutShutdown: 3,
Homepage: HomepageConfig{
UseDefaultCategories: true,
},
}
}
func GetInstance() ConfigInstance {
instanceMu.RLock()
defer instanceMu.RUnlock()
return instance
}
func SetInstance(cfg ConfigInstance) {
instanceMu.Lock()
defer instanceMu.Unlock()
instance = cfg
}
func HasInstance() bool {
instanceMu.RLock()
defer instanceMu.RUnlock()
return instance != nil
func init() {
ActiveConfig.Store(DefaultConfig())
}
func Validate(data []byte) gperr.Error {
@@ -97,6 +49,15 @@ func Validate(data []byte) gperr.Error {
return serialization.UnmarshalValidateYAML(data, &model)
}
func DefaultConfig() *Config {
return &Config{
TimeoutShutdown: 3,
Homepage: homepage.Config{
UseDefaultCategories: true,
},
}
}
var matchDomainsRegex = regexp.MustCompile(`^[^\.]?([\w\d\-_]\.?)+[^\.]?$`)
func init() {

View File

@@ -0,0 +1,34 @@
package config
import (
"context"
"iter"
"net/http"
"github.com/yusing/godoxy/internal/types"
"github.com/yusing/godoxy/internal/utils/atomic"
"github.com/yusing/goutils/server"
"github.com/yusing/goutils/task"
)
type State interface {
// InitFromFileOrExit logs and exits on critical errors, non-critical ones will be returned
InitFromFileOrExit(filename string) error
Init(data []byte) error
Task() *task.Task
Context() context.Context
Value() *Config
EntrypointHandler() http.Handler
AutoCertProvider() server.CertProvider
LoadOrStoreProvider(key string, value types.RouteProvider) (actual types.RouteProvider, loaded bool)
DeleteProvider(key string)
IterProviders() iter.Seq2[string, types.RouteProvider]
NumProviders() int
}
// could be nil
var ActiveState atomic.Value[State]

View File

@@ -1,10 +1,9 @@
package entrypoint
import (
"errors"
"fmt"
"net/http"
"strings"
"sync/atomic"
"github.com/rs/zerolog/log"
"github.com/yusing/godoxy/internal/logging/accesslog"
@@ -16,15 +15,21 @@ import (
)
type Entrypoint struct {
middleware *middleware.Middleware
accessLogger *accesslog.AccessLogger
findRouteFunc func(host string) (types.HTTPRoute, error)
middleware *middleware.Middleware
accessLogger *accesslog.AccessLogger
findRouteFunc func(host string) types.HTTPRoute
}
var ErrNoSuchRoute = errors.New("no such route")
// nil-safe
var ActiveConfig atomic.Pointer[entrypoint.Config]
func NewEntrypoint() *Entrypoint {
return &Entrypoint{
func init() {
// make sure it's not nil
ActiveConfig.Store(&entrypoint.Config{})
}
func NewEntrypoint() Entrypoint {
return Entrypoint{
findRouteFunc: findRouteAnyDomain,
}
}
@@ -72,8 +77,10 @@ func (ep *Entrypoint) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w = accesslog.NewResponseRecorder(w)
defer ep.accessLogger.Log(r, w.(*accesslog.ResponseRecorder).Response())
}
route, err := ep.findRouteFunc(r.Host)
if err == nil {
route := ep.findRouteFunc(r.Host)
switch {
case route != nil:
r = routes.WithRouteContext(r, route)
if ep.middleware != nil {
ep.middleware.ServeHTTP(route.ServeHTTP, w, r)
@@ -87,11 +94,11 @@ func (ep *Entrypoint) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Then scraper / scanners will know the subdomain is invalid.
// With StatusNotFound, they won't know whether it's the path, or the subdomain that is invalid.
if served := middleware.ServeStaticErrorPageFile(w, r); !served {
log.Err(err).
log.Error().
Str("method", r.Method).
Str("url", r.URL.String()).
Str("remote", r.RemoteAddr).
Msg("request")
Msgf("not found: %s", r.Host)
errorPage, ok := errorpage.GetErrorPageByStatus(http.StatusNotFound)
if ok {
w.WriteHeader(http.StatusNotFound)
@@ -100,39 +107,39 @@ func (ep *Entrypoint) ServeHTTP(w http.ResponseWriter, r *http.Request) {
log.Err(err).Msg("failed to write error page")
}
} else {
http.Error(w, err.Error(), http.StatusNotFound)
http.NotFound(w, r)
}
}
}
func findRouteAnyDomain(host string) (types.HTTPRoute, error) {
func findRouteAnyDomain(host string) types.HTTPRoute {
idx := strings.IndexByte(host, '.')
if idx != -1 {
target := host[:idx]
if r, ok := routes.HTTP.Get(target); ok {
return r, nil
return r
}
}
if r, ok := routes.HTTP.Get(host); ok {
return r, nil
return r
}
return nil, fmt.Errorf("%w: %s", ErrNoSuchRoute, host)
return nil
}
func findRouteByDomains(domains []string) func(host string) (types.HTTPRoute, error) {
return func(host string) (types.HTTPRoute, error) {
func findRouteByDomains(domains []string) func(host string) types.HTTPRoute {
return func(host string) types.HTTPRoute {
for _, domain := range domains {
if target, ok := strings.CutSuffix(host, domain); ok {
if r, ok := routes.HTTP.Get(target); ok {
return r, nil
return r
}
}
}
// fallback to exact match
if r, ok := routes.HTTP.Get(host); ok {
return r, nil
return r
}
return nil, fmt.Errorf("%w: %s", ErrNoSuchRoute, host)
return nil
}
}

View File

@@ -29,16 +29,15 @@ func run(t *testing.T, match []string, noMatch []string) {
for _, test := range match {
t.Run(test, func(t *testing.T) {
found, err := ep.findRouteFunc(test)
expect.NoError(t, err)
found := ep.findRouteFunc(test)
expect.NotNil(t, found)
})
}
for _, test := range noMatch {
t.Run(test, func(t *testing.T) {
_, err := ep.findRouteFunc(test)
expect.ErrorIs(t, ErrNoSuchRoute, err)
found := ep.findRouteFunc(test)
expect.Nil(t, found)
})
}
}

View File

@@ -0,0 +1,16 @@
package entrypoint
import (
"github.com/yusing/godoxy/internal/logging/accesslog"
"github.com/yusing/godoxy/internal/route/rules"
)
type Config struct {
SupportProxyProtocol bool `json:"support_proxy_protocol"`
Rules struct {
CatchAll rules.Rules `json:"catch_all"`
NotFound rules.Rules `json:"not_found"`
} `json:"rules"`
Middlewares []map[string]any `json:"middlewares"`
AccessLog *accesslog.RequestLoggerConfig `json:"access_log" validate:"omitempty"`
}

View File

@@ -1,13 +1,16 @@
package idlewatcher
import (
"context"
"fmt"
"net/http"
"strconv"
api "github.com/yusing/godoxy/internal/api/v1"
"github.com/yusing/godoxy/internal/homepage"
httputils "github.com/yusing/goutils/http"
"github.com/yusing/goutils/http/httpheaders"
_ "unsafe"
)
type ForceCacheControl struct {
@@ -44,6 +47,9 @@ func isFaviconPath(path string) bool {
return path == "/favicon.ico"
}
//go:linkname GetFavIconFromAlias v1.GetFavIconFromAlias
func GetFavIconFromAlias(ctx context.Context, alias string) (homepage.FetchResult, error)
func (w *Watcher) redirectToStartEndpoint(rw http.ResponseWriter, r *http.Request) {
uri := "/"
if w.cfg.StartEndpoint != "" {
@@ -62,7 +68,7 @@ func (w *Watcher) wakeFromHTTP(rw http.ResponseWriter, r *http.Request) (shouldN
// handle favicon request
if isFaviconPath(r.URL.Path) {
result, err := api.GetFavIconFromAlias(r.Context(), w.route.Name())
result, err := GetFavIconFromAlias(r.Context(), w.route.Name())
if err != nil {
rw.WriteHeader(result.StatusCode)
fmt.Fprint(rw, err)

View File

@@ -8,7 +8,7 @@ import (
"github.com/bytedance/sonic"
"github.com/lithammer/fuzzysearch/fuzzy"
config "github.com/yusing/godoxy/internal/config/types"
statequery "github.com/yusing/godoxy/internal/config/query"
"github.com/yusing/godoxy/internal/metrics/period"
metricsutils "github.com/yusing/godoxy/internal/metrics/utils"
"github.com/yusing/godoxy/internal/route/routes"
@@ -133,7 +133,7 @@ func (rs RouteStatuses) aggregate(limit int, offset int) Aggregated {
r, ok := routes.Get(alias)
if !ok {
// also search for excluded routes
r = config.GetInstance().SearchRoute(alias)
r = statequery.SearchRoute(alias)
}
if r != nil {
displayName = r.DisplayName()

View File

@@ -25,7 +25,7 @@ type (
FieldsBody []LogField
ListBody []string
MessageBody string
ErrorBody struct {
errorBody struct {
Error error
}
)
@@ -40,6 +40,10 @@ func MakeLogFields(fields ...LogField) LogBody {
return FieldsBody(fields)
}
func ErrorBody(err error) LogBody {
return errorBody{Error: err}
}
func (f *LogFormat) Parse(format string) error {
switch format {
case "":
@@ -116,7 +120,7 @@ func (m MessageBody) Format(format *LogFormat) ([]byte, error) {
return m.Format(LogFormatMarkdown)
}
func (e ErrorBody) Format(format *LogFormat) ([]byte, error) {
func (e errorBody) Format(format *LogFormat) ([]byte, error) {
switch format {
case LogFormatRawJSON:
return sonic.Marshal(e.Error)

View File

@@ -6,7 +6,6 @@ import (
"github.com/bytedance/sonic"
"github.com/luthermonson/go-proxmox"
"github.com/yusing/godoxy/internal/utils/pool"
)
type Client struct {
@@ -15,8 +14,6 @@ type Client struct {
Version *proxmox.Version
}
var Clients = pool.New[*Client]("proxmox_clients")
func NewClient(baseUrl string, opts ...proxmox.Option) *Client {
return &Client{Client: proxmox.NewClient(baseUrl, opts...)}
}

View File

@@ -6,7 +6,9 @@ import (
"github.com/yusing/godoxy/agent/pkg/agent"
"github.com/yusing/godoxy/internal/homepage"
nettypes "github.com/yusing/godoxy/internal/net/types"
provider "github.com/yusing/godoxy/internal/route/provider/types"
"github.com/yusing/godoxy/internal/utils/pool"
gperr "github.com/yusing/goutils/errs"
"github.com/yusing/goutils/http/reverseproxy"
"github.com/yusing/goutils/task"
)
@@ -55,9 +57,15 @@ type (
Stream() nettypes.Stream
}
RouteProvider interface {
Start(task.Parent) gperr.Error
LoadRoutes() gperr.Error
GetRoute(alias string) (r Route, ok bool)
IterRoutes(yield func(alias string, r Route) bool)
NumRoutes() int
FindService(project, service string) (r Route, ok bool)
Statistics() ProviderStats
GetType() provider.Type
ShortName() string
String() string
}
)