Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6902422229 | ||
|
|
5f5b2bc84e | ||
|
|
81a411a42c | ||
|
|
37c6b85489 | ||
|
|
0e3778ac47 |
@@ -1,5 +1,12 @@
|
||||
# Changelog
|
||||
|
||||
## v1.3.0
|
||||
### Fixed
|
||||
* Fixes an error being returned to the client when attempting to restart a server when the container no longer exists on the machine.
|
||||
|
||||
### Changed
|
||||
* Updated server transfer logic to use newer file archiving tools to avoid frequent errors when transferring symlinked files.
|
||||
|
||||
## v1.3.0
|
||||
### Fixed
|
||||
* Fixes improper error handling when attempting to create a new Docker network.
|
||||
|
||||
@@ -3,9 +3,14 @@ package docker
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"emperror.dev/errors"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"emperror.dev/errors"
|
||||
"github.com/apex/log"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
@@ -15,16 +20,9 @@ import (
|
||||
"github.com/pterodactyl/wings/config"
|
||||
"github.com/pterodactyl/wings/environment"
|
||||
"github.com/pterodactyl/wings/system"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type imagePullStatus struct {
|
||||
Status string `json:"status"`
|
||||
Progress string `json:"progress"`
|
||||
}
|
||||
var ErrNotAttached = errors.Sentinel("not attached to instance")
|
||||
|
||||
// A custom console writer that allows us to keep a function blocked until the
|
||||
// given stream is properly closed. This does nothing special, only exists to
|
||||
@@ -38,14 +36,14 @@ func (nw noopWriter) Write(b []byte) (int, error) {
|
||||
return len(b), nil
|
||||
}
|
||||
|
||||
// Attaches to the docker container itself and ensures that we can pipe data in and out
|
||||
// of the process stream. This should not be used for reading console data as you *will*
|
||||
// miss important output at the beginning because of the time delay with attaching to the
|
||||
// output.
|
||||
// Attach attaches to the docker container itself and ensures that we can pipe
|
||||
// data in and out of the process stream. This should not be used for reading
|
||||
// console data as you *will* miss important output at the beginning because of
|
||||
// the time delay with attaching to the output.
|
||||
//
|
||||
// Calling this function will poll resources for the container in the background until the
|
||||
// provided context is canceled by the caller. Failure to cancel said context will cause
|
||||
// background memory leaks as the goroutine will not exit.
|
||||
// Calling this function will poll resources for the container in the background
|
||||
// until the provided context is canceled by the caller. Failure to cancel said
|
||||
// context will cause background memory leaks as the goroutine will not exit.
|
||||
func (e *Environment) Attach() error {
|
||||
if e.IsAttached() {
|
||||
return nil
|
||||
@@ -108,27 +106,15 @@ func (e *Environment) Attach() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *Environment) resources() container.Resources {
|
||||
l := e.Configuration.Limits()
|
||||
|
||||
return container.Resources{
|
||||
Memory: l.BoundedMemoryLimit(),
|
||||
MemoryReservation: l.MemoryLimit * 1_000_000,
|
||||
MemorySwap: l.ConvertedSwap(),
|
||||
CPUQuota: l.ConvertedCpuLimit(),
|
||||
CPUPeriod: 100_000,
|
||||
CPUShares: 1024,
|
||||
BlkioWeight: l.IoWeight,
|
||||
OomKillDisable: &l.OOMDisabled,
|
||||
CpusetCpus: l.Threads,
|
||||
}
|
||||
}
|
||||
|
||||
// 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.
|
||||
// InSituUpdate 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 (e *Environment) InSituUpdate() error {
|
||||
if _, err := e.client.ContainerInspect(context.Background(), e.Id); err != nil {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
|
||||
defer cancel()
|
||||
|
||||
if _, err := e.client.ContainerInspect(ctx, e.Id); 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
|
||||
@@ -138,25 +124,24 @@ func (e *Environment) InSituUpdate() error {
|
||||
if client.IsErrNotFound(err) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return err
|
||||
return errors.Wrap(err, "environment/docker: could not inspect container")
|
||||
}
|
||||
|
||||
u := container.UpdateConfig{
|
||||
// CPU pinning cannot be removed once it is applied to a container. The same is true
|
||||
// for removing memory limits, a container must be re-created.
|
||||
//
|
||||
// @see https://github.com/moby/moby/issues/41946
|
||||
if _, err := e.client.ContainerUpdate(ctx, e.Id, container.UpdateConfig{
|
||||
Resources: e.resources(),
|
||||
}); err != nil {
|
||||
return errors.Wrap(err, "environment/docker: could not update container")
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
|
||||
defer cancel()
|
||||
if _, err := e.client.ContainerUpdate(ctx, e.Id, u); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Creates a new container for the server using all of the data that is currently
|
||||
// available for it. If the container already exists it will be returnee.
|
||||
// Create creates a new container for the server using all of the data that is
|
||||
// currently available for it. If the container already exists it will be
|
||||
// returned.
|
||||
func (e *Environment) Create() error {
|
||||
// If the container already exists don't hit the user with an error, just return
|
||||
// the current information about it which is what we would do when creating the
|
||||
@@ -251,23 +236,8 @@ func (e *Environment) Create() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *Environment) convertMounts() []mount.Mount {
|
||||
var out []mount.Mount
|
||||
|
||||
for _, m := range e.Configuration.Mounts() {
|
||||
out = append(out, mount.Mount{
|
||||
Type: mount.TypeBind,
|
||||
Source: m.Source,
|
||||
Target: m.Target,
|
||||
ReadOnly: m.ReadOnly,
|
||||
})
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
// Remove the Docker container from the machine. If the container is currently running
|
||||
// it will be forcibly stopped by Docker.
|
||||
// Destroy will remove the Docker container from the server. If the container
|
||||
// is currently running it will be forcibly stopped by Docker.
|
||||
func (e *Environment) Destroy() error {
|
||||
// We set it to stopping than offline to prevent crash detection from being triggered.
|
||||
e.SetState(environment.ProcessStoppingState)
|
||||
@@ -291,9 +261,55 @@ func (e *Environment) Destroy() error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Attaches to the log for the container. This avoids us missing crucial output that
|
||||
// happens in the split seconds before the code moves from 'Starting' to 'Attaching'
|
||||
// on the process.
|
||||
// SendCommand sends the specified command to the stdin of the running container
|
||||
// instance. There is no confirmation that this data is sent successfully, only
|
||||
// that it gets pushed into the stdin.
|
||||
func (e *Environment) SendCommand(c string) error {
|
||||
if !e.IsAttached() {
|
||||
return errors.Wrap(ErrNotAttached, "environment/docker: cannot send command to container")
|
||||
}
|
||||
|
||||
e.mu.RLock()
|
||||
defer e.mu.RUnlock()
|
||||
|
||||
// If the command being processed is the same as the process stop command then we
|
||||
// want to mark the server as entering the stopping state otherwise the process will
|
||||
// stop and Wings will think it has crashed and attempt to restart it.
|
||||
if e.meta.Stop.Type == "command" && c == e.meta.Stop.Value {
|
||||
e.SetState(environment.ProcessStoppingState)
|
||||
}
|
||||
|
||||
_, err := e.stream.Conn.Write([]byte(c + "\n"))
|
||||
|
||||
return errors.Wrap(err, "environment/docker: could not write to container stream")
|
||||
}
|
||||
|
||||
// Readlog reads the log file for the server. This does not care if the server
|
||||
// is running or not, it will simply try to read the last X bytes of the file
|
||||
// and return them.
|
||||
func (e *Environment) Readlog(lines int) ([]string, error) {
|
||||
r, err := e.client.ContainerLogs(context.Background(), e.Id, types.ContainerLogsOptions{
|
||||
ShowStdout: true,
|
||||
ShowStderr: true,
|
||||
Tail: strconv.Itoa(lines),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
defer r.Close()
|
||||
|
||||
var out []string
|
||||
scanner := bufio.NewScanner(r)
|
||||
for scanner.Scan() {
|
||||
out = append(out, scanner.Text())
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// Attaches to the log for the container. This avoids us missing crucial output
|
||||
// that happens in the split seconds before the code moves from 'Starting' to
|
||||
// 'Attaching' on the process.
|
||||
func (e *Environment) followOutput() error {
|
||||
if exists, err := e.Exists(); !exists {
|
||||
if err != nil {
|
||||
@@ -346,14 +362,19 @@ func (e *Environment) scanOutput(reader io.ReadCloser) {
|
||||
go e.followOutput()
|
||||
}
|
||||
|
||||
// Pulls the image from Docker. If there is an error while pulling the image from the source
|
||||
// but the image already exists locally, we will report that error to the logger but continue
|
||||
// with the process.
|
||||
type imagePullStatus struct {
|
||||
Status string `json:"status"`
|
||||
Progress string `json:"progress"`
|
||||
}
|
||||
|
||||
// Pulls the image from Docker. If there is an error while pulling the image
|
||||
// from the source but the image already exists locally, we will report that
|
||||
// error to the logger but continue with the process.
|
||||
//
|
||||
// The reasoning behind this is that Quay has had some serious outages as of late, and we don't
|
||||
// need to block all of the servers from booting just because of that. I'd imagine in a lot of
|
||||
// cases an outage shouldn't affect users too badly. It'll at least keep existing servers working
|
||||
// correctly if anything.
|
||||
// The reasoning behind this is that Quay has had some serious outages as of
|
||||
// late, and we don't need to block all of the servers from booting just because
|
||||
// of that. I'd imagine in a lot of cases an outage shouldn't affect users too
|
||||
// badly. It'll at least keep existing servers working correctly if anything.
|
||||
func (e *Environment) ensureImageExists(image string) error {
|
||||
e.Events().Publish(environment.DockerImagePullStarted, "")
|
||||
defer e.Events().Publish(environment.DockerImagePullCompleted, "")
|
||||
@@ -447,3 +468,34 @@ func (e *Environment) ensureImageExists(image string) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *Environment) convertMounts() []mount.Mount {
|
||||
var out []mount.Mount
|
||||
|
||||
for _, m := range e.Configuration.Mounts() {
|
||||
out = append(out, mount.Mount{
|
||||
Type: mount.TypeBind,
|
||||
Source: m.Source,
|
||||
Target: m.Target,
|
||||
ReadOnly: m.ReadOnly,
|
||||
})
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func (e *Environment) resources() container.Resources {
|
||||
l := e.Configuration.Limits()
|
||||
|
||||
return container.Resources{
|
||||
Memory: l.BoundedMemoryLimit(),
|
||||
MemoryReservation: l.MemoryLimit * 1_000_000,
|
||||
MemorySwap: l.ConvertedSwap(),
|
||||
CPUQuota: l.ConvertedCpuLimit(),
|
||||
CPUPeriod: 100_000,
|
||||
CPUShares: 1024,
|
||||
BlkioWeight: l.IoWeight,
|
||||
OomKillDisable: &l.OOMDisabled,
|
||||
CpusetCpus: l.Threads,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,11 @@ package docker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"emperror.dev/errors"
|
||||
"github.com/apex/log"
|
||||
"github.com/docker/docker/api/types"
|
||||
@@ -9,11 +14,6 @@ import (
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/pterodactyl/wings/environment"
|
||||
"github.com/pterodactyl/wings/remote"
|
||||
|
||||
"os"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Run before the container starts and get the process configuration from the Panel.
|
||||
@@ -174,7 +174,7 @@ func (e *Environment) Stop() error {
|
||||
e.SetState(environment.ProcessOfflineState)
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
return errors.Wrap(err, "environment/docker: cannot stop container")
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -208,7 +208,9 @@ func (e *Environment) WaitForStop(seconds uint, terminate bool) error {
|
||||
return ctxErr
|
||||
}
|
||||
case err := <-errChan:
|
||||
if err != nil {
|
||||
// If the error stems from the container not existing there is no point in wasting
|
||||
// CPU time to then try and terminate it.
|
||||
if err != nil && !client.IsErrNotFound(err) {
|
||||
if terminate {
|
||||
l := log.WithField("container_id", e.Id)
|
||||
if errors.Is(err, context.DeadlineExceeded) {
|
||||
@@ -219,8 +221,7 @@ func (e *Environment) WaitForStop(seconds uint, terminate bool) error {
|
||||
|
||||
return e.Terminate(os.Kill)
|
||||
}
|
||||
|
||||
return err
|
||||
return errors.WrapIf(err, "environment/docker: error waiting on container to enter \"not-running\" state")
|
||||
}
|
||||
case <-ok:
|
||||
}
|
||||
@@ -232,7 +233,12 @@ func (e *Environment) WaitForStop(seconds uint, terminate bool) error {
|
||||
func (e *Environment) Terminate(signal os.Signal) error {
|
||||
c, err := e.client.ContainerInspect(context.Background(), e.Id)
|
||||
if err != nil {
|
||||
return err
|
||||
// Treat missing containers as an okay error state, means it is obviously
|
||||
// already terminated at this point.
|
||||
if client.IsErrNotFound(err) {
|
||||
return nil
|
||||
}
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if !c.State.Running {
|
||||
@@ -249,13 +255,10 @@ func (e *Environment) Terminate(signal os.Signal) error {
|
||||
|
||||
// We set it to stopping than offline to prevent crash detection from being triggered.
|
||||
e.SetState(environment.ProcessStoppingState)
|
||||
|
||||
sig := strings.TrimSuffix(strings.TrimPrefix(signal.String(), "signal "), "ed")
|
||||
|
||||
if err := e.client.ContainerKill(context.Background(), e.Id, sig); err != nil && !client.IsErrNotFound(err) {
|
||||
return err
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
e.SetState(environment.ProcessOfflineState)
|
||||
|
||||
return nil
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"emperror.dev/errors"
|
||||
"encoding/json"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/pterodactyl/wings/environment"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type dockerLogLine struct {
|
||||
Log string `json:"log"`
|
||||
}
|
||||
|
||||
var ErrNotAttached = errors.New("not attached to instance")
|
||||
|
||||
func (e *Environment) setStream(s *types.HijackedResponse) {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
|
||||
e.stream = s
|
||||
}
|
||||
|
||||
// Sends the specified command to the stdin of the running container instance. There is no
|
||||
// confirmation that this data is sent successfully, only that it gets pushed into the stdin.
|
||||
func (e *Environment) SendCommand(c string) error {
|
||||
if !e.IsAttached() {
|
||||
return ErrNotAttached
|
||||
}
|
||||
|
||||
e.mu.RLock()
|
||||
defer e.mu.RUnlock()
|
||||
|
||||
// If the command being processed is the same as the process stop command then we want to mark
|
||||
// the server as entering the stopping state otherwise the process will stop and Wings will think
|
||||
// it has crashed and attempt to restart it.
|
||||
if e.meta.Stop.Type == "command" && c == e.meta.Stop.Value {
|
||||
e.SetState(environment.ProcessStoppingState)
|
||||
}
|
||||
|
||||
_, err := e.stream.Conn.Write([]byte(c + "\n"))
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// Reads the log file for the server. This does not care if the server is running or not, it will
|
||||
// simply try to read the last X bytes of the file and return them.
|
||||
func (e *Environment) Readlog(lines int) ([]string, error) {
|
||||
r, err := e.client.ContainerLogs(context.Background(), e.Id, types.ContainerLogsOptions{
|
||||
ShowStdout: true,
|
||||
ShowStderr: true,
|
||||
Tail: strconv.Itoa(lines),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer r.Close()
|
||||
|
||||
var out []string
|
||||
|
||||
scanner := bufio.NewScanner(r)
|
||||
for scanner.Scan() {
|
||||
out = append(out, scanner.Text())
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// Docker stores the logs for server output in a JSON format. This function will iterate over the JSON
|
||||
// that was read from the log file and parse it into a more human readable format.
|
||||
func (e *Environment) parseLogToStrings(b []byte) ([]string, error) {
|
||||
hasError := false
|
||||
var out []string
|
||||
|
||||
scanner := bufio.NewScanner(bytes.NewReader(b))
|
||||
for scanner.Scan() {
|
||||
var l dockerLogLine
|
||||
|
||||
// Unmarshal the contents and allow up to a single error before bailing out of the process. We
|
||||
// do this because if you're arbitrarily reading a length of the file you'll likely end up
|
||||
// with the first line in the output being improperly formatted JSON. In those cases we want to
|
||||
// just skip over it. However if we see another error we're going to bail out because that is an
|
||||
// abnormal situation.
|
||||
if err := json.Unmarshal([]byte(scanner.Text()), &l); err != nil {
|
||||
if hasError {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
hasError = true
|
||||
continue
|
||||
}
|
||||
|
||||
out = append(out, l.Log)
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
@@ -204,12 +204,6 @@ func deleteServer(c *gin.Context) {
|
||||
s.Events().Destroy()
|
||||
s.Websockets().CancelAll()
|
||||
|
||||
// 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 {
|
||||
s.Log().WithField("error", err).Warn("failed to delete server archive during deletion process")
|
||||
}
|
||||
|
||||
// Remove any pending remote file downloads for the server.
|
||||
for _, dl := range downloader.ByServer(s.Id()) {
|
||||
dl.Cancel()
|
||||
|
||||
@@ -29,6 +29,7 @@ import (
|
||||
"github.com/pterodactyl/wings/router/middleware"
|
||||
"github.com/pterodactyl/wings/router/tokens"
|
||||
"github.com/pterodactyl/wings/server"
|
||||
"github.com/pterodactyl/wings/server/filesystem"
|
||||
"github.com/pterodactyl/wings/system"
|
||||
)
|
||||
|
||||
@@ -51,6 +52,10 @@ type serverTransferRequest struct {
|
||||
Server json.RawMessage `json:"server"`
|
||||
}
|
||||
|
||||
func getArchivePath(sID string) string {
|
||||
return filepath.Join(config.Get().System.ArchiveDirectory, sID+".tar.gz")
|
||||
}
|
||||
|
||||
// Returns the archive for a server so that it can be transferred to a new node.
|
||||
func getServerArchive(c *gin.Context) {
|
||||
auth := strings.SplitN(c.GetHeader("Authorization"), " ", 2)
|
||||
@@ -77,36 +82,51 @@ func getServerArchive(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
st, err := s.Archiver.Stat()
|
||||
archivePath := getArchivePath(s.Id())
|
||||
|
||||
// Stat the archive file.
|
||||
st, err := os.Lstat(archivePath)
|
||||
if err != nil {
|
||||
if !errors.Is(err, os.ErrNotExist) {
|
||||
WithError(c, err)
|
||||
_ = WithError(c, err)
|
||||
return
|
||||
}
|
||||
c.AbortWithStatus(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
checksum, err := s.Archiver.Checksum()
|
||||
// Compute sha1 checksum.
|
||||
h := sha256.New()
|
||||
f, err := os.Open(archivePath)
|
||||
if err != nil {
|
||||
NewServerError(err, s).SetMessage("failed to calculate checksum").Abort(c)
|
||||
return
|
||||
}
|
||||
if _, err := io.Copy(h, bufio.NewReader(f)); err != nil {
|
||||
_ = f.Close()
|
||||
_ = WithError(c, err)
|
||||
return
|
||||
}
|
||||
if err := f.Close(); err != nil {
|
||||
_ = WithError(c, err)
|
||||
return
|
||||
}
|
||||
checksum := hex.EncodeToString(h.Sum(nil))
|
||||
|
||||
file, err := os.Open(s.Archiver.Path())
|
||||
// Stream the file to the client.
|
||||
f, err = os.Open(archivePath)
|
||||
if err != nil {
|
||||
WithError(c, err)
|
||||
_ = WithError(c, err)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
defer f.Close()
|
||||
|
||||
c.Header("X-Checksum", checksum)
|
||||
c.Header("X-Mime-Type", st.Mimetype)
|
||||
c.Header("X-Mime-Type", "application/tar+gzip")
|
||||
c.Header("Content-Length", strconv.Itoa(int(st.Size())))
|
||||
c.Header("Content-Disposition", "attachment; filename="+strconv.Quote(s.Archiver.Name()))
|
||||
c.Header("Content-Disposition", "attachment; filename="+strconv.Quote(s.Id()+".tar.gz"))
|
||||
c.Header("Content-Type", "application/octet-stream")
|
||||
|
||||
bufio.NewReader(file).WriteTo(c.Writer)
|
||||
_, _ = bufio.NewReader(f).WriteTo(c.Writer)
|
||||
}
|
||||
|
||||
func postServerArchive(c *gin.Context) {
|
||||
@@ -164,8 +184,13 @@ func postServerArchive(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Create an archive of the entire server's data directory.
|
||||
a := &filesystem.Archive{
|
||||
BasePath: s.Filesystem().Path(),
|
||||
}
|
||||
|
||||
// Attempt to get an archive of the server.
|
||||
if err := s.Archiver.Archive(); err != nil {
|
||||
if err := a.Create(getArchivePath(s.Id())); err != nil {
|
||||
sendTransferLog("An error occurred while archiving the server: " + err.Error())
|
||||
l.WithField("error", err).Error("failed to get transfer archive for server")
|
||||
return
|
||||
@@ -227,7 +252,7 @@ func (str serverTransferRequest) downloadArchive() (*http.Response, error) {
|
||||
|
||||
// Returns the path to the local archive on the system.
|
||||
func (str serverTransferRequest) path() string {
|
||||
return filepath.Join(config.Get().System.ArchiveDirectory, str.ServerID+".tar.gz")
|
||||
return getArchivePath(str.ServerID)
|
||||
}
|
||||
|
||||
// Creates the archive location on this machine by first checking that the required file
|
||||
@@ -260,17 +285,16 @@ func (str serverTransferRequest) removeArchivePath() {
|
||||
// expected value from the transfer request. The string value returned is the computed
|
||||
// checksum on the system.
|
||||
func (str serverTransferRequest) verifyChecksum(matches string) (bool, string, error) {
|
||||
file, err := os.Open(str.path())
|
||||
f, err := os.Open(str.path())
|
||||
if err != nil {
|
||||
return false, "", err
|
||||
}
|
||||
defer file.Close()
|
||||
hash := sha256.New()
|
||||
buf := make([]byte, 1024*4)
|
||||
if _, err := io.CopyBuffer(hash, file, buf); err != nil {
|
||||
defer f.Close()
|
||||
h := sha256.New()
|
||||
if _, err := io.Copy(h, bufio.NewReader(f)); err != nil {
|
||||
return false, "", err
|
||||
}
|
||||
checksum := hex.EncodeToString(hash.Sum(nil))
|
||||
checksum := hex.EncodeToString(h.Sum(nil))
|
||||
return checksum == matches, checksum, nil
|
||||
}
|
||||
|
||||
@@ -362,7 +386,7 @@ func postTransfer(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != 200 {
|
||||
if res.StatusCode != http.StatusOK {
|
||||
data.log().WithField("error", err).WithField("status", res.StatusCode).Error("unexpected error response from transfer endpoint")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1,120 +0,0 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"emperror.dev/errors"
|
||||
"github.com/mholt/archiver/v3"
|
||||
"github.com/pterodactyl/wings/config"
|
||||
"github.com/pterodactyl/wings/server/filesystem"
|
||||
)
|
||||
|
||||
// Archiver represents a Server Archiver.
|
||||
type Archiver struct {
|
||||
Server *Server
|
||||
}
|
||||
|
||||
// Path returns the path to the server's archive.
|
||||
func (a *Archiver) Path() string {
|
||||
return filepath.Join(config.Get().System.ArchiveDirectory, a.Name())
|
||||
}
|
||||
|
||||
// Name returns the name of the server's archive.
|
||||
func (a *Archiver) Name() string {
|
||||
return a.Server.Id() + ".tar.gz"
|
||||
}
|
||||
|
||||
// Exists returns a boolean based off if the archive exists.
|
||||
func (a *Archiver) Exists() bool {
|
||||
if _, err := os.Stat(a.Path()); os.IsNotExist(err) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Stat stats the archive file.
|
||||
func (a *Archiver) Stat() (*filesystem.Stat, error) {
|
||||
s, err := os.Stat(a.Path())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &filesystem.Stat{
|
||||
FileInfo: s,
|
||||
Mimetype: "application/tar+gzip",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Archive creates an archive of the server and deletes the previous one.
|
||||
func (a *Archiver) Archive() error {
|
||||
path := a.Server.Filesystem().Path()
|
||||
|
||||
// Get the list of root files and directories to archive.
|
||||
var files []string
|
||||
fileInfo, err := ioutil.ReadDir(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, file := range fileInfo {
|
||||
f := filepath.Join(path, file.Name())
|
||||
// If the file is a symlink we cannot safely assume that the result of a filepath.Join() will be
|
||||
// a safe destination. We need to check if the file is a symlink, and if so pass off to the SafePath
|
||||
// function to resolve it to the final destination.
|
||||
//
|
||||
// ioutil.ReadDir() calls Lstat, so this will work correctly. If it did not call Lstat, but rather
|
||||
// just did a normal Stat call, this would fail since that would be looking at the symlink destination
|
||||
// and not the actual file in this listing.
|
||||
if file.Mode()&os.ModeSymlink != 0 {
|
||||
f, err = a.Server.Filesystem().SafePath(filepath.Join(path, file.Name()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
files = append(files, f)
|
||||
}
|
||||
|
||||
if err := a.DeleteIfExists(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return archiver.NewTarGz().Archive(files, a.Path())
|
||||
}
|
||||
|
||||
// DeleteIfExists deletes the archive if it exists.
|
||||
func (a *Archiver) DeleteIfExists() error {
|
||||
if _, err := a.Stat(); err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
return errors.WithMessage(os.Remove(a.Path()), "archiver: failed to delete archive from system")
|
||||
}
|
||||
|
||||
// Checksum computes a SHA256 checksum of the server's archive.
|
||||
func (a *Archiver) Checksum() (string, error) {
|
||||
file, err := os.Open(a.Path())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
hash := sha256.New()
|
||||
|
||||
buf := make([]byte, 1024*4)
|
||||
if _, err := io.CopyBuffer(hash, file, buf); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return hex.EncodeToString(hash.Sum(nil)), nil
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package backup
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/pterodactyl/wings/server/filesystem"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
@@ -19,7 +20,7 @@ var _ BackupInterface = (*LocalBackup)(nil)
|
||||
func NewLocal(client remote.Client, uuid string, ignore string) *LocalBackup {
|
||||
return &LocalBackup{
|
||||
Backup{
|
||||
client: client,
|
||||
client: client,
|
||||
Uuid: uuid,
|
||||
Ignore: ignore,
|
||||
adapter: LocalBackupAdapter,
|
||||
@@ -56,7 +57,7 @@ func (b *LocalBackup) WithLogContext(c map[string]interface{}) {
|
||||
// Generate generates a backup of the selected files and pushes it to the
|
||||
// defined location for this instance.
|
||||
func (b *LocalBackup) Generate(basePath, ignore string) (*ArchiveDetails, error) {
|
||||
a := &Archive{
|
||||
a := &filesystem.Archive{
|
||||
BasePath: basePath,
|
||||
Ignore: ignore,
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/pterodactyl/wings/server/filesystem"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
@@ -47,7 +48,7 @@ func (s *S3Backup) WithLogContext(c map[string]interface{}) {
|
||||
func (s *S3Backup) Generate(basePath, ignore string) (*ArchiveDetails, error) {
|
||||
defer s.Remove()
|
||||
|
||||
a := &Archive{
|
||||
a := &filesystem.Archive{
|
||||
BasePath: basePath,
|
||||
Ignore: ignore,
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package backup
|
||||
package filesystem
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/mholt/archiver/v3"
|
||||
"github.com/pterodactyl/wings/server/backup"
|
||||
"github.com/pterodactyl/wings/system"
|
||||
)
|
||||
|
||||
@@ -39,7 +38,7 @@ func (fs *Filesystem) CompressFiles(dir string, paths []string) (os.FileInfo, er
|
||||
return nil, err
|
||||
}
|
||||
|
||||
a := &backup.Archive{BasePath: cleanedRootDir, Files: cleaned}
|
||||
a := &Archive{BasePath: cleanedRootDir, Files: cleaned}
|
||||
d := path.Join(
|
||||
cleanedRootDir,
|
||||
fmt.Sprintf("archive-%s.tar.gz", strings.ReplaceAll(time.Now().Format(time.RFC3339), ":", "")),
|
||||
@@ -144,4 +143,3 @@ func (fs *Filesystem) DecompressFile(dir string, file string) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -175,7 +175,6 @@ func (m *Manager) InitServer(data remote.ServerConfigurationResponse) (*Server,
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.Archiver = Archiver{Server: s}
|
||||
s.fs = filesystem.New(filepath.Join(config.Get().System.Data, s.Id()), s.DiskSpace(), s.Config().Egg.FileDenylist)
|
||||
|
||||
// Right now we only support a Docker based environment, so I'm going to hard code
|
||||
|
||||
@@ -42,7 +42,6 @@ type Server struct {
|
||||
crasher CrashHandler
|
||||
|
||||
resources ResourceUsage
|
||||
Archiver Archiver `json:"-"`
|
||||
Environment environment.ProcessEnvironment `json:"-"`
|
||||
|
||||
fs *filesystem.Filesystem
|
||||
|
||||
Reference in New Issue
Block a user