Compare commits

...

11 Commits

Author SHA1 Message Date
Dane Everitt
4c3d4f112b Bump for release 2020-06-29 20:57:01 -07:00
Dane Everitt
d6a3d9adb1 Don't obliterate custom SSL locations if defined
closes pterodactyl/panel#2121
2020-06-29 20:56:13 -07:00
Dane Everitt
d284c4aec9 Fix lock obtainment to avoid freeze 2020-06-29 20:42:26 -07:00
Dane Everitt
05a4730489 Fix configuration file saving to disk using the config command
closes pterodactyl/panel#2135
2020-06-29 20:33:54 -07:00
Dane Everitt
2dad3102e0 Fix saving of ini configuration files to the disk 2020-06-29 20:21:41 -07:00
Dane Everitt
b33f14ddd9 Correctly handle replacements with escaped values; closes #2041 2020-06-29 20:08:36 -07:00
Dane Everitt
1f6789cba3 Acquire exclusive lock when installing a server
Also allows aborting a server install mid-process when the server is deleted before the process finishes.
2020-06-22 21:38:16 -07:00
Dane Everitt
073247e4e1 Use 15 minute context timeout for pulling, not 10 seconds... closes #2130 2020-06-22 20:56:55 -07:00
Dane Everitt
a3d83d23bd Don't try to send space available when loading from a configuration
Server is not always installed when this function is called, this will cause errors in those cases.
2020-06-22 20:52:23 -07:00
Dane Everitt
f318962371 Ensure that more error stacks get recorded 2020-06-22 20:51:52 -07:00
Dane Everitt
db31722cfc Don't cause a double stacktrace on certain errors 2020-06-22 20:51:41 -07:00
13 changed files with 193 additions and 68 deletions

View File

@@ -130,6 +130,12 @@ func (r *PanelRequest) HttpResponseCode() int {
return r.Response.StatusCode
}
func IsRequestError(err error) bool {
_, ok := err.(*RequestError)
return ok
}
type RequestError struct {
Code string `json:"code"`
Status string `json:"status"`
@@ -137,10 +143,14 @@ type RequestError struct {
}
// Returns the error response in a string form that can be more easily consumed.
func (re *RequestError) String() string {
func (re *RequestError) Error() string {
return fmt.Sprintf("%s: %s (HTTP/%s)", re.Code, re.Detail, re.Status)
}
func (re *RequestError) String() string {
return re.Error()
}
type RequestErrorBag struct {
Errors []RequestError `json:"errors"`
}

View File

@@ -6,7 +6,6 @@ import (
"fmt"
"github.com/AlecAivazis/survey/v2"
"github.com/AlecAivazis/survey/v2/terminal"
"github.com/creasty/defaults"
"github.com/pterodactyl/wings/config"
"github.com/spf13/cobra"
"io/ioutil"
@@ -147,8 +146,8 @@ func configureCmdRun(cmd *cobra.Command, args []string) {
b, err := ioutil.ReadAll(res.Body)
cfg := new(config.Configuration)
if err := defaults.Set(cfg); err != nil {
cfg, err := config.NewFromPath(configPath)
if err != nil {
panic(err)
}

View File

@@ -132,7 +132,7 @@ func ReadConfiguration(path string) (*Configuration, error) {
}
// Track the location where we created this configuration.
c.path = path
c.unsafeSetPath(path)
// Replace environment variables within the configuration file with their
// values from the host system.
@@ -186,8 +186,32 @@ func GetJwtAlgorithm() *jwt.HMACSHA {
return _jwtAlgo
}
// Create a new struct and set the path where it should be stored.
func NewFromPath(path string) (*Configuration, error) {
c := new(Configuration)
if err := defaults.Set(c); err != nil {
return c, err
}
c.unsafeSetPath(path)
return c, nil
}
// Sets the path where the configuration file is located on the server. This function should
// not be called except by processes that are generating the configuration such as the configration
// command shipped with this software.
func (c *Configuration) unsafeSetPath(path string) {
c.Lock()
c.path = path
c.Unlock()
}
// Returns the path for this configuration file.
func (c *Configuration) GetPath() string {
c.RLock()
defer c.RUnlock()
return c.path
}
@@ -245,11 +269,10 @@ func (c *Configuration) setSystemUser(u *user.User) error {
gid, _ := strconv.Atoi(u.Gid)
c.Lock()
defer c.Unlock()
c.System.Username = u.Username
c.System.User.Uid = uid
c.System.User.Gid = gid
c.Unlock()
return c.WriteToDisk()
}
@@ -310,6 +333,10 @@ func (c *Configuration) EnsureFilePermissions() error {
// lock on the file. This prevents something else from writing at the exact same time and
// leading to bad data conditions.
func (c *Configuration) WriteToDisk() error {
// Obtain an exclusive write against the configuration file.
c.writeLock.Lock()
defer c.writeLock.Unlock()
ccopy := *c
// If debugging is set with the flag, don't save that to the configuration file, otherwise
// you'll always end up in debug mode.
@@ -326,10 +353,6 @@ func (c *Configuration) WriteToDisk() error {
return err
}
// Obtain an exclusive write against the configuration file.
c.writeLock.Lock()
defer c.writeLock.Unlock()
if err := ioutil.WriteFile(c.GetPath(), b, 0644); err != nil {
return err
}

View File

@@ -70,6 +70,8 @@ func (h *Handler) HandleLog(e *log.Entry) error {
}
if err, ok := e.Fields.Get("error").(error); ok {
var br = color2.New(color2.Bold, color2.FgRed)
if e, ok := errors.Cause(err).(tracer); ok {
st := e.StackTrace()
@@ -78,11 +80,9 @@ func (h *Handler) HandleLog(e *log.Entry) error {
l = 5
}
br := color2.New(color2.Bold, color2.FgRed)
fmt.Fprintf(h.Writer, "\n%s%+v\n\n", br.Sprintf("Stacktrace:"), st[0:l])
} else {
fmt.Printf("\n\nINVALID TRACER\n\n")
fmt.Fprintf(h.Writer, "\n%s\n%+v\n\n", br.Sprintf("Stacktrace:"), err)
}
} else {
fmt.Printf("\n\nINVALID ERROR\n\n")

View File

@@ -96,12 +96,12 @@ func (f *ConfigurationFile) IterateOverJson(data []byte) (*gabs.Container, error
// If the child is a null value, nothing will happen. Seems reasonable as of the
// time this code is being written.
for _, child := range parsed.Path(strings.Trim(parts[0], ".")).Children() {
if err := v.SetAtPathway(child, strings.Trim(parts[1], "."), value); err != nil {
if err := v.SetAtPathway(child, strings.Trim(parts[1], "."), []byte(value)); err != nil {
return nil, err
}
}
} else {
if err = v.SetAtPathway(parsed, v.Match, value); err != nil {
if err = v.SetAtPathway(parsed, v.Match, []byte(value)); err != nil {
return nil, err
}
}
@@ -149,12 +149,12 @@ func (cfr *ConfigurationFileReplacement) SetAtPathway(c *gabs.Container, path st
}
// Looks up a configuration value on the Daemon given a dot-notated syntax.
func (f *ConfigurationFile) LookupConfigurationValue(cfr ConfigurationFileReplacement) ([]byte, error) {
func (f *ConfigurationFile) LookupConfigurationValue(cfr ConfigurationFileReplacement) (string, error) {
// If this is not something that we can do a regex lookup on then just continue
// on our merry way. If the value isn't a string, we're not going to be doing anything
// with it anyways.
if cfr.ReplaceWith.Type() != jsonparser.String || !configMatchRegex.Match(cfr.ReplaceWith.Value()) {
return cfr.ReplaceWith.Value(), nil
return cfr.ReplaceWith.String(), nil
}
// If there is a match, lookup the value in the configuration for the Daemon. If no key
@@ -174,17 +174,15 @@ func (f *ConfigurationFile) LookupConfigurationValue(cfr ConfigurationFileReplac
match, _, _, err := jsonparser.Get(f.configuration, path...)
if err != nil {
if err != jsonparser.KeyPathNotFoundError {
return match, errors.WithStack(err)
return string(match), errors.WithStack(err)
}
log.WithFields(log.Fields{"path": path, "filename": f.FileName}).Debug("attempted to load a configuration value that does not exist")
// If there is no key, keep the original value intact, that way it is obvious there
// is a replace issue at play.
return match, nil
return string(match), nil
} else {
replaced := []byte(configMatchRegex.ReplaceAllString(cfr.ReplaceWith.String(), string(match)))
return replaced, nil
return configMatchRegex.ReplaceAllString(cfr.ReplaceWith.String(), string(match)), nil
}
}

View File

@@ -236,13 +236,13 @@ func (f *ConfigurationFile) parseXmlFile(path string) error {
// Iterate over the elements we found and update their values.
for _, element := range doc.FindElements(path) {
if xmlValueMatchRegex.Match(value) {
k := xmlValueMatchRegex.ReplaceAllString(string(value), "$1")
v := xmlValueMatchRegex.ReplaceAllString(string(value), "$2")
if xmlValueMatchRegex.MatchString(value) {
k := xmlValueMatchRegex.ReplaceAllString(value, "$1")
v := xmlValueMatchRegex.ReplaceAllString(value, "$2")
element.CreateAttr(k, v)
} else {
element.SetText(string(value))
element.SetText(value)
}
}
}
@@ -273,12 +273,13 @@ func (f *ConfigurationFile) parseXmlFile(path string) error {
// Parses an ini file.
func (f *ConfigurationFile) parseIniFile(path string) error {
// Ini package can't handle a non-existent file, so handle that automatically here
// by creating it if not exists.
// by creating it if not exists. Then, immediately close the file since we will use
// other methods to write the new contents.
file, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, 0644)
if err != nil {
return err
}
defer file.Close()
file.Close()
cfg, err := ini.Load(path)
if err != nil {
@@ -313,24 +314,15 @@ func (f *ConfigurationFile) parseIniFile(path string) error {
// If the key exists in the file go ahead and set the value, otherwise try to
// create it in the section.
if s.HasKey(k) {
s.Key(k).SetValue(string(value))
s.Key(k).SetValue(value)
} else {
if _, err := s.NewKey(k, string(value)); err != nil {
if _, err := s.NewKey(k, value); err != nil {
return err
}
}
}
// Truncate the file before attempting to write the changes.
if err := os.Truncate(path, 0); err != nil {
return err
}
if _, err := cfg.WriteTo(file); err != nil {
return err
}
return nil
return cfg.SaveTo(path)
}
// Parses a json file updating any matching key/value pairs. If a match is not found, the
@@ -452,7 +444,7 @@ func (f *ConfigurationFile) parsePropertiesFile(path string) error {
continue
}
if _, _, err := p.Set(replace.Match, string(data)); err != nil {
if _, _, err := p.Set(replace.Match, data); err != nil {
return err
}
}

View File

@@ -14,7 +14,9 @@ func (cv *ReplaceValue) Value() []byte {
}
func (cv *ReplaceValue) String() string {
return string(cv.value)
str, _ := jsonparser.ParseString(cv.value)
return str
}
func (cv *ReplaceValue) Type() jsonparser.ValueType {

View File

@@ -164,6 +164,11 @@ func deleteServer(c *gin.Context) {
// to start it while this process is running.
s.Suspended = true
// If the server is currently installing, abort it.
if s.IsInstalling() {
s.AbortInstallation()
}
// Delete the server's archive if it exists. We intentionally don't return
// here, if the archive fails to delete, the server can still be removed.
if err := s.Archiver.DeleteIfExists(); err != nil {

View File

@@ -9,6 +9,7 @@ import (
"github.com/pterodactyl/wings/server"
"github.com/pterodactyl/wings/system"
"net/http"
"strings"
)
// Returns information about the system that wings is running on.
@@ -78,6 +79,16 @@ func postUpdateConfiguration(c *gin.Context) {
return
}
// Keep the SSL certificates the same since the Panel will send through Lets Encrypt
// default locations. However, if we picked a different location manually we don't
// want to override that.
//
// If you pass through manual locations in the API call this logic will be skipped.
if strings.HasPrefix(cfg.Api.Ssl.KeyFile, "/etc/letsencrypt/live/") {
cfg.Api.Ssl.KeyFile = ccopy.Api.Ssl.KeyFile
cfg.Api.Ssl.CertificateFile = ccopy.Api.Ssl.CertificateFile
}
config.Set(&cfg)
if err := config.Get().WriteToDisk(); err != nil {
// If there was an error writing to the disk, revert back to the configuration we had

View File

@@ -564,7 +564,10 @@ func (d *DockerEnvironment) DisableResourcePolling() error {
//
// @todo handle authorization & local images
func (d *DockerEnvironment) ensureImageExists(c *client.Client) error {
ctx, _ := context.WithTimeout(context.Background(), time.Second*10)
// Give it up to 15 minutes to pull the image. I think this should cover 99.8% of cases where an
// image pull might fail. I can't imagine it will ever take more than 15 minutes to fully pull
// an image. Let me know when I am inevitably wrong here...
ctx, _ := context.WithTimeout(context.Background(), time.Minute*15)
out, err := c.ImagePull(ctx, d.Server.Container.Image, types.ImagePullOptions{All: false})
if err != nil {

View File

@@ -12,12 +12,14 @@ import (
"github.com/pkg/errors"
"github.com/pterodactyl/wings/api"
"github.com/pterodactyl/wings/config"
"golang.org/x/sync/semaphore"
"io"
"io/ioutil"
"os"
"path"
"path/filepath"
"sync"
"time"
)
// Executes the installation stack for a server process. Bubbles any errors up to the calling
@@ -27,10 +29,16 @@ func (s *Server) Install() error {
s.Log().Debug("notifying panel of server install state")
if serr := s.SyncInstallState(err == nil); serr != nil {
s.Log().WithFields(log.Fields{
"was_successful": err == nil,
"error": serr,
}).Warn("failed to notify panel of server install state")
l := s.Log().WithField("was_successful", err == nil)
// If the request was successful but there was an error with this request, attach the
// error to this log entry. Otherwise ignore it in this log since whatever is calling
// this function should handle the error and will end up logging the same one.
if err == nil {
l.WithField("error", serr)
}
l.Warn("failed to notify panel of server install state")
}
return err
@@ -78,8 +86,8 @@ type InstallationProcess struct {
Server *Server
Script *api.InstallationScript
client *client.Client
mutex *sync.Mutex
client *client.Client
context context.Context
}
// Generates a new installation process struct that will be used to create containers,
@@ -88,21 +96,70 @@ func NewInstallationProcess(s *Server, script *api.InstallationScript) (*Install
proc := &InstallationProcess{
Script: script,
Server: s,
mutex: &sync.Mutex{},
}
ctx, cancel := context.WithCancel(context.Background())
s.installer.cancel = &cancel
if c, err := client.NewClientWithOpts(client.FromEnv); err != nil {
return nil, errors.WithStack(err)
} else {
proc.client = c
proc.context = ctx
}
return proc, nil
}
// Try to obtain an exclusive lock on the installation process for the server. Waits up to 10
// seconds before aborting with a context timeout.
func (s *Server) acquireInstallationLock() error {
if s.installer.sem == nil {
s.installer.sem = semaphore.NewWeighted(1)
}
ctx, _ := context.WithTimeout(context.Background(), time.Second*10)
return s.installer.sem.Acquire(ctx, 1)
}
// Determines if the server is actively running the installation process by checking the status
// of the semaphore lock.
func (s *Server) IsInstalling() bool {
if s.installer.sem == nil {
return false
}
if s.installer.sem.TryAcquire(1) {
// If we made it into this block it means we were able to obtain an exclusive lock
// on the semaphore. In that case, go ahead and release that lock immediately, and
// return false.
s.installer.sem.Release(1)
return false
}
return true
}
// Aborts the server installation process by calling the cancel function on the installer
// context.
func (s *Server) AbortInstallation() {
if !s.IsInstalling() {
return
}
if s.installer.cancel != nil {
cancel := *s.installer.cancel
s.Log().Warn("aborting running installation process")
cancel()
}
}
// Removes the installer container for the server.
func (ip *InstallationProcess) RemoveContainer() {
err := ip.client.ContainerRemove(context.Background(), ip.Server.Uuid+"_installer", types.ContainerRemoveOptions{
err := ip.client.ContainerRemove(ip.context, ip.Server.Uuid+"_installer", types.ContainerRemoveOptions{
RemoveVolumes: true,
Force: true,
})
@@ -118,6 +175,20 @@ func (ip *InstallationProcess) RemoveContainer() {
// Once the container finishes installing the results will be stored in an installation
// log in the server's configuration directory.
func (ip *InstallationProcess) Run() error {
ip.Server.Log().Debug("acquiring installation process lock")
if err := ip.Server.acquireInstallationLock(); err != nil {
return err
}
// We now have an exclusive lock on this installation process. Ensure that whenever this
// process is finished that the semaphore is released so that other processes and be executed
// without encounting a wait timeout.
defer func() {
ip.Server.Log().Debug("releasing installation process lock")
ip.Server.installer.sem.Release(1)
ip.Server.installer.cancel = nil
}()
installPath, err := ip.BeforeExecute()
if err != nil {
return err
@@ -177,7 +248,7 @@ func (ip *InstallationProcess) writeScriptToDisk() (string, error) {
// Pulls the docker image to be used for the installation container.
func (ip *InstallationProcess) pullInstallationImage() error {
r, err := ip.client.ImagePull(context.Background(), ip.Script.ContainerImage, types.ImagePullOptions{})
r, err := ip.client.ImagePull(ip.context, ip.Script.ContainerImage, types.ImagePullOptions{})
if err != nil {
return errors.WithStack(err)
}
@@ -231,7 +302,7 @@ func (ip *InstallationProcess) BeforeExecute() (string, error) {
Force: true,
}
if err := ip.client.ContainerRemove(context.Background(), ip.Server.Uuid+"_installer", opts); err != nil {
if err := ip.client.ContainerRemove(ip.context, ip.Server.Uuid+"_installer", opts); err != nil {
if !client.IsErrNotFound(err) {
e = append(e, err)
}
@@ -258,11 +329,10 @@ func (ip *InstallationProcess) GetLogPath() string {
// process to store in the server configuration directory, and then destroys the associated
// installation container.
func (ip *InstallationProcess) AfterExecute(containerId string) error {
ctx := context.Background()
defer ip.RemoveContainer()
ip.Server.Log().WithField("container_id", containerId).Debug("pulling installation logs for server")
reader, err := ip.client.ContainerLogs(ctx, containerId, types.ContainerLogsOptions{
reader, err := ip.client.ContainerLogs(ip.context, containerId, types.ContainerLogsOptions{
ShowStdout: true,
ShowStderr: true,
Follow: false,
@@ -289,8 +359,6 @@ func (ip *InstallationProcess) AfterExecute(containerId string) error {
// Executes the installation process inside a specially created docker container.
func (ip *InstallationProcess) Execute(installPath string) (string, error) {
ctx := context.Background()
conf := &container.Config{
Hostname: "installer",
AttachStdout: true,
@@ -339,13 +407,13 @@ func (ip *InstallationProcess) Execute(installPath string) (string, error) {
}
ip.Server.Log().WithField("install_script", installPath+"/install.sh").Info("creating install container for server process")
r, err := ip.client.ContainerCreate(ctx, conf, hostConf, nil, ip.Server.Uuid+"_installer")
r, err := ip.client.ContainerCreate(ip.context, conf, hostConf, nil, ip.Server.Uuid+"_installer")
if err != nil {
return "", errors.WithStack(err)
}
ip.Server.Log().WithField("container_id", r.ID).Info("running installation script for server in container")
if err := ip.client.ContainerStart(ctx, r.ID, types.ContainerStartOptions{}); err != nil {
if err := ip.client.ContainerStart(ip.context, r.ID, types.ContainerStartOptions{}); err != nil {
return "", err
}
@@ -357,7 +425,7 @@ func (ip *InstallationProcess) Execute(installPath string) (string, error) {
ip.Server.Events().Publish(DaemonMessageEvent, "Installation process completed.")
}(r.ID)
sChann, eChann := ip.client.ContainerWait(ctx, r.ID, container.WaitConditionNotRunning)
sChann, eChann := ip.client.ContainerWait(ip.context, r.ID, container.WaitConditionNotRunning)
select {
case err := <-eChann:
if err != nil {
@@ -373,7 +441,7 @@ func (ip *InstallationProcess) Execute(installPath string) (string, error) {
// directory, as well as to a websocket listener so that the process can be viewed in
// the panel by administrators.
func (ip *InstallationProcess) StreamOutput(id string) error {
reader, err := ip.client.ContainerLogs(context.Background(), id, types.ContainerLogsOptions{
reader, err := ip.client.ContainerLogs(ip.context, id, types.ContainerLogsOptions{
ShowStdout: true,
ShowStderr: true,
Follow: true,
@@ -393,7 +461,7 @@ func (ip *InstallationProcess) StreamOutput(id string) error {
if err := s.Err(); err != nil {
ip.Server.Log().WithFields(log.Fields{
"container_id": id,
"error": errors.WithStack(err),
"error": errors.WithStack(err),
}).Warn("error processing scanner line in installation output for server")
}

View File

@@ -1,6 +1,7 @@
package server
import (
"context"
"fmt"
"github.com/apex/log"
"github.com/creasty/defaults"
@@ -9,6 +10,7 @@ import (
"github.com/pterodactyl/wings/api"
"github.com/pterodactyl/wings/config"
"github.com/remeh/sizedwaitgroup"
"golang.org/x/sync/semaphore"
"math"
"os"
"strings"
@@ -71,11 +73,27 @@ type Server struct {
// started, and then cached here.
processConfiguration *api.ProcessConfiguration
// Tracks the installation process for this server and prevents a server from running
// two installer processes at the same time. This also allows us to cancel a running
// installation process, for example when a server is deleted from the panel while the
// installer process is still running.
installer InstallerDetails
// Internal mutex used to block actions that need to occur sequentially, such as
// writing the configuration to the disk.
sync.RWMutex
}
type InstallerDetails struct {
// The cancel function for the installer. This will be a non-nil value while there
// is an installer running for the server.
cancel *context.CancelFunc
// Installer lock. You should obtain an exclusive lock on this context while running
// the installation process and release it when finished.
sem *semaphore.Weighted
}
// The build settings for a given server that impact docker container creation and
// resource limits for a server instance.
type BuildSettings struct {
@@ -247,10 +265,6 @@ func FromConfiguration(data *api.ServerConfigurationResponse) (*Server, error) {
}
s.Resources = ResourceUsage{}
// Force the disk usage to become cached to return in a resources response
// or when connecting to the websocket of an offline server.
go s.Filesystem.HasSpaceAvailable()
// Forces the configuration to be synced with the panel.
if err := s.SyncWithConfiguration(data); err != nil {
return nil, err

View File

@@ -2,5 +2,5 @@ package system
const (
// The current version of this software.
Version = "0.0.1"
Version = "1.0.0-beta.7"
)