Compare commits
11 Commits
v1.0.0-bet
...
v1.0.0-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4c3d4f112b | ||
|
|
d6a3d9adb1 | ||
|
|
d284c4aec9 | ||
|
|
05a4730489 | ||
|
|
2dad3102e0 | ||
|
|
b33f14ddd9 | ||
|
|
1f6789cba3 | ||
|
|
073247e4e1 | ||
|
|
a3d83d23bd | ||
|
|
f318962371 | ||
|
|
db31722cfc |
12
api/api.go
12
api/api.go
@@ -130,6 +130,12 @@ func (r *PanelRequest) HttpResponseCode() int {
|
|||||||
return r.Response.StatusCode
|
return r.Response.StatusCode
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func IsRequestError(err error) bool {
|
||||||
|
_, ok := err.(*RequestError)
|
||||||
|
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
type RequestError struct {
|
type RequestError struct {
|
||||||
Code string `json:"code"`
|
Code string `json:"code"`
|
||||||
Status string `json:"status"`
|
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.
|
// 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)
|
return fmt.Sprintf("%s: %s (HTTP/%s)", re.Code, re.Detail, re.Status)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (re *RequestError) String() string {
|
||||||
|
return re.Error()
|
||||||
|
}
|
||||||
|
|
||||||
type RequestErrorBag struct {
|
type RequestErrorBag struct {
|
||||||
Errors []RequestError `json:"errors"`
|
Errors []RequestError `json:"errors"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"github.com/AlecAivazis/survey/v2"
|
"github.com/AlecAivazis/survey/v2"
|
||||||
"github.com/AlecAivazis/survey/v2/terminal"
|
"github.com/AlecAivazis/survey/v2/terminal"
|
||||||
"github.com/creasty/defaults"
|
|
||||||
"github.com/pterodactyl/wings/config"
|
"github.com/pterodactyl/wings/config"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
@@ -147,8 +146,8 @@ func configureCmdRun(cmd *cobra.Command, args []string) {
|
|||||||
|
|
||||||
b, err := ioutil.ReadAll(res.Body)
|
b, err := ioutil.ReadAll(res.Body)
|
||||||
|
|
||||||
cfg := new(config.Configuration)
|
cfg, err := config.NewFromPath(configPath)
|
||||||
if err := defaults.Set(cfg); err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ func ReadConfiguration(path string) (*Configuration, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Track the location where we created this configuration.
|
// Track the location where we created this configuration.
|
||||||
c.path = path
|
c.unsafeSetPath(path)
|
||||||
|
|
||||||
// Replace environment variables within the configuration file with their
|
// Replace environment variables within the configuration file with their
|
||||||
// values from the host system.
|
// values from the host system.
|
||||||
@@ -186,8 +186,32 @@ func GetJwtAlgorithm() *jwt.HMACSHA {
|
|||||||
return _jwtAlgo
|
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.
|
// Returns the path for this configuration file.
|
||||||
func (c *Configuration) GetPath() string {
|
func (c *Configuration) GetPath() string {
|
||||||
|
c.RLock()
|
||||||
|
defer c.RUnlock()
|
||||||
|
|
||||||
return c.path
|
return c.path
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -245,11 +269,10 @@ func (c *Configuration) setSystemUser(u *user.User) error {
|
|||||||
gid, _ := strconv.Atoi(u.Gid)
|
gid, _ := strconv.Atoi(u.Gid)
|
||||||
|
|
||||||
c.Lock()
|
c.Lock()
|
||||||
defer c.Unlock()
|
|
||||||
|
|
||||||
c.System.Username = u.Username
|
c.System.Username = u.Username
|
||||||
c.System.User.Uid = uid
|
c.System.User.Uid = uid
|
||||||
c.System.User.Gid = gid
|
c.System.User.Gid = gid
|
||||||
|
c.Unlock()
|
||||||
|
|
||||||
return c.WriteToDisk()
|
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
|
// lock on the file. This prevents something else from writing at the exact same time and
|
||||||
// leading to bad data conditions.
|
// leading to bad data conditions.
|
||||||
func (c *Configuration) WriteToDisk() error {
|
func (c *Configuration) WriteToDisk() error {
|
||||||
|
// Obtain an exclusive write against the configuration file.
|
||||||
|
c.writeLock.Lock()
|
||||||
|
defer c.writeLock.Unlock()
|
||||||
|
|
||||||
ccopy := *c
|
ccopy := *c
|
||||||
// If debugging is set with the flag, don't save that to the configuration file, otherwise
|
// If debugging is set with the flag, don't save that to the configuration file, otherwise
|
||||||
// you'll always end up in debug mode.
|
// you'll always end up in debug mode.
|
||||||
@@ -326,10 +353,6 @@ func (c *Configuration) WriteToDisk() error {
|
|||||||
return err
|
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 {
|
if err := ioutil.WriteFile(c.GetPath(), b, 0644); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,6 +70,8 @@ func (h *Handler) HandleLog(e *log.Entry) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err, ok := e.Fields.Get("error").(error); ok {
|
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 {
|
if e, ok := errors.Cause(err).(tracer); ok {
|
||||||
st := e.StackTrace()
|
st := e.StackTrace()
|
||||||
|
|
||||||
@@ -78,11 +80,9 @@ func (h *Handler) HandleLog(e *log.Entry) error {
|
|||||||
l = 5
|
l = 5
|
||||||
}
|
}
|
||||||
|
|
||||||
br := color2.New(color2.Bold, color2.FgRed)
|
|
||||||
|
|
||||||
fmt.Fprintf(h.Writer, "\n%s%+v\n\n", br.Sprintf("Stacktrace:"), st[0:l])
|
fmt.Fprintf(h.Writer, "\n%s%+v\n\n", br.Sprintf("Stacktrace:"), st[0:l])
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf("\n\nINVALID TRACER\n\n")
|
fmt.Fprintf(h.Writer, "\n%s\n%+v\n\n", br.Sprintf("Stacktrace:"), err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf("\n\nINVALID ERROR\n\n")
|
fmt.Printf("\n\nINVALID ERROR\n\n")
|
||||||
|
|||||||
@@ -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
|
// If the child is a null value, nothing will happen. Seems reasonable as of the
|
||||||
// time this code is being written.
|
// time this code is being written.
|
||||||
for _, child := range parsed.Path(strings.Trim(parts[0], ".")).Children() {
|
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
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} 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
|
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.
|
// 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
|
// 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
|
// on our merry way. If the value isn't a string, we're not going to be doing anything
|
||||||
// with it anyways.
|
// with it anyways.
|
||||||
if cfr.ReplaceWith.Type() != jsonparser.String || !configMatchRegex.Match(cfr.ReplaceWith.Value()) {
|
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
|
// 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...)
|
match, _, _, err := jsonparser.Get(f.configuration, path...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err != jsonparser.KeyPathNotFoundError {
|
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")
|
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
|
// If there is no key, keep the original value intact, that way it is obvious there
|
||||||
// is a replace issue at play.
|
// is a replace issue at play.
|
||||||
return match, nil
|
return string(match), nil
|
||||||
} else {
|
} else {
|
||||||
replaced := []byte(configMatchRegex.ReplaceAllString(cfr.ReplaceWith.String(), string(match)))
|
return configMatchRegex.ReplaceAllString(cfr.ReplaceWith.String(), string(match)), nil
|
||||||
|
|
||||||
return replaced, nil
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -236,13 +236,13 @@ func (f *ConfigurationFile) parseXmlFile(path string) error {
|
|||||||
|
|
||||||
// Iterate over the elements we found and update their values.
|
// Iterate over the elements we found and update their values.
|
||||||
for _, element := range doc.FindElements(path) {
|
for _, element := range doc.FindElements(path) {
|
||||||
if xmlValueMatchRegex.Match(value) {
|
if xmlValueMatchRegex.MatchString(value) {
|
||||||
k := xmlValueMatchRegex.ReplaceAllString(string(value), "$1")
|
k := xmlValueMatchRegex.ReplaceAllString(value, "$1")
|
||||||
v := xmlValueMatchRegex.ReplaceAllString(string(value), "$2")
|
v := xmlValueMatchRegex.ReplaceAllString(value, "$2")
|
||||||
|
|
||||||
element.CreateAttr(k, v)
|
element.CreateAttr(k, v)
|
||||||
} else {
|
} else {
|
||||||
element.SetText(string(value))
|
element.SetText(value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -273,12 +273,13 @@ func (f *ConfigurationFile) parseXmlFile(path string) error {
|
|||||||
// Parses an ini file.
|
// Parses an ini file.
|
||||||
func (f *ConfigurationFile) parseIniFile(path string) error {
|
func (f *ConfigurationFile) parseIniFile(path string) error {
|
||||||
// Ini package can't handle a non-existent file, so handle that automatically here
|
// 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)
|
file, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, 0644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer file.Close()
|
file.Close()
|
||||||
|
|
||||||
cfg, err := ini.Load(path)
|
cfg, err := ini.Load(path)
|
||||||
if err != nil {
|
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
|
// If the key exists in the file go ahead and set the value, otherwise try to
|
||||||
// create it in the section.
|
// create it in the section.
|
||||||
if s.HasKey(k) {
|
if s.HasKey(k) {
|
||||||
s.Key(k).SetValue(string(value))
|
s.Key(k).SetValue(value)
|
||||||
} else {
|
} else {
|
||||||
if _, err := s.NewKey(k, string(value)); err != nil {
|
if _, err := s.NewKey(k, value); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Truncate the file before attempting to write the changes.
|
return cfg.SaveTo(path)
|
||||||
if err := os.Truncate(path, 0); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := cfg.WriteTo(file); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parses a json file updating any matching key/value pairs. If a match is not found, the
|
// 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
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, _, err := p.Set(replace.Match, string(data)); err != nil {
|
if _, _, err := p.Set(replace.Match, data); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,9 @@ func (cv *ReplaceValue) Value() []byte {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (cv *ReplaceValue) String() string {
|
func (cv *ReplaceValue) String() string {
|
||||||
return string(cv.value)
|
str, _ := jsonparser.ParseString(cv.value)
|
||||||
|
|
||||||
|
return str
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cv *ReplaceValue) Type() jsonparser.ValueType {
|
func (cv *ReplaceValue) Type() jsonparser.ValueType {
|
||||||
|
|||||||
@@ -164,6 +164,11 @@ func deleteServer(c *gin.Context) {
|
|||||||
// to start it while this process is running.
|
// to start it while this process is running.
|
||||||
s.Suspended = true
|
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
|
// 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.
|
// here, if the archive fails to delete, the server can still be removed.
|
||||||
if err := s.Archiver.DeleteIfExists(); err != nil {
|
if err := s.Archiver.DeleteIfExists(); err != nil {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"github.com/pterodactyl/wings/server"
|
"github.com/pterodactyl/wings/server"
|
||||||
"github.com/pterodactyl/wings/system"
|
"github.com/pterodactyl/wings/system"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Returns information about the system that wings is running on.
|
// Returns information about the system that wings is running on.
|
||||||
@@ -78,6 +79,16 @@ func postUpdateConfiguration(c *gin.Context) {
|
|||||||
return
|
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)
|
config.Set(&cfg)
|
||||||
if err := config.Get().WriteToDisk(); err != nil {
|
if err := config.Get().WriteToDisk(); err != nil {
|
||||||
// If there was an error writing to the disk, revert back to the configuration we had
|
// If there was an error writing to the disk, revert back to the configuration we had
|
||||||
|
|||||||
@@ -564,7 +564,10 @@ func (d *DockerEnvironment) DisableResourcePolling() error {
|
|||||||
//
|
//
|
||||||
// @todo handle authorization & local images
|
// @todo handle authorization & local images
|
||||||
func (d *DockerEnvironment) ensureImageExists(c *client.Client) error {
|
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})
|
out, err := c.ImagePull(ctx, d.Server.Container.Image, types.ImagePullOptions{All: false})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -12,12 +12,14 @@ import (
|
|||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/pterodactyl/wings/api"
|
"github.com/pterodactyl/wings/api"
|
||||||
"github.com/pterodactyl/wings/config"
|
"github.com/pterodactyl/wings/config"
|
||||||
|
"golang.org/x/sync/semaphore"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Executes the installation stack for a server process. Bubbles any errors up to the calling
|
// 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")
|
s.Log().Debug("notifying panel of server install state")
|
||||||
if serr := s.SyncInstallState(err == nil); serr != nil {
|
if serr := s.SyncInstallState(err == nil); serr != nil {
|
||||||
s.Log().WithFields(log.Fields{
|
l := s.Log().WithField("was_successful", err == nil)
|
||||||
"was_successful": err == nil,
|
|
||||||
"error": serr,
|
// If the request was successful but there was an error with this request, attach the
|
||||||
}).Warn("failed to notify panel of server install state")
|
// 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
|
return err
|
||||||
@@ -78,8 +86,8 @@ type InstallationProcess struct {
|
|||||||
Server *Server
|
Server *Server
|
||||||
Script *api.InstallationScript
|
Script *api.InstallationScript
|
||||||
|
|
||||||
client *client.Client
|
client *client.Client
|
||||||
mutex *sync.Mutex
|
context context.Context
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generates a new installation process struct that will be used to create containers,
|
// 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{
|
proc := &InstallationProcess{
|
||||||
Script: script,
|
Script: script,
|
||||||
Server: s,
|
Server: s,
|
||||||
mutex: &sync.Mutex{},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
s.installer.cancel = &cancel
|
||||||
|
|
||||||
if c, err := client.NewClientWithOpts(client.FromEnv); err != nil {
|
if c, err := client.NewClientWithOpts(client.FromEnv); err != nil {
|
||||||
return nil, errors.WithStack(err)
|
return nil, errors.WithStack(err)
|
||||||
} else {
|
} else {
|
||||||
proc.client = c
|
proc.client = c
|
||||||
|
proc.context = ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
return proc, nil
|
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.
|
// Removes the installer container for the server.
|
||||||
func (ip *InstallationProcess) RemoveContainer() {
|
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,
|
RemoveVolumes: true,
|
||||||
Force: true,
|
Force: true,
|
||||||
})
|
})
|
||||||
@@ -118,6 +175,20 @@ func (ip *InstallationProcess) RemoveContainer() {
|
|||||||
// Once the container finishes installing the results will be stored in an installation
|
// Once the container finishes installing the results will be stored in an installation
|
||||||
// log in the server's configuration directory.
|
// log in the server's configuration directory.
|
||||||
func (ip *InstallationProcess) Run() error {
|
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()
|
installPath, err := ip.BeforeExecute()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -177,7 +248,7 @@ func (ip *InstallationProcess) writeScriptToDisk() (string, error) {
|
|||||||
|
|
||||||
// Pulls the docker image to be used for the installation container.
|
// Pulls the docker image to be used for the installation container.
|
||||||
func (ip *InstallationProcess) pullInstallationImage() error {
|
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 {
|
if err != nil {
|
||||||
return errors.WithStack(err)
|
return errors.WithStack(err)
|
||||||
}
|
}
|
||||||
@@ -231,7 +302,7 @@ func (ip *InstallationProcess) BeforeExecute() (string, error) {
|
|||||||
Force: true,
|
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) {
|
if !client.IsErrNotFound(err) {
|
||||||
e = append(e, 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
|
// process to store in the server configuration directory, and then destroys the associated
|
||||||
// installation container.
|
// installation container.
|
||||||
func (ip *InstallationProcess) AfterExecute(containerId string) error {
|
func (ip *InstallationProcess) AfterExecute(containerId string) error {
|
||||||
ctx := context.Background()
|
|
||||||
defer ip.RemoveContainer()
|
defer ip.RemoveContainer()
|
||||||
|
|
||||||
ip.Server.Log().WithField("container_id", containerId).Debug("pulling installation logs for server")
|
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,
|
ShowStdout: true,
|
||||||
ShowStderr: true,
|
ShowStderr: true,
|
||||||
Follow: false,
|
Follow: false,
|
||||||
@@ -289,8 +359,6 @@ func (ip *InstallationProcess) AfterExecute(containerId string) error {
|
|||||||
|
|
||||||
// Executes the installation process inside a specially created docker container.
|
// Executes the installation process inside a specially created docker container.
|
||||||
func (ip *InstallationProcess) Execute(installPath string) (string, error) {
|
func (ip *InstallationProcess) Execute(installPath string) (string, error) {
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
conf := &container.Config{
|
conf := &container.Config{
|
||||||
Hostname: "installer",
|
Hostname: "installer",
|
||||||
AttachStdout: true,
|
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")
|
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 {
|
if err != nil {
|
||||||
return "", errors.WithStack(err)
|
return "", errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
ip.Server.Log().WithField("container_id", r.ID).Info("running installation script for server in container")
|
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
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -357,7 +425,7 @@ func (ip *InstallationProcess) Execute(installPath string) (string, error) {
|
|||||||
ip.Server.Events().Publish(DaemonMessageEvent, "Installation process completed.")
|
ip.Server.Events().Publish(DaemonMessageEvent, "Installation process completed.")
|
||||||
}(r.ID)
|
}(r.ID)
|
||||||
|
|
||||||
sChann, eChann := ip.client.ContainerWait(ctx, r.ID, container.WaitConditionNotRunning)
|
sChann, eChann := ip.client.ContainerWait(ip.context, r.ID, container.WaitConditionNotRunning)
|
||||||
select {
|
select {
|
||||||
case err := <-eChann:
|
case err := <-eChann:
|
||||||
if err != nil {
|
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
|
// directory, as well as to a websocket listener so that the process can be viewed in
|
||||||
// the panel by administrators.
|
// the panel by administrators.
|
||||||
func (ip *InstallationProcess) StreamOutput(id string) error {
|
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,
|
ShowStdout: true,
|
||||||
ShowStderr: true,
|
ShowStderr: true,
|
||||||
Follow: true,
|
Follow: true,
|
||||||
@@ -393,7 +461,7 @@ func (ip *InstallationProcess) StreamOutput(id string) error {
|
|||||||
if err := s.Err(); err != nil {
|
if err := s.Err(); err != nil {
|
||||||
ip.Server.Log().WithFields(log.Fields{
|
ip.Server.Log().WithFields(log.Fields{
|
||||||
"container_id": id,
|
"container_id": id,
|
||||||
"error": errors.WithStack(err),
|
"error": errors.WithStack(err),
|
||||||
}).Warn("error processing scanner line in installation output for server")
|
}).Warn("error processing scanner line in installation output for server")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/apex/log"
|
"github.com/apex/log"
|
||||||
"github.com/creasty/defaults"
|
"github.com/creasty/defaults"
|
||||||
@@ -9,6 +10,7 @@ import (
|
|||||||
"github.com/pterodactyl/wings/api"
|
"github.com/pterodactyl/wings/api"
|
||||||
"github.com/pterodactyl/wings/config"
|
"github.com/pterodactyl/wings/config"
|
||||||
"github.com/remeh/sizedwaitgroup"
|
"github.com/remeh/sizedwaitgroup"
|
||||||
|
"golang.org/x/sync/semaphore"
|
||||||
"math"
|
"math"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -71,11 +73,27 @@ type Server struct {
|
|||||||
// started, and then cached here.
|
// started, and then cached here.
|
||||||
processConfiguration *api.ProcessConfiguration
|
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
|
// Internal mutex used to block actions that need to occur sequentially, such as
|
||||||
// writing the configuration to the disk.
|
// writing the configuration to the disk.
|
||||||
sync.RWMutex
|
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
|
// The build settings for a given server that impact docker container creation and
|
||||||
// resource limits for a server instance.
|
// resource limits for a server instance.
|
||||||
type BuildSettings struct {
|
type BuildSettings struct {
|
||||||
@@ -247,10 +265,6 @@ func FromConfiguration(data *api.ServerConfigurationResponse) (*Server, error) {
|
|||||||
}
|
}
|
||||||
s.Resources = ResourceUsage{}
|
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.
|
// Forces the configuration to be synced with the panel.
|
||||||
if err := s.SyncWithConfiguration(data); err != nil {
|
if err := s.SyncWithConfiguration(data); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -2,5 +2,5 @@ package system
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
// The current version of this software.
|
// The current version of this software.
|
||||||
Version = "1.0.0-beta.6"
|
Version = "1.0.0-beta.7"
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user