From 9f4518fc58887b327ed0029333e43553f8305147 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sun, 24 Nov 2019 15:08:38 -0800 Subject: [PATCH] Add (unchecked) code to do an in-situ replacement of build settings --- go.mod | 1 + go.sum | 2 + http.go | 16 ++++++++ installer/installer.go | 28 ++------------ server/environment.go | 5 +++ server/environment_docker.go | 68 ++++++++++++++++++++++++++-------- server/server.go | 19 +++++++++- server/server_configuration.go | 31 ++++++++++++++++ server/server_update.go | 34 +++++++++++++++++ 9 files changed, 163 insertions(+), 41 deletions(-) create mode 100644 server/server_configuration.go create mode 100644 server/server_update.go diff --git a/go.mod b/go.mod index 3d6912f..83453f2 100644 --- a/go.mod +++ b/go.mod @@ -19,6 +19,7 @@ require ( github.com/google/uuid v1.1.1 github.com/gorilla/websocket v1.4.0 github.com/gotestyourself/gotestyourself v2.2.0+incompatible // indirect + github.com/imdario/mergo v0.3.8 // indirect github.com/julienschmidt/httprouter v1.2.0 github.com/kr/pretty v0.1.0 // indirect github.com/olebedev/emitter v0.0.0-20190110104742-e8d1457e6aee diff --git a/go.sum b/go.sum index 369b4bf..1ff9d63 100644 --- a/go.sum +++ b/go.sum @@ -39,6 +39,8 @@ github.com/gotestyourself/gotestyourself v2.2.0+incompatible h1:AQwinXlbQR2HvPjQ github.com/gotestyourself/gotestyourself v2.2.0+incompatible/go.mod h1:zZKM6oeNM8k+FRljX1mnzVYeS8wiGgQyvST1/GafPbY= github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/imdario/mergo v0.3.8 h1:CGgOkSJeqMRmt0D9XLWExdT4m4F1vd3FV3VPt+0VxkQ= +github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/julienschmidt/httprouter v1.2.0 h1:TDTW5Yz1mjftljbcKqRcrYhd4XeOoI98t+9HbQbYf7g= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= diff --git a/http.go b/http.go index 4fe01c3..ca0c86e 100644 --- a/http.go +++ b/http.go @@ -407,6 +407,21 @@ func (rt *Router) routeServerSendCommand(w http.ResponseWriter, r *http.Request, w.WriteHeader(http.StatusNoContent) } +func (rt *Router) routeServerUpdate(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + s := rt.Servers.Get(ps.ByName("server")) + defer r.Body.Close() + + data := rt.ReaderToBytes(r.Body) + if err := s.UpdateDataStructure(data); err != nil { + zap.S().Errorw("failed to update a server's data structure", zap.String("server", s.Uuid), zap.Error(err)) + + http.Error(w, "failed to update data structure", http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusNoContent) +} + func (rt *Router) routeCreateServer(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { defer r.Body.Close() @@ -457,6 +472,7 @@ func (rt *Router) ConfigureRouter() *httprouter.Router { router.POST("/api/servers/:server/files/delete", rt.AuthenticateRequest(rt.routeServerDeleteFile)) router.POST("/api/servers/:server/power", rt.AuthenticateRequest(rt.routeServerPower)) router.POST("/api/servers/:server/commands", rt.AuthenticateRequest(rt.routeServerSendCommand)) + router.PATCH("/api/servers/:server", rt.AuthenticateRequest(rt.routeServerUpdate)) return router } diff --git a/installer/installer.go b/installer/installer.go index 82dd01b..5fc44ed 100644 --- a/installer/installer.go +++ b/installer/installer.go @@ -8,8 +8,6 @@ import ( "github.com/pterodactyl/wings/config" "github.com/pterodactyl/wings/server" "go.uber.org/zap" - "gopkg.in/yaml.v2" - "os" ) type Installer struct { @@ -46,6 +44,8 @@ func New(data []byte) (error, *Installer) { }, } + s.Init() + s.Allocations.DefaultMapping.Ip = getString(data, "allocations", "default", "ip") s.Allocations.DefaultMapping.Port = int(getInt(data, "allocations", "default", "port")) @@ -68,7 +68,7 @@ func New(data []byte) (error, *Installer) { s.Container.Image = getString(data, "container", "image") - b, err := WriteConfigurationToDisk(s) + b, err := s.WriteConfigurationToDisk() if err != nil { return err, nil } @@ -108,28 +108,6 @@ func (i *Installer) Execute() { } } -// Writes the server configuration to the disk and return the byte representation -// of the configuration object. This allows us to pass it directly into the -// servers.FromConfiguration() function. -func WriteConfigurationToDisk(s *server.Server) ([]byte, error) { - f, err := os.Create("data/servers/" + s.Uuid + ".yml") - if err != nil { - return nil, err - } - defer f.Close() - - b, err := yaml.Marshal(&s) - if err != nil { - return nil, err - } - - if _, err := f.Write(b); err != nil { - return nil, err - } - - return b, nil -} - // Returns a string value from the JSON data provided. func getString(data []byte, key ...string) string { value, _ := jsonparser.GetString(data, key...) diff --git a/server/environment.go b/server/environment.go index 9bbf5c2..aef7175 100644 --- a/server/environment.go +++ b/server/environment.go @@ -14,6 +14,11 @@ type Environment interface { // for this specific server instance. IsRunning() (bool, error) + // Performs an update of server resource limits without actually stopping the server + // process. This only executes if the environment supports it, otherwise it is + // a no-op. + InSituUpdate() error + // Runs before the environment is started. If an error is returned starting will // not occur, otherwise proceeds as normal. OnBeforeStart() error diff --git a/server/environment_docker.go b/server/environment_docker.go index 43eaf44..74fd2f9 100644 --- a/server/environment_docker.go +++ b/server/environment_docker.go @@ -114,6 +114,35 @@ func (d *DockerEnvironment) IsRunning() (bool, error) { return c.State.Running, nil } +// Performs an in-place update of the Docker container's resource limits without actually +// making any changes to the operational state of the container. This allows memory, cpu, +// and IO limitations to be adjusted on the fly for individual instances. +func (d *DockerEnvironment) InSituUpdate() error { + if _, err := d.Client.ContainerInspect(context.Background(), d.Server.Uuid); err != nil { + // If the container doesn't exist for some reason there really isn't anything + // we can do to fix that in this process (it doesn't make sense at least). In those + // cases just return without doing anything since we still want to save the configuration + // to the disk. + // + // We'll let a boot process make modifications to the container if needed at this point. + if client.IsErrNotFound(err) { + return nil + } + + return errors.WithStack(err) + } + + u := container.UpdateConfig{ + Resources: d.getResourcesForServer(), + } + + if _, err := d.Client.ContainerUpdate(context.Background(), d.Server.Uuid, u); err != nil { + return errors.WithStack(err) + } + + return nil +} + // Run before the container starts and get the process configuration from the Panel. // This is important since we use this to check configuration files as well as ensure // we always have the latest version of an egg available for server processes. @@ -412,8 +441,6 @@ func (d *DockerEnvironment) Create() error { return errors.WithStack(err) } - var oomDisabled = true - // Ensure the data directory exists before getting too far through this process. if err := d.Server.Filesystem.EnsureDataDirectory(); err != nil { return errors.WithStack(err) @@ -477,19 +504,7 @@ func (d *DockerEnvironment) Create() error { // Define resource limits for the container based on the data passed through // from the Panel. - Resources: container.Resources{ - // @todo memory limit should be slightly higher than the reservation - Memory: d.Server.Build.MemoryLimit * 1000000, - MemoryReservation: d.Server.Build.MemoryLimit * 1000000, - MemorySwap: d.Server.Build.ConvertedSwap(), - - CPUQuota: d.Server.Build.ConvertedCpuLimit(), - CPUPeriod: 100000, - CPUShares: 1024, - - BlkioWeight: d.Server.Build.IoWeight, - OomKillDisable: &oomDisabled, - }, + Resources: d.getResourcesForServer(), // @todo make this configurable again DNS: []string{"1.1.1.1", "8.8.8.8"}, @@ -677,3 +692,26 @@ func (d *DockerEnvironment) exposedPorts() nat.PortSet { return out } + +// Formats the resources available to a server instance in such as way that Docker will +// generate a matching environment in the container. +func (d *DockerEnvironment) getResourcesForServer() container.Resources { + b := true + oomDisabled := d.Server.Container.OomDisabled + + if oomDisabled == nil { + oomDisabled = &b + } + + return container.Resources{ + // @todo memory limit should be slightly higher than the reservation + Memory: d.Server.Build.MemoryLimit * 1000000, + MemoryReservation: d.Server.Build.MemoryLimit * 1000000, + MemorySwap: d.Server.Build.ConvertedSwap(), + CPUQuota: d.Server.Build.ConvertedCpuLimit(), + CPUPeriod: 100000, + CPUShares: 1024, + BlkioWeight: d.Server.Build.IoWeight, + OomKillDisable: oomDisabled, + } +} diff --git a/server/server.go b/server/server.go index 66e98f7..c5f4872 100644 --- a/server/server.go +++ b/server/server.go @@ -13,6 +13,7 @@ import ( "os" "path" "strings" + "sync" "time" ) @@ -43,6 +44,11 @@ type Server struct { Container struct { // Defines the Docker image that will be used for this server Image string `json:"image,omitempty"` + // If set to true, OOM killer will be disabled on the server's Docker container. + // If not present (nil) we will default to disabling it. + OomDisabled *bool `json:"oom_disabled,omitempty"` + // Defines if the container needs to be rebuilt on the next boot. + RebuildRequired bool `json:"rebuild_required,omitempty"` } `json:"container,omitempty"` Environment Environment `json:"-" yaml:"-"` @@ -62,6 +68,10 @@ type Server struct { // fetched from the Pterodactyl Server instance each time the server process is // started, and then cached here. processConfiguration *api.ServerConfiguration + + // Internal mutex used to block actions that need to occur sequentially, such as + // writing the configuration to the disk. + mutex *sync.Mutex } // The build settings for a given server that impact docker container creation and @@ -179,11 +189,18 @@ func LoadDirectory(dir string, cfg *config.SystemConfiguration) ([]*Server, erro return servers, nil } +// Initializes the default required internal struct components for a Server. +func (s *Server) Init() { + s.listeners = make(map[string][]EventListenerFunction) + s.mutex = &sync.Mutex{} +} + // Initalizes a server using a data byte array. This will be marshaled into the // given struct using a YAML marshaler. This will also configure the given environment // for a server. func FromConfiguration(data []byte, cfg *config.SystemConfiguration) (*Server, error) { s := &Server{} + s.Init() if err := yaml.Unmarshal(data, s); err != nil { return nil, err @@ -277,4 +294,4 @@ func (s *Server) SetState(state string) error { // Gets the process configuration data for the server. func (s *Server) GetProcessConfiguration() (*api.ServerConfiguration, error) { return api.NewRequester().GetServerConfiguration(s.Uuid) -} \ No newline at end of file +} diff --git a/server/server_configuration.go b/server/server_configuration.go new file mode 100644 index 0000000..c23be9f --- /dev/null +++ b/server/server_configuration.go @@ -0,0 +1,31 @@ +package server + +import ( + "github.com/pkg/errors" + "gopkg.in/yaml.v2" + "os" +) + +// Writes the server configuration to the disk. The saved configuration will be returned +// back to the calling function to use if desired. +func (s *Server) WriteConfigurationToDisk() ([]byte, error) { + s.mutex.Lock() + defer s.mutex.Unlock() + + f, err := os.Create("data/servers/" + s.Uuid + ".yml") + if err != nil { + return nil, errors.WithStack(err) + } + defer f.Close() + + b, err := yaml.Marshal(&s) + if err != nil { + return nil, errors.WithStack(err) + } + + if _, err := f.Write(b); err != nil { + return nil, errors.WithStack(err) + } + + return b, nil +} \ No newline at end of file diff --git a/server/server_update.go b/server/server_update.go new file mode 100644 index 0000000..626f25f --- /dev/null +++ b/server/server_update.go @@ -0,0 +1,34 @@ +package server + +import ( + "encoding/json" + "github.com/imdario/mergo" + "github.com/pkg/errors" +) + +// Merges data passed through in JSON form into the existing server object. +// Any changes to the build settings will apply immediately in the environment +// if the environment supports it. +// +// The server will be marked as requiring a rebuild on the next boot sequence, +// it is up to the specific environment to determine what needs to happen when +// that is the case. +func (s *Server) UpdateDataStructure(data []byte) error { + src := Server{} + if err := json.Unmarshal(data, &src); err != nil { + return errors.WithStack(err) + } + + // Merge the new data object that we have received with the existing server data object + // and then save it to the disk so it is persistent. + if err := mergo.Merge(&s, src); err != nil { + return errors.WithStack(err) + } + + s.Container.RebuildRequired = true + if _, err := s.WriteConfigurationToDisk(); err != nil { + return errors.WithStack(err) + } + + return s.Environment.InSituUpdate() +} \ No newline at end of file