From 787ab54ca498544946550bfbdd3c0b5e80949d43 Mon Sep 17 00:00:00 2001 From: Lasse15 <36111747+LasseR15@users.noreply.github.com> Date: Mon, 5 Dec 2022 14:58:13 +0100 Subject: [PATCH] Persistent routes from REST API (#145) --- README.md | 2 + cmd/mc-router/main.go | 8 ++ server/routes.go | 3 + server/routes_config.go | 173 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 186 insertions(+) create mode 100644 server/routes_config.go diff --git a/README.md b/README.md index 7824988..acc1f61 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,8 @@ Routes Minecraft client connections to backend servers based upon the requested (env METRICS_BACKEND_CONFIG_INFLUXDB_USERNAME) -port port The port bound to listen for Minecraft client connections (env PORT) (default 25565) + -routes-config string + The path to the routes config file (env ROUTES_CONFIG) -version Output version and exit (env VERSION) ``` diff --git a/cmd/mc-router/main.go b/cmd/mc-router/main.go index ba87230..4340c6c 100644 --- a/cmd/mc-router/main.go +++ b/cmd/mc-router/main.go @@ -46,6 +46,7 @@ type Config struct { MetricsBackend string `default:"discard" usage:"Backend to use for metrics exposure/publishing: discard,expvar,influxdb"` UseProxyProtocol bool `default:"false" usage:"Send PROXY protocol to backend servers"` MetricsBackendConfig MetricsBackendConfig + RoutesConfig string `usage:"Name or full path to routes config file"` SimplifySRV bool `default:"false" usage:"Simplify fully qualified SRV records for mapping"` } @@ -100,6 +101,13 @@ func main() { c := make(chan os.Signal, 1) signal.Notify(c, syscall.SIGINT, syscall.SIGTERM) + if config.RoutesConfig != "" { + err := server.RoutesConfig.ReadRoutesConfig(config.RoutesConfig) + if err != nil { + logrus.WithError(err).Error("Unable to load routes from config file") + } + } + server.Routes.RegisterAll(parseMappings(config.Mapping)) if config.ConnectionRateLimit < 1 { diff --git a/server/routes.go b/server/routes.go index 4eb6263..ded52f3 100644 --- a/server/routes.go +++ b/server/routes.go @@ -37,6 +37,7 @@ func routesListHandler(writer http.ResponseWriter, request *http.Request) { 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) @@ -63,6 +64,7 @@ func routesCreateHandler(writer http.ResponseWriter, request *http.Request) { } Routes.CreateMapping(definition.ServerAddress, definition.Backend, func(ctx context.Context) error { return nil }) + RoutesConfig.AddMapping(definition.ServerAddress, definition.Backend) writer.WriteHeader(http.StatusCreated) } @@ -82,6 +84,7 @@ func routesSetDefault(writer http.ResponseWriter, request *http.Request) { } Routes.SetDefaultRoute(body.Backend) + RoutesConfig.SetDefaultRoute(body.Backend) writer.WriteHeader(http.StatusOK) } diff --git a/server/routes_config.go b/server/routes_config.go new file mode 100644 index 0000000..8f73e39 --- /dev/null +++ b/server/routes_config.go @@ -0,0 +1,173 @@ +package server + +import ( + "encoding/json" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "io/fs" + "os" + "sync" +) + +type IRoutesConfig interface { + ReadRoutesConfig(routesConfig string) + AddMapping(serverAddress string, backend string) + DeleteMapping(serverAddress string) + SetDefaultRoute(backend string) +} + +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) 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 +}