From 05c57c3b8517351d3d434184e3a0697c3bf2f330 Mon Sep 17 00:00:00 2001 From: Geoff Bourne Date: Sat, 5 Jul 2025 19:32:33 -0500 Subject: [PATCH] Refactored server setup and run out of main (#425) --- cmd/mc-router/main.go | 254 +++++---------------------------- go.mod | 6 +- go.sum | 7 + server/configs.go | 50 +++++++ server/routes_config_loader.go | 4 + server/server.go | 210 +++++++++++++++++++++++++++ 6 files changed, 308 insertions(+), 223 deletions(-) create mode 100644 server/configs.go create mode 100644 server/server.go diff --git a/cmd/mc-router/main.go b/cmd/mc-router/main.go index d2a617a..10c6fb9 100644 --- a/cmd/mc-router/main.go +++ b/cmd/mc-router/main.go @@ -3,70 +3,14 @@ package main import ( "context" "fmt" - "net" - "os" - "os/signal" - "runtime/pprof" - "strconv" - "syscall" - "time" - "github.com/itzg/go-flagsfiller" "github.com/itzg/mc-router/server" "github.com/sirupsen/logrus" + "os" + "os/signal" + "syscall" ) -type WebhookConfig struct { - Url string `usage:"If set, a POST request that contains connection status notifications will be sent to this HTTP address"` - RequireUser bool `default:"false" usage:"Indicates if the webhook will only be called if a user is connecting rather than just server list/ping"` -} - -type AutoScale struct { - Up bool `usage:"Increase Kubernetes StatefulSet Replicas (only) from 0 to 1 on respective backend servers when accessed"` - Down bool `default:"false" usage:"Decrease Kubernetes StatefulSet Replicas (only) from 1 to 0 on respective backend servers after there are no connections"` - DownAfter string `default:"10m" usage:"Server scale down delay after there are no connections"` - AllowDeny string `usage:"Path to config for server allowlists and denylists. If a global/server entry is specified, only players allowed to connect to the server will be able to trigger a scale up when -auto-scale-up is enabled or cancel active down scalers when -auto-scale-down is enabled"` -} - -type RoutesConfig struct { - Config string `usage:"Name or full [path] to routes config file"` - ConfigWatch bool `usage:"Watch for config file changes"` -} - -type Config struct { - Port int `default:"25565" usage:"The [port] bound to listen for Minecraft client connections"` - Default string `usage:"host:port of a default Minecraft server to use when mapping not found"` - Mapping map[string]string `usage:"Comma or newline delimited or repeated mappings of externalHostname=host:port"` - ApiBinding string `usage:"The [host:port] bound for servicing API requests"` - Version bool `usage:"Output version and exit"` - CpuProfile string `usage:"Enables CPU profiling and writes to given path"` - Debug bool `usage:"Enable debug logs"` - ConnectionRateLimit int `default:"1" usage:"Max number of connections to allow per second"` - InKubeCluster bool `usage:"Use in-cluster Kubernetes config"` - KubeConfig string `usage:"The path to a Kubernetes configuration file"` - InDocker bool `usage:"Use Docker service discovery"` - InDockerSwarm bool `usage:"Use Docker Swarm service discovery"` - DockerSocket string `default:"unix:///var/run/docker.sock" usage:"Path to Docker socket to use"` - DockerTimeout int `default:"0" usage:"Timeout configuration in seconds for the Docker integrations"` - DockerRefreshInterval int `default:"15" usage:"Refresh interval in seconds for the Docker integrations"` - MetricsBackend string `default:"discard" usage:"Backend to use for metrics exposure/publishing: discard,expvar,influxdb,prometheus"` - MetricsBackendConfig server.MetricsBackendConfig - UseProxyProtocol bool `default:"false" usage:"Send PROXY protocol to backend servers"` - ReceiveProxyProtocol bool `default:"false" usage:"Receive PROXY protocol from backend servers, by default trusts every proxy header that it receives, combine with -trusted-proxies to specify a list of trusted proxies"` - TrustedProxies []string `usage:"Comma delimited list of CIDR notation IP blocks to trust when receiving PROXY protocol"` - RecordLogins bool `default:"false" usage:"Log and generate metrics on player logins. Metrics only supported with influxdb or prometheus backend"` - Routes RoutesConfig - NgrokToken string `usage:"If set, an ngrok tunnel will be established. It is HIGHLY recommended to pass as an environment variable."` - AutoScale AutoScale - - ClientsToAllow []string `usage:"Zero or more client IP addresses or CIDRs to allow. Takes precedence over deny."` - ClientsToDeny []string `usage:"Zero or more client IP addresses or CIDRs to deny. Ignored if any configured to allow"` - - SimplifySRV bool `default:"false" usage:"Simplify fully qualified SRV records for mapping"` - - Webhook WebhookConfig `usage:"Webhook configuration"` -} - var ( version = "dev" commit = "none" @@ -77,190 +21,60 @@ func showVersion() { fmt.Printf("%v, commit %v, built at %v", version, commit, date) } +type CliConfig struct { + Version bool `usage:"Output version and exit"` + Debug bool `usage:"Enable debug logs"` + + ServerConfig server.Config `flatten:"true"` +} + func main() { - var config Config - err := flagsfiller.Parse(&config, flagsfiller.WithEnv("")) + var cliConfig CliConfig + err := flagsfiller.Parse(&cliConfig, flagsfiller.WithEnv("")) if err != nil { logrus.Fatal(err) } - if config.Version { + if cliConfig.Version { showVersion() os.Exit(0) } - if config.Debug { + if cliConfig.Debug { logrus.SetLevel(logrus.DebugLevel) logrus.Debug("Debug logs enabled") } - if config.CpuProfile != "" { - cpuProfileFile, err := os.Create(config.CpuProfile) - if err != nil { - logrus.WithError(err).Fatal("trying to create cpu profile file") - } - //goland:noinspection GoUnhandledErrorResult - defer cpuProfileFile.Close() - - logrus.WithField("file", config.CpuProfile).Info("Starting cpu profiling") - err = pprof.StartCPUProfile(cpuProfileFile) - if err != nil { - logrus.WithError(err).Fatal("trying to start cpu profile") - } - defer pprof.StopCPUProfile() - } - - var autoScaleAllowDenyConfig *server.AllowDenyConfig = nil - if config.AutoScale.AllowDeny != "" { - autoScaleAllowDenyConfig, err = server.ParseAllowDenyConfig(config.AutoScale.AllowDeny) - if err != nil { - logrus.WithError(err).Fatal("trying to parse autoscale up allow-deny-list file") - } - } - ctx, cancel := context.WithCancel(context.Background()) defer cancel() - metricsBuilder := server.NewMetricsBuilder(config.MetricsBackend, &config.MetricsBackendConfig) + signals := make(chan os.Signal, 1) + signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP) - downScalerEnabled := config.AutoScale.Down && (config.InKubeCluster || config.KubeConfig != "") - downScalerDelay, err := time.ParseDuration(config.AutoScale.DownAfter) + s, err := server.NewServer(ctx, &cliConfig.ServerConfig) if err != nil { - logrus.WithError(err).Fatal("Unable to parse auto scale down after duration") - } - // Only one instance should be created - server.DownScaler = server.NewDownScaler(ctx, downScalerEnabled, downScalerDelay) - - c := make(chan os.Signal, 1) - signal.Notify(c, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP) - - if config.Routes.Config != "" { - err := server.RoutesConfigLoader.Load(config.Routes.Config) - if err != nil { - logrus.WithError(err).Fatal("Unable to load routes from config file") - } - - if config.Routes.ConfigWatch { - err := server.RoutesConfigLoader.WatchForChanges(ctx) - if err != nil { - logrus.WithError(err).Fatal("Unable to watch for changes") - } - } + logrus.WithError(err).Fatal("Could not setup server") } - server.Routes.RegisterAll(config.Mapping) - if config.Default != "" { - server.Routes.SetDefaultRoute(config.Default) - } + go s.Run() - if config.ConnectionRateLimit < 1 { - config.ConnectionRateLimit = 1 - } - - trustedIpNets := make([]*net.IPNet, 0) - for _, ip := range config.TrustedProxies { - _, ipNet, err := net.ParseCIDR(ip) - if err != nil { - logrus.WithError(err).Fatal("Unable to parse trusted proxy CIDR block") - } - trustedIpNets = append(trustedIpNets, ipNet) - } - - connector := server.NewConnector(metricsBuilder.BuildConnectorMetrics(), config.UseProxyProtocol, config.ReceiveProxyProtocol, trustedIpNets, config.RecordLogins, autoScaleAllowDenyConfig) - - clientFilter, err := server.NewClientFilter(config.ClientsToAllow, config.ClientsToDeny) - if err != nil { - logrus.WithError(err).Fatal("Unable to create client filter") - } - connector.SetClientFilter(clientFilter) - - if config.Webhook.Url != "" { - logrus. - WithField("url", config.Webhook.Url). - WithField("require-user", config.Webhook.RequireUser). - Info("Using webhook for connection status notifications") - connector.SetConnectionNotifier( - server.NewWebhookNotifier(config.Webhook.Url, config.Webhook.RequireUser)) - } - - if config.NgrokToken != "" { - connector.UseNgrok(config.NgrokToken) - } - err = connector.StartAcceptingConnections(ctx, - net.JoinHostPort("", strconv.Itoa(config.Port)), - config.ConnectionRateLimit, - ) - if err != nil { - logrus.Fatal(err) - } - - if config.ApiBinding != "" { - server.StartApiServer(config.ApiBinding) - } - - if config.InKubeCluster { - err = server.K8sWatcher.StartInCluster(config.AutoScale.Up, config.AutoScale.Down) - if err != nil { - logrus.WithError(err).Fatal("Unable to start k8s integration") - } else { - defer server.K8sWatcher.Stop() - } - } else if config.KubeConfig != "" { - err := server.K8sWatcher.StartWithConfig(config.KubeConfig, config.AutoScale.Up, config.AutoScale.Down) - if err != nil { - logrus.WithError(err).Fatal("Unable to start k8s integration") - } else { - defer server.K8sWatcher.Stop() - } - } - - if config.InDocker { - err = server.DockerWatcher.Start(config.DockerSocket, config.DockerTimeout, config.DockerRefreshInterval, config.AutoScale.Up, config.AutoScale.Down) - if err != nil { - logrus.WithError(err).Fatal("Unable to start docker integration") - } else { - defer server.DockerWatcher.Stop() - } - } - - if config.InDockerSwarm { - err = server.DockerSwarmWatcher.Start(config.DockerSocket, config.DockerTimeout, config.DockerRefreshInterval, config.AutoScale.Up, config.AutoScale.Down) - if err != nil { - logrus.WithError(err).Fatal("Unable to start docker swarm integration") - } else { - defer server.DockerSwarmWatcher.Stop() - } - } - - server.Routes.SimplifySRV(config.SimplifySRV) - - err = metricsBuilder.Start(ctx) - if err != nil { - logrus.WithError(err).Fatal("Unable to start metrics reporter") - } - - // handle signals for { - sig := <-c - switch sig { - case syscall.SIGHUP: - if config.Routes.Config != "" { - logrus.Info("Received SIGHUP, reloading routes config...") - if err := server.RoutesConfigLoader.Reload(); err != nil { - logrus. - WithError(err). - WithField("routesConfig", config.Routes.Config). - Error("Could not re-read the routes config file") - } - } - case syscall.SIGINT, syscall.SIGTERM: - logrus.WithField("signal", sig).Info("Stopping. Waiting for connections to complete...") - signal.Stop(c) - connector.WaitForConnections() - logrus.Info("Stopped") + select { + case <-s.Done(): return - default: - logrus.WithField("signal", sig).Warn("Received unexpected signal") + + case sig := <-signals: + switch sig { + case syscall.SIGHUP: + s.ReloadConfig() + + case syscall.SIGINT, syscall.SIGTERM: + cancel() + // but wait for the server to be done + + default: + logrus.WithField("signal", sig).Warn("Received unexpected signal") + } } } } diff --git a/go.mod b/go.mod index 939df14..2ee3be4 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/go-kit/kit v0.13.0 github.com/gorilla/mux v1.8.1 github.com/influxdata/influxdb1-client v0.0.0-20220302092344-a9ab5670611c - github.com/itzg/go-flagsfiller v1.15.2 + github.com/itzg/go-flagsfiller v1.16.0 github.com/juju/ratelimit v1.0.2 github.com/pires/go-proxyproto v0.8.1 github.com/pkg/errors v0.9.1 @@ -80,7 +80,7 @@ require ( github.com/Microsoft/go-winio v0.6.2 // indirect github.com/VividCortex/gohistogram v1.0.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/docker/docker v28.3.0+incompatible + github.com/docker/docker v28.3.1+incompatible github.com/docker/go-connections v0.5.0 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/go-kit/log v0.2.1 // indirect @@ -98,7 +98,7 @@ require ( github.com/opencontainers/image-spec v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/spf13/pflag v1.0.6 // indirect - golang.org/x/net v0.40.0 // indirect + golang.org/x/net v0.41.0 // indirect golang.org/x/oauth2 v0.30.0 // indirect golang.org/x/sys v0.33.0 // indirect golang.org/x/term v0.32.0 // indirect diff --git a/go.sum b/go.sum index 7e9031c..8b58941 100644 --- a/go.sum +++ b/go.sum @@ -25,6 +25,8 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/docker/docker v28.3.0+incompatible h1:ffS62aKWupCWdvcee7nBU9fhnmknOqDPaJAMtfK0ImQ= github.com/docker/docker v28.3.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v28.3.1+incompatible h1:20+BmuA9FXlCX4ByQ0vYJcUEnOmRM6XljDnFWR+jCyY= +github.com/docker/docker v28.3.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= @@ -89,6 +91,8 @@ github.com/influxdata/influxdb1-client v0.0.0-20220302092344-a9ab5670611c h1:qSH github.com/influxdata/influxdb1-client v0.0.0-20220302092344-a9ab5670611c/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= github.com/itzg/go-flagsfiller v1.15.2 h1:DvhhOKuqzawoa6C/3Q/y8pFbdO5PBdgnIvz84ZGkY0g= github.com/itzg/go-flagsfiller v1.15.2/go.mod h1:XmllPPi99O7vXTG9wa/Hzmhnkv6BXBF1W57ifbQTVs4= +github.com/itzg/go-flagsfiller v1.16.0 h1:YNwjLzFIeFzZpctT2RiN8T5qxiGrCX33bGSwtN6OSAA= +github.com/itzg/go-flagsfiller v1.16.0/go.mod h1:XmllPPi99O7vXTG9wa/Hzmhnkv6BXBF1W57ifbQTVs4= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= @@ -205,6 +209,7 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= +golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -214,6 +219,8 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= +golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= +golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= diff --git a/server/configs.go b/server/configs.go new file mode 100644 index 0000000..d34c467 --- /dev/null +++ b/server/configs.go @@ -0,0 +1,50 @@ +package server + +type WebhookConfig struct { + Url string `usage:"If set, a POST request that contains connection status notifications will be sent to this HTTP address"` + RequireUser bool `default:"false" usage:"Indicates if the webhook will only be called if a user is connecting rather than just server list/ping"` +} + +type AutoScale struct { + Up bool `usage:"Increase Kubernetes StatefulSet Replicas (only) from 0 to 1 on respective backend servers when accessed"` + Down bool `default:"false" usage:"Decrease Kubernetes StatefulSet Replicas (only) from 1 to 0 on respective backend servers after there are no connections"` + DownAfter string `default:"10m" usage:"Server scale down delay after there are no connections"` + AllowDeny string `usage:"Path to config for server allowlists and denylists. If a global/server entry is specified, only players allowed to connect to the server will be able to trigger a scale up when -auto-scale-up is enabled or cancel active down scalers when -auto-scale-down is enabled"` +} + +type RoutesConfig struct { + Config string `usage:"Name or full [path] to routes config file"` + ConfigWatch bool `usage:"Watch for config file changes"` +} + +type Config struct { + Port int `default:"25565" usage:"The [port] bound to listen for Minecraft client connections"` + Default string `usage:"host:port of a default Minecraft server to use when mapping not found"` + Mapping map[string]string `usage:"Comma or newline delimited or repeated mappings of externalHostname=host:port"` + ApiBinding string `usage:"The [host:port] bound for servicing API requests"` + CpuProfile string `usage:"Enables CPU profiling and writes to given path"` + ConnectionRateLimit int `default:"1" usage:"Max number of connections to allow per second"` + InKubeCluster bool `usage:"Use in-cluster Kubernetes config"` + KubeConfig string `usage:"The path to a Kubernetes configuration file"` + InDocker bool `usage:"Use Docker service discovery"` + InDockerSwarm bool `usage:"Use Docker Swarm service discovery"` + DockerSocket string `default:"unix:///var/run/docker.sock" usage:"Path to Docker socket to use"` + DockerTimeout int `default:"0" usage:"Timeout configuration in seconds for the Docker integrations"` + DockerRefreshInterval int `default:"15" usage:"Refresh interval in seconds for the Docker integrations"` + MetricsBackend string `default:"discard" usage:"Backend to use for metrics exposure/publishing: discard,expvar,influxdb,prometheus"` + MetricsBackendConfig MetricsBackendConfig + UseProxyProtocol bool `default:"false" usage:"Send PROXY protocol to backend servers"` + ReceiveProxyProtocol bool `default:"false" usage:"Receive PROXY protocol from backend servers, by default trusts every proxy header that it receives, combine with -trusted-proxies to specify a list of trusted proxies"` + TrustedProxies []string `usage:"Comma delimited list of CIDR notation IP blocks to trust when receiving PROXY protocol"` + RecordLogins bool `default:"false" usage:"Log and generate metrics on player logins. Metrics only supported with influxdb or prometheus backend"` + Routes RoutesConfig + NgrokToken string `usage:"If set, an ngrok tunnel will be established. It is HIGHLY recommended to pass as an environment variable."` + AutoScale AutoScale + + ClientsToAllow []string `usage:"Zero or more client IP addresses or CIDRs to allow. Takes precedence over deny."` + ClientsToDeny []string `usage:"Zero or more client IP addresses or CIDRs to deny. Ignored if any configured to allow"` + + SimplifySRV bool `default:"false" usage:"Simplify fully qualified SRV records for mapping"` + + Webhook WebhookConfig `usage:"Webhook configuration"` +} diff --git a/server/routes_config_loader.go b/server/routes_config_loader.go index 8f88475..43a9aeb 100644 --- a/server/routes_config_loader.go +++ b/server/routes_config_loader.go @@ -49,6 +49,10 @@ func (r *routesConfigLoader) Load(routesConfigFileName string) error { } func (r *routesConfigLoader) Reload() error { + if !r.isEnabled() { + return nil + } + config, readErr := r.readFile() if readErr != nil { diff --git a/server/server.go b/server/server.go new file mode 100644 index 0000000..f20cdb8 --- /dev/null +++ b/server/server.go @@ -0,0 +1,210 @@ +package server + +import ( + "context" + "fmt" + "github.com/sirupsen/logrus" + "net" + "os" + "runtime/pprof" + "strconv" + "time" +) + +type Server struct { + ctx context.Context + config *Config + connector *Connector + reloadConfigChan chan struct{} + doneChan chan struct{} +} + +func NewServer(ctx context.Context, config *Config) (*Server, error) { + if config.CpuProfile != "" { + cpuProfileFile, err := os.Create(config.CpuProfile) + if err != nil { + return nil, fmt.Errorf("could not create cpu profile file: %w", err) + } + //goland:noinspection GoUnhandledErrorResult + defer cpuProfileFile.Close() + + logrus.WithField("file", config.CpuProfile).Info("Starting cpu profiling") + err = pprof.StartCPUProfile(cpuProfileFile) + if err != nil { + return nil, fmt.Errorf("could not start cpu profile: %w", err) + } + defer pprof.StopCPUProfile() + } + + var err error + + var autoScaleAllowDenyConfig *AllowDenyConfig = nil + if config.AutoScale.AllowDeny != "" { + autoScaleAllowDenyConfig, err = ParseAllowDenyConfig(config.AutoScale.AllowDeny) + if err != nil { + return nil, fmt.Errorf("could not parse autoscale allow-deny-list: %w", err) + } + } + + metricsBuilder := NewMetricsBuilder(config.MetricsBackend, &config.MetricsBackendConfig) + + downScalerEnabled := config.AutoScale.Down && (config.InKubeCluster || config.KubeConfig != "") + downScalerDelay, err := time.ParseDuration(config.AutoScale.DownAfter) + if err != nil { + return nil, fmt.Errorf("could not parse auto-scale-down-after duration: %w", err) + } + // Only one instance should be created + DownScaler = NewDownScaler(ctx, downScalerEnabled, downScalerDelay) + + if config.Routes.Config != "" { + err := RoutesConfigLoader.Load(config.Routes.Config) + if err != nil { + return nil, fmt.Errorf("could not load routes config file: %w", err) + } + + if config.Routes.ConfigWatch { + err := RoutesConfigLoader.WatchForChanges(ctx) + if err != nil { + return nil, fmt.Errorf("could not watch for changes to routes config file: %w", err) + } + } + } + + Routes.RegisterAll(config.Mapping) + if config.Default != "" { + Routes.SetDefaultRoute(config.Default) + } + + if config.ConnectionRateLimit < 1 { + config.ConnectionRateLimit = 1 + } + + trustedIpNets := make([]*net.IPNet, 0) + for _, ip := range config.TrustedProxies { + _, ipNet, err := net.ParseCIDR(ip) + if err != nil { + return nil, fmt.Errorf("could not parse trusted proxy CIDR block: %w", err) + } + trustedIpNets = append(trustedIpNets, ipNet) + } + + connector := NewConnector(metricsBuilder.BuildConnectorMetrics(), config.UseProxyProtocol, config.ReceiveProxyProtocol, trustedIpNets, config.RecordLogins, autoScaleAllowDenyConfig) + + clientFilter, err := NewClientFilter(config.ClientsToAllow, config.ClientsToDeny) + if err != nil { + return nil, fmt.Errorf("could not create client filter: %w", err) + } + connector.SetClientFilter(clientFilter) + + if config.Webhook.Url != "" { + logrus.WithField("url", config.Webhook.Url). + WithField("require-user", config.Webhook.RequireUser). + Info("Using webhook for connection status notifications") + connector.SetConnectionNotifier( + NewWebhookNotifier(config.Webhook.Url, config.Webhook.RequireUser)) + } + + if config.NgrokToken != "" { + connector.UseNgrok(config.NgrokToken) + } + + if config.ApiBinding != "" { + StartApiServer(config.ApiBinding) + } + + if config.InKubeCluster { + err = K8sWatcher.StartInCluster(config.AutoScale.Up, config.AutoScale.Down) + if err != nil { + return nil, fmt.Errorf("could not start in-cluster k8s integration: %w", err) + } else { + defer K8sWatcher.Stop() + } + } else if config.KubeConfig != "" { + err := K8sWatcher.StartWithConfig(config.KubeConfig, config.AutoScale.Up, config.AutoScale.Down) + if err != nil { + return nil, fmt.Errorf("could not start k8s integration with kube config: %w", err) + } else { + defer K8sWatcher.Stop() + } + } + + if config.InDocker { + err = DockerWatcher.Start(config.DockerSocket, config.DockerTimeout, config.DockerRefreshInterval, config.AutoScale.Up, config.AutoScale.Down) + if err != nil { + return nil, fmt.Errorf("could not start docker integration: %w", err) + } else { + defer DockerWatcher.Stop() + } + } + + if config.InDockerSwarm { + err = DockerSwarmWatcher.Start(config.DockerSocket, config.DockerTimeout, config.DockerRefreshInterval, config.AutoScale.Up, config.AutoScale.Down) + if err != nil { + return nil, fmt.Errorf("could not start docker swarm integration: %w", err) + } else { + defer DockerSwarmWatcher.Stop() + } + } + + Routes.SimplifySRV(config.SimplifySRV) + + err = metricsBuilder.Start(ctx) + if err != nil { + return nil, fmt.Errorf("could not start metrics reporter: %w", err) + } + + return &Server{ + ctx: ctx, + config: config, + connector: connector, + reloadConfigChan: make(chan struct{}), + doneChan: make(chan struct{}), + }, nil +} + +// Done provides a channel notified when the server has closed all connections, etc +func (s *Server) Done() <-chan struct{} { + return s.doneChan +} + +func (s *Server) notifyDone() { + s.doneChan <- struct{}{} +} + +// ReloadConfig indicates that an external request, such as a SIGHUP, +// is requesting the routes config file to be reloaded, if enabled +func (s *Server) ReloadConfig() { + s.reloadConfigChan <- struct{}{} +} + +// Run will run the server until the context is done or a fatal error occurs, so this should be +// in a go routine. +func (s *Server) Run() { + err := s.connector.StartAcceptingConnections(s.ctx, + net.JoinHostPort("", strconv.Itoa(s.config.Port)), + s.config.ConnectionRateLimit, + ) + if err != nil { + logrus.WithError(err).Error("Could not start accepting connections") + s.notifyDone() + return + } + + for { + select { + case <-s.reloadConfigChan: + if err := RoutesConfigLoader.Reload(); err != nil { + logrus.WithError(err). + Error("Could not re-read the routes config file") + } + + case <-s.ctx.Done(): + logrus.Info("Stopping. Waiting for connections to complete...") + s.connector.WaitForConnections() + logrus.Info("Stopped") + s.notifyDone() + return + } + } + +}