mirror of
https://github.com/yusing/godoxy.git
synced 2026-04-28 19:58:02 +02:00
v0.26.0
This commit is contained in:
@@ -14,10 +14,12 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/yusing/godoxy/internal/agentpool"
|
||||
config "github.com/yusing/godoxy/internal/config/types"
|
||||
"github.com/yusing/godoxy/internal/docker"
|
||||
entrypoint "github.com/yusing/godoxy/internal/entrypoint/types"
|
||||
"github.com/yusing/godoxy/internal/health/monitor"
|
||||
"github.com/yusing/godoxy/internal/homepage"
|
||||
iconlist "github.com/yusing/godoxy/internal/homepage/icons/list"
|
||||
@@ -33,7 +35,6 @@ import (
|
||||
|
||||
"github.com/yusing/godoxy/internal/common"
|
||||
"github.com/yusing/godoxy/internal/logging/accesslog"
|
||||
"github.com/yusing/godoxy/internal/route/routes"
|
||||
"github.com/yusing/godoxy/internal/route/rules"
|
||||
rulepresets "github.com/yusing/godoxy/internal/route/rules/presets"
|
||||
route "github.com/yusing/godoxy/internal/route/types"
|
||||
@@ -46,7 +47,6 @@ type (
|
||||
Host string `json:"host,omitempty"`
|
||||
Port route.Port `json:"port"`
|
||||
|
||||
// for TCP and UDP routes, bind address to listen on
|
||||
Bind string `json:"bind,omitempty" validate:"omitempty,ip_addr" extensions:"x-nullable"`
|
||||
|
||||
Root string `json:"root,omitempty"`
|
||||
@@ -57,7 +57,7 @@ type (
|
||||
PathPatterns []string `json:"path_patterns,omitempty" extensions:"x-nullable"`
|
||||
Rules rules.Rules `json:"rules,omitempty" extensions:"x-nullable"`
|
||||
RuleFile string `json:"rule_file,omitempty" extensions:"x-nullable"`
|
||||
HealthCheck types.HealthCheckConfig `json:"healthcheck,omitempty" extensions:"x-nullable"` // null on load-balancer routes
|
||||
HealthCheck types.HealthCheckConfig `json:"healthcheck,omitzero" extensions:"x-nullable"` // null on load-balancer routes
|
||||
LoadBalance *types.LoadBalancerConfig `json:"load_balance,omitempty" extensions:"x-nullable"`
|
||||
Middlewares map[string]types.LabelMap `json:"middlewares,omitempty" extensions:"x-nullable"`
|
||||
Homepage *homepage.ItemConfig `json:"homepage"`
|
||||
@@ -108,17 +108,17 @@ type (
|
||||
)
|
||||
|
||||
type lockedError struct {
|
||||
err gperr.Error
|
||||
err error
|
||||
lock sync.Mutex
|
||||
}
|
||||
|
||||
func (le *lockedError) Get() gperr.Error {
|
||||
func (le *lockedError) Get() error {
|
||||
le.lock.Lock()
|
||||
defer le.lock.Unlock()
|
||||
return le.err
|
||||
}
|
||||
|
||||
func (le *lockedError) Set(err gperr.Error) {
|
||||
func (le *lockedError) Set(err error) {
|
||||
le.lock.Lock()
|
||||
defer le.lock.Unlock()
|
||||
le.err = err
|
||||
@@ -131,7 +131,7 @@ func (r Routes) Contains(alias string) bool {
|
||||
return ok
|
||||
}
|
||||
|
||||
func (r *Route) Validate() gperr.Error {
|
||||
func (r *Route) Validate() error {
|
||||
// wait for alias to be set
|
||||
if r.Alias == "" {
|
||||
return nil
|
||||
@@ -150,13 +150,13 @@ func (r *Route) Validate() gperr.Error {
|
||||
return r.valErr.Get()
|
||||
}
|
||||
|
||||
func (r *Route) validate() gperr.Error {
|
||||
func (r *Route) validate() error {
|
||||
// if strings.HasPrefix(r.Alias, "godoxy") {
|
||||
// log.Debug().Any("route", r).Msg("validating route")
|
||||
// }
|
||||
if r.Agent != "" {
|
||||
if r.Container != nil {
|
||||
return gperr.Errorf("specifying agent is not allowed for docker container routes")
|
||||
return errors.New("specifying agent is not allowed for docker container routes")
|
||||
}
|
||||
var ok bool
|
||||
// by agent address
|
||||
@@ -165,7 +165,7 @@ func (r *Route) validate() gperr.Error {
|
||||
// fallback to get agent by name
|
||||
r.agent, ok = agentpool.GetAgent(r.Agent)
|
||||
if !ok {
|
||||
return gperr.Errorf("agent %s not found", r.Agent)
|
||||
return fmt.Errorf("agent %s not found", r.Agent)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -200,7 +200,11 @@ func (r *Route) validate() gperr.Error {
|
||||
|
||||
if (r.Proxmox == nil || r.Proxmox.Node == "" || r.Proxmox.VMID == nil) && r.Container == nil {
|
||||
wasNotNil := r.Proxmox != nil
|
||||
proxmoxProviders := config.WorkingState.Load().Value().Providers.Proxmox
|
||||
workingState := config.WorkingState.Load()
|
||||
var proxmoxProviders []*proxmox.Config
|
||||
if workingState != nil { // nil in tests
|
||||
proxmoxProviders = workingState.Value().Providers.Proxmox
|
||||
}
|
||||
if len(proxmoxProviders) > 0 {
|
||||
// it's fine if ip is nil
|
||||
hostname := r.Host
|
||||
@@ -208,40 +212,34 @@ func (r *Route) validate() gperr.Error {
|
||||
for _, p := range proxmoxProviders {
|
||||
// First check if hostname, IP, or alias matches a node (node-level route)
|
||||
if nodeName := p.Client().ReverseLookupNode(hostname, ip, r.Alias); nodeName != "" {
|
||||
zero := 0
|
||||
zero := uint64(0)
|
||||
if r.Proxmox == nil {
|
||||
r.Proxmox = &proxmox.NodeConfig{}
|
||||
}
|
||||
r.Proxmox.Node = nodeName
|
||||
r.Proxmox.VMID = &zero
|
||||
r.Proxmox.VMName = ""
|
||||
log.Info().
|
||||
Str("node", nodeName).
|
||||
Msgf("found proxmox node for route %q", r.Alias)
|
||||
log.Info().EmbedObject(r).Msg("found proxmox node")
|
||||
break
|
||||
}
|
||||
|
||||
// Then check if hostname, IP, or alias matches a VM resource
|
||||
resource, _ := p.Client().ReverseLookupResource(ip, hostname, r.Alias)
|
||||
if resource != nil {
|
||||
vmid := int(resource.VMID)
|
||||
vmid := resource.VMID
|
||||
if r.Proxmox == nil {
|
||||
r.Proxmox = &proxmox.NodeConfig{}
|
||||
}
|
||||
r.Proxmox.Node = resource.Node
|
||||
r.Proxmox.VMID = &vmid
|
||||
r.Proxmox.VMName = resource.Name
|
||||
log.Info().
|
||||
Str("node", resource.Node).
|
||||
Int("vmid", int(resource.VMID)).
|
||||
Str("vmname", resource.Name).
|
||||
Msgf("found proxmox resource for route %q", r.Alias)
|
||||
log.Info().EmbedObject(r).Msg("found proxmox resource")
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if wasNotNil && (r.Proxmox.Node == "" || r.Proxmox.VMID == nil) {
|
||||
log.Warn().Msgf("no proxmox node / resource found for route %q", r.Alias)
|
||||
log.Warn().EmbedObject(r).Msg("no proxmox node / resource found")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -260,7 +258,7 @@ func (r *Route) validate() gperr.Error {
|
||||
switch r.Port.Proxy {
|
||||
case common.ProxyHTTPPort, common.ProxyHTTPSPort, common.APIHTTPPort:
|
||||
if r.Scheme.IsReverseProxy() || r.Scheme == route.SchemeTCP {
|
||||
return gperr.Errorf("localhost:%d is reserved for godoxy", r.Port.Proxy)
|
||||
return fmt.Errorf("localhost:%d is reserved for godoxy", r.Port.Proxy)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -271,27 +269,19 @@ func (r *Route) validate() gperr.Error {
|
||||
errs.Add(err)
|
||||
}
|
||||
|
||||
var impl types.Route
|
||||
var err gperr.Error
|
||||
|
||||
switch r.Scheme {
|
||||
case route.SchemeFileServer:
|
||||
r.Host = ""
|
||||
r.Port.Proxy = 0
|
||||
r.ProxyURL = gperr.Collect(&errs, nettypes.ParseURL, "file://"+r.Root)
|
||||
case route.SchemeHTTP, route.SchemeHTTPS, route.SchemeH2C:
|
||||
if r.Port.Listening != 0 {
|
||||
errs.Addf("unexpected listening port for %s scheme", r.Scheme)
|
||||
}
|
||||
if r.ShouldExclude() {
|
||||
r.ProxyURL = gperr.Collect(&errs, nettypes.ParseURL, fmt.Sprintf("%s://%s", r.Scheme, net.JoinHostPort(r.Host, strconv.Itoa(r.Port.Proxy))))
|
||||
case route.SchemeTCP, route.SchemeUDP:
|
||||
if r.ShouldExclude() {
|
||||
// should exclude, we don't care the scheme here.
|
||||
} else {
|
||||
switch r.Scheme {
|
||||
case route.SchemeFileServer:
|
||||
r.Host = ""
|
||||
r.Port.Proxy = 0
|
||||
r.LisURL = gperr.Collect(&errs, nettypes.ParseURL, "https://"+net.JoinHostPort(r.Bind, strconv.Itoa(r.Port.Listening)))
|
||||
r.ProxyURL = gperr.Collect(&errs, nettypes.ParseURL, "file://"+r.Root)
|
||||
case route.SchemeHTTP, route.SchemeHTTPS, route.SchemeH2C:
|
||||
r.LisURL = gperr.Collect(&errs, nettypes.ParseURL, "https://"+net.JoinHostPort(r.Bind, strconv.Itoa(r.Port.Listening)))
|
||||
r.ProxyURL = gperr.Collect(&errs, nettypes.ParseURL, fmt.Sprintf("%s://%s", r.Scheme, net.JoinHostPort(r.Host, strconv.Itoa(r.Port.Proxy))))
|
||||
} else {
|
||||
if r.Bind == "" {
|
||||
r.Bind = "0.0.0.0"
|
||||
}
|
||||
case route.SchemeTCP, route.SchemeUDP:
|
||||
bindIP := net.ParseIP(r.Bind)
|
||||
remoteIP := net.ParseIP(r.Host)
|
||||
toNetwork := func(ip net.IP, scheme route.Scheme) string {
|
||||
@@ -325,6 +315,8 @@ func (r *Route) validate() gperr.Error {
|
||||
return errs.Error()
|
||||
}
|
||||
|
||||
var impl types.Route
|
||||
var err error
|
||||
switch r.Scheme {
|
||||
case route.SchemeFileServer:
|
||||
impl, err = NewFileServer(r)
|
||||
@@ -360,8 +352,8 @@ func (r *Route) validateRules() error {
|
||||
return errors.New("rule preset `webui.yml` not found")
|
||||
}
|
||||
r.Rules = rules
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if r.RuleFile != "" && len(r.Rules) > 0 {
|
||||
@@ -397,7 +389,7 @@ func (r *Route) validateRules() error {
|
||||
}
|
||||
|
||||
func (r *Route) validateProxmox() {
|
||||
l := log.With().Str("route", r.Alias).Logger()
|
||||
l := log.With().EmbedObject(r).Logger()
|
||||
|
||||
nodeName := r.Proxmox.Node
|
||||
vmid := r.Proxmox.VMID
|
||||
@@ -426,7 +418,7 @@ func (r *Route) validateProxmox() {
|
||||
} else {
|
||||
res, err := node.Client().GetResource("lxc", *vmid)
|
||||
if err != nil { // ErrResourceNotFound
|
||||
l.Err(err).Msgf("failed to get resource %d", *vmid)
|
||||
l.Error().Err(err).Msgf("failed to get resource %d", *vmid)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -445,24 +437,22 @@ func (r *Route) validateProxmox() {
|
||||
return
|
||||
}
|
||||
|
||||
l = l.With().Str("container", containerName).Logger()
|
||||
|
||||
l.Info().Msgf("checking if container is running")
|
||||
l.Info().Str("container", containerName).Msg("checking if container is running")
|
||||
running, err := node.LXCIsRunning(ctx, *vmid)
|
||||
if err != nil {
|
||||
l.Err(err).Msgf("failed to check container state")
|
||||
l.Error().Err(err).Msgf("failed to check container state")
|
||||
return
|
||||
}
|
||||
|
||||
if !running {
|
||||
l.Info().Msgf("starting container")
|
||||
l.Info().Msg("starting container")
|
||||
if err := node.LXCAction(ctx, *vmid, proxmox.LXCStart); err != nil {
|
||||
l.Err(err).Msgf("failed to start container")
|
||||
l.Error().Err(err).Msg("failed to start container")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
l.Info().Msgf("finding reachable ip addresses")
|
||||
l.Info().Msg("finding reachable ip addresses")
|
||||
errs := gperr.NewBuilder("failed to find reachable ip addresses")
|
||||
for _, ip := range ips {
|
||||
if err := netutils.PingTCP(ctx, ip, r.Port.Proxy); err != nil {
|
||||
@@ -488,23 +478,23 @@ func (r *Route) Task() *task.Task {
|
||||
return r.task
|
||||
}
|
||||
|
||||
func (r *Route) Start(parent task.Parent) gperr.Error {
|
||||
func (r *Route) Start(parent task.Parent) error {
|
||||
r.onceStart.Do(func() {
|
||||
r.startErr.Set(r.start(parent))
|
||||
})
|
||||
return r.startErr.Get()
|
||||
}
|
||||
|
||||
func (r *Route) start(parent task.Parent) gperr.Error {
|
||||
func (r *Route) start(parent task.Parent) error {
|
||||
if r.impl == nil { // should not happen
|
||||
return gperr.New("route not initialized")
|
||||
return errors.New("route not initialized")
|
||||
}
|
||||
defer close(r.started)
|
||||
|
||||
// skip checking for excluded routes
|
||||
excluded := r.ShouldExclude()
|
||||
if !excluded {
|
||||
if err := checkExists(r); err != nil {
|
||||
if err := checkExists(parent.Context(), r); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -518,15 +508,23 @@ func (r *Route) start(parent task.Parent) gperr.Error {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
r.task = parent.Subtask("excluded."+r.Name(), true)
|
||||
routes.Excluded.Add(r.impl)
|
||||
ep := entrypoint.FromCtx(parent.Context())
|
||||
if ep == nil {
|
||||
return errors.New("entrypoint not initialized")
|
||||
}
|
||||
|
||||
r.task = parent.Subtask("excluded."+r.Name(), false)
|
||||
r.task.SetValue(monitor.DisplayNameKey{}, r.DisplayName())
|
||||
ep.ExcludedRoutes().Add(r.impl)
|
||||
r.task.OnCancel("remove_route_from_excluded", func() {
|
||||
routes.Excluded.Del(r.impl)
|
||||
ep.ExcludedRoutes().Del(r.impl)
|
||||
})
|
||||
if r.UseHealthCheck() {
|
||||
r.HealthMon = monitor.NewMonitor(r.impl)
|
||||
err := r.HealthMon.Start(r.task)
|
||||
return err
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
@@ -564,6 +562,10 @@ func (r *Route) ProviderName() string {
|
||||
return r.Provider
|
||||
}
|
||||
|
||||
func (r *Route) ListenURL() *nettypes.URL {
|
||||
return r.LisURL
|
||||
}
|
||||
|
||||
func (r *Route) TargetURL() *nettypes.URL {
|
||||
return r.ProxyURL
|
||||
}
|
||||
@@ -587,10 +589,9 @@ func (r *Route) References() []string {
|
||||
return []string{r.Proxmox.VMName, aliasRef, r.Proxmox.Services[0]}
|
||||
}
|
||||
return []string{r.Proxmox.Services[0], aliasRef}
|
||||
} else {
|
||||
if r.Proxmox.VMName != aliasRef {
|
||||
return []string{r.Proxmox.VMName, aliasRef}
|
||||
}
|
||||
}
|
||||
if r.Proxmox.VMName != aliasRef {
|
||||
return []string{r.Proxmox.VMName, aliasRef}
|
||||
}
|
||||
}
|
||||
return []string{aliasRef}
|
||||
@@ -678,6 +679,44 @@ func (r *Route) DisplayName() string {
|
||||
return r.Homepage.Name
|
||||
}
|
||||
|
||||
func (r *Route) MarshalZerologObject(e *zerolog.Event) {
|
||||
e.Str("alias", r.Alias)
|
||||
switch r := r.impl.(type) {
|
||||
case *ReverseProxyRoute:
|
||||
e.Str("type", "reverse_proxy").
|
||||
Str("scheme", r.Scheme.String()).
|
||||
Str("bind", r.LisURL.Host).
|
||||
Str("target", r.ProxyURL.URL.String())
|
||||
case *FileServer:
|
||||
e.Str("type", "file_server").
|
||||
Str("root", r.Root)
|
||||
case *StreamRoute:
|
||||
e.Str("type", "stream").
|
||||
Str("scheme", r.LisURL.Scheme+"->"+r.ProxyURL.Scheme)
|
||||
if r.stream != nil {
|
||||
// listening port could be zero (random),
|
||||
// use LocalAddr() to get the actual listening host+port.
|
||||
e.Str("bind", r.stream.LocalAddr().String())
|
||||
} else {
|
||||
// not yet started
|
||||
e.Str("bind", r.LisURL.Host)
|
||||
}
|
||||
e.Str("target", r.ProxyURL.URL.String())
|
||||
}
|
||||
if r.Proxmox != nil {
|
||||
e.Str("proxmox", r.Proxmox.Node)
|
||||
if r.Proxmox.VMID != nil {
|
||||
e.Uint64("vmid", *r.Proxmox.VMID)
|
||||
}
|
||||
if r.Proxmox.VMName != "" {
|
||||
e.Str("vmname", r.Proxmox.VMName)
|
||||
}
|
||||
}
|
||||
if r.Container != nil {
|
||||
e.Str("container", r.Container.ContainerName)
|
||||
}
|
||||
}
|
||||
|
||||
// PreferOver implements pool.Preferable to resolve duplicate route keys deterministically.
|
||||
// Preference policy:
|
||||
// - Prefer routes with rules over routes without rules.
|
||||
@@ -689,7 +728,7 @@ func (r *Route) PreferOver(other any) bool {
|
||||
switch v := other.(type) {
|
||||
case *Route:
|
||||
or = v
|
||||
case *ReveseProxyRoute:
|
||||
case *ReverseProxyRoute:
|
||||
or = v.Route
|
||||
case *FileServer:
|
||||
or = v.Route
|
||||
@@ -932,6 +971,13 @@ func (r *Route) Finalize() {
|
||||
}
|
||||
}
|
||||
|
||||
switch r.Scheme {
|
||||
case route.SchemeTCP, route.SchemeUDP:
|
||||
if r.Bind == "" {
|
||||
r.Bind = "0.0.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
r.Port.Listening, r.Port.Proxy = lp, pp
|
||||
|
||||
workingState := config.WorkingState.Load()
|
||||
@@ -942,7 +988,8 @@ func (r *Route) Finalize() {
|
||||
panic("bug: working state is nil")
|
||||
}
|
||||
|
||||
r.HealthCheck.ApplyDefaults(config.WorkingState.Load().Value().Defaults.HealthCheck)
|
||||
// TODO: default value from context
|
||||
r.HealthCheck.ApplyDefaults(workingState.Value().Defaults.HealthCheck)
|
||||
}
|
||||
|
||||
func (r *Route) FinalizeHomepageConfig() {
|
||||
|
||||
Reference in New Issue
Block a user