Files
godoxy-yusing/internal/route/route.go
yusing b3d4255868 refactor(types): decouple Proxmox config from proxmox package
Decouple the types package from the internal/proxmox package by defining
a standalone ProxmoxConfig struct. This reduces circular dependencies
and allows the types package to define its own configuration structures
without importing the proxmox package.

The route validation logic now converts between types.ProxmoxConfig and
proxmox.NodeConfig where needed for internal operations.
2026-01-25 17:19:25 +08:00

1004 lines
26 KiB
Go

package route
import (
"context"
"errors"
"fmt"
"net"
"net/url"
"os"
"reflect"
"runtime"
"strconv"
"strings"
"sync"
"time"
"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"
"github.com/yusing/godoxy/internal/health/monitor"
"github.com/yusing/godoxy/internal/homepage"
iconlist "github.com/yusing/godoxy/internal/homepage/icons/list"
homepagecfg "github.com/yusing/godoxy/internal/homepage/types"
netutils "github.com/yusing/godoxy/internal/net"
nettypes "github.com/yusing/godoxy/internal/net/types"
"github.com/yusing/godoxy/internal/proxmox"
"github.com/yusing/godoxy/internal/serialization"
"github.com/yusing/godoxy/internal/types"
gperr "github.com/yusing/goutils/errs"
strutils "github.com/yusing/goutils/strings"
"github.com/yusing/goutils/task"
"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"
)
type (
Route struct {
Alias string `json:"alias"`
Scheme route.Scheme `json:"scheme,omitempty" swaggertype:"string" enums:"http,https,h2c,tcp,udp,fileserver"`
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"`
SPA bool `json:"spa,omitempty"` // Single-page app mode: serves index for non-existent paths
Index string `json:"index,omitempty"` // Index file to serve for single-page app mode
route.HTTPConfig
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
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"`
AccessLog *accesslog.RequestLoggerConfig `json:"access_log,omitempty" extensions:"x-nullable"`
Agent string `json:"agent,omitempty"`
Proxmox *proxmox.NodeConfig `json:"proxmox,omitempty" extensions:"x-nullable"`
Idlewatcher *types.IdlewatcherConfig `json:"idlewatcher,omitempty" extensions:"x-nullable"`
Metadata `deserialize:"-"`
}
Metadata struct {
/* Docker only */
Container *types.Container `json:"container,omitempty" extensions:"x-nullable"`
Provider string `json:"provider,omitempty" extensions:"x-nullable"` // for backward compatibility
// private fields
LisURL *nettypes.URL `json:"lurl,omitempty" swaggertype:"string" extensions:"x-nullable"`
ProxyURL *nettypes.URL `json:"purl,omitempty" swaggertype:"string"`
Excluded bool `json:"excluded,omitempty" extensions:"x-nullable"`
ExcludedReason ExcludedReason `json:"excluded_reason,omitempty" swaggertype:"string" extensions:"x-nullable"`
HealthMon types.HealthMonitor `json:"health,omitempty" swaggerignore:"true"`
// for swagger
HealthJSON *types.HealthJSON `json:",omitempty" form:"health"`
impl types.Route
task *task.Task
// ensure err is read after validation or start
valErr lockedError
startErr lockedError
provider types.RouteProvider
agent *agentpool.Agent
started chan struct{}
onceStart sync.Once
onceValidate sync.Once
}
Routes map[string]*Route
Port = route.Port
)
type lockedError struct {
err gperr.Error
lock sync.Mutex
}
func (le *lockedError) Get() gperr.Error {
le.lock.Lock()
defer le.lock.Unlock()
return le.err
}
func (le *lockedError) Set(err gperr.Error) {
le.lock.Lock()
defer le.lock.Unlock()
le.err = err
}
const DefaultHost = "localhost"
func (r Routes) Contains(alias string) bool {
_, ok := r[alias]
return ok
}
func (r *Route) Validate() gperr.Error {
// wait for alias to be set
if r.Alias == "" {
return nil
}
// pcs := make([]uintptr, 1)
// runtime.Callers(2, pcs)
// f := runtime.FuncForPC(pcs[0])
// fname := f.Name()
r.onceValidate.Do(func() {
// filename, line := f.FileLine(pcs[0])
// if strings.HasPrefix(r.Alias, "godoxy") {
// log.Debug().Str("route", r.Alias).Str("caller", fname).Str("file", filename).Int("line", line).Msg("validating route")
// }
r.valErr.Set(r.validate())
})
return r.valErr.Get()
}
func (r *Route) validate() gperr.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")
}
var ok bool
// by agent address
r.agent, ok = agentpool.Get(r.Agent)
if !ok {
// fallback to get agent by name
r.agent, ok = agentpool.GetAgent(r.Agent)
if !ok {
return gperr.Errorf("agent %s not found", r.Agent)
}
}
}
r.Finalize()
r.started = make(chan struct{})
// close the channel when the route is destroyed (if not closed yet).
runtime.AddCleanup(r, func(ch chan struct{}) {
select {
case <-ch:
default:
close(ch)
}
}, r.started)
if r.Proxmox != nil && r.Idlewatcher != nil {
r.Idlewatcher.Proxmox = &types.ProxmoxConfig{
Node: r.Proxmox.Node,
VMID: r.Proxmox.VMID,
}
}
if r.Proxmox == nil && r.Idlewatcher != nil && r.Idlewatcher.Proxmox != nil {
r.Proxmox = &proxmox.NodeConfig{
Node: r.Idlewatcher.Proxmox.Node,
VMID: r.Idlewatcher.Proxmox.VMID,
}
}
if r.Proxmox != nil {
nodeName := r.Proxmox.Node
vmid := r.Proxmox.VMID
if nodeName == "" {
return gperr.Errorf("node (proxmox node name) is required")
}
node, ok := proxmox.Nodes.Get(nodeName)
if !ok {
return gperr.Errorf("proxmox node %s not found in pool", nodeName)
}
// Node-level route (VMID = 0) - no container control needed
if vmid > 0 {
res, err := node.Client().GetResource("lxc", vmid)
if err != nil {
return gperr.Wrap(err) // ErrResourceNotFound
}
r.Proxmox.VMName = res.Name
if r.Host == DefaultHost {
containerName := r.Idlewatcher.ContainerName()
// get ip addresses of the vmid
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
ips := res.IPs
if len(ips) == 0 {
return gperr.Multiline().
Addf("no ip addresses found for %s", containerName).
Adds("make sure you have set static ip address for container instead of dhcp").
Subject(containerName)
}
l := log.With().Str("container", containerName).Logger()
l.Info().Msg("checking if container is running")
running, err := node.LXCIsRunning(ctx, vmid)
if err != nil {
return gperr.New("failed to check container state").With(err)
}
if !running {
l.Info().Msg("starting container")
if err := node.LXCAction(ctx, vmid, proxmox.LXCStart); err != nil {
return gperr.New("failed to start container").With(err)
}
}
l.Info().Msgf("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 {
errs.Add(gperr.Unwrap(err).Subjectf("%s:%d", ip, r.Port.Proxy))
} else {
r.Host = ip.String()
l.Info().Msgf("using ip %s", r.Host)
break
}
}
if r.Host == DefaultHost {
return gperr.Multiline().
Addf("no reachable ip addresses found, tried %d IPs", len(ips)).
With(errs.Error()).
Subject(containerName)
}
}
}
}
if r.Container != nil && r.Container.IdlewatcherConfig != nil {
r.Idlewatcher = r.Container.IdlewatcherConfig
}
// return error if route is localhost:<godoxy_port> but route is not agent
if !r.IsAgent() {
switch r.Host {
case "localhost", "127.0.0.1":
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)
}
}
}
}
var errs gperr.Builder
if err := r.validateRules(); err != nil {
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)
}
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.
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"
}
bindIP := net.ParseIP(r.Bind)
remoteIP := net.ParseIP(r.Host)
toNetwork := func(ip net.IP, scheme route.Scheme) string {
if ip == nil { // hostname, indeterminate
return scheme.String()
}
if ip.To4() == nil {
if scheme == route.SchemeTCP {
return "tcp6"
}
return "udp6"
}
if scheme == route.SchemeTCP {
return "tcp4"
}
return "udp4"
}
lScheme := toNetwork(bindIP, r.Scheme)
rScheme := toNetwork(remoteIP, r.Scheme)
r.LisURL = gperr.Collect(&errs, nettypes.ParseURL, fmt.Sprintf("%s://%s", lScheme, net.JoinHostPort(r.Bind, strconv.Itoa(r.Port.Listening))))
r.ProxyURL = gperr.Collect(&errs, nettypes.ParseURL, fmt.Sprintf("%s://%s", rScheme, net.JoinHostPort(r.Host, strconv.Itoa(r.Port.Proxy))))
}
}
if r.Proxmox == nil && r.Container == nil {
proxmoxProviders := config.WorkingState.Load().Value().Providers.Proxmox
if len(proxmoxProviders) > 0 {
// it's fine if ip is nil
hostname := r.ProxyURL.Hostname()
ip := net.ParseIP(hostname)
for _, p := range config.WorkingState.Load().Value().Providers.Proxmox {
// First check if hostname, IP, or alias matches a node (node-level route)
if nodeName := p.Client().ReverseLookupNode(hostname, ip, r.Alias); nodeName != "" {
r.Proxmox = &proxmox.NodeConfig{
Node: nodeName,
VMID: 0, // node-level route, no specific VM
VMName: "",
}
log.Info().
Str("node", nodeName).
Msgf("found proxmox node for route %q", r.Alias)
break
}
// Then check if hostname, IP, or alias matches a VM resource
resource, _ := p.Client().ReverseLookupResource(ip, hostname, r.Alias)
if resource != nil {
r.Proxmox = &proxmox.NodeConfig{
Node: resource.Node,
VMID: int(resource.VMID),
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)
break
}
}
}
}
if !r.UseHealthCheck() && (r.UseLoadBalance() || r.UseIdleWatcher()) {
errs.Adds("cannot disable healthcheck when loadbalancer or idle watcher is enabled")
}
if errs.HasError() {
return errs.Error()
}
switch r.Scheme {
case route.SchemeFileServer:
impl, err = NewFileServer(r)
case route.SchemeHTTP, route.SchemeHTTPS, route.SchemeH2C:
impl, err = NewReverseProxyRoute(r)
case route.SchemeTCP, route.SchemeUDP:
impl, err = NewStreamRoute(r)
default:
panic(fmt.Errorf("unexpected scheme %s for alias %s", r.Scheme, r.Alias))
}
if err != nil {
return err
}
r.impl = impl
r.Excluded = r.ShouldExclude()
if r.Excluded {
r.ExcludedReason = r.findExcludedReason()
}
return nil
}
func (r *Route) validateRules() error {
// FIXME: hardcoded here as a workaround
// there's already a label "proxy.#1.rule_file=embed://webui.yml"
// but it's not working as expected sometimes.
// TODO: investigate why it's not working and fix it.
if cont := r.ContainerInfo(); cont != nil {
if cont.Image.Name == "godoxy-frontend" {
rules, ok := rulepresets.GetRulePreset("webui.yml")
if !ok {
return errors.New("rule preset `webui.yml` not found")
}
r.Rules = rules
}
return nil
}
if r.RuleFile != "" && len(r.Rules) > 0 {
return errors.New("`rule_file` and `rules` cannot be used together")
} else if r.RuleFile != "" {
src, err := url.Parse(r.RuleFile)
if err != nil {
return fmt.Errorf("failed to parse rule file url %q: %w", r.RuleFile, err)
}
switch src.Scheme {
case "embed": // embed://<preset_file_name>
rules, ok := rulepresets.GetRulePreset(src.Host)
if !ok {
return fmt.Errorf("rule preset %q not found", src.Host)
} else {
r.Rules = rules
}
case "file", "":
content, err := os.ReadFile(src.Path)
if err != nil {
return fmt.Errorf("failed to read rule file %q: %w", src.Path, err)
} else {
_, err = serialization.ConvertString(string(content), reflect.ValueOf(&r.Rules))
if err != nil {
return fmt.Errorf("failed to unmarshal rule file %q: %w", src.Path, err)
}
}
default:
return fmt.Errorf("unsupported rule file scheme %q", src.Scheme)
}
}
return nil
}
func (r *Route) Impl() types.Route {
return r.impl
}
func (r *Route) Task() *task.Task {
return r.task
}
func (r *Route) Start(parent task.Parent) gperr.Error {
r.onceStart.Do(func() {
r.startErr.Set(r.start(parent))
})
return r.startErr.Get()
}
func (r *Route) start(parent task.Parent) gperr.Error {
if r.impl == nil { // should not happen
return gperr.New("route not initialized")
}
defer close(r.started)
// skip checking for excluded routes
excluded := r.ShouldExclude()
if !excluded {
if err := checkExists(r); err != nil {
return err
}
}
if cont := r.ContainerInfo(); cont != nil {
docker.SetDockerCfgByContainerID(cont.ContainerID, cont.DockerCfg)
}
if !excluded {
if err := r.impl.Start(parent); err != nil {
return err
}
} else {
r.task = parent.Subtask("excluded."+r.Name(), true)
routes.Excluded.Add(r.impl)
r.task.OnCancel("remove_route_from_excluded", func() {
routes.Excluded.Del(r.impl)
})
if r.UseHealthCheck() {
r.HealthMon = monitor.NewMonitor(r.impl)
err := r.HealthMon.Start(r.task)
return err
}
}
return nil
}
func (r *Route) Finish(reason any) {
if cont := r.ContainerInfo(); cont != nil {
docker.DeleteDockerCfgByContainerID(cont.ContainerID)
}
r.FinishAndWait(reason)
}
func (r *Route) FinishAndWait(reason any) {
if r.impl == nil {
return
}
r.task.FinishAndWait(reason)
r.impl = nil
}
func (r *Route) Started() <-chan struct{} {
return r.started
}
func (r *Route) GetProvider() types.RouteProvider {
return r.provider
}
func (r *Route) SetProvider(p types.RouteProvider) {
r.provider = p
r.Provider = p.ShortName()
}
func (r *Route) ProviderName() string {
return r.Provider
}
func (r *Route) TargetURL() *nettypes.URL {
return r.ProxyURL
}
func (r *Route) References() []string {
aliasRef, _, ok := strings.Cut(r.Alias, ".")
if !ok {
aliasRef = r.Alias
}
if r.Container != nil {
if r.Container.ContainerName != aliasRef {
return []string{r.Container.ContainerName, aliasRef, r.Container.Image.Name, r.Container.Image.Author}
}
return []string{r.Container.Image.Name, aliasRef, r.Container.Image.Author}
}
if r.Proxmox != nil {
if r.Proxmox.Service != "" && r.Proxmox.Service != aliasRef {
if r.Proxmox.VMName != aliasRef {
return []string{r.Proxmox.VMName, aliasRef, r.Proxmox.Service}
}
return []string{r.Proxmox.Service, aliasRef}
} else {
if r.Proxmox.VMName != aliasRef {
return []string{r.Proxmox.VMName, aliasRef}
}
}
}
return []string{aliasRef}
}
// Name implements pool.Object.
func (r *Route) Name() string {
return r.Alias
}
// Key implements pool.Object.
func (r *Route) Key() string {
if r.UseLoadBalance() || r.ShouldExclude() {
// for excluded routes and load balanced routes, use provider:alias[-container_id[:8]] as key to make them unique.
if r.Container != nil {
return r.Provider + ":" + r.Alias + "-" + r.Container.ContainerID[:8]
}
return r.Provider + ":" + r.Alias
}
// we need to use alias as key for non-excluded routes because it's being used for subdomain / fqdn lookup for http routes.
return r.Alias
}
func (r *Route) Type() route.RouteType {
switch r.Scheme {
case route.SchemeHTTP, route.SchemeHTTPS, route.SchemeFileServer:
return route.RouteTypeHTTP
case route.SchemeTCP, route.SchemeUDP:
return route.RouteTypeStream
}
panic(fmt.Errorf("unexpected scheme %s for alias %s", r.Scheme, r.Alias))
}
func (r *Route) GetAgent() *agentpool.Agent {
if r.Container != nil && r.Container.Agent != nil {
return r.Container.Agent
}
return r.agent
}
func (r *Route) IsAgent() bool {
return r.GetAgent() != nil
}
func (r *Route) HealthMonitor() types.HealthMonitor {
return r.HealthMon
}
func (r *Route) SetHealthMonitor(m types.HealthMonitor) {
if r.HealthMon != nil && r.HealthMon != m {
r.HealthMon.Finish("health monitor replaced")
}
r.HealthMon = m
}
func (r *Route) IdlewatcherConfig() *types.IdlewatcherConfig {
return r.Idlewatcher
}
func (r *Route) HealthCheckConfig() types.HealthCheckConfig {
return r.HealthCheck
}
func (r *Route) LoadBalanceConfig() *types.LoadBalancerConfig {
return r.LoadBalance
}
func (r *Route) HomepageItem() homepage.Item {
containerID := ""
if r.Container != nil {
containerID = r.Container.ContainerID
}
return homepage.Item{
Alias: r.Alias,
Provider: r.Provider,
ItemConfig: *r.Homepage,
ContainerID: containerID,
}.GetOverride()
}
func (r *Route) DisplayName() string {
if r.Homepage == nil { // should only happen in tests, Validate() should initialize it
return r.Alias
}
return r.Homepage.Name
}
// PreferOver implements pool.Preferable to resolve duplicate route keys deterministically.
// Preference policy:
// - Prefer routes with rules over routes without rules.
// - If rules tie, prefer non-docker routes (explicit config) over docker-discovered routes.
// - Otherwise, prefer the new route to preserve existing semantics.
func (r *Route) PreferOver(other any) bool {
// Try to get the underlying *Route of the other value
var or *Route
switch v := other.(type) {
case *Route:
or = v
case *ReveseProxyRoute:
or = v.Route
case *FileServer:
or = v.Route
case *StreamRoute:
or = v.Route
default:
// Unknown type, allow replacement
return true
}
// Prefer routes that have rules
if len(r.Rules) > 0 && len(or.Rules) == 0 {
return true
}
if len(r.Rules) == 0 && len(or.Rules) > 0 {
return false
}
// Prefer explicit (non-docker) over docker auto-discovered
if (r.Container == nil) != (or.Container == nil) {
return r.Container == nil
}
// Default: allow replacement
return true
}
func (r *Route) ContainerInfo() *types.Container {
return r.Container
}
func (r *Route) IsDocker() bool {
if r.Container == nil {
return false
}
return r.Container.ContainerID != ""
}
func (r *Route) IsZeroPort() bool {
return r.Port.Proxy == 0
}
func (r *Route) ShouldExclude() bool {
if r.ExcludedReason != ExcludedReasonNone {
return true
}
return r.findExcludedReason() != ExcludedReasonNone
}
type ExcludedReason uint8
const (
ExcludedReasonNone ExcludedReason = iota
ExcludedReasonError
ExcludedReasonManual
ExcludedReasonNoPortContainer
ExcludedReasonNoPortSpecified
ExcludedReasonBlacklisted
ExcludedReasonBuildx
ExcludedReasonOld
)
func (re ExcludedReason) String() string {
switch re {
case ExcludedReasonNone:
return ""
case ExcludedReasonError:
return "Error"
case ExcludedReasonManual:
return "Manual exclusion"
case ExcludedReasonNoPortContainer:
return "No port exposed in container"
case ExcludedReasonNoPortSpecified:
return "No port specified"
case ExcludedReasonBlacklisted:
return "Blacklisted (backend service or database)"
case ExcludedReasonBuildx:
return "Buildx"
case ExcludedReasonOld:
return "Container renaming intermediate state"
default:
return "Unknown"
}
}
func (re ExcludedReason) MarshalJSON() ([]byte, error) {
return strconv.AppendQuote(nil, re.String()), nil
}
// no need to unmarshal json because we don't store this
func (r *Route) findExcludedReason() ExcludedReason {
if r.valErr.Get() != nil {
return ExcludedReasonError
}
if r.ExcludedReason != ExcludedReasonNone {
return r.ExcludedReason
}
if r.Container != nil {
switch {
case r.Container.IsExcluded:
return ExcludedReasonManual
case r.IsZeroPort() && !r.UseIdleWatcher():
return ExcludedReasonNoPortContainer
case !r.Container.IsExplicit && docker.IsBlacklisted(r.Container):
return ExcludedReasonBlacklisted
case strings.HasPrefix(r.Container.ContainerName, "buildx_"):
return ExcludedReasonBuildx
}
} else if r.IsZeroPort() && r.Scheme != route.SchemeFileServer {
return ExcludedReasonNoPortSpecified
}
if strings.HasSuffix(r.Alias, "-old") {
return ExcludedReasonOld
}
return ExcludedReasonNone
}
func (r *Route) UseLoadBalance() bool {
return r.LoadBalance != nil && r.LoadBalance.Link != ""
}
func (r *Route) UseIdleWatcher() bool {
return r.Idlewatcher != nil && r.Idlewatcher.IdleTimeout > 0 && r.Idlewatcher.ValErr() == nil
}
func (r *Route) UseHealthCheck() bool {
if r.Container != nil {
switch {
case r.Container.Image.Name == "godoxy-agent":
return false
case !r.Container.Running && !r.UseIdleWatcher():
return false
case strings.HasPrefix(r.Container.ContainerName, "buildx_"):
return false
}
}
return !r.HealthCheck.Disable
}
func (r *Route) UseAccessLog() bool {
return r.AccessLog != nil
}
func (r *Route) Finalize() {
r.Alias = strings.ToLower(strings.TrimSpace(r.Alias))
r.Host = strings.ToLower(strings.TrimSpace(r.Host))
isDocker := r.Container != nil
cont := r.Container
if r.Host == "" {
switch {
case !isDocker:
r.Host = "localhost"
case cont.PrivateHostname != "":
r.Host = cont.PrivateHostname
case cont.PublicHostname != "":
r.Host = cont.PublicHostname
}
}
lp, pp := r.Port.Listening, r.Port.Proxy
if isDocker {
scheme, port, ok := getSchemePortByImageName(cont.Image.Name)
if ok {
if r.Scheme == route.SchemeNone {
r.Scheme = scheme
}
if pp == 0 {
pp = port
}
}
}
if scheme, port, ok := getSchemePortByAlias(r.Alias); ok {
if r.Scheme == route.SchemeNone {
r.Scheme = scheme
}
if pp == 0 {
pp = port
}
}
if pp == 0 {
switch {
case isDocker:
if cont.IsHostNetworkMode {
pp = preferredPort(cont.PublicPortMapping)
} else {
pp = preferredPort(cont.PrivatePortMapping)
}
case r.Scheme == route.SchemeHTTPS:
pp = 443
default:
pp = 80
}
}
if isDocker {
if r.Scheme == route.SchemeNone {
for _, p := range cont.PublicPortMapping {
if int(p.PrivatePort) == pp && p.Type == "udp" {
r.Scheme = route.SchemeUDP
break
}
}
}
// replace private port with public port if using public IP.
if r.Host == cont.PublicHostname {
if p, ok := cont.PrivatePortMapping[pp]; ok {
pp = int(p.PublicPort)
}
} 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 == route.SchemeNone {
switch {
case lp != 0:
r.Scheme = route.SchemeTCP
case pp%1000 == 443:
r.Scheme = route.SchemeHTTPS
default: // assume its http
r.Scheme = route.SchemeHTTP
}
}
r.Port.Listening, r.Port.Proxy = lp, pp
workingState := config.WorkingState.Load()
if workingState == nil {
if common.IsTest { // in tests, working state might be nil
return
}
panic("bug: working state is nil")
}
r.HealthCheck.ApplyDefaults(config.WorkingState.Load().Value().Defaults.HealthCheck)
}
func (r *Route) FinalizeHomepageConfig() {
if r.Alias == "" {
panic("alias is empty")
}
isDocker := r.Container != nil
if r.Homepage == nil {
r.Homepage = &homepage.ItemConfig{
Show: true,
}
}
if r.ShouldExclude() && isDocker {
r.Homepage.Show = false
r.Homepage.Name = r.Container.ContainerName // still show container name in metrics page
return
}
hp := r.Homepage
refs := r.References()
for _, ref := range refs {
meta, ok := iconlist.GetMetadata(ref)
if ok {
if hp.Name == "" {
hp.Name = meta.DisplayName
}
if hp.Category == "" {
hp.Category = meta.Tag
}
break
}
}
if hp.Name == "" {
hp.Name = strutils.Title(
strings.ReplaceAll(
strings.ReplaceAll(refs[0], "-", " "),
"_", " ",
),
)
}
if hp.Category == "" {
if homepagecfg.ActiveConfig.Load().UseDefaultCategories {
for _, ref := range refs {
if category, ok := homepage.PredefinedCategories[ref]; ok {
hp.Category = category
break
}
}
}
if hp.Category == "" {
switch {
case r.UseLoadBalance():
hp.Category = "Load-balanced"
case isDocker:
hp.Category = "Docker"
default:
hp.Category = "Others"
}
}
}
}
var preferredPortOrder = []int{
80,
8080,
3000,
8000,
443,
8443,
}
func preferredPort(portMapping types.PortMapping) (res int) {
for _, port := range preferredPortOrder {
if _, ok := portMapping[port]; ok {
return port
}
}
// fallback to lowest port
cmp := (uint16)(65535)
for port, v := range portMapping {
if v.PrivatePort < cmp {
cmp = v.PrivatePort
res = port
}
}
return res
}