Fix startup variables not being properly updated on server reboot; closes pterodactyl/panel#2255

This commit is contained in:
Dane Everitt 2020-08-27 21:08:33 -07:00
parent 711ee2258c
commit 7d8710824c
No known key found for this signature in database
GPG Key ID: EEA66103B3D71F53
11 changed files with 119 additions and 51 deletions

View File

@ -4,7 +4,7 @@ import (
"sync" "sync"
) )
type configurationSettings struct { type Settings struct {
Mounts []Mount Mounts []Mount
Allocations Allocations Allocations Allocations
Limits Limits Limits Limits
@ -16,20 +16,35 @@ type Configuration struct {
mu sync.RWMutex mu sync.RWMutex
environmentVariables []string environmentVariables []string
settings configurationSettings settings Settings
} }
func NewConfiguration(m []Mount, a Allocations, l Limits, envVars []string) *Configuration { // Returns a new environment configuration with the given settings and environment variables
// defined within it.
func NewConfiguration(s Settings, envVars []string) *Configuration {
return &Configuration{ return &Configuration{
environmentVariables: envVars, environmentVariables: envVars,
settings: configurationSettings{ settings: s,
Mounts: m,
Allocations: a,
Limits: l,
},
} }
} }
// Updates the settings struct for this environment on the fly. This allows modified servers to
// automatically push those changes to the environment.
func (c *Configuration) SetSettings(s Settings) {
c.mu.Lock()
c.settings = s
c.mu.Unlock()
}
// Updates the environment variables associated with this environment by replacing the entire
// array of them with a new one.
func (c *Configuration) SetEnvironmentVariables(ev []string) {
c.mu.Lock()
c.environmentVariables = ev
c.mu.Unlock()
}
// Returns the limits assigned to this environment.
func (c *Configuration) Limits() Limits { func (c *Configuration) Limits() Limits {
c.mu.RLock() c.mu.RLock()
defer c.mu.RUnlock() defer c.mu.RUnlock()
@ -37,6 +52,7 @@ func (c *Configuration) Limits() Limits {
return c.settings.Limits return c.settings.Limits
} }
// Rturns the allocations associated with this environment.
func (c *Configuration) Allocations() Allocations { func (c *Configuration) Allocations() Allocations {
c.mu.RLock() c.mu.RLock()
defer c.mu.RUnlock() defer c.mu.RUnlock()
@ -44,6 +60,7 @@ func (c *Configuration) Allocations() Allocations {
return c.settings.Allocations return c.settings.Allocations
} }
// Returns all of the mounts associated with this environment.
func (c *Configuration) Mounts() []Mount { func (c *Configuration) Mounts() []Mount {
c.mu.RLock() c.mu.RLock()
defer c.mu.RUnlock() defer c.mu.RUnlock()
@ -51,6 +68,7 @@ func (c *Configuration) Mounts() []Mount {
return c.settings.Mounts return c.settings.Mounts
} }
// Returns the environment variables associated with this instance.
func (c *Configuration) EnvironmentVariables() []string { func (c *Configuration) EnvironmentVariables() []string {
c.mu.RLock() c.mu.RLock()
defer c.mu.RUnlock() defer c.mu.RUnlock()

View File

@ -70,12 +70,6 @@ func New(id string, m *Metadata, c *environment.Configuration) (*Environment, er
return e, nil return e, nil
} }
func (e *Environment) SetStopConfiguration(c *api.ProcessStopConfiguration) {
e.mu.Lock()
e.meta.Stop = c
e.mu.Unlock()
}
func (e *Environment) Type() string { func (e *Environment) Type() string {
return "docker" return "docker"
} }
@ -166,3 +160,19 @@ func (e *Environment) ExitState() (uint32, bool, error) {
return uint32(c.State.ExitCode), c.State.OOMKilled, nil return uint32(c.State.ExitCode), c.State.OOMKilled, nil
} }
// Returns the environment configuration allowing a process to make modifications of the
// environment on the fly.
func (e *Environment) Config() *environment.Configuration {
e.mu.RLock()
defer e.mu.RUnlock()
return e.Configuration
}
// Sets the stop configuration for the environment.
func (e *Environment) SetStopConfiguration(c *api.ProcessStopConfiguration) {
e.mu.Lock()
e.meta.Stop = c
e.mu.Unlock()
}

View File

@ -24,6 +24,9 @@ type ProcessEnvironment interface {
// Returns the name of the environment. // Returns the name of the environment.
Type() string Type() string
// Returns the environment configuration to the caller.
Config() *Configuration
// Returns an event emitter instance that can be hooked into to listen for different // Returns an event emitter instance that can be hooked into to listen for different
// events that are fired by the environment. This should not allow someone to publish // events that are fired by the environment. This should not allow someone to publish
// events, only subscribe to them. // events, only subscribe to them.

View File

@ -134,11 +134,13 @@ func patchServer(c *gin.Context) {
buf := bytes.Buffer{} buf := bytes.Buffer{}
buf.ReadFrom(c.Request.Body) buf.ReadFrom(c.Request.Body)
if err := s.UpdateDataStructure(buf.Bytes(), true); err != nil { if err := s.UpdateDataStructure(buf.Bytes()); err != nil {
TrackedServerError(err, s).AbortWithServerError(c) TrackedServerError(err, s).AbortWithServerError(c)
return return
} }
s.SyncWithEnvironment()
c.Status(http.StatusNoContent) c.Status(http.StatusNoContent)
} }

View File

@ -43,6 +43,20 @@ func (s *Server) Config() *Configuration {
return &s.cfg return &s.cfg
} }
func (s *Server) DiskSpace() int64 {
s.cfg.mu.RLock()
defer s.cfg.mu.RUnlock()
return s.cfg.Build.DiskSpace
}
func (s *Server) MemoryLimit() int64 {
s.cfg.mu.RLock()
defer s.cfg.mu.RUnlock()
return s.cfg.Build.MemoryLimit
}
func (c *Configuration) GetUuid() string { func (c *Configuration) GetUuid() string {
c.mu.RLock() c.mu.RLock()
defer c.mu.RUnlock() defer c.mu.RUnlock()

View File

@ -221,7 +221,7 @@ func (fs *Filesystem) HasSpaceAvailable() bool {
// been allocated. // been allocated.
fs.Server.Proc().SetDisk(size) fs.Server.Proc().SetDisk(size)
space := fs.Server.Build().DiskSpace space := fs.Server.DiskSpace()
// If space is -1 or 0 just return true, means they're allowed unlimited. // If space is -1 or 0 just return true, means they're allowed unlimited.
// //
// Technically we could skip disk space calculation because we don't need to check if the server exceeds it's limit // Technically we could skip disk space calculation because we don't need to check if the server exceeds it's limit

View File

@ -19,7 +19,7 @@ import (
func (fs *Filesystem) SpaceAvailableForDecompression(dir string, file string) (bool, error) { func (fs *Filesystem) SpaceAvailableForDecompression(dir string, file string) (bool, error) {
// Don't waste time trying to determine this if we know the server will have the space for // Don't waste time trying to determine this if we know the server will have the space for
// it since there is no limit. // it since there is no limit.
if fs.Server.Build().DiskSpace <= 0 { if fs.Server.DiskSpace() <= 0 {
return true, nil return true, nil
} }
@ -58,7 +58,7 @@ func (fs *Filesystem) SpaceAvailableForDecompression(dir string, file string) (b
wg.Wait() wg.Wait()
return ((dirSize + size) / 1000.0 / 1000.0) <= fs.Server.Build().DiskSpace, cErr return ((dirSize + size) / 1000.0 / 1000.0) <= fs.Server.DiskSpace(), cErr
} }
// Decompress a file in a given directory by using the archiver tool to infer the file // Decompress a file in a given directory by using the archiver tool to infer the file

View File

@ -93,7 +93,7 @@ func FromConfiguration(data *api.ServerConfigurationResponse) (*Server, error) {
} }
s.cfg = cfg s.cfg = cfg
if err := s.UpdateDataStructure(data.Settings, false); err != nil { if err := s.UpdateDataStructure(data.Settings); err != nil {
return nil, err return nil, err
} }
@ -103,7 +103,13 @@ func FromConfiguration(data *api.ServerConfigurationResponse) (*Server, error) {
// Right now we only support a Docker based environment, so I'm going to hard code // Right now we only support a Docker based environment, so I'm going to hard code
// this logic in. When we're ready to support other environment we'll need to make // this logic in. When we're ready to support other environment we'll need to make
// some modifications here obviously. // some modifications here obviously.
envCfg := environment.NewConfiguration(s.Mounts(), s.cfg.Allocations, s.cfg.Build, s.GetEnvironmentVariables()) settings := environment.Settings{
Mounts: s.Mounts(),
Allocations: s.cfg.Allocations,
Limits: s.cfg.Build,
}
envCfg := environment.NewConfiguration(settings, s.GetEnvironmentVariables())
meta := docker.Metadata{ meta := docker.Metadata{
Image: s.Config().Container.Image, Image: s.Config().Container.Image,
} }

View File

@ -126,16 +126,21 @@ func (s *Server) HandlePowerAction(action PowerAction, waitSeconds ...int) error
// Execute a few functions before actually calling the environment start commands. This ensures // Execute a few functions before actually calling the environment start commands. This ensures
// that everything is ready to go for environment booting, and that the server can even be started. // that everything is ready to go for environment booting, and that the server can even be started.
func (s *Server) onBeforeStart() error { func (s *Server) onBeforeStart() error {
// Disallow start & restart if the server is suspended.
if s.IsSuspended() {
return new(suspendedError)
}
s.Log().Info("syncing server configuration with panel") s.Log().Info("syncing server configuration with panel")
if err := s.Sync(); err != nil { if err := s.Sync(); err != nil {
return errors.Wrap(err, "unable to sync server data from Panel instance") return errors.Wrap(err, "unable to sync server data from Panel instance")
} }
// Disallow start & restart if the server is suspended. Do this check after performing a sync
// action with the Panel to ensure that we have the most up-to-date information for that server.
if s.IsSuspended() {
return new(suspendedError)
}
// Ensure we sync the server information with the environment so that any new environment variables
// and process resource limits are correctly applied.
s.SyncWithEnvironment()
if !s.Filesystem.HasSpaceAvailable() { if !s.Filesystem.HasSpaceAvailable() {
return errors.New("cannot start server, not enough disk space available") return errors.New("cannot start server, not enough disk space available")
} }

View File

@ -77,7 +77,7 @@ func (s *Server) GetEnvironmentVariables() []string {
var out = []string{ var out = []string{
fmt.Sprintf("TZ=%s", zone), fmt.Sprintf("TZ=%s", zone),
fmt.Sprintf("STARTUP=%s", s.Config().Invocation), fmt.Sprintf("STARTUP=%s", s.Config().Invocation),
fmt.Sprintf("SERVER_MEMORY=%d", s.Build().MemoryLimit), fmt.Sprintf("SERVER_MEMORY=%d", s.MemoryLimit()),
fmt.Sprintf("SERVER_IP=%s", s.Config().Allocations.DefaultMapping.Ip), fmt.Sprintf("SERVER_IP=%s", s.Config().Allocations.DefaultMapping.Ip),
fmt.Sprintf("SERVER_PORT=%d", s.Config().Allocations.DefaultMapping.Port), fmt.Sprintf("SERVER_PORT=%d", s.Config().Allocations.DefaultMapping.Port),
} }
@ -125,7 +125,7 @@ func (s *Server) Sync() error {
func (s *Server) SyncWithConfiguration(cfg *api.ServerConfigurationResponse) error { func (s *Server) SyncWithConfiguration(cfg *api.ServerConfigurationResponse) error {
// Update the data structure and persist it to the disk. // Update the data structure and persist it to the disk.
if err := s.UpdateDataStructure(cfg.Settings, false); err != nil { if err := s.UpdateDataStructure(cfg.Settings); err != nil {
return errors.WithStack(err) return errors.WithStack(err)
} }
@ -177,10 +177,6 @@ func (s *Server) IsSuspended() bool {
return s.Config().Suspended return s.Config().Suspended
} }
func (s *Server) Build() *environment.Limits {
return &s.Config().Build
}
func (s *Server) ProcessConfiguration() *api.ProcessConfiguration { func (s *Server) ProcessConfiguration() *api.ProcessConfiguration {
s.RLock() s.RLock()
defer s.RUnlock() defer s.RUnlock()

View File

@ -15,7 +15,7 @@ import (
// The server will be marked as requiring a rebuild on the next boot sequence, // 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 // it is up to the specific environment to determine what needs to happen when
// that is the case. // that is the case.
func (s *Server) UpdateDataStructure(data []byte, background bool) error { func (s *Server) UpdateDataStructure(data []byte) error {
src := new(Configuration) src := new(Configuration)
if err := json.Unmarshal(data, src); err != nil { if err := json.Unmarshal(data, src); err != nil {
return errors.WithStack(err) return errors.WithStack(err)
@ -97,36 +97,50 @@ func (s *Server) UpdateDataStructure(data []byte, background bool) error {
// Update the configuration once we have a lock on the configuration object. // Update the configuration once we have a lock on the configuration object.
s.cfg = c s.cfg = c
if background {
go s.runBackgroundActions()
}
return nil return nil
} }
// Runs through different actions once a server's configuration has been persisted // Updates the environment for the server to match any of the changed data. This pushes new settings and
// to the disk. This function does not return anything as any failures should be logged // environment variables to the environment. In addition, the in-situ update method is called on the
// but have no effect on actually updating the server itself. // environment which will allow environments that make use of it (such as Docker) to immediately apply
// some settings without having to wait on a server to restart.
// //
// These tasks run in independent threads where relevant to speed up any updates // This functionality allows a server's resources limits to be modified on the fly and have them apply
// that need to happen. // right away allowing for dynamic resource allocation and responses to abusive server processes.
func (s *Server) runBackgroundActions() { func (s *Server) SyncWithEnvironment() {
// Check if the s is now suspended, and if so and the process is not terminated s.Log().Debug("syncing server settings with environment")
// yet, do it immediately.
if s.IsSuspended() && s.GetState() != environment.ProcessOfflineState {
s.Log().Info("server suspended with running process state, terminating now")
if err := s.Environment.WaitForStop(10, true); err != nil { // Update the environment settings using the new information from this server.
s.Log().WithField("error", err).Warn("failed to terminate server environment after suspension") s.Environment.Config().SetSettings(environment.Settings{
} Mounts: s.Mounts(),
} Allocations: s.Config().Allocations,
Limits: s.Config().Build,
})
// If build limits are changed, environment variables also change. Plus, any modifications to
// the startup command also need to be properly propagated to this environment.
//
// @see https://github.com/pterodactyl/panel/issues/2255
s.Environment.Config().SetEnvironmentVariables(s.GetEnvironmentVariables())
if !s.IsSuspended() { if !s.IsSuspended() {
// Update the environment in place, allowing memory and CPU usage to be adjusted // Update the environment in place, allowing memory and CPU usage to be adjusted
// on the fly without the user needing to reboot (theoretically). // on the fly without the user needing to reboot (theoretically).
s.Log().Info("performing server limit modification on-the-fly") s.Log().Info("performing server limit modification on-the-fly")
if err := s.Environment.InSituUpdate(); err != nil { if err := s.Environment.InSituUpdate(); err != nil {
// This is not a failure, the process is still running fine and will fix itself on the
// next boot, or fail out entirely in a more logical position.
s.Log().WithField("error", err).Warn("failed to perform on-the-fly update of the server environment") s.Log().WithField("error", err).Warn("failed to perform on-the-fly update of the server environment")
} }
} else {
// Checks if the server is now in a suspended state. If so and a server process is currently running it
// will be gracefully stopped (and terminated if it refuses to stop).
s.Log().Info("server suspended with running process state, terminating now")
go func (s *Server) {
if err := s.Environment.WaitForStop(60, true); err != nil {
s.Log().WithField("error", err).Warn("failed to terminate server environment after suspension")
}
}(s)
} }
} }