mirror of
https://github.com/Octops/quake-kube.git
synced 2026-04-05 17:20:33 +00:00
Add yaml server config, restart server when ConfigMap changes
This commit is contained in:
@ -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"`
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
30
internal/util/exec/exec.go
Normal file
30
internal/util/exec/exec.go
Normal 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...)}
|
||||
}
|
||||
Reference in New Issue
Block a user