Skip to content

Commit

Permalink
feat: Auto config reload on file update (#235)
Browse files Browse the repository at this point in the history
  • Loading branch information
robinbraemer authored Aug 21, 2023
1 parent add9dec commit 5af2c7d
Show file tree
Hide file tree
Showing 23 changed files with 620 additions and 274 deletions.
29 changes: 21 additions & 8 deletions .web/docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,28 +111,37 @@ export default defineConfig({
link: '/developers/',
},
{
text: 'Configurations',
link: '/guide/config/',
text: 'Lite Mode',
link: '/guide/lite'
},
{
text: 'Security & DDoS Protection',
link: '/guide/security'
},
{
text: 'Lite Mode',
link: '/guide/lite'
text: 'Compatibility',
link: '/guide/compatibility'
},
]
},
{
text: 'Configuration',
items: [
{
text: 'Enabling Connect',
link: '/guide/connect'
},
{
text: 'Builtin Commands',
link: '/guide/builtin-commands'
text: 'Complete Configuration',
link: '/guide/config/',
},
{
text: 'Compatibility',
link: '/guide/compatibility'
text: 'Auto Reload',
link: '/guide/config/reload',
},
{
text: 'Builtin Commands',
link: '/guide/builtin-commands'
},
{
text: 'Rate Limiting',
Expand Down Expand Up @@ -164,6 +173,10 @@ export default defineConfig({
},
]
},
{
text: 'Back to Guides',
link: '/guide/',
},
],
// '/config/': [
// {
Expand Down
41 changes: 41 additions & 0 deletions .web/docs/guide/config/reload.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Auto Config Reload

_Gate watches your file for updates._

---

Gate supports automatic config reloading without restarting the proxy by watching your config file for changes
without disconnecting players.

This is useful for example when you want to change **any setting in the config** like servers, the motd or
switch to Lite mode while staying live.

::: tip
Generally all settings can be changed without disconnecting players,
however some session-related properties like `online-mode` will only apply to newly connected
players that joined after the config update and does not kick players that are already connected with another
online-mode.
:::

## How it works

Gate watches your config file for changes and reloads it automatically when it detects a change.
This is seen as a safe operation, as the config is validated before it is applied.
If it is invalid, the reload is aborted and the proxy continues to run with the last valid config.

## Switching to Lite mode and Connect

If you want to switch to [Lite mode](/guide/lite) or [Connect](/guide/connect), you can do so without restarting the
proxy.
This is useful if you want to test it out or if you want to switch to Lite mode temporarily for maintenance
or migration purposes.

## How to enable it

This feature is always enabled by default, given that you have a config file.

## How to disable it

This can not be disabled.
If you feel like you need to disable it, please [open an issue](
https://github.com/minekube/gate/issues/new?title=Disable%20auto%20config%20reload&body=I%20want%20to%20disable%20auto%20config%20reload%20because%20...).
5 changes: 3 additions & 2 deletions .web/docs/guide/connect.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
# Enabling Connect Integration

Gate has a builtin integration with [Connect](https://connect.minekube.com/) to list your proxy on the Connect network.
Gate has a builtin integration with [Connect](https://connect.minekube.com/) to list your proxy on
the [Connect network](https://connect.minekube.com/guide/#the-connect-network).

Great side effect is that it exposes your locally running Gate proxy to the Internet
Great side effect is that it also exposes your locally running Gate proxy to the Internet
and allows players to connect to it from anywhere using the free provided domain
`<my-server-name>.play.minekube.net`.

Expand Down
70 changes: 10 additions & 60 deletions cmd/gate/root.go
Original file line number Diff line number Diff line change
@@ -1,24 +1,19 @@
package gate

import (
"bytes"
"encoding/json"
"errors"
"fmt"
"math"
"os"
"path"
"strings"

"github.com/go-logr/logr"
"github.com/go-logr/zapr"
"github.com/spf13/viper"
"github.com/urfave/cli/v2"
"go.minekube.com/gate/pkg/gate"
"go.minekube.com/gate/pkg/gate/config"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"gopkg.in/yaml.v3"
)

// Execute runs App() and calls os.Exit when finished.
Expand Down Expand Up @@ -76,7 +71,12 @@ Visit the website https://gate.minekube.com/ for more information.`
// Load config
cfg, err := gate.LoadConfig(v)
if err != nil {
return cli.Exit(err, 1)
// A config file is only required to exist when explicit config flag was specified.
// Otherwise, we just use the default config.
if !(errors.As(err, &viper.ConfigFileNotFoundError{}) || os.IsNotExist(err)) || c.IsSet("config") {
err = fmt.Errorf("error reading config file %q: %w", v.ConfigFileUsed(), err)
return cli.Exit(err, 2)
}
}

// Flags overwrite config
Expand All @@ -98,7 +98,10 @@ Visit the website https://gate.minekube.com/ for more information.`
log.Info("using config file", "config", v.ConfigFileUsed())

// Start Gate
if err = gate.Start(c.Context, gate.WithConfig(*cfg)); err != nil {
if err = gate.Start(c.Context,
gate.WithConfig(*cfg),
gate.WithAutoConfigReload(v.ConfigFileUsed()),
); err != nil {
return cli.Exit(fmt.Errorf("error running Gate: %w", err), 1)
}
return nil
Expand All @@ -118,62 +121,9 @@ func initViper(c *cli.Context, configFile string) (*viper.Viper, error) {
v.SetEnvPrefix("GATE")
v.AutomaticEnv() // read in environment variables that match
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
// Read in config.
cfgCopy := func() config.Config { return config.DefaultConfig }()
if err := FixedReadInConfig(v, configFile, &cfgCopy); err != nil {
// A config file is only required to exist when explicit config flag was specified.
if !(errors.As(err, &viper.ConfigFileNotFoundError{}) || os.IsNotExist(err)) || c.IsSet("config") {
return nil, fmt.Errorf("error reading config file %q: %w", v.ConfigFileUsed(), err)
}
}
return v, nil
}

// FixedReadInConfig is a workaround for https://github.com/minekube/gate/issues/218#issuecomment-1632800775
func FixedReadInConfig[T any](v *viper.Viper, configFile string, defaultConfig *T) error {
if defaultConfig == nil {
return v.ReadInConfig()
}

if configFile == "" {
// Try to find config file using Viper's config finder logic
if err := v.ReadInConfig(); err != nil {
return err
}
configFile = v.ConfigFileUsed()
if configFile == "" {
return nil // no config file found
}
}

var (
unmarshal func([]byte, any) error
marshal func(any) ([]byte, error)
)
switch path.Ext(configFile) {
case ".yaml", ".yml":
unmarshal = yaml.Unmarshal
marshal = yaml.Marshal
case ".json":
unmarshal = json.Unmarshal
marshal = json.Marshal
default:
return fmt.Errorf("unsupported config file format %q", configFile)
}
b, err := os.ReadFile(configFile)
if err != nil {
return fmt.Errorf("error reading config file %q: %w", configFile, err)
}
if err = unmarshal(b, defaultConfig); err != nil {
return fmt.Errorf("error unmarshaling config file %q to %T: %w", configFile, defaultConfig, err)
}
if b, err = marshal(defaultConfig); err != nil {
return fmt.Errorf("error marshaling config file %q: %w", configFile, err)
}

return v.ReadConfig(bytes.NewReader(b))
}

// newLogger returns a new zap logger with a modified production
// or development default config to ensure human readability.
func newLogger(debug bool, v int) (l logr.Logger, err error) {
Expand Down
2 changes: 1 addition & 1 deletion config-lite.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,4 @@ config:
version:
name: §eTry example.com
protocol: -1
favicon: server-icon.png
#favicon: server-icon.png
2 changes: 1 addition & 1 deletion config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ config:
version:
name: §eTry example.com
protocol: -1
favicon: server-icon.png
#favicon: server-icon.png

# Configuration for Connect, a network that organizes all Minecraft servers/proxies
# and makes them universally accessible for all players.
Expand Down
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ require (
github.com/google/uuid v1.3.0
github.com/gookit/color v1.5.4
github.com/jellydator/ttlcache/v3 v3.0.1
github.com/knadh/koanf/providers/file v0.1.0
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646
github.com/pires/go-proxyproto v0.7.0
github.com/robinbraemer/event v0.0.1
Expand All @@ -26,7 +27,6 @@ require (
go.minekube.com/common v0.0.5
go.minekube.com/connect v0.5.3
go.uber.org/atomic v1.11.0
go.uber.org/multierr v1.11.0
go.uber.org/zap v1.25.0
golang.org/x/sync v0.3.0
golang.org/x/text v0.12.0
Expand Down Expand Up @@ -64,6 +64,7 @@ require (
github.com/subosito/gotenv v1.4.2 // indirect
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/image v0.9.0 // indirect
golang.org/x/sys v0.10.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20230726155614-23370e0ffb3e // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,8 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o
github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I=
github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/knadh/koanf/providers/file v0.1.0 h1:fs6U7nrV58d3CFAFh8VTde8TM262ObYf3ODrc//Lp+c=
github.com/knadh/koanf/providers/file v0.1.0/go.mod h1:rjJ/nHQl64iYCtAW2QQnF0eSmDEX/YZ/eNFj5yR6BvA=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
Expand Down
41 changes: 32 additions & 9 deletions pkg/edition/java/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ import (
"fmt"

liteconfig "go.minekube.com/gate/pkg/edition/java/lite/config"
"go.minekube.com/gate/pkg/edition/java/proto/version"
"go.minekube.com/gate/pkg/util/componentutil"
"go.minekube.com/gate/pkg/util/configutil"
"go.minekube.com/gate/pkg/util/favicon"
"go.minekube.com/gate/pkg/util/validation"
)

Expand All @@ -18,7 +22,7 @@ var DefaultConfig = Config{
},
Status: Status{
ShowMaxPlayers: 1000,
Motd: "§bA Gate Proxy\n§bVisit ➞ §fgit.luolix.top/minekube/gate",
Motd: defaultMotd(),
// Contains Gate's icon
Favicon: "data:image/png;base64,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",
LogPingRequests: false,
Expand Down Expand Up @@ -61,11 +65,18 @@ var DefaultConfig = Config{
RequireBuiltinCommandPermissions: false,
AnnounceProxyCommands: true,
Debug: false,
ShutdownReason: "§cGate proxy is shutting down...\nPlease reconnect in a moment!",
ShutdownReason: defaultShutdownReason(),
ForceKeyAuthentication: true,
Lite: liteconfig.DefaultConfig,
}

func defaultMotd() *configutil.TextComponent {
return text("§bA Gate Proxy\n§bVisit ➞ §fgit.luolix.top/minekube/gate")
}
func defaultShutdownReason() *configutil.TextComponent {
return text("§cGate proxy is shutting down...\nPlease reconnect in a moment!")
}

// Config is the configuration of the proxy.
type Config struct { // TODO use https://github.com/projectdiscovery/yamldoc-go for generating output yaml and markdown for the docs
Bind string `yaml:"bind"` // The address to listen for connections.
Expand Down Expand Up @@ -101,19 +112,19 @@ type Config struct { // TODO use https://github.com/projectdiscovery/yamldoc-go
AnnounceProxyCommands bool `yaml:"announceProxyCommands"`
ForceKeyAuthentication bool `yaml:"forceKeyAuthentication"` // Added in 1.19

Debug bool `yaml:"debug"`
ShutdownReason string `yaml:"shutdownReason"`
Debug bool `yaml:"debug"`
ShutdownReason *configutil.TextComponent `yaml:"shutdownReason"`

Lite liteconfig.Config `yaml:"lite"`
}

type (
ForcedHosts map[string][]string // virtualhost:server names
Status struct {
ShowMaxPlayers int `yaml:"showMaxPlayers"`
Motd string `yaml:"motd"`
Favicon string `yaml:"favicon"`
LogPingRequests bool `yaml:"logPingRequests"`
ShowMaxPlayers int `yaml:"showMaxPlayers"`
Motd *configutil.TextComponent `yaml:"motd"`
Favicon favicon.Favicon `yaml:"favicon"`
LogPingRequests bool `yaml:"logPingRequests"`
}
Query struct {
Enabled bool `yaml:"enabled"`
Expand All @@ -136,7 +147,7 @@ type (
}
QuotaSettings struct {
Enabled bool `yaml:"enabled"` // If false, there is no such limiting.
OPS float32 `yaml:"OPS"` // Allowed operations/events per second, per IP block
OPS float32 `yaml:"ops"` // Allowed operations/events per second, per IP block
Burst int `yaml:"burst"` // The maximum events per second, per block; the size of the token bucket
MaxEntries int `yaml:"maxEntries"` // Maximum number of IP blocks to keep track of in cache
}
Expand Down Expand Up @@ -243,3 +254,15 @@ func (c *Config) Validate() (warns []error, errs []error) {

return
}

func text(s string) *configutil.TextComponent {
return (*configutil.TextComponent)(must(componentutil.ParseTextComponent(
version.MinimumVersion.Protocol, s)))
}

func must[T any](t T, err error) T {
if err != nil {
panic(err)
}
return t
}
12 changes: 12 additions & 0 deletions pkg/edition/java/config/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package config

import (
"testing"

"github.com/stretchr/testify/require"
)

func Test_texts(t *testing.T) {
require.NotNil(t, defaultMotd())
require.NotNil(t, defaultShutdownReason())
}
Loading

0 comments on commit 5af2c7d

Please sign in to comment.