Code cleanup of routes config loader and API server (#424)

This commit is contained in:
Geoff Bourne
2025-07-03 19:26:35 -05:00
committed by GitHub
parent 7d5bc8d25d
commit 1ee3eb4de3
10 changed files with 302 additions and 385 deletions
+1 -1
View File
@@ -10,7 +10,7 @@ jobs:
release: release:
uses: itzg/github-workflows/.github/workflows/go-with-releaser-image.yml@main uses: itzg/github-workflows/.github/workflows/go-with-releaser-image.yml@main
with: with:
go-version: "1.24.4" go-version-file: 'go.mod'
enable-ghcr: true enable-ghcr: true
secrets: secrets:
image-registry-username: ${{ secrets.DOCKERHUB_USERNAME }} image-registry-username: ${{ secrets.DOCKERHUB_USERNAME }}
+1 -1
View File
@@ -12,4 +12,4 @@ jobs:
build: build:
uses: itzg/github-workflows/.github/workflows/go-test.yml@main uses: itzg/github-workflows/.github/workflows/go-test.yml@main
with: with:
go-version: "1.24.4" go-version-file: 'go.mod'
+1 -1
View File
@@ -178,7 +178,7 @@ The following shows a JSON file for routes config, where `default-server` can al
} }
``` ```
Sending a SIGHUP signal will cause mc-router to reload the routes config from disk. Sending a SIGHUP signal will cause mc-router to reload the routes config from disk. The file can also be watched for changes by setting `-routes-config-watch` or the env variable `ROUTES_CONFIG_WATCH` to "true".
## Auto Scale Allow/Deny List ## Auto Scale Allow/Deny List
+9 -21
View File
@@ -16,18 +16,6 @@ import (
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
type MetricsBackendConfig struct {
Influxdb struct {
Interval time.Duration `default:"1m"`
Tags map[string]string `usage:"any extra tags to be included with all reported metrics"`
Addr string
Username string
Password string
Database string
RetentionPolicy string
}
}
type WebhookConfig struct { type WebhookConfig struct {
Url string `usage:"If set, a POST request that contains connection status notifications will be sent to this HTTP address"` 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"` RequireUser bool `default:"false" usage:"Indicates if the webhook will only be called if a user is connecting rather than just server list/ping"`
@@ -62,11 +50,11 @@ type Config struct {
DockerTimeout int `default:"0" usage:"Timeout configuration in seconds for the Docker integrations"` 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"` 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"` MetricsBackend string `default:"discard" usage:"Backend to use for metrics exposure/publishing: discard,expvar,influxdb,prometheus"`
UseProxyProtocol bool `default:"false" usage:"Send PROXY protocol to backend servers"` MetricsBackendConfig server.MetricsBackendConfig
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"` UseProxyProtocol bool `default:"false" usage:"Send PROXY protocol to backend servers"`
TrustedProxies []string `usage:"Comma delimited list of CIDR notation IP blocks to trust when receiving PROXY protocol"` 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"`
RecordLogins bool `default:"false" usage:"Log and generate metrics on player logins. Metrics only supported with influxdb or prometheus backend"` TrustedProxies []string `usage:"Comma delimited list of CIDR notation IP blocks to trust when receiving PROXY protocol"`
MetricsBackendConfig MetricsBackendConfig RecordLogins bool `default:"false" usage:"Log and generate metrics on player logins. Metrics only supported with influxdb or prometheus backend"`
Routes RoutesConfig Routes RoutesConfig
NgrokToken string `usage:"If set, an ngrok tunnel will be established. It is HIGHLY recommended to pass as an environment variable."` NgrokToken string `usage:"If set, an ngrok tunnel will be established. It is HIGHLY recommended to pass as an environment variable."`
AutoScale AutoScale AutoScale AutoScale
@@ -133,7 +121,7 @@ func main() {
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
defer cancel() defer cancel()
metricsBuilder := NewMetricsBuilder(config.MetricsBackend, &config.MetricsBackendConfig) metricsBuilder := server.NewMetricsBuilder(config.MetricsBackend, &config.MetricsBackendConfig)
downScalerEnabled := config.AutoScale.Down && (config.InKubeCluster || config.KubeConfig != "") downScalerEnabled := config.AutoScale.Down && (config.InKubeCluster || config.KubeConfig != "")
downScalerDelay, err := time.ParseDuration(config.AutoScale.DownAfter) downScalerDelay, err := time.ParseDuration(config.AutoScale.DownAfter)
@@ -147,13 +135,13 @@ func main() {
signal.Notify(c, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP) signal.Notify(c, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)
if config.Routes.Config != "" { if config.Routes.Config != "" {
err := server.RoutesConfig.ReadRoutesConfig(config.Routes.Config) err := server.RoutesConfigLoader.Load(config.Routes.Config)
if err != nil { if err != nil {
logrus.WithError(err).Fatal("Unable to load routes from config file") logrus.WithError(err).Fatal("Unable to load routes from config file")
} }
if config.Routes.ConfigWatch { if config.Routes.ConfigWatch {
err := server.RoutesConfig.WatchForChanges(ctx) err := server.RoutesConfigLoader.WatchForChanges(ctx)
if err != nil { if err != nil {
logrus.WithError(err).Fatal("Unable to watch for changes") logrus.WithError(err).Fatal("Unable to watch for changes")
} }
@@ -258,7 +246,7 @@ func main() {
case syscall.SIGHUP: case syscall.SIGHUP:
if config.Routes.Config != "" { if config.Routes.Config != "" {
logrus.Info("Received SIGHUP, reloading routes config...") logrus.Info("Received SIGHUP, reloading routes config...")
if err := server.RoutesConfig.ReloadRoutesConfig(); err != nil { if err := server.RoutesConfigLoader.Reload(); err != nil {
logrus. logrus.
WithError(err). WithError(err).
WithField("routesConfig", config.Routes.Config). WithField("routesConfig", config.Routes.Config).
+1 -3
View File
@@ -1,8 +1,6 @@
module github.com/itzg/mc-router module github.com/itzg/mc-router
go 1.24.0 go 1.24.4
toolchain go1.24.4
require ( require (
github.com/fsnotify/fsnotify v1.9.0 github.com/fsnotify/fsnotify v1.9.0
+85 -2
View File
@@ -1,6 +1,7 @@
package server package server
import ( import (
"encoding/json"
"expvar" "expvar"
"net/http" "net/http"
@@ -9,11 +10,12 @@ import (
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
var apiRoutes = mux.NewRouter()
func StartApiServer(apiBinding string) { func StartApiServer(apiBinding string) {
logrus.WithField("binding", apiBinding).Info("Serving API requests") logrus.WithField("binding", apiBinding).Info("Serving API requests")
var apiRoutes = mux.NewRouter()
registerApiRoutes(apiRoutes)
apiRoutes.Path("/vars").Handler(expvar.Handler()) apiRoutes.Path("/vars").Handler(expvar.Handler())
apiRoutes.Path("/metrics").Handler(promhttp.Handler()) apiRoutes.Path("/metrics").Handler(promhttp.Handler())
@@ -23,3 +25,84 @@ func StartApiServer(apiBinding string) {
http.ListenAndServe(apiBinding, apiRoutes)).Error("API server failed") http.ListenAndServe(apiBinding, apiRoutes)).Error("API server failed")
}() }()
} }
func registerApiRoutes(apiRoutes *mux.Router) {
apiRoutes.Path("/routes").Methods("GET").
HandlerFunc(routesListHandler)
apiRoutes.Path("/routes").Methods("POST").
HandlerFunc(routesCreateHandler)
apiRoutes.Path("/defaultRoute").Methods("POST").
HandlerFunc(routesSetDefault)
apiRoutes.Path("/routes/{serverAddress}").Methods("DELETE").HandlerFunc(routesDeleteHandler)
}
func routesListHandler(writer http.ResponseWriter, _ *http.Request) {
mappings := Routes.GetMappings()
bytes, err := json.Marshal(mappings)
if err != nil {
logrus.WithError(err).Error("Failed to marshal mappings")
writer.WriteHeader(http.StatusInternalServerError)
return
}
writer.Header().Set("Content-Type", "application/json")
_, err = writer.Write(bytes)
if err != nil {
logrus.WithError(err).Error("Failed to write response")
}
}
func routesDeleteHandler(writer http.ResponseWriter, request *http.Request) {
serverAddress := mux.Vars(request)["serverAddress"]
if serverAddress != "" {
if Routes.DeleteMapping(serverAddress) {
writer.WriteHeader(http.StatusOK)
} else {
writer.WriteHeader(http.StatusNotFound)
}
RoutesConfigLoader.SaveRoutes()
}
}
func routesCreateHandler(writer http.ResponseWriter, request *http.Request) {
var definition = struct {
ServerAddress string
Backend string
}{}
//goland:noinspection GoUnhandledErrorResult
defer request.Body.Close()
decoder := json.NewDecoder(request.Body)
err := decoder.Decode(&definition)
if err != nil {
logrus.WithError(err).Error("Unable to get request body")
writer.WriteHeader(http.StatusBadRequest)
return
}
Routes.CreateMapping(definition.ServerAddress, definition.Backend, EmptyScalerFunc, EmptyScalerFunc)
RoutesConfigLoader.SaveRoutes()
writer.WriteHeader(http.StatusCreated)
}
func routesSetDefault(writer http.ResponseWriter, request *http.Request) {
var body = struct {
Backend string
}{}
//goland:noinspection GoUnhandledErrorResult
defer request.Body.Close()
decoder := json.NewDecoder(request.Body)
err := decoder.Decode(&body)
if err != nil {
logrus.WithError(err).Error("Unable to parse request")
writer.WriteHeader(http.StatusBadRequest)
return
}
Routes.SetDefaultRoute(body.Backend)
RoutesConfigLoader.SaveRoutes()
writer.WriteHeader(http.StatusOK)
}
+22 -11
View File
@@ -1,4 +1,4 @@
package main package server
import ( import (
"context" "context"
@@ -13,14 +13,13 @@ import (
kitinflux "github.com/go-kit/kit/metrics/influx" kitinflux "github.com/go-kit/kit/metrics/influx"
prometheusMetrics "github.com/go-kit/kit/metrics/prometheus" prometheusMetrics "github.com/go-kit/kit/metrics/prometheus"
influx "github.com/influxdata/influxdb1-client/v2" influx "github.com/influxdata/influxdb1-client/v2"
"github.com/itzg/mc-router/server"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto" "github.com/prometheus/client_golang/prometheus/promauto"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
type MetricsBuilder interface { type MetricsBuilder interface {
BuildConnectorMetrics() *server.ConnectorMetrics BuildConnectorMetrics() *ConnectorMetrics
Start(ctx context.Context) error Start(ctx context.Context) error
} }
@@ -31,6 +30,18 @@ const (
MetricsBackendDiscard = "discard" MetricsBackendDiscard = "discard"
) )
type MetricsBackendConfig struct {
Influxdb struct {
Interval time.Duration `default:"1m"`
Tags map[string]string `usage:"any extra tags to be included with all reported metrics"`
Addr string
Username string
Password string
Database string
RetentionPolicy string
}
}
// NewMetricsBuilder creates a new MetricsBuilder based on the specified backend. // NewMetricsBuilder creates a new MetricsBuilder based on the specified backend.
// If the backend is not recognized, a discard builder is returned. // If the backend is not recognized, a discard builder is returned.
// config can be nil if the backend is not influxdb. // config can be nil if the backend is not influxdb.
@@ -57,9 +68,9 @@ func (b expvarMetricsBuilder) Start(ctx context.Context) error {
return nil return nil
} }
func (b expvarMetricsBuilder) BuildConnectorMetrics() *server.ConnectorMetrics { func (b expvarMetricsBuilder) BuildConnectorMetrics() *ConnectorMetrics {
c := expvarMetrics.NewCounter("connections") c := expvarMetrics.NewCounter("connections")
return &server.ConnectorMetrics{ return &ConnectorMetrics{
Errors: expvarMetrics.NewCounter("errors").With("subsystem", "connector"), Errors: expvarMetrics.NewCounter("errors").With("subsystem", "connector"),
BytesTransmitted: expvarMetrics.NewCounter("bytes"), BytesTransmitted: expvarMetrics.NewCounter("bytes"),
ConnectionsFrontend: c, ConnectionsFrontend: c,
@@ -79,8 +90,8 @@ func (b discardMetricsBuilder) Start(ctx context.Context) error {
return nil return nil
} }
func (b discardMetricsBuilder) BuildConnectorMetrics() *server.ConnectorMetrics { func (b discardMetricsBuilder) BuildConnectorMetrics() *ConnectorMetrics {
return &server.ConnectorMetrics{ return &ConnectorMetrics{
Errors: discardMetrics.NewCounter(), Errors: discardMetrics.NewCounter(),
BytesTransmitted: discardMetrics.NewCounter(), BytesTransmitted: discardMetrics.NewCounter(),
ConnectionsFrontend: discardMetrics.NewCounter(), ConnectionsFrontend: discardMetrics.NewCounter(),
@@ -121,7 +132,7 @@ func (b *influxMetricsBuilder) Start(ctx context.Context) error {
return nil return nil
} }
func (b *influxMetricsBuilder) BuildConnectorMetrics() *server.ConnectorMetrics { func (b *influxMetricsBuilder) BuildConnectorMetrics() *ConnectorMetrics {
influxConfig := &b.config.Influxdb influxConfig := &b.config.Influxdb
metrics := kitinflux.New(influxConfig.Tags, influx.BatchPointsConfig{ metrics := kitinflux.New(influxConfig.Tags, influx.BatchPointsConfig{
@@ -132,7 +143,7 @@ func (b *influxMetricsBuilder) BuildConnectorMetrics() *server.ConnectorMetrics
b.metrics = metrics b.metrics = metrics
c := metrics.NewCounter("mc_router_connections") c := metrics.NewCounter("mc_router_connections")
return &server.ConnectorMetrics{ return &ConnectorMetrics{
Errors: metrics.NewCounter("mc_router_errors"), Errors: metrics.NewCounter("mc_router_errors"),
BytesTransmitted: metrics.NewCounter("mc_router_transmitted_bytes"), BytesTransmitted: metrics.NewCounter("mc_router_transmitted_bytes"),
ConnectionsFrontend: c.With("side", "frontend"), ConnectionsFrontend: c.With("side", "frontend"),
@@ -155,13 +166,13 @@ func (b prometheusMetricsBuilder) Start(ctx context.Context) error {
return nil return nil
} }
func (b prometheusMetricsBuilder) BuildConnectorMetrics() *server.ConnectorMetrics { func (b prometheusMetricsBuilder) BuildConnectorMetrics() *ConnectorMetrics {
pcv = prometheusMetrics.NewCounter(promauto.NewCounterVec(prometheus.CounterOpts{ pcv = prometheusMetrics.NewCounter(promauto.NewCounterVec(prometheus.CounterOpts{
Namespace: "mc_router", Namespace: "mc_router",
Name: "errors", Name: "errors",
Help: "The total number of errors", Help: "The total number of errors",
}, []string{"type"})) }, []string{"type"}))
return &server.ConnectorMetrics{ return &ConnectorMetrics{
Errors: pcv, Errors: pcv,
BytesTransmitted: prometheusMetrics.NewCounter(promauto.NewCounterVec(prometheus.CounterOpts{ BytesTransmitted: prometheusMetrics.NewCounter(promauto.NewCounterVec(prometheus.CounterOpts{
Namespace: "mc_router", Namespace: "mc_router",
+5 -85
View File
@@ -2,13 +2,10 @@ package server
import ( import (
"context" "context"
"encoding/json"
"net/http"
"regexp" "regexp"
"strings" "strings"
"sync" "sync"
"github.com/gorilla/mux"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
@@ -18,88 +15,6 @@ var EmptyScalerFunc = func(ctx context.Context) error { return nil }
var tcpShieldPattern = regexp.MustCompile("///.*") var tcpShieldPattern = regexp.MustCompile("///.*")
func init() {
apiRoutes.Path("/routes").Methods("GET").
Headers("Accept", "application/json").
HandlerFunc(routesListHandler)
apiRoutes.Path("/routes").Methods("POST").
Headers("Content-Type", "application/json").
HandlerFunc(routesCreateHandler)
apiRoutes.Path("/defaultRoute").Methods("POST").
Headers("Content-Type", "application/json").
HandlerFunc(routesSetDefault)
apiRoutes.Path("/routes/{serverAddress}").Methods("DELETE").HandlerFunc(routesDeleteHandler)
}
func routesListHandler(writer http.ResponseWriter, _ *http.Request) {
mappings := Routes.GetMappings()
bytes, err := json.Marshal(mappings)
if err != nil {
logrus.WithError(err).Error("Failed to marshal mappings")
writer.WriteHeader(http.StatusInternalServerError)
return
}
_, err = writer.Write(bytes)
if err != nil {
logrus.WithError(err).Error("Failed to write response")
}
}
func routesDeleteHandler(writer http.ResponseWriter, request *http.Request) {
serverAddress := mux.Vars(request)["serverAddress"]
RoutesConfig.DeleteMapping(serverAddress)
if serverAddress != "" {
if Routes.DeleteMapping(serverAddress) {
writer.WriteHeader(http.StatusOK)
} else {
writer.WriteHeader(http.StatusNotFound)
}
}
}
func routesCreateHandler(writer http.ResponseWriter, request *http.Request) {
var definition = struct {
ServerAddress string
Backend string
}{}
//goland:noinspection GoUnhandledErrorResult
defer request.Body.Close()
decoder := json.NewDecoder(request.Body)
err := decoder.Decode(&definition)
if err != nil {
logrus.WithError(err).Error("Unable to get request body")
writer.WriteHeader(http.StatusBadRequest)
return
}
Routes.CreateMapping(definition.ServerAddress, definition.Backend, EmptyScalerFunc, EmptyScalerFunc)
RoutesConfig.AddMapping(definition.ServerAddress, definition.Backend)
writer.WriteHeader(http.StatusCreated)
}
func routesSetDefault(writer http.ResponseWriter, request *http.Request) {
var body = struct {
Backend string
}{}
//goland:noinspection GoUnhandledErrorResult
defer request.Body.Close()
decoder := json.NewDecoder(request.Body)
err := decoder.Decode(&body)
if err != nil {
logrus.WithError(err).Error("Unable to parse request")
writer.WriteHeader(http.StatusBadRequest)
return
}
Routes.SetDefaultRoute(body.Backend)
RoutesConfig.SetDefaultRoute(body.Backend)
writer.WriteHeader(http.StatusOK)
}
type IRoutes interface { type IRoutes interface {
Reset() Reset()
RegisterAll(mappings map[string]string) RegisterAll(mappings map[string]string)
@@ -112,6 +27,7 @@ type IRoutes interface {
DeleteMapping(serverAddress string) bool DeleteMapping(serverAddress string) bool
CreateMapping(serverAddress string, backend string, waker ScalerFunc, sleeper ScalerFunc) CreateMapping(serverAddress string, backend string, waker ScalerFunc, sleeper ScalerFunc)
SetDefaultRoute(backend string) SetDefaultRoute(backend string)
GetDefaultRoute() string
SimplifySRV(srvEnabled bool) SimplifySRV(srvEnabled bool)
} }
@@ -157,6 +73,10 @@ func (r *routesImpl) SetDefaultRoute(backend string) {
}).Info("Using default route") }).Info("Using default route")
} }
func (r *routesImpl) GetDefaultRoute() string {
return r.defaultRoute
}
func (r *routesImpl) SimplifySRV(srvEnabled bool) { func (r *routesImpl) SimplifySRV(srvEnabled bool) {
r.simplifySRV = srvEnabled r.simplifySRV = srvEnabled
} }
-260
View File
@@ -1,260 +0,0 @@
package server
import (
"context"
"encoding/json"
"time"
"github.com/fsnotify/fsnotify"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"io/fs"
"os"
"sync"
)
type IRoutesConfig interface {
ReadRoutesConfig(routesConfig string)
ReloadRoutesConfig()
AddMapping(serverAddress string, backend string)
DeleteMapping(serverAddress string)
SetDefaultRoute(backend string)
WatchForChanges(ctx context.Context) error
}
const debounceConfigRereadDuration = time.Second * 5
var RoutesConfig = &routesConfigImpl{}
type routesConfigImpl struct {
sync.RWMutex
fileName string
}
type routesConfigStructure struct {
DefaultServer string `json:"default-server"`
Mappings map[string]string `json:"mappings"`
}
func (r *routesConfigImpl) ReadRoutesConfig(routesConfig string) error {
r.fileName = routesConfig
logrus.WithField("routesConfig", r.fileName).Info("Loading routes config file")
config, readErr := r.readRoutesConfigFile()
if readErr != nil {
if errors.Is(readErr, fs.ErrNotExist) {
logrus.WithField("routesConfig", r.fileName).Info("Routes config file doses not exist, skipping reading it")
// File doesn't exist -> ignore it
return nil
}
return errors.Wrap(readErr, "Could not load the routes config file")
}
Routes.RegisterAll(config.Mappings)
Routes.SetDefaultRoute(config.DefaultServer)
return nil
}
func (r *routesConfigImpl) ReloadRoutesConfig() error {
config, readErr := r.readRoutesConfigFile()
if readErr != nil {
return readErr
}
logrus.WithField("routesConfig", r.fileName).Info("Re-loading routes config file")
Routes.Reset()
Routes.RegisterAll(config.Mappings)
Routes.SetDefaultRoute(config.DefaultServer)
return nil
}
func (r *routesConfigImpl) WatchForChanges(ctx context.Context) error {
if r.fileName == "" {
return errors.New("routes config file needs to be specified first")
}
watcher, err := fsnotify.NewWatcher()
if err != nil {
return errors.Wrap(err, "Could not create a watcher")
}
err = watcher.Add(r.fileName)
if err != nil {
return errors.Wrap(err, "Could not watch the routes config file")
}
go func() {
logrus.WithField("file", r.fileName).Info("Watching routes config file")
debounceTimerChan := make(<-chan time.Time)
var debounceTimer *time.Timer
//goland:noinspection GoUnhandledErrorResult
defer watcher.Close()
for {
select {
case event, ok := <-watcher.Events:
if !ok {
logrus.Debug("Watcher events channel closed")
return
}
logrus.
WithField("file", event.Name).
WithField("op", event.Op).
Trace("fs event received")
if event.Op.Has(fsnotify.Write) || event.Op.Has(fsnotify.Create) {
if debounceTimer == nil {
debounceTimer = time.NewTimer(debounceConfigRereadDuration)
} else {
debounceTimer.Reset(debounceConfigRereadDuration)
}
debounceTimerChan = debounceTimer.C
logrus.WithField("delay", debounceConfigRereadDuration).Debug("Will re-read config file after delay")
}
case <-debounceTimerChan:
readErr := r.ReadRoutesConfig(r.fileName)
if readErr != nil {
logrus.
WithError(readErr).
WithField("routesConfig", r.fileName).
Error("Could not re-read the routes config file")
}
case <-ctx.Done():
return
}
}
}()
return nil
}
func (r *routesConfigImpl) AddMapping(serverAddress string, backend string) {
if !r.isRoutesConfigEnabled() {
return
}
config, readErr := r.readRoutesConfigFile()
if readErr != nil && !errors.Is(readErr, fs.ErrNotExist) {
logrus.WithError(readErr).Error("Could not read the routes config file")
return
}
if config.Mappings == nil {
config.Mappings = make(map[string]string)
}
config.Mappings[serverAddress] = backend
writeErr := r.writeRoutesConfigFile(config)
if writeErr != nil {
logrus.WithError(writeErr).Error("Could not write to the routes config file")
return
}
logrus.WithFields(logrus.Fields{
"serverAddress": serverAddress,
"backend": backend,
}).Info("Added route to routes config")
return
}
func (r *routesConfigImpl) SetDefaultRoute(backend string) {
if !r.isRoutesConfigEnabled() {
return
}
config, readErr := r.readRoutesConfigFile()
if readErr != nil && !errors.Is(readErr, fs.ErrNotExist) {
logrus.WithError(readErr).Error("Could not read the routes config file")
return
}
config.DefaultServer = backend
writeErr := r.writeRoutesConfigFile(config)
if writeErr != nil {
logrus.WithError(writeErr).Error("Could not write to the routes config file")
return
}
logrus.WithFields(logrus.Fields{
"backend": backend,
}).Info("Set default route in routes config")
return
}
func (r *routesConfigImpl) DeleteMapping(serverAddress string) {
if !r.isRoutesConfigEnabled() {
return
}
config, readErr := r.readRoutesConfigFile()
if readErr != nil && !errors.Is(readErr, fs.ErrNotExist) {
logrus.WithError(readErr).Error("Could not read the routes config file")
return
}
delete(config.Mappings, serverAddress)
writeErr := r.writeRoutesConfigFile(config)
if writeErr != nil {
logrus.WithError(writeErr).Error("Could not write to the routes config file")
return
}
logrus.WithField("serverAddress", serverAddress).Info("Deleted route in routes config")
return
}
func (r *routesConfigImpl) isRoutesConfigEnabled() bool {
return r.fileName != ""
}
func (r *routesConfigImpl) readRoutesConfigFile() (routesConfigStructure, error) {
r.RLock()
defer r.RUnlock()
config := routesConfigStructure{
"",
make(map[string]string),
}
file, fileErr := os.ReadFile(r.fileName)
if fileErr != nil {
return config, errors.Wrap(fileErr, "Could not load the routes config file")
}
parseErr := json.Unmarshal(file, &config)
if parseErr != nil {
return config, errors.Wrap(parseErr, "Could not parse the json routes config file")
}
return config, nil
}
func (r *routesConfigImpl) writeRoutesConfigFile(config routesConfigStructure) error {
r.Lock()
defer r.Unlock()
newFileContent, parseErr := json.Marshal(config)
if parseErr != nil {
return errors.Wrap(parseErr, "Could not parse the routes to json")
}
fileErr := os.WriteFile(r.fileName, newFileContent, 0664)
if fileErr != nil {
return errors.Wrap(fileErr, "Could not write to the routes config file")
}
return nil
}
+177
View File
@@ -0,0 +1,177 @@
package server
import (
"context"
"encoding/json"
"time"
"github.com/fsnotify/fsnotify"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"io/fs"
"os"
)
const debounceConfigRereadDuration = time.Second * 5
var RoutesConfigLoader = &routesConfigLoader{}
type routesConfigLoader struct {
fileName string
}
// RoutesConfigSchema declares the schema of the json file that can provide routes to serve
type RoutesConfigSchema struct {
DefaultServer string `json:"default-server"`
Mappings map[string]string `json:"mappings"`
}
func (r *routesConfigLoader) Load(routesConfigFileName string) error {
r.fileName = routesConfigFileName
logrus.WithField("routesConfigFileName", r.fileName).Info("Loading routes config file")
config, readErr := r.readFile()
if readErr != nil {
if errors.Is(readErr, fs.ErrNotExist) {
logrus.WithField("routesConfigFileName", r.fileName).Info("Routes config file doses not exist, skipping reading it")
// File doesn't exist -> ignore it
return nil
}
return errors.Wrap(readErr, "Could not load the routes config file")
}
Routes.RegisterAll(config.Mappings)
Routes.SetDefaultRoute(config.DefaultServer)
return nil
}
func (r *routesConfigLoader) Reload() error {
config, readErr := r.readFile()
if readErr != nil {
return readErr
}
logrus.WithField("routesConfig", r.fileName).Info("Re-loading routes config file")
Routes.Reset()
Routes.RegisterAll(config.Mappings)
Routes.SetDefaultRoute(config.DefaultServer)
return nil
}
func (r *routesConfigLoader) WatchForChanges(ctx context.Context) error {
if r.fileName == "" {
return errors.New("routes config file needs to be specified first")
}
watcher, err := fsnotify.NewWatcher()
if err != nil {
return errors.Wrap(err, "Could not create a watcher")
}
err = watcher.Add(r.fileName)
if err != nil {
return errors.Wrap(err, "Could not watch the routes config file")
}
go func() {
logrus.WithField("file", r.fileName).Info("Watching routes config file")
debounceTimerChan := make(<-chan time.Time)
var debounceTimer *time.Timer
//goland:noinspection GoUnhandledErrorResult
defer watcher.Close()
for {
select {
case event, ok := <-watcher.Events:
if !ok {
logrus.Debug("Watcher events channel closed")
return
}
logrus.
WithField("file", event.Name).
WithField("op", event.Op).
Trace("fs event received")
if event.Op.Has(fsnotify.Write) || event.Op.Has(fsnotify.Create) {
if debounceTimer == nil {
debounceTimer = time.NewTimer(debounceConfigRereadDuration)
} else {
debounceTimer.Reset(debounceConfigRereadDuration)
}
debounceTimerChan = debounceTimer.C
logrus.WithField("delay", debounceConfigRereadDuration).Debug("Will re-read config file after delay")
}
case <-debounceTimerChan:
readErr := r.Load(r.fileName)
if readErr != nil {
logrus.
WithError(readErr).
WithField("routesConfig", r.fileName).
Error("Could not re-read the routes config file")
}
case <-ctx.Done():
return
}
}
}()
return nil
}
func (r *routesConfigLoader) SaveRoutes() {
if !r.isEnabled() {
return
}
err := r.writeFile(&RoutesConfigSchema{
DefaultServer: Routes.GetDefaultRoute(),
Mappings: Routes.GetMappings(),
})
if err != nil {
logrus.WithError(err).Error("Could not save the routes config file")
return
}
logrus.Info("Saved routes config")
}
func (r *routesConfigLoader) isEnabled() bool {
return r.fileName != ""
}
func (r *routesConfigLoader) readFile() (*RoutesConfigSchema, error) {
var config RoutesConfigSchema
content, err := os.ReadFile(r.fileName)
if err != nil {
return &config, errors.Wrap(err, "Could not load the routes config file")
}
parseErr := json.Unmarshal(content, &config)
if parseErr != nil {
return &config, errors.Wrap(parseErr, "Could not parse the json routes config file")
}
return &config, nil
}
func (r *routesConfigLoader) writeFile(config *RoutesConfigSchema) error {
newFileContent, err := json.Marshal(config)
if err != nil {
return errors.Wrap(err, "Could not parse the routes to json")
}
err = os.WriteFile(r.fileName, newFileContent, 0664)
if err != nil {
return errors.Wrap(err, "Could not write to the routes config file")
}
return nil
}