diff --git a/Makefile b/Makefile index c898483..0dba590 100644 --- a/Makefile +++ b/Makefile @@ -10,7 +10,7 @@ build-win: generate GOOS=windows CGO_ENABLED=0 go build -o bin/svctl.exe ./ test: generate - go run gotest.tools/gotestsum@v1.11.0 -- -count=1 ./... + go run gotest.tools/gotestsum@v1.13.0 -- -count=1 ./... lint: go run github.com/golangci/golangci-lint/cmd/golangci-lint run diff --git a/internal/daemon/daemon.go b/internal/daemon/daemon.go index 26a4485..254f36b 100644 --- a/internal/daemon/daemon.go +++ b/internal/daemon/daemon.go @@ -150,11 +150,6 @@ func (s *Daemon) Start(path string) error { return err } - err = sv.Server().Render(false) - if err != nil { - return err - } - err = sv.Event(fsm.EventStart) if err != nil { return err diff --git a/internal/fsm/fsm.go b/internal/fsm/fsm.go index 3c63227..3860308 100644 --- a/internal/fsm/fsm.go +++ b/internal/fsm/fsm.go @@ -14,6 +14,7 @@ type GameServer interface { IsRunning() (bool, error) Render(bool) error Status() (*server.Status, error) + ApplyPatches() error } type FSM struct { diff --git a/internal/fsm/fsm_test.go b/internal/fsm/fsm_test.go index 33b8fbd..cf2880a 100644 --- a/internal/fsm/fsm_test.go +++ b/internal/fsm/fsm_test.go @@ -19,6 +19,8 @@ func (s *FSMSuite) TestStartStop() { state := NewStateStopped() fsm := New(gameServerMock, slog.Default(), state) + gameServerMock.EXPECT().ApplyPatches().Return(nil) + gameServerMock.EXPECT().Render(false).Return(nil) gameServerMock.EXPECT().Start().Return(nil) err := fsm.Event(EventStart) diff --git a/internal/fsm/mock_game_server.go b/internal/fsm/mock_game_server.go index ea40ca6..45cf33e 100644 --- a/internal/fsm/mock_game_server.go +++ b/internal/fsm/mock_game_server.go @@ -40,6 +40,20 @@ func (m *MockGameServer) EXPECT() *MockGameServerMockRecorder { return m.recorder } +// ApplyPatches mocks base method. +func (m *MockGameServer) ApplyPatches() error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ApplyPatches") + ret0, _ := ret[0].(error) + return ret0 +} + +// ApplyPatches indicates an expected call of ApplyPatches. +func (mr *MockGameServerMockRecorder) ApplyPatches() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ApplyPatches", reflect.TypeOf((*MockGameServer)(nil).ApplyPatches)) +} + // IsRunning mocks base method. func (m *MockGameServer) IsRunning() (bool, error) { m.ctrl.T.Helper() diff --git a/internal/fsm/state_restarting.go b/internal/fsm/state_restarting.go index a15b05d..e90effc 100644 --- a/internal/fsm/state_restarting.go +++ b/internal/fsm/state_restarting.go @@ -36,8 +36,14 @@ func (s *StateRestarting) OnEnter(fsm *FSM) { } } + log.Info("Applying patches") + err := fsm.Server().ApplyPatches() + if err != nil { + log.Error("Failed to apply patches", "err", err) + } + log.Info("Rendering templates") - err := fsm.Server().Render(false) + err = fsm.Server().Render(false) if err != nil { log.Error("Failed to render templates", "err", err) } diff --git a/internal/fsm/state_stopped.go b/internal/fsm/state_stopped.go index ad40282..8f22fd6 100644 --- a/internal/fsm/state_stopped.go +++ b/internal/fsm/state_stopped.go @@ -22,7 +22,17 @@ func (s *StateStopped) EventHandler(event Event, fsm *FSM) (State, error) { switch event { case EventStart: - err := fsm.Server().Start() + err := fsm.Server().ApplyPatches() + if err != nil { + return NewStateErrored(err), err + } + + err = fsm.Server().Render(false) + if err != nil { + return NewStateErrored(err), err + } + + err = fsm.Server().Start() if err != nil { return NewStateErrored(err), err } diff --git a/internal/fsm/states_test.go b/internal/fsm/states_test.go index cb05809..c7fffd4 100644 --- a/internal/fsm/states_test.go +++ b/internal/fsm/states_test.go @@ -32,6 +32,8 @@ func (s *StatesSuite) TestStateStopped() { state := NewStateStopped() fsm := New(gameServerMock, slog.Default(), state) + gameServerMock.EXPECT().ApplyPatches().Return(nil) + gameServerMock.EXPECT().Render(false).Return(nil) gameServerMock.EXPECT().Start().Return(nil) nextState, stateErr := state.EventHandler(EventStart, fsm) @@ -107,11 +109,13 @@ func (s *StatesSuite) TestStateRestarting() { state := NewStateRestarting(NewRestartCounter(3)) + gameServerMock.EXPECT().ApplyPatches().Return(nil) gameServerMock.EXPECT().Render(false).Return(nil) gameServerMock.EXPECT().Start().Return(nil) fsm := New(gameServerMock, slog.Default(), state) s.Run("Succesfull restart", func() { + gameServerMock.EXPECT().ApplyPatches().Return(nil) gameServerMock.EXPECT().Render(false).Return(nil) gameServerMock.EXPECT().Start().Return(nil) state.OnEnter(fsm) diff --git a/internal/game/docker/server.go b/internal/game/docker/server.go index 7bb5415..a64bba3 100644 --- a/internal/game/docker/server.go +++ b/internal/game/docker/server.go @@ -6,6 +6,7 @@ import ( "context" "errors" "io" + "os" "path" "github.com/docker/docker/api/types/container" @@ -71,10 +72,13 @@ func (c *Container) IsRunning() (bool, error) { } func (c *Container) WriteFile(filePath string, data []byte) error { + return c.WriteFileFromReader(filePath, bytes.NewReader(data), int64(len(data))) +} + +func (c *Container) WriteFileFromReader(filePath string, reader io.Reader, size int64) error { ctx := context.Background() fullPath := path.Join(c.workDir, filePath) - mode := int64(0644) stat, err := c.docker.ContainerStatPath(ctx, c.name, fullPath) @@ -82,12 +86,20 @@ func (c *Container) WriteFile(filePath string, data []byte) error { mode = int64(stat.Mode) } - buf := new(bytes.Buffer) + f, err := os.CreateTemp("", "svctl-tar-*.tar") + if err != nil { + return err + } - tarWriter := tar.NewWriter(buf) + defer func() { + f.Close() + os.Remove(f.Name()) + }() + + tarWriter := tar.NewWriter(f) err = tarWriter.WriteHeader(&tar.Header{ Name: path.Base(filePath), - Size: int64(len(data)), + Size: size, Mode: mode, Uid: c.uid, Gid: c.gid, @@ -96,7 +108,7 @@ func (c *Container) WriteFile(filePath string, data []byte) error { return err } - _, err = tarWriter.Write(data) + _, err = io.Copy(tarWriter, reader) if err != nil { return err } @@ -106,7 +118,12 @@ func (c *Container) WriteFile(filePath string, data []byte) error { return err } - return c.docker.CopyToContainer(ctx, c.name, path.Dir(fullPath), buf, container.CopyToContainerOptions{}) + _, err = f.Seek(0, io.SeekStart) + if err != nil { + return err + } + + return c.docker.CopyToContainer(ctx, c.name, path.Dir(fullPath), f, container.CopyToContainerOptions{}) } func (c *Container) ReadFile(filePath string) ([]byte, error) { diff --git a/internal/game/game.go b/internal/game/game.go index d586710..35d1053 100644 --- a/internal/game/game.go +++ b/internal/game/game.go @@ -1,6 +1,9 @@ package game -import "errors" +import ( + "errors" + "io" +) type GameServer interface { Start() error @@ -8,6 +11,7 @@ type GameServer interface { Stop() error ReadFile(path string) ([]byte, error) WriteFile(path string, data []byte) error + WriteFileFromReader(path string, reader io.Reader, size int64) error } var ( diff --git a/internal/game/local/game.go b/internal/game/local/game.go index e3a9c74..ecba962 100644 --- a/internal/game/local/game.go +++ b/internal/game/local/game.go @@ -8,6 +8,8 @@ import ( "runtime" "strconv" "strings" + + "github.com/sboon-gg/svctl/internal/game" ) const ( @@ -21,6 +23,8 @@ type runningProcess interface { PID() int } +var _ game.GameServer = &Server{} + type Server struct { path string process runningProcess @@ -54,6 +58,24 @@ func (s *Server) WriteFile(path string, data []byte) error { return os.WriteFile(fullPath, data, 0644) } +func (s *Server) WriteFileFromReader(path string, r io.Reader, _ int64) error { + fullPath := filepath.Join(s.path, path) + + if _, err := os.Stat(filepath.Dir(fullPath)); os.IsNotExist(err) { + // Ignore error, the write will fail if the directory doesn't exist. + _ = os.MkdirAll(filepath.Dir(fullPath), 0755) + } + + f, err := os.Create(fullPath) + if err != nil { + return err + } + defer f.Close() + + _, err = io.Copy(f, r) + return err +} + func (s *Server) Update(ctx context.Context, outW io.Writer, inR io.Reader, errW io.Writer) error { return s.update(ctx, outW, inR, errW) } diff --git a/internal/server/server.go b/internal/server/server.go index 1070348..33d1564 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -1,6 +1,7 @@ package server import ( + "os" "path/filepath" "github.com/docker/docker/client" @@ -96,3 +97,34 @@ func (s *Server) DryRender() ([]templates.RenderOutput, error) { return s.Settings.Templates.Render(gameConfig, values) } + +func (s *Server) ApplyPatches() error { + cfg, err := s.Settings.Config() + if err != nil { + return err + } + + for _, patch := range cfg.Patches { + if patch.Source == "" || patch.Destination == "" { + continue + } + + f, err := os.Open(filepath.Join(s.Settings.Path, patch.Source)) + if err != nil { + return err + } + defer f.Close() + + stat, err := f.Stat() + if err != nil { + return err + } + + err = s.WriteFileFromReader(patch.Destination, f, stat.Size()) + if err != nil { + return err + } + } + + return nil +} diff --git a/internal/settings/config.go b/internal/settings/config.go index 7d1d7aa..7c67a1c 100644 --- a/internal/settings/config.go +++ b/internal/settings/config.go @@ -23,10 +23,16 @@ type DockerConfig struct { ContainerName string `yaml:"containerName"` } +type PatchConfig struct { + Source string `yaml:"source"` + Destination string `yaml:"destination"` +} + type Config struct { Values []ValuesSource `yaml:"values"` Loggers []LoggerConfig `yaml:"loggers"` TemplatesPath string `yaml:"templates"` + Patches []PatchConfig `yaml:"patches"` Game GameConfig `yaml:"game"` Docker *DockerConfig `yaml:"docker,omtempty"` } @@ -34,7 +40,7 @@ type Config struct { func (s *Settings) Config() (*Config, error) { var config Config - content, err := os.ReadFile(filepath.Join(s.path, configFileName)) + content, err := os.ReadFile(filepath.Join(s.Path, configFileName)) if err != nil { return nil, err } diff --git a/internal/settings/settings.go b/internal/settings/settings.go index 425276d..49faec0 100644 --- a/internal/settings/settings.go +++ b/internal/settings/settings.go @@ -16,14 +16,14 @@ const ( ) type Settings struct { - path string + Path string Templates *templates.Renderer Log *slog.Logger } func Open(path string) (*Settings, error) { s := &Settings{ - path: path, + Path: path, } config, err := s.Config() diff --git a/internal/settings/templates.go b/internal/settings/templates.go index 4be6567..d03131a 100644 --- a/internal/settings/templates.go +++ b/internal/settings/templates.go @@ -27,7 +27,7 @@ func (s *Settings) TemplateData() (templates.Values, *GameConfig, error) { if source.File != "" { sourceFile := source.File if !filepath.IsAbs(sourceFile) { - sourceFile = filepath.Join(s.path, sourceFile) + sourceFile = filepath.Join(s.Path, sourceFile) } content, err := os.ReadFile(sourceFile) diff --git a/pkg/templates/funcs.go b/pkg/templates/funcs.go index e51da54..8a398a5 100644 --- a/pkg/templates/funcs.go +++ b/pkg/templates/funcs.go @@ -4,6 +4,7 @@ import ( "encoding/json" "errors" "fmt" + "maps" "os" "text/template" @@ -21,14 +22,12 @@ func (r *Renderer) FuncMap() template.FuncMap { "maplist": r.maplist, } - for k, v := range extra { - f[k] = v - } + maps.Copy(f, extra) return f } -func (t *Renderer) maplist(filterMap interface{}, rawMaplist string) (string, error) { +func (t *Renderer) maplist(filterMap any, rawMaplist string) (string, error) { var filter maplist.MapInfo if f, ok := filterMap.(string); ok { filter = maplist.Parse(fmt.Sprintf("%s %s", maplist.MaplistAppendStr, f))[0] diff --git a/pkg/templates/templates_test.go b/pkg/templates/templates_test.go index 1cffb1e..be55787 100644 --- a/pkg/templates/templates_test.go +++ b/pkg/templates/templates_test.go @@ -49,8 +49,8 @@ mapList.append sahel gpm_coop 64 "test": "changed-string", "quoted": "but different", "overriddenByZeroValue": 0, - "maps": []interface{}{ - map[string]interface{}{ + "maps": []any{ + map[string]any{ "name": "saaremaa", }, },