Code cleanup of routes config loader and API server (#424)
This commit is contained in:
@@ -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 }}
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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
@@ -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,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
@@ -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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user