Add yaml server config, restart server when ConfigMap changes

This commit is contained in:
chris
2020-08-03 16:35:41 -04:00
committed by Chris Marshall
parent ffca54fdb8
commit 1ce209bdbc
9 changed files with 326 additions and 149 deletions

View File

@ -9,6 +9,7 @@ import (
"time"
"github.com/pkg/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
type GameType int
@ -57,21 +58,24 @@ func (gt *GameType) UnmarshalText(data []byte) error {
}
type Config struct {
FragLimit int `name:"fraglimit"`
TimeLimit time.Duration `name:"timelimit"`
FragLimit int `name:"fraglimit"`
TimeLimit metav1.Duration `name:"timelimit"`
GameConfig
ServerConfig
GameConfig `json:"game"`
FileServerConfig `json:"fs"`
ServerConfig `json:"server"`
Maps
}
type GameConfig struct {
ForceRespawn bool `name:"g_forcerespawn"`
GameType GameType `name:"g_gametype"`
Inactivity time.Duration `name:"g_inactivity"`
Log string `name:"g_log"`
MOTD string `name:"g_motd"`
QuadFactor int `name:"g_quadfactor"`
WeaponRespawn int `name:"g_weaponrespawn"`
ForceRespawn bool `name:"g_forcerespawn"`
GameType GameType `json:"type" name:"g_gametype"`
Inactivity metav1.Duration `name:"g_inactivity"`
Log string `name:"g_log"`
MOTD string `name:"g_motd"`
QuadFactor int `name:"g_quadfactor"`
WeaponRespawn int `name:"g_weaponrespawn"`
}
type FileServerConfig struct {
@ -117,6 +121,14 @@ func writeStruct(v reflect.Value) ([]byte, error) {
return nil, err
}
b.Write(data)
case reflect.Slice:
switch val := fv.Interface().(type) {
case Maps:
data, _ := val.Marshal()
b.Write(data)
default:
panic(fmt.Errorf("received unknown type %T", val))
}
default:
tv, ok := v.Type().Field(i).Tag.Lookup("name")
if !ok {
@ -142,7 +154,7 @@ func toString(name string, v reflect.Value) string {
return val
case int:
return strconv.Itoa(val)
case time.Duration:
case metav1.Duration:
switch name {
case "TimeLimit":
return fmt.Sprintf("%d", int(val.Minutes()))
@ -156,6 +168,9 @@ func toString(name string, v reflect.Value) string {
return "0"
case GameType:
return fmt.Sprintf("%d", val)
case Maps:
data, _ := val.Marshal()
return string(data)
default:
panic(fmt.Errorf("received unknown type %T", v.Interface()))
}
@ -163,15 +178,15 @@ func toString(name string, v reflect.Value) string {
func Default() *Config {
return &Config{
TimeLimit: 15 * time.Minute,
FragLimit: 25,
TimeLimit: metav1.Duration{Duration: 15 * time.Minute},
GameConfig: GameConfig{
Log: "",
MOTD: "Welcome to Critical Stack",
QuadFactor: 3,
GameType: FreeForAll,
WeaponRespawn: 3,
Inactivity: 10 * time.Minute,
Inactivity: metav1.Duration{Duration: 10 * time.Minute},
ForceRespawn: false,
},
ServerConfig: ServerConfig{
@ -179,9 +194,22 @@ func Default() *Config {
Hostname: "quakekube",
Password: "changeme",
},
Maps: Maps{
{Name: "q3dm7", Type: FreeForAll},
{Name: "q3dm17", Type: FreeForAll},
},
}
}
type Map struct {
Name string `json:"name"`
Type GameType `json:"type"`
CaptureLimit int `json:"captureLimit"`
FragLimit int `json:"fragLimit"`
TimeLimit time.Duration `json:"timeLimit"`
}
type Maps []Map
func (maps Maps) Marshal() ([]byte, error) {
@ -210,12 +238,3 @@ func (maps Maps) Marshal() ([]byte, error) {
b.WriteString("vstr d0")
return b.Bytes(), nil
}
type Map struct {
Name string `json:"name"`
Type GameType `json:"type"`
CaptureLimit int `json:"captureLimit"`
FragLimit int `json:"fragLimit"`
TimeLimit time.Duration `json:"timeLimit"`
}

View File

@ -8,35 +8,22 @@ import (
"sigs.k8s.io/yaml"
)
const expectedConfig = `seta fraglimit "25"
seta timelimit "15"
seta g_forcerespawn "0"
seta g_gametype "0"
seta g_inactivity "600"
seta g_log ""
seta g_motd "Welcome to Critical Stack"
seta g_quadfactor "3"
seta g_weaponrespawn "3"
seta sv_allowDownload "0"
seta sv_hostname "quakekube"
seta sv_maxclients "12"
seta rconpassword "changeme"
`
func TestConfigMarshal(t *testing.T) {
c := Default()
data, err := c.Marshal()
if err != nil {
t.Fatal(err)
}
fmt.Printf("data = %s\n", data)
if diff := cmp.Diff(string(data), expectedConfig); diff != "" {
t.Fatalf(diff)
}
}
const mapConfig = `- name: q3dm7
const config = `
fragLimit: 25
timeLimit: 15m
game:
motd: "Welcome to Critical Stack"
type: FreeForAll
forceRespawn: false
inactivity: 10m
quadFactor: 3
weaponRespawn: 3
server:
hostname: "quakekube"
maxClients: 12
password: "changeme"
maps:
- name: q3dm7
type: FreeForAll
- name: q3dm17
type: FreeForAll
@ -52,7 +39,24 @@ const mapConfig = `- name: q3dm7
type: Tournament
`
const expectedMapConfig = `set d0 "seta g_gametype 0 ; map q3dm7 ; set nextmap vstr d1"
const expectedConfig = `seta fraglimit "25"
seta g_forcerespawn "0"
seta g_gametype "0"
seta g_log ""
seta g_motd "Welcome to Critical Stack"
seta g_quadfactor "3"
seta g_weaponrespawn "3"
seta fs_basegame ""
seta fs_basepath ""
seta fs_copyfiles "0"
seta fs_debug "0"
seta fs_game ""
seta fs_homepath ""
seta sv_allowDownload "0"
seta sv_hostname "quakekube"
seta sv_maxclients "12"
seta rconpassword "changeme"
set d0 "seta g_gametype 0 ; map q3dm7 ; set nextmap vstr d1"
set d1 "seta g_gametype 0 ; map q3dm17 ; set nextmap vstr d2"
set d2 "seta g_gametype 4 ; capturelimit 8 ; map q3wctf1 ; set nextmap vstr d3"
set d3 "seta g_gametype 1 ; map q3tourney2 ; set nextmap vstr d4"
@ -60,21 +64,18 @@ set d4 "seta g_gametype 4 ; capturelimit 8 ; map q3wctf3 ; set nextmap vstr d5"
set d5 "seta g_gametype 1 ; map ztn3tourney1 ; set nextmap vstr d0"
vstr d0`
func TestMapRead(t *testing.T) {
var maps Maps
if err := yaml.Unmarshal([]byte(mapConfig), &maps); err != nil {
func TestConfigMarshal(t *testing.T) {
var cfg *Config
if err := yaml.Unmarshal([]byte(config), &cfg); err != nil {
t.Fatal(err)
}
for _, m := range maps {
fmt.Printf("m = %+v\n", m)
}
data, err := maps.Marshal()
data, err := cfg.Marshal()
if err != nil {
t.Fatal(err)
}
if diff := cmp.Diff(string(data), expectedMapConfig); diff != "" {
fmt.Printf("%s\n", data)
if diff := cmp.Diff(string(data), expectedConfig); diff != "" {
t.Fatalf(diff)
}
}

View File

@ -2,17 +2,126 @@ package server
import (
"context"
"io/ioutil"
"log"
"os"
"os/exec"
"path/filepath"
"time"
"sigs.k8s.io/yaml"
"github.com/criticalstack/quake-kube/internal/util/exec"
)
func Start(ctx context.Context, dir string) error {
cmd := exec.CommandContext(ctx, "ioq3ded", "+set", "dedicated", "1", "+exec", "server.cfg", "+exec", "maps.cfg")
cmd.Dir = dir
type Server struct {
Dir string
WatchInterval time.Duration
ConfigFile string
}
func (s *Server) Start(ctx context.Context) error {
cmd := exec.CommandContext(ctx, "ioq3ded", "+set", "dedicated", "1", "+exec", "server.cfg")
cmd.Dir = s.Dir
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if s.ConfigFile == "" {
cfg := Default()
data, err := cfg.Marshal()
if err != nil {
return err
}
if err := ioutil.WriteFile(filepath.Join(s.Dir, "baseq3/server.cfg"), data, 0644); err != nil {
return err
}
if err := cmd.Start(); err != nil {
return err
}
return cmd.Wait()
}
if err := s.reload(); err != nil {
return err
}
if err := cmd.Start(); err != nil {
return err
}
return cmd.Wait()
go func() {
if err := cmd.Wait(); err != nil {
log.Println(err)
}
}()
ch, err := s.watch(ctx)
if err != nil {
return err
}
for {
select {
case <-ch:
if err := s.reload(); err != nil {
return err
}
if err := cmd.Restart(ctx); err != nil {
return err
}
go func() {
if err := cmd.Wait(); err != nil {
log.Println(err)
}
}()
case <-ctx.Done():
return ctx.Err()
}
}
}
func (s *Server) reload() error {
data, err := ioutil.ReadFile(s.ConfigFile)
if err != nil {
return err
}
var cfg *Config
if err := yaml.Unmarshal(data, &cfg); err != nil {
return err
}
data, err = cfg.Marshal()
if err != nil {
return err
}
return ioutil.WriteFile(filepath.Join(s.Dir, "baseq3/server.cfg"), data, 0644)
}
func (s *Server) watch(ctx context.Context) (<-chan struct{}, error) {
if s.WatchInterval == 0 {
s.WatchInterval = 15 * time.Second
}
cur, err := os.Stat(s.ConfigFile)
if err != nil {
return nil, err
}
ch := make(chan struct{})
go func() {
ticker := time.NewTicker(s.WatchInterval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
if fi, err := os.Stat(s.ConfigFile); err == nil {
if fi.ModTime().After(cur.ModTime()) {
ch <- struct{}{}
}
cur = fi
}
case <-ctx.Done():
return
}
}
}()
return ch, nil
}

View File

@ -0,0 +1,30 @@
package exec
import (
"context"
"os/exec"
)
type Cmd struct {
*exec.Cmd
}
func (cmd *Cmd) Restart(ctx context.Context) error {
if cmd.Process != nil {
if err := cmd.Process.Kill(); err != nil {
return err
}
}
newCmd := exec.CommandContext(ctx, cmd.Args[0], cmd.Args[1:]...)
newCmd.Dir = cmd.Dir
newCmd.Env = cmd.Env
newCmd.Stdin = cmd.Stdin
newCmd.Stdout = cmd.Stdout
newCmd.Stderr = cmd.Stderr
cmd.Cmd = newCmd
return cmd.Start()
}
func CommandContext(ctx context.Context, name string, args ...string) *Cmd {
return &Cmd{Cmd: exec.CommandContext(ctx, name, args...)}
}