Compare commits

..

17 Commits

Author SHA1 Message Date
Pterodactyl CI
1040b7d8ef bump version for release 2022-11-22 20:54:03 +00:00
Matthew Penner
105f0150f6 Update CHANGELOG.md 2022-11-22 13:49:10 -07:00
Matthew Penner
aeec51632e Update README.md 2022-11-22 13:48:56 -07:00
Dane Everitt
ff50d0e5bd Cleanup request error handling; properly handle os.ErrNotExist errors (#150) 2022-11-22 11:18:27 -07:00
Matthew Penner
9226ccae31 system: more detailed system information 2022-11-21 16:01:14 -07:00
Matthew Penner
2fd0edbff9 environment(docker): fix timeout when sending a stop signal
Previously, Docker would terminate the container when it's stop
configuration was configured to send a signal to the container.
This was due to Docker's API wanting the value as a duration string
(`1s`) rather than a number, so our value of `-1` was being formatted
to `0s` rather than `-1s` like we needed.

Closes https://github.com/pterodactyl/panel/issues/4555
2022-11-21 15:06:38 -07:00
Matthew Penner
1457470fff environment(docker): fix outgoing ip not changing
Fixes an issue where when a server has it's primary allocation
changed, the outgoing ip is not updated on the Docker network.

The only downside of this change is old networks are not cleaned up.

Closes https://github.com/pterodactyl/panel/issues/4547
2022-11-21 14:59:53 -07:00
Matthew Penner
da94f750ad server(install): update installation status request
The new request includes a `reinstall` option to denote
whether the installation failed for the first time, or
during a reinstall.

ref https://github.com/pterodactyl/panel/issues/1994
2022-11-21 14:57:44 -07:00
Matthew Penner
51cb6dfa42 ci(release): fix wrong directory name 2022-11-14 20:33:06 -07:00
Matthew Penner
47f94b8358 Update CHANGELOG.md 2022-11-14 20:26:20 -07:00
Matthew Penner
57e7eb714c Streaming Transfers (#153) 2022-11-14 18:25:01 -07:00
Matthew Penner
4781eeaedc ci: fix arm64 failures 2022-11-06 14:39:02 -07:00
Matthew Penner
a5a8200eb8 ci: cleanup 2022-11-06 13:55:49 -07:00
Matthew Penner
eb4df39d14 server(filesystem): fix inaccurate archive progress (#145) 2022-11-06 13:38:30 -07:00
Matthew Penner
3337362955 environment(docker): fix podman compatibility (#151) 2022-11-06 13:33:01 -07:00
Matthew Penner
f577f5521f server(filesystem): fix panic with archive Progress 2022-11-04 11:24:19 -06:00
Cubxity
5a760a0dcc Add customizable container labels (#146) 2022-10-16 17:17:27 -06:00
70 changed files with 2014 additions and 2656 deletions

1
.github/FUNDING.yaml vendored Normal file
View File

@@ -0,0 +1 @@
github: [matthewpi]

1
.github/FUNDING.yml vendored
View File

@@ -1 +0,0 @@
github: [ matthewpi ]

View File

@@ -1,4 +1,5 @@
name: CodeQL
on:
push:
branches:
@@ -7,24 +8,35 @@ on:
branches:
- develop
schedule:
- cron: '0 9 * * 4'
- cron: "0 9 * * 4"
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'go' ]
language:
- go
steps:
- uses: actions/checkout@v2
- name: Code Checkout
uses: actions/checkout@v3
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
- uses: github/codeql-action/autobuild@v1
- uses: github/codeql-action/analyze@v1
- name: Autobuild
uses: github/codeql-action/autobuild@v2
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2

View File

@@ -1,58 +1,72 @@
name: Publish Docker Image
name: Docker
on:
push:
branches:
- develop
tags:
- 'v*'
- "v*"
jobs:
push:
name: Push
build-and-push:
name: Build and Push
runs-on: ubuntu-20.04
# Always run against a tag, even if the commit into the tag has [docker skip] within the commit message.
if: "!contains(github.ref, 'develop') || (!contains(github.event.head_commit.message, 'skip docker') && !contains(github.event.head_commit.message, 'docker skip'))"
steps:
- name: Code Checkout
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Docker Meta
id: docker_meta
uses: crazy-max/ghaction-docker-meta@v1
with:
images: ghcr.io/pterodactyl/wings
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
uses: docker/setup-qemu-action@v2
- name: Install buildx
uses: docker/setup-buildx-action@v1
with:
version: v0.5.1
uses: docker/setup-buildx-action@v2
- name: Login to GitHub Container Registry
uses: docker/login-action@v1
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.REGISTRY_TOKEN }}
- name: Get Build Information
id: build_info
run: |
echo "::set-output name=version_tag::${GITHUB_REF/refs\/tags\/v/}"
echo "::set-output name=short_sha::$(git rev-parse --short HEAD)"
- name: Release Production Build
- name: Build and push (latest)
uses: docker/build-push-action@v2
if: "!contains(github.ref, 'develop')"
with:
context: .
file: ./Dockerfile
push: true
platforms: linux/amd64,linux/arm64
build-args: |
VERSION=${{ steps.build_info.outputs.version_tag }}
labels: ${{ steps.docker_meta.outputs.labels }}
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.docker_meta.outputs.tags }}
- name: Release Development Build
- name: Build and push (develop)
uses: docker/build-push-action@v2
if: "contains(github.ref, 'develop')"
with:
context: .
file: ./Dockerfile
push: ${{ github.event_name != 'pull_request' }}
platforms: linux/amd64,linux/arm64
build-args: |
VERSION=dev-${{ steps.build_info.outputs.short_sha }}
labels: ${{ steps.docker_meta.outputs.labels }}
platforms: linux/amd64,linux/arm64
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.docker_meta.outputs.tags }}
cache-from: type=gha
cache-to: type=gha,mode=max

View File

@@ -1,4 +1,5 @@
name: Run Tests
name: Push
on:
push:
branches:
@@ -6,24 +7,29 @@ on:
pull_request:
branches:
- develop
jobs:
build:
build-and-test:
name: Build and Test
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ ubuntu-20.04 ]
go: [ '1.18.7' ]
goos: [ linux ]
goarch: [ amd64, arm64 ]
runs-on: ${{ matrix.os }}
os: [ubuntu-20.04]
go: ["1.18.8", "1.19.3"]
goos: [linux]
goarch: [amd64, arm64]
steps:
- name: Code Checkout
uses: actions/checkout@v2
- name: Setup Go v${{ matrix.go }}
uses: actions/setup-go@v2
- name: Setup Go
uses: actions/setup-go@v3
with:
go-version: ${{ matrix.go }}
- name: Print Environment
- name: Code Checkout
uses: actions/checkout@v3
- name: Gather environment variables
id: env
run: |
printf "Go Executable Path: $(which go)\n"
@@ -33,22 +39,27 @@ jobs:
printf "\n\nSystem Environment:\n\n"
env
printf "Git Version: $(git version)\n\n"
echo "::set-output name=version_tag::${GITHUB_REF/refs\/tags\//}"
echo "::set-output name=short_sha::$(git rev-parse --short HEAD)"
echo "::set-output name=go_cache::$(go env GOCACHE)"
echo "::set-output name=go_mod_cache::$(go env GOMODCACHE)"
echo "version_tag=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_OUTPUT
echo "short_sha=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
echo "go_cache=$(go env GOCACHE)" >> $GITHUB_OUTPUT
echo "go_mod_cache=$(go env GOMODCACHE)" >> $GITHUB_OUTPUT
- name: Build Cache
uses: actions/cache@v2
uses: actions/cache@v3
with:
key: ${{ runner.os }}-go${{ matrix.go }}-${{ hashFiles('**/go.sum') }}
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go${{ matrix.go }}-
${{ runner.os }}-go-
path: |
${{ steps.env.outputs.go_cache }}
${{ steps.env.outputs.go_mod_cache }}
- name: Get Dependencies
- name: go mod download
env:
CGO_ENABLED: 0
run: |
go get -v -t -d ./...
go mod download
- name: Build
env:
GOOS: ${{ matrix.goos }}
@@ -56,20 +67,34 @@ jobs:
CGO_ENABLED: 0
SRC_PATH: github.com/pterodactyl/wings
run: |
go build -v -trimpath -ldflags="-s -w -X ${SRC_PATH}/system.Version=dev-${GIT_COMMIT:0:7}" -o build/wings_${GOOS}_${GOARCH} wings.go
go build -v -trimpath -ldflags="-X ${SRC_PATH}/system.Version=dev-${GIT_COMMIT:0:7}" -o build/wings_${GOOS}_${GOARCH}_debug wings.go
chmod +x build/*
- name: Tests
run: go test -race ./...
go build -v -trimpath -ldflags="-s -w -X ${SRC_PATH}/system.Version=dev-${GIT_COMMIT:0:7}" -o dist/wings ${SRC_PATH}
go build -v -trimpath -ldflags="-X ${SRC_PATH}/system.Version=dev-${GIT_COMMIT:0:7}" -o dist/wings_debug ${SRC_PATH}
chmod 755 dist/*
- name: go test
if: ${{ matrix.goarch == 'amd64' }}
env:
CGO_ENABLED: 0
run: |
go test $(go list ./...)
- name: go test -race
if: ${{ matrix.goarch == 'amd64' }}
env:
CGO_ENABLED: 1
run: |
go test -race $(go list ./...)
- name: Upload Release Artifact
uses: actions/upload-artifact@v2
if: ${{ github.ref == 'refs/heads/develop' || github.event_name == 'pull_request' }}
with:
name: wings_linux_${{ matrix.goarch }}
path: build/wings_linux_${{ matrix.goarch }}
path: dist/wings
- name: Upload Debug Artifact
uses: actions/upload-artifact@v2
if: ${{ github.ref == 'refs/heads/develop' || github.event_name == 'pull_request' }}
with:
name: wings_linux_${{ matrix.goarch }}_debug
path: build/wings_linux_${{ matrix.goarch }}_debug
path: dist/wings_debug

View File

@@ -1,41 +1,48 @@
name: Create Release
name: Release
on:
push:
tags:
- 'v*'
- "v*"
jobs:
release:
name: Release
runs-on: ubuntu-20.04
steps:
- name: Code Checkout
uses: actions/checkout@v2
- uses: actions/setup-go@v2
uses: actions/checkout@v3
- name: Setup Go
uses: actions/setup-go@v3
with:
go-version: '1.18.7'
- name: Build
go-version: "1.18.8"
- name: Build release binaries
env:
CGO_ENABLED: 0
REF: ${{ github.ref }}
run: |
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w -X github.com/pterodactyl/wings/system.Version=${REF:11}" -o build/wings_linux_amd64 -v wings.go
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags="-s -w -X github.com/pterodactyl/wings/system.Version=${REF:11}" -o build/wings_linux_arm64 -v wings.go
- name: Test
run: go test ./...
- name: Compress binary and make it executable
run: |
chmod +x build/wings_linux_amd64
chmod +x build/wings_linux_arm64
GOARCH=amd64 go build -o dist/wings_linux_amd64 -v -trimpath -ldflags="-s -w -X github.com/pterodactyl/wings/system.Version=${REF:11}" github.com/pterodactyl/wings
chmod 755 dist/wings_linux_amd64
GOARCH=arm64 go build -o dist/wings_linux_arm64 -v -trimpath -ldflags="-s -w -X github.com/pterodactyl/wings/system.Version=${REF:11}" github.com/pterodactyl/wings
chmod 755 dist/wings_linux_arm64
- name: Extract changelog
env:
REF: ${{ github.ref }}
run: |
sed -n "/^## ${REF:10}/,/^## /{/^## /b;p}" CHANGELOG.md > ./RELEASE_CHANGELOG
echo ::set-output name=version_name::`sed -nr "s/^## (${REF:10} .*)$/\1/p" CHANGELOG.md`
echo "version_name=`sed -nr "s/^## (${REF:10} .*)$/\1/p" CHANGELOG.md`" > $GITHUB_OUTPUT
- name: Create checksum and add to changelog
run: |
SUM=`cd build && sha256sum wings_linux_amd64`
SUM2=`cd build && sha256sum wings_linux_arm64`
SUM=`cd dist && sha256sum wings_linux_amd64`
SUM2=`cd dist && sha256sum wings_linux_arm64`
echo -e "\n#### SHA256 Checksum\n\`\`\`\n$SUM\n$SUM2\n\`\`\`\n" >> ./RELEASE_CHANGELOG
echo -e "$SUM\n$SUM2" > checksums.txt
- name: Create release branch
env:
REF: ${{ github.ref }}
@@ -49,7 +56,8 @@ jobs:
git add system/const.go
git commit -m "bump version for release"
git push
- name: Create Release
- name: Create release
id: create_release
uses: actions/create-release@v1
env:
@@ -60,24 +68,27 @@ jobs:
body_path: ./RELEASE_CHANGELOG
draft: true
prerelease: ${{ contains(github.ref, 'beta') || contains(github.ref, 'alpha') }}
- name: Upload amd64 Binary
- name: Upload amd64 binary
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: build/wings_linux_amd64
asset_path: dist/wings_linux_amd64
asset_name: wings_linux_amd64
asset_content_type: application/octet-stream
- name: Upload arm64 Binary
- name: Upload arm64 binary
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: build/wings_linux_arm64
asset_path: dist/wings_linux_arm64
asset_name: wings_linux_arm64
asset_content_type: application/octet-stream
- name: Upload checksum
uses: actions/upload-release-asset@v1
env:

2
.gitignore vendored
View File

@@ -23,6 +23,8 @@
# ignore configuration file
/config.yml
/config*.yml
/config.yaml
/config*.yaml
# Ignore Vagrant stuff
/.vagrant

View File

@@ -1,5 +1,28 @@
# Changelog
## v1.11.0-rc.2
### Added
* More detailed information returned by the `/api/system` endpoint when using the `?v=2` query parameter.
### Changed
* Send reinstallation status separately from installation status.
### Fixed
* Fixed servers outgoing IP not being updated whenever a server's primary allocation is changed when using the Force Outgoing IP option.
* Fixed servers being terminated rather than gracefully stopped when a signal is used to stop the container rather than a command.
* Fixed file not found errors being treated as an internal error, they are now treated as a 404.
## v1.11.0-rc.1
### Changed
* Wings release versions will now follow the major and minor version of the panel.
* Transfers no longer buffer to disk, instead they are fully streamed with only a small amount of memory used for buffering.
* Release binaries are no longer compressed with UPX.
### Fixed
* Wings can be run with podman instead of Docker, this is still experimental and not recommended for production use.
* Archive progress is now reported correctly.
* Labels for containers can now be set by the Panel.
## v1.7.2
### Fixed
* The S3 backup driver now supports Cloudflare R2

View File

@@ -4,9 +4,6 @@ build:
GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -gcflags "all=-trimpath=$(pwd)" -o build/wings_linux_amd64 -v wings.go
GOOS=linux GOARCH=arm64 go build -ldflags="-s -w" -gcflags "all=-trimpath=$(pwd)" -o build/wings_linux_arm64 -v wings.go
race:
go build -ldflags="-X github.com/pterodactyl/wings/system.Version=$(GIT_HEAD)" -race
debug:
go build -ldflags="-X github.com/pterodactyl/wings/system.Version=$(GIT_HEAD)"
sudo ./wings --debug --ignore-certificate-errors --config config.yml --pprof --pprof-block-rate 1

View File

@@ -5,6 +5,7 @@
[![Go Report Card](https://goreportcard.com/badge/github.com/pterodactyl/wings)](https://goreportcard.com/report/github.com/pterodactyl/wings)
# Pterodactyl Wings
Wings is Pterodactyl's server control plane, built for the rapidly changing gaming industry and designed to be
highly performant and secure. Wings provides an HTTP API allowing you to interface directly with running server
instances, fetch server logs, generate backups, and control all aspects of the server lifecycle.
@@ -13,31 +14,32 @@ In addition, Wings ships with a built-in SFTP server allowing your system to rem
dependencies, and allowing users to authenticate with the same credentials they would normally use to access the Panel.
## Sponsors
I would like to extend my sincere thanks to the following sponsors for helping find Pterodactyl's developement.
[Interested in becoming a sponsor?](https://github.com/sponsors/matthewpi)
| Company | About |
| ------- | ----- |
|-----------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [**WISP**](https://wisp.gg) | Extra features. |
| [**BisectHosting**](https://www.bisecthosting.com/) | BisectHosting provides Minecraft, Valheim and other server hosting services with the highest reliability and lightning fast support since 2012. |
| [**Fragnet**](https://fragnet.net) | Providing low latency, high-end game hosting solutions to gamers, game studios and eSports platforms. |
| [**Tempest**](https://tempest.net/) | Tempest Hosting is a subsidiary of Path Network, Inc. offering unmetered DDoS protected 10Gbps dedicated servers, starting at just $80/month. Full anycast, tons of filters. |
| [**Bloom.host**](https://bloom.host) | Bloom.host offers dedicated core VPS and Minecraft hosting with Ryzen 9 processors. With owned-hardware, we offer truly unbeatable prices on high-performance hosting. |
| [**RocketNode**](https://rocketnode.com/) | Innovative game server hosting combined with a straightforward control panel, affordable prices, and Rocket-Fast support. |
| [**Aussie Server Hosts**](https://aussieserverhosts.com/) | No frills Australian Owned and operated High Performance Server hosting for some of the most demanding games serving Australia and New Zealand. |
| [**BisectHosting**](https://www.bisecthosting.com/) | BisectHosting provides Minecraft, Valheim and other server hosting services with the highest reliability and lightning fast support since 2012. |
| [**MineStrator**](https://minestrator.com/) | Looking for the most highend French hosting company for your minecraft server? More than 24,000 members on our discord trust us. Give us a try! |
| [**Skynode**](https://www.skynode.pro/) | Skynode provides blazing fast game servers along with a top-notch user experience. Whatever our clients are looking for, we're able to provide it! |
| [**DeinServerHost**](https://deinserverhost.de/) | DeinServerHost offers Dedicated, vps and Gameservers for many popular Games like Minecraft and Rust in Germany since 2013. |
| [**Aussie Server Hosts**](https://aussieserverhosts.com/) | No frills Australian Owned and operated High Performance Server hosting for some of the most demanding games serving Australia and New Zealand. |
| [**HostEZ**](https://hostez.io) | Providing North America Valheim, Minecraft and other popular games with low latency, high uptime and maximum availability. EZ! |
| [**VibeGAMES**](https://vibegames.net/) | VibeGAMES is a game server provider that specializes in DDOS protection for the games we offer. We have multiple locations in the US, Brazil, France, Germany, Singapore, Australia and South Africa.|
| [**Gamenodes**](https://gamenodes.nl) | Gamenodes love quality. For Minecraft, Discord Bots and other services, among others. With our own programmers, we provide just that little bit of extra service! |
| [**VibeGAMES**](https://vibegames.net/) | VibeGAMES is a game server provider that specializes in DDOS protection for the games we offer. We have multiple locations in the US, Brazil, France, Germany, Singapore, Australia and South Africa. |
| [**Pterodactyl Market**](https://pterodactylmarket.com/) | Pterodactyl Market is a one-and-stop shop for Pterodactyl. In our market, you can find Add-ons, Themes, Eggs, and more for Pterodactyl. |
| [**UltraServers**](https://ultraservers.com/) | Deploy premium games hosting with the click of a button. Manage and swap games with ease and let us take care of the rest. We currently support Minecraft, Rust, ARK, 7 Days to Die, Garys MOD, CS:GO, Satisfactory and others. |
## Documentation
* [Panel Documentation](https://pterodactyl.io/panel/1.0/getting_started.html)
* [Wings Documentation](https://pterodactyl.io/wings/1.0/installing.html)
* [Community Guides](https://pterodactyl.io/community/about.html)
* Or, get additional help [via Discord](https://discord.gg/pterodactyl)
## Reporting Issues
Please use the [pterodactyl/panel](https://github.com/pterodactyl/panel) repository to report any issues or make
feature requests for Wings. In addition, the [security policy](https://github.com/pterodactyl/panel/security/policy) listed
within that repository also applies to Wings.

View File

@@ -65,7 +65,7 @@ func newDiagnosticsCommand() *cobra.Command {
// - the docker debug output
// - running docker containers
// - logs
func diagnosticsCmdRun(cmd *cobra.Command, args []string) {
func diagnosticsCmdRun(*cobra.Command, []string) {
questions := []*survey.Question{
{
Name: "IncludeEndpoints",

View File

@@ -1,127 +0,0 @@
package cmd
import (
"context"
"os"
"os/exec"
"strings"
"emperror.dev/errors"
"github.com/apex/log"
"github.com/pterodactyl/wings/config"
"github.com/pterodactyl/wings/internal/vhd"
"github.com/pterodactyl/wings/loggers/cli"
"github.com/pterodactyl/wings/remote"
"github.com/pterodactyl/wings/server"
"github.com/spf13/cobra"
)
type MigrateVHDCommand struct {
manager *server.Manager
}
func newMigrateVHDCommand() *cobra.Command {
return &cobra.Command{
Use: "migrate-vhd",
Short: "migrates existing data from a directory tree into virtual hard-disks",
PreRun: func(cmd *cobra.Command, args []string) {
log.SetLevel(log.DebugLevel)
log.SetHandler(cli.Default)
},
Run: func(cmd *cobra.Command, args []string) {
client := remote.NewFromConfig(config.Get())
manager, err := server.NewManager(cmd.Context(), client, true)
if err != nil {
log.WithField("error", err).Fatal("failed to create new server manager")
}
c := &MigrateVHDCommand{
manager: manager,
}
if err := c.Run(cmd.Context()); err != nil {
log.WithField("error", err).Fatal("failed to execute command")
}
},
}
}
// Run executes the migration command.
func (m *MigrateVHDCommand) Run(ctx context.Context) error {
if !vhd.Enabled() {
return errors.New("cannot migrate to vhd: the underlying driver must be set to \"vhd\"")
}
for _, s := range m.manager.All() {
s.Log().Debug("starting migration of server contents to virtual disk...")
v := vhd.New(s.DiskSpace(), vhd.DiskPath(s.ID()), s.Filesystem().Path())
s.Log().WithField("disk_image", v.Path()).Info("creating virtual disk for server")
if err := v.Allocate(ctx); err != nil {
return errors.WithStackIf(err)
}
s.Log().Info("creating virtual filesystem for server")
if err := v.MakeFilesystem(ctx); err != nil {
// If the filesystem already exists no worries, just move on with our
// day here.
if !errors.Is(err, vhd.ErrFilesystemExists) {
return errors.WithStack(err)
}
}
bak := strings.TrimSuffix(s.Filesystem().Path(), "/") + "_bak"
mounted, err := v.IsMounted(ctx)
if err != nil {
return err
} else if !mounted {
s.Log().WithField("backup_dir", bak).Debug("virtual disk is not yet mounted, creating backup directory")
// Create a backup directory of the server files if one does not already exist
// at that location. If one does exists we'll just assume it is good to go and
// rely on it to provide the files we'll need.
if _, err := os.Lstat(bak); os.IsNotExist(err) {
if err := os.Rename(s.Filesystem().Path(), bak); err != nil {
return errors.Wrap(err, "failed to rename existing data directory for backup")
}
} else if err != nil {
return errors.WithStack(err)
}
if err := os.RemoveAll(s.Filesystem().Path()); err != nil && !os.IsNotExist(err) {
return errors.Wrap(err, "failed to remove base server files path")
}
} else {
s.Log().Warn("server appears to already have existing mount, not creating data backup")
}
// Attempt to mount the disk at the expected path now that we've created
// a backup of the server files.
if err := v.Mount(ctx); err != nil && !errors.Is(err, vhd.ErrFilesystemMounted) {
return errors.WithStackIf(err)
}
// Copy over the files from the backup for this server but only
// if we have a backup directory currently.
_, err = os.Lstat(bak)
if err != nil {
if !os.IsNotExist(err) {
s.Log().WithField("error", err).Warn("failed to stat backup directory")
} else {
s.Log().Info("no backup data directory exists, not restoring files")
}
} else {
cmd := exec.CommandContext(ctx, "cp", "-r", bak+"/.", s.Filesystem().Path())
if err := cmd.Run(); err != nil {
return errors.Wrap(err, "migrate: failed to move old server files into new direcotry")
} else {
if err := os.RemoveAll(bak); err != nil {
s.Log().WithField("directory", bak).WithField("error", err).Warn("failed to remove backup directory")
}
}
}
s.Log().Info("updating server file ownership...")
if err := s.Filesystem().Chown("/"); err != nil {
s.Log().WithField("error", err).Warn("failed to update ownership of new server files")
}
s.Log().Info("finished migration to virtual disk...")
}
return nil
}

View File

@@ -16,9 +16,6 @@ import (
"strings"
"time"
"github.com/pterodactyl/wings/internal/cron"
"github.com/pterodactyl/wings/internal/database"
"github.com/NYTimes/logrotate"
"github.com/apex/log"
"github.com/apex/log/handlers/multi"
@@ -31,6 +28,8 @@ import (
"github.com/pterodactyl/wings/config"
"github.com/pterodactyl/wings/environment"
"github.com/pterodactyl/wings/internal/cron"
"github.com/pterodactyl/wings/internal/database"
"github.com/pterodactyl/wings/loggers/cli"
"github.com/pterodactyl/wings/remote"
"github.com/pterodactyl/wings/router"
@@ -47,16 +46,8 @@ var (
var rootCommand = &cobra.Command{
Use: "wings",
Short: "Runs the API server allowing programmatic control of game servers for Pterodactyl Panel.",
PersistentPreRun: func(cmd *cobra.Command, args []string) {
initConfig()
if ok, _ := cmd.Flags().GetBool("ignore-certificate-errors"); ok {
log.Warn("running with --ignore-certificate-errors: TLS certificate host chains and name will not be verified")
http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{
InsecureSkipVerify: true,
}
}
},
PreRun: func(cmd *cobra.Command, args []string) {
initConfig()
initLogging()
if tls, _ := cmd.Flags().GetBool("auto-tls"); tls {
if host, _ := cmd.Flags().GetString("tls-hostname"); host == "" {
@@ -85,7 +76,6 @@ func Execute() {
func init() {
rootCommand.PersistentFlags().StringVar(&configPath, "config", config.DefaultLocation, "set the location for the configuration file")
rootCommand.PersistentFlags().BoolVar(&debug, "debug", false, "pass in order to run wings in debug mode")
rootCommand.PersistentFlags().Bool("ignore-certificate-errors", false, "ignore certificate verification errors when executing API calls")
// Flags specifically used when running the API.
rootCommand.Flags().Bool("pprof", false, "if the pprof profiler should be enabled. The profiler will bind to localhost:6060 by default")
@@ -93,11 +83,11 @@ func init() {
rootCommand.Flags().Int("pprof-port", 6060, "If provided with --pprof, the port it will run on")
rootCommand.Flags().Bool("auto-tls", false, "pass in order to have wings generate and manage its own SSL certificates using Let's Encrypt")
rootCommand.Flags().String("tls-hostname", "", "required with --auto-tls, the FQDN for the generated SSL certificate")
rootCommand.Flags().Bool("ignore-certificate-errors", false, "ignore certificate verification errors when executing API calls")
rootCommand.AddCommand(versionCommand)
rootCommand.AddCommand(configureCmd)
rootCommand.AddCommand(newDiagnosticsCommand())
rootCommand.AddCommand(newMigrateVHDCommand())
}
func rootCmdRun(cmd *cobra.Command, _ []string) {
@@ -105,6 +95,13 @@ func rootCmdRun(cmd *cobra.Command, _ []string) {
log.Debug("running in debug mode")
log.WithField("config_file", configPath).Info("loading configuration from file")
if ok, _ := cmd.Flags().GetBool("ignore-certificate-errors"); ok {
log.Warn("running with --ignore-certificate-errors: TLS certificate host chains and name will not be verified")
http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{
InsecureSkipVerify: true,
}
}
if err := config.ConfigureTimezone(); err != nil {
log.WithField("error", err).Fatal("failed to detect system timezone or use supplied configuration value")
}
@@ -113,7 +110,6 @@ func rootCmdRun(cmd *cobra.Command, _ []string) {
log.WithField("error", err).Fatal("failed to configure system directories for pterodactyl")
return
}
log.WithField("username", config.Get().System.User).Info("checking for pterodactyl system user")
if err := config.EnsurePterodactylUser(); err != nil {
log.WithField("error", err).Fatal("failed to create pterodactyl system user")
}
@@ -139,7 +135,7 @@ func rootCmdRun(cmd *cobra.Command, _ []string) {
log.WithField("error", err).Fatal("failed to initialize database")
}
manager, err := server.NewManager(cmd.Context(), pclient, false)
manager, err := server.NewManager(cmd.Context(), pclient)
if err != nil {
log.WithField("error", err).Fatal("failed to load server configurations")
}
@@ -366,7 +362,7 @@ func rootCmdRun(cmd *cobra.Command, _ []string) {
return
}
// Check if main http server should run with TLS. Otherwise reset the TLS
// Check if main http server should run with TLS. Otherwise, reset the TLS
// config on the server and then serve it over normal HTTP.
if api.Ssl.Enabled {
if err := s.ListenAndServeTLS(api.Ssl.CertificateFile, api.Ssl.KeyFile); err != nil {

View File

@@ -16,8 +16,8 @@ import (
"time"
"emperror.dev/errors"
"github.com/acobaugh/osrelease"
"github.com/apex/log"
"github.com/cobaugh/osrelease"
"github.com/creasty/defaults"
"github.com/gbrlsnchs/jwt/v3"
"gopkg.in/yaml.v2"
@@ -152,9 +152,23 @@ type SystemConfiguration struct {
// Definitions for the user that gets created to ensure that we can quickly access
// this information without constantly having to do a system lookup.
User struct {
Uid int
Gid int
}
// Rootless controls settings related to rootless container daemons.
Rootless struct {
// Enabled controls whether rootless containers are enabled.
Enabled bool `yaml:"enabled" default:"false"`
// ContainerUID controls the UID of the user inside the container.
// This should likely be set to 0 so the container runs as the user
// running Wings.
ContainerUID int `yaml:"container_uid" default:"0"`
// ContainerGID controls the GID of the user inside the container.
// This should likely be set to 0 so the container runs as the user
// running Wings.
ContainerGID int `yaml:"container_gid" default:"0"`
} `yaml:"rootless"`
Uid int `yaml:"uid"`
Gid int `yaml:"gid"`
} `yaml:"user"`
// The amount of time in seconds that can elapse before a server's disk space calculation is
// considered stale and a re-check should occur. DANGER: setting this value too low can seriously
@@ -305,11 +319,6 @@ type Configuration struct {
// is only required by users running Wings without SSL certificates and using internal IP
// addresses in order to connect. Most users should NOT enable this setting.
AllowCORSPrivateNetwork bool `json:"allow_cors_private_network" yaml:"allow_cors_private_network"`
// Servers contains all of the settings that are used when configuring individual servers
// on the system. This is a global configuration for all server instances, not to be confused
// with the per-server configurations provided by the Panel API.
Servers Servers `json:"servers" yaml:"servers"`
}
// NewAtPath creates a new struct and set the path where it should be stored.
@@ -430,6 +439,19 @@ func EnsurePterodactylUser() error {
return nil
}
if _config.System.User.Rootless.Enabled {
log.Info("rootless mode is enabled, skipping user creation...")
u, err := user.Current()
if err != nil {
return err
}
_config.System.Username = u.Username
_config.System.User.Uid = system.MustInt(u.Uid)
_config.System.User.Gid = system.MustInt(u.Gid)
return nil
}
log.WithField("username", _config.System.Username).Info("checking for pterodactyl system user")
u, err := user.Lookup(_config.System.Username)
// If an error is returned but it isn't the unknown user error just abort
// the process entirely. If we did find a user, return it immediately.

View File

@@ -5,6 +5,7 @@ import (
"sort"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/goccy/go-json"
)
@@ -86,6 +87,22 @@ type DockerConfiguration struct {
// if the value is "host", then the pterodactyl containers are started with user namespace
// remapping disabled
UsernsMode string `default:"" json:"userns_mode" yaml:"userns_mode"`
LogConfig struct {
Type string `default:"local" json:"type" yaml:"type"`
Config map[string]string `default:"{\"max-size\":\"5m\",\"max-file\":\"1\",\"compress\":\"false\",\"mode\":\"non-blocking\"}" json:"config" yaml:"config"`
} `json:"log_config" yaml:"log_config"`
}
func (c DockerConfiguration) ContainerLogConfig() container.LogConfig {
if c.LogConfig.Type == "" {
return container.LogConfig{}
}
return container.LogConfig{
Type: c.LogConfig.Type,
Config: c.LogConfig.Config,
}
}
// RegistryConfiguration defines the authentication credentials for a given

View File

@@ -1,28 +0,0 @@
package config
type FSDriver string
const (
FSDriverLocal FSDriver = "local"
FSDriverVHD FSDriver = "vhd"
)
type Servers struct {
// Filesystem defines all of the filesystem specific settings used for servers.
Filesystem Filesystem `json:"filesystem" yaml:"filesystem"`
}
type Filesystem struct {
// Driver defines the underlying filesystem driver that is used when a server is
// created on the system. This currently supports either of the following drivers:
//
// local: the local driver is the default one used by Wings. This offloads all of the
// disk limit enforcement to Wings itself. This has a performance impact but is
// the most compatiable with all systems.
// vhd: the vhd driver uses "virtual" disks on the host system to enforce disk limits
// on the server. This is more performant since calculations do not need to be made
// by Wings itself when enforcing limits. It also avoids vulnerabilities that exist
// in the local driver which allow malicious processes to quickly create massive files
// before Wings is able to detect and stop them from being written.
Driver FSDriver `default:"local" json:"driver" yaml:"driver"`
}

View File

@@ -8,6 +8,7 @@ type Settings struct {
Mounts []Mount
Allocations Allocations
Limits Limits
Labels map[string]string
}
// Defines the actual configuration struct for the environment with all of the settings
@@ -68,6 +69,14 @@ func (c *Configuration) Mounts() []Mount {
return c.settings.Mounts
}
// Labels returns the container labels associated with this instance.
func (c *Configuration) Labels() map[string]string {
c.mu.RLock()
defer c.mu.RUnlock()
return c.settings.Labels
}
// Returns the environment variables associated with this instance.
func (c *Configuration) EnvironmentVariables() []string {
c.mu.RLock()

View File

@@ -16,7 +16,6 @@ import (
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/mount"
"github.com/docker/docker/client"
"github.com/docker/docker/daemon/logger/local"
"github.com/pterodactyl/wings/config"
"github.com/pterodactyl/wings/environment"
@@ -43,17 +42,13 @@ func (nw noopWriter) Write(b []byte) (int, error) {
//
// Calling this function will poll resources for the container in the background
// until the container is stopped. The context provided to this function is used
// for the purposes of attaching to the container, a seecond context is created
// for the purposes of attaching to the container, a second context is created
// within the function for managing polling.
func (e *Environment) Attach(ctx context.Context) error {
if e.IsAttached() {
return nil
}
if err := e.followOutput(); err != nil {
return err
}
opts := types.ContainerAttachOptions{
Stdin: true,
Stdout: true,
@@ -90,20 +85,13 @@ func (e *Environment) Attach(ctx context.Context) error {
}
}()
// Block the completion of this routine until the container is no longer running. This allows
// the pollResources function to run until it needs to be stopped. Because the container
// can be polled for resource usage, even when stopped, we need to have this logic present
// in order to cancel the context and therefore stop the routine that is spawned.
//
// For now, DO NOT use client#ContainerWait from the Docker package. There is a nasty
// bug causing containers to hang on deletion and cause servers to lock up on the system.
//
// This weird code isn't intuitive, but it keeps the function from ending until the container
// is stopped and therefore the stream reader ends up closed.
// @see https://github.com/moby/moby/issues/41827
c := new(noopWriter)
if _, err := io.Copy(c, e.stream.Reader); err != nil {
e.log().WithField("error", err).Error("could not copy from environment stream to noop writer")
if err := system.ScanReader(e.stream.Reader, func(v []byte) {
e.logCallbackMx.Lock()
defer e.logCallbackMx.Unlock()
e.logCallback(v)
}); err != nil && err != io.EOF {
log.WithField("error", err).WithField("container_id", e.Id).Warn("error processing scanner line in console output")
return
}
}()
@@ -163,21 +151,30 @@ func (e *Environment) Create() error {
return errors.WithStackIf(err)
}
cfg := config.Get()
a := e.Configuration.Allocations()
evs := e.Configuration.EnvironmentVariables()
for i, v := range evs {
// Convert 127.0.0.1 to the pterodactyl0 network interface if the environment is Docker
// so that the server operates as expected.
if v == "SERVER_IP=127.0.0.1" {
evs[i] = "SERVER_IP=" + config.Get().Docker.Network.Interface
evs[i] = "SERVER_IP=" + cfg.Docker.Network.Interface
}
}
// Merge user-provided labels with system labels
confLabels := e.Configuration.Labels()
labels := make(map[string]string, 2+len(confLabels))
for key := range confLabels {
labels[key] = confLabels[key]
}
labels["Service"] = "Pterodactyl"
labels["ContainerType"] = "server_process"
conf := &container.Config{
Hostname: e.Id,
Domainname: config.Get().Docker.Domainname,
User: strconv.Itoa(config.Get().System.User.Uid) + ":" + strconv.Itoa(config.Get().System.User.Gid),
Domainname: cfg.Docker.Domainname,
AttachStdin: true,
AttachStdout: true,
AttachStderr: true,
@@ -186,16 +183,20 @@ func (e *Environment) Create() error {
ExposedPorts: a.Exposed(),
Image: strings.TrimPrefix(e.meta.Image, "~"),
Env: e.Configuration.EnvironmentVariables(),
Labels: map[string]string{
"Service": "Pterodactyl",
"ContainerType": "server_process",
},
Labels: labels,
}
networkMode := container.NetworkMode(config.Get().Docker.Network.Mode)
// Set the user running the container properly depending on what mode we are operating in.
if cfg.System.User.Rootless.Enabled {
conf.User = fmt.Sprintf("%d:%d", cfg.System.User.Rootless.ContainerUID, cfg.System.User.Rootless.ContainerGID)
} else {
conf.User = strconv.Itoa(cfg.System.User.Uid) + ":" + strconv.Itoa(cfg.System.User.Gid)
}
networkMode := container.NetworkMode(cfg.Docker.Network.Mode)
if a.ForceOutgoingIP {
e.log().Debug("environment/docker: forcing outgoing IP address")
networkName := strings.ReplaceAll(e.Id, "-", "")
networkName := "ip-" + strings.ReplaceAll(strings.ReplaceAll(a.DefaultMapping.Ip, ".", "-"), ":", "-")
networkMode = container.NetworkMode(networkName)
if _, err := e.client.NetworkInspect(ctx, networkName, types.NetworkInspectOptions{}); err != nil {
@@ -231,28 +232,20 @@ func (e *Environment) Create() error {
// Configure the /tmp folder mapping in containers. This is necessary for some
// games that need to make use of it for downloads and other installation processes.
Tmpfs: map[string]string{
"/tmp": "rw,exec,nosuid,size=" + strconv.Itoa(int(config.Get().Docker.TmpfsSize)) + "M",
"/tmp": "rw,exec,nosuid,size=" + strconv.Itoa(int(cfg.Docker.TmpfsSize)) + "M",
},
// Define resource limits for the container based on the data passed through
// from the Panel.
Resources: e.Configuration.Limits().AsContainerResources(),
DNS: config.Get().Docker.Network.Dns,
DNS: cfg.Docker.Network.Dns,
// Configure logging for the container to make it easier on the Daemon to grab
// the server output. Ensure that we don't use too much space on the host machine
// since we only need it for the last few hundred lines of output and don't care
// about anything else in it.
LogConfig: container.LogConfig{
Type: local.Name,
Config: map[string]string{
"max-size": "5m",
"max-file": "1",
"compress": "false",
"mode": "non-blocking",
},
},
LogConfig: cfg.Docker.ContainerLogConfig(),
SecurityOpt: []string{"no-new-privileges"},
ReadonlyRootfs: true,
@@ -261,7 +254,7 @@ func (e *Environment) Create() error {
"fowner", "fsetid", "net_bind_service", "sys_chroot", "setfcap",
},
NetworkMode: networkMode,
UsernsMode: container.UsernsMode(config.Get().Docker.UsernsMode),
UsernsMode: container.UsernsMode(cfg.Docker.UsernsMode),
}
if _, err := e.client.ContainerCreate(ctx, conf, hostConf, nil, nil, e.Id); err != nil {
@@ -342,59 +335,6 @@ func (e *Environment) Readlog(lines int) ([]string, error) {
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 {
return err
}
return errors.New(fmt.Sprintf("no such container: %s", e.Id))
}
opts := types.ContainerLogsOptions{
ShowStderr: true,
ShowStdout: true,
Follow: true,
Since: time.Now().Format(time.RFC3339),
}
reader, err := e.client.ContainerLogs(context.Background(), e.Id, opts)
if err != nil {
return err
}
go e.scanOutput(reader)
return nil
}
func (e *Environment) scanOutput(reader io.ReadCloser) {
defer reader.Close()
if err := system.ScanReader(reader, func(v []byte) {
e.logCallbackMx.Lock()
defer e.logCallbackMx.Unlock()
e.logCallback(v)
}); err != nil && err != io.EOF {
log.WithField("error", err).WithField("container_id", e.Id).Warn("error processing scanner line in console output")
return
}
// Return here if the server is offline or currently stopping.
if e.State() == environment.ProcessStoppingState || e.State() == environment.ProcessOfflineState {
return
}
// Close the current reader before starting a new one, the defer will still run,
// but it will do nothing if we already closed the stream.
_ = reader.Close()
// Start following the output of the server again.
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.

View File

@@ -96,7 +96,7 @@ func (e *Environment) SetStream(s *types.HijackedResponse) {
e.mu.Unlock()
}
// IsAttached determine if the this process is currently attached to the
// IsAttached determines if this process is currently attached to the
// container instance by checking if the stream is nil or not.
func (e *Environment) IsAttached() bool {
e.mu.RLock()

View File

@@ -39,7 +39,7 @@ func (e *Environment) OnBeforeStart(ctx context.Context) error {
//
// This won't actually run an installation process however, it is just here to ensure the
// environment gets created properly if it is missing and the server is started. We're making
// an assumption that all of the files will still exist at this point.
// an assumption that all the files will still exist at this point.
if err := e.Create(); err != nil {
return err
}
@@ -107,7 +107,7 @@ func (e *Environment) Start(ctx context.Context) error {
}
// If we cannot start & attach to the container in 30 seconds something has gone
// quite sideways and we should stop trying to avoid a hanging situation.
// quite sideways, and we should stop trying to avoid a hanging situation.
actx, cancel := context.WithTimeout(ctx, time.Second*30)
defer cancel()
@@ -179,8 +179,12 @@ func (e *Environment) Stop(ctx context.Context) error {
// Allow the stop action to run for however long it takes, similar to executing a command
// and using a different logic pathway to wait for the container to stop successfully.
t := time.Duration(-1)
if err := e.client.ContainerStop(ctx, e.Id, &t); err != nil {
//
// Using a negative timeout here will allow the container to stop gracefully,
// rather than forcefully terminating it, this value MUST be at least 1
// second, otherwise it will be ignored.
timeout := -1 * time.Second
if err := e.client.ContainerStop(ctx, e.Id, &timeout); err != nil {
// If the container does not exist just mark the process as stopped and return without
// an error.
if client.IsErrNotFound(err) {

View File

@@ -134,7 +134,11 @@ func calculateDockerAbsoluteCpu(pStats types.CPUStats, stats types.CPUStats) flo
percent := 0.0
if systemDelta > 0.0 && cpuDelta > 0.0 {
percent = (cpuDelta / systemDelta) * cpus * 100.0
percent = (cpuDelta / systemDelta) * 100.0
if cpus > 0 {
percent *= cpus
}
}
return math.Round(percent*1000) / 1000

11
go.mod
View File

@@ -7,12 +7,12 @@ require (
github.com/AlecAivazis/survey/v2 v2.3.6
github.com/Jeffail/gabs/v2 v2.6.1
github.com/NYTimes/logrotate v1.0.0
github.com/acobaugh/osrelease v0.1.0
github.com/apex/log v1.9.0
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d
github.com/beevik/etree v1.1.0
github.com/buger/jsonparser v1.1.1
github.com/cenkalti/backoff/v4 v4.1.3
github.com/cobaugh/osrelease v0.0.0-20181218015638-a93a0a55a249
github.com/creasty/defaults v1.6.0
github.com/docker/docker v20.10.18+incompatible
github.com/docker/go-connections v0.4.0
@@ -35,12 +35,11 @@ require (
github.com/klauspost/pgzip v1.2.5
github.com/magiconair/properties v1.8.6
github.com/mattn/go-colorable v0.1.13
github.com/mholt/archiver/v3 v3.5.1
github.com/mholt/archiver/v4 v4.0.0-alpha.7
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db
github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/pkg/sftp v1.13.5
github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06
github.com/spf13/afero v1.9.2
github.com/spf13/cobra v1.5.0
github.com/stretchr/testify v1.8.0
golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be
@@ -89,7 +88,7 @@ require (
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/nwaples/rardecode v1.1.3 // indirect
github.com/nwaples/rardecode/v2 v2.0.0-beta.2 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0-rc2 // indirect
github.com/pelletier/go-toml/v2 v2.0.5 // indirect
@@ -104,16 +103,16 @@ require (
github.com/robfig/cron/v3 v3.0.1 // indirect
github.com/sirupsen/logrus v1.9.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/therootcompany/xz v1.0.1 // indirect
github.com/ugorji/go/codec v1.2.7 // indirect
github.com/ulikunitz/xz v0.5.10 // indirect
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
go.uber.org/atomic v1.10.0 // indirect
go.uber.org/multierr v1.8.0 // indirect
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
golang.org/x/net v0.0.0-20221004154528-8021a29435af // indirect
golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec // indirect
golang.org/x/term v0.0.0-20220919170432-7a66f970e087 // indirect
golang.org/x/text v0.3.8 // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/time v0.0.0-20220922220347-f3bd1da661af // indirect
golang.org/x/tools v0.1.12 // indirect
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect

79
go.sum
View File

@@ -4,7 +4,6 @@ cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
@@ -15,9 +14,6 @@ cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKV
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
@@ -35,7 +31,6 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
emperror.dev/errors v0.8.1 h1:UavXZ5cSX/4u9iyvH6aDcuGkVjeexUGJ7Ij7G4VfQT0=
emperror.dev/errors v0.8.1/go.mod h1:YcRvLPh626Ubn2xqtoprejnA5nFha+TJ+2vew48kWuE=
@@ -92,13 +87,14 @@ github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbt
github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ=
github.com/acobaugh/osrelease v0.1.0 h1:Yb59HQDGGNhCj4suHaFQQfBps5wyoKLSSX/J/+UifRE=
github.com/acobaugh/osrelease v0.1.0/go.mod h1:4bFEs0MtgHNHBrmHCt67gNisnabCRAlzdVasCEGHTWY=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
github.com/alexflint/go-filemutex v0.0.0-20171022225611-72bdc8eae2ae/go.mod h1:CgnQgUtFrFz9mxFNtED3jI5tLDjKlOM+oUF/sTk6ps0=
github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
@@ -154,11 +150,8 @@ github.com/cilium/ebpf v0.4.0/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJ
github.com/cilium/ebpf v0.6.2/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJXRs=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cobaugh/osrelease v0.0.0-20181218015638-a93a0a55a249 h1:R0IDH8daQ3lODvu8YtxnIqqth5qMGCJyADoUQvmLx4o=
github.com/cobaugh/osrelease v0.0.0-20181218015638-a93a0a55a249/go.mod h1:EHKW9yNEYSBpTKzuu7Y9oOrft/UlzH57rMIB03oev6M=
github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8=
github.com/containerd/aufs v0.0.0-20200908144142-dab0cbea06f4/go.mod h1:nukgQABAEopAHvB6j7cnP5zJ+/3aVcE7hCYqvIwAHyE=
github.com/containerd/aufs v0.0.0-20201003224125-76a6863f2989/go.mod h1:AkGGQs9NM2vtYHaUen+NljV0/baGCAPELGm2q9ZXpWU=
@@ -317,7 +310,6 @@ github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
@@ -436,7 +428,6 @@ github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/snappy v0.0.2/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
@@ -459,7 +450,6 @@ github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/
github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
@@ -467,9 +457,6 @@ github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hf
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
@@ -480,7 +467,6 @@ github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg=
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/handlers v0.0.0-20150720190736-60c7bfde3e33/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ=
github.com/gorilla/mux v1.7.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
@@ -510,7 +496,6 @@ github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpO
github.com/iancoleman/strcase v0.2.0 h1:05I4QRnGpI0m37iZQRuskXh+w77mr6Z41lwQzuHLwW0=
github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/icza/dyno v0.0.0-20220812133438-f0b6f8a18845 h1:H+uM0Bv88eur3ZSsd2NGKg3YIiuXxwxtlN7HjE66UTU=
github.com/icza/dyno v0.0.0-20220812133438-f0b6f8a18845/go.mod h1:c1tRKs5Tx7E2+uHGSyyncziFjvGpgv4H2HrqXeUQ/Uk=
github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
@@ -557,7 +542,6 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/compress v1.11.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
github.com/klauspost/compress v1.11.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
github.com/klauspost/compress v1.11.13/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
github.com/klauspost/compress v1.15.11 h1:Lcadnb3RKGin4FYM/orgq0qde+nc15E5Cbqg4B9Sx9c=
github.com/klauspost/compress v1.15.11/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM=
@@ -619,8 +603,8 @@ github.com/maxbrunsfeld/counterfeiter/v6 v6.2.2/go.mod h1:eD9eIE7cdwcMi9rYluz88J
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/mholt/archiver/v3 v3.5.1 h1:rDjOBX9JSF5BvoJGvjqK479aL70qh9DIpZCl+k7Clwo=
github.com/mholt/archiver/v3 v3.5.1/go.mod h1:e3dqJ7H78uzsRSEACH1joayhuSyhnonssnDhppzS1L4=
github.com/mholt/archiver/v4 v4.0.0-alpha.7 h1:xzByj8G8tj0Oq7ZYYU4+ixL/CVb5ruWCm0EZQ1PjOkE=
github.com/mholt/archiver/v4 v4.0.0-alpha.7/go.mod h1:Fs8qUkO74HHaidabihzYephJH8qmGD/nCP6tE5xC9BM=
github.com/miekg/pkcs11 v1.0.3/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
github.com/mistifyio/go-zfs v2.1.2-0.20190413222219-f784269be439+incompatible/go.mod h1:8AuVvqP/mXw1px98n46wfvcGfQ4ci2FwoAjKYxuo3Z4=
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ=
@@ -652,9 +636,8 @@ github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRW
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
github.com/ncw/swift v1.0.47/go.mod h1:23YIA4yWVnGwv2dQlN4bB7egfYX6YLn0Yo/S6zZO/ZM=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/nwaples/rardecode v1.1.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
github.com/nwaples/rardecode v1.1.3 h1:cWCaZwfM5H7nAD6PyEdcVnczzV8i/JtotnyW/dD9lEc=
github.com/nwaples/rardecode v1.1.3/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
github.com/nwaples/rardecode/v2 v2.0.0-beta.2 h1:e3mzJFJs4k83GXBEiTaQ5HgSc/kOK8q0rDaRO0MPaOk=
github.com/nwaples/rardecode/v2 v2.0.0-beta.2/go.mod h1:yntwv/HfMc/Hbvtq9I19D1n58te3h6KsqCf3GxyfBGY=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
@@ -707,7 +690,6 @@ github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrap
github.com/pelletier/go-toml/v2 v2.0.5 h1:ipoSadvV8oGUjnUbMub59IDPPwfxF694nG/jwbMiyQg=
github.com/pelletier/go-toml/v2 v2.0.5/go.mod h1:OMHamSCAODeSsVrwwvcJOaoN0LIUIaFVNZzmWyNfXas=
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
github.com/pierrec/lz4/v4 v4.1.2/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pierrec/lz4/v4 v4.1.17 h1:kV4Ip+/hUBC+8T6+2EgburRtkE9ef4nbY3f4dFhGjMc=
github.com/pierrec/lz4/v4 v4.1.17/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
@@ -716,7 +698,6 @@ github.com/pkg/errors v0.8.1-0.20171018195549-f15c970de5b7/go.mod h1:bwawxfHBFNV
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
github.com/pkg/sftp v1.13.5 h1:a3RLUqkyjYRtBTZJZ1VRrKbN3zhuPLlUc3sphVz81go=
github.com/pkg/sftp v1.13.5/go.mod h1:wHDZ0IZX6JcBYRK1TH9bcVq8G7TLpVHYIGJRFnmPfxg=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -804,8 +785,6 @@ github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4k
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
github.com/spf13/afero v1.9.2 h1:j49Hj62F0n+DaZ1dDCvhABaPNSGNkt32oRFxI33IEMw=
github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
@@ -840,6 +819,8 @@ github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG
github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
github.com/tchap/go-patricia v2.2.6+incompatible/go.mod h1:bmLyhP68RS6kStMGxByiQ23RP/odRBOTVjwp2cDyi6I=
github.com/therootcompany/xz v1.0.1 h1:CmOtsn1CbtmyYiusbfmhmkpAAETj0wBIH6kCYaX+xzw=
github.com/therootcompany/xz v1.0.1/go.mod h1:3K3UH1yCKgBneZYhuQUvJ9HPD19UEXEI0BWbMn8qNMY=
github.com/tj/assert v0.0.0-20171129193455-018094318fb0/go.mod h1:mZ9/Rh9oLWpLLDRpvE+3b7gP/C2YyLFYxNmcLnPTMe0=
github.com/tj/assert v0.0.3 h1:Df/BlaZ20mq6kuai7f5z2TvPFiwC3xaWJSDQNiIS3Rk=
github.com/tj/assert v0.0.3/go.mod h1:Ne6X72Q+TB1AteidzQncjw9PabbMp4PBMZ1k+vd1Pvk=
@@ -854,7 +835,6 @@ github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6
github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0=
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/ulikunitz/xz v0.5.9/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/ulikunitz/xz v0.5.10 h1:t92gobL9l3HE202wg3rlk19F6X+JOxl9BBrCCMYEYd8=
github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/urfave/cli v0.0.0-20171014202726-7bc6a0acffa5/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
@@ -872,8 +852,6 @@ github.com/willf/bitset v1.1.11/go.mod h1:83CECat5yLh5zVOf4P1ErAgKA5UDvKtgyUABdr
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/xeipuuv/gojsonschema v0.0.0-20180618132009-1d523034197f/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs=
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo=
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@@ -895,7 +873,6 @@ go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
@@ -923,8 +900,6 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be h1:fmw3UbQh+nxngCAHrDCCztao/kbYFnWjoqop8dHx05A=
golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
@@ -950,7 +925,6 @@ golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHl
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
@@ -959,8 +933,6 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
@@ -1002,9 +974,7 @@ golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81R
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
@@ -1021,10 +991,6 @@ golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4Iltr
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -1098,7 +1064,6 @@ golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200817155316-9781c653f443/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200916030750-2334cc1a136f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200922070232-aee5d888a860/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -1109,14 +1074,10 @@ golang.org/x/sys v0.0.0-20201117170446-d9b008d0a637/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201202213521-69691e467435/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -1146,9 +1107,8 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -1208,16 +1168,9 @@ golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roY
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
golang.org/x/tools v0.0.0-20200916195026-c9a70fc28ce3/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
@@ -1244,16 +1197,12 @@ google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0M
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/cloud v0.0.0-20151119220103-975617b05ea8/go.mod h1:0H1ncTHf11KCFhTc/+EFRbzSCOZx+VUbRMk55Yv5MYk=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
@@ -1288,14 +1237,7 @@ google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7Fc
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
@@ -1312,11 +1254,8 @@ google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKa
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=

View File

@@ -5,7 +5,7 @@ import (
"time"
"emperror.dev/errors"
log2 "github.com/apex/log"
"github.com/apex/log"
"github.com/go-co-op/gocron"
"github.com/pterodactyl/wings/config"
@@ -24,7 +24,7 @@ func Scheduler(ctx context.Context, m *server.Manager) (*gocron.Scheduler, error
if !o.SwapIf(true) {
return nil, errors.New("cron: cannot call scheduler more than once in application lifecycle")
}
l, err := time.LoadLocation(config.Get().System.Timezone)
location, err := time.LoadLocation(config.Get().System.Timezone)
if err != nil {
return nil, errors.Wrap(err, "cron: failed to parse configured system timezone")
}
@@ -41,30 +41,30 @@ func Scheduler(ctx context.Context, m *server.Manager) (*gocron.Scheduler, error
max: config.Get().System.ActivitySendCount,
}
s := gocron.NewScheduler(l)
log := log2.WithField("subsystem", "cron")
s := gocron.NewScheduler(location)
l := log.WithField("subsystem", "cron")
interval := time.Duration(config.Get().System.ActivitySendInterval) * time.Second
log.WithField("interval", interval).Info("configuring system crons")
l.WithField("interval", interval).Info("configuring system crons")
_, _ = s.Tag("activity").Every(interval).Do(func() {
log.WithField("cron", "activity").Debug("sending internal activity events to Panel")
l.WithField("cron", "activity").Debug("sending internal activity events to Panel")
if err := activity.Run(ctx); err != nil {
if errors.Is(err, ErrCronRunning) {
log.WithField("cron", "activity").Warn("activity process is already running, skipping...")
l.WithField("cron", "activity").Warn("activity process is already running, skipping...")
} else {
log.WithField("cron", "activity").WithField("error", err).Error("activity process failed to execute")
l.WithField("cron", "activity").WithField("error", err).Error("activity process failed to execute")
}
}
})
_, _ = s.Tag("sftp").Every(interval).Do(func() {
log.WithField("cron", "sftp").Debug("sending sftp events to Panel")
l.WithField("cron", "sftp").Debug("sending sftp events to Panel")
if err := sftp.Run(ctx); err != nil {
if errors.Is(err, ErrCronRunning) {
log.WithField("cron", "sftp").Warn("sftp events process already running, skipping...")
l.WithField("cron", "sftp").Warn("sftp events process already running, skipping...")
} else {
log.WithField("cron", "sftp").WithField("error", err).Error("sftp events process failed to execute")
l.WithField("cron", "sftp").WithField("error", err).Error("sftp events process failed to execute")
}
}
})

View File

@@ -0,0 +1,90 @@
package progress
import (
"io"
"strings"
"sync/atomic"
"github.com/pterodactyl/wings/system"
)
// Progress is used to track the progress of any I/O operation that are being
// performed.
type Progress struct {
// written is the total size of the files that have been written to the writer.
written uint64
// Total is the total size of the archive in bytes.
total uint64
// Writer .
Writer io.Writer
}
// NewProgress returns a new progress tracker for the given total size.
func NewProgress(total uint64) *Progress {
return &Progress{total: total}
}
// Written returns the total number of bytes written.
// This function should be used when the progress is tracking data being written.
func (p *Progress) Written() uint64 {
return atomic.LoadUint64(&p.written)
}
// Total returns the total size in bytes.
func (p *Progress) Total() uint64 {
return atomic.LoadUint64(&p.total)
}
// SetTotal sets the total size of the archive in bytes. This function is safe
// to call concurrently and can be used to update the total size if it changes,
// such as when the total size is simultaneously being calculated as data is
// being written through the progress writer.
func (p *Progress) SetTotal(total uint64) {
atomic.StoreUint64(&p.total, total)
}
// Write totals the number of bytes that have been written to the writer.
func (p *Progress) Write(v []byte) (int, error) {
n := len(v)
atomic.AddUint64(&p.written, uint64(n))
if p.Writer != nil {
return p.Writer.Write(v)
}
return n, nil
}
// Progress returns a formatted progress string for the current progress.
func (p *Progress) Progress(width int) string {
// current = 100 (Progress, dynamic)
// total = 1000 (Content-Length, dynamic)
// width = 25 (Number of ticks to display, static)
// widthPercentage = 100 / width (What percentage does each tick represent, static)
//
// percentageDecimal = current / total = 0.1
// percentage = percentageDecimal * 100 = 10%
// ticks = percentage / widthPercentage = 2.5
//
// ticks is a float64, so we cast it to an int which rounds it down to 2.
// Values are cast to floats to prevent integer division.
current := p.Written()
total := p.Total()
// width := is passed as a parameter
widthPercentage := float64(100) / float64(width)
percentageDecimal := float64(current) / float64(total)
percentage := percentageDecimal * 100
ticks := int(percentage / widthPercentage)
// Ensure that we never get a negative number of ticks, this will prevent strings#Repeat
// from panicking. A negative number of ticks is likely to happen when the total size is
// inaccurate, such as when we are going off of rough disk usage calculation.
if ticks < 0 {
ticks = 0
} else if ticks > width {
ticks = width
}
bar := strings.Repeat("=", ticks) + strings.Repeat(" ", width-ticks)
return "[" + bar + "] " + system.FormatBytes(current) + " / " + system.FormatBytes(total)
}

View File

@@ -0,0 +1,50 @@
package progress_test
import (
"bytes"
"testing"
"github.com/franela/goblin"
"github.com/pterodactyl/wings/internal/progress"
)
func TestProgress(t *testing.T) {
g := goblin.Goblin(t)
g.Describe("Progress", func() {
g.It("properly initializes", func() {
total := uint64(1000)
p := progress.NewProgress(total)
g.Assert(p).IsNotNil()
g.Assert(p.Total()).Equal(total)
g.Assert(p.Written()).Equal(uint64(0))
})
g.It("increments written when Write is called", func() {
v := []byte("hello")
p := progress.NewProgress(1000)
_, err := p.Write(v)
g.Assert(err).IsNil()
g.Assert(p.Written()).Equal(uint64(len(v)))
})
g.It("renders a progress bar", func() {
v := bytes.Repeat([]byte{' '}, 100)
p := progress.NewProgress(1000)
_, err := p.Write(v)
g.Assert(err).IsNil()
g.Assert(p.Written()).Equal(uint64(len(v)))
g.Assert(p.Progress(25)).Equal("[== ] 100 B / 1000 B")
})
g.It("renders a progress bar when written exceeds total", func() {
v := bytes.Repeat([]byte{' '}, 1001)
p := progress.NewProgress(1000)
_, err := p.Write(v)
g.Assert(err).IsNil()
g.Assert(p.Written()).Equal(uint64(len(v)))
g.Assert(p.Progress(25)).Equal("[=========================] 1001 B / 1000 B")
})
})
}

View File

@@ -1,330 +0,0 @@
package vhd
import (
"context"
"emperror.dev/errors"
"fmt"
"github.com/pterodactyl/wings/config"
"github.com/spf13/afero"
"os"
"os/exec"
"path"
"path/filepath"
"strings"
"sync"
"sync/atomic"
)
var (
ErrInvalidDiskPathTarget = errors.Sentinel("vhd: disk path is a directory or symlink")
ErrMountPathNotDirectory = errors.Sentinel("vhd: mount point is not a directory")
ErrFilesystemMounted = errors.Sentinel("vhd: filesystem is already mounted")
ErrFilesystemNotMounted = errors.Sentinel("vhd: filesystem is not mounted")
ErrFilesystemExists = errors.Sentinel("vhd: filesystem already exists on disk")
)
var useDdAllocation bool
var setDdAllocator sync.Once
// hasExitCode allows this code to test the response error to see if there is
// an exit code available from the command call that can be used to determine if
// something went wrong.
type hasExitCode interface {
ExitCode() int
}
// Commander defines an interface that must be met for executing commands on the
// underlying OS. By default the vhd package will use Go's exec.Cmd type for
// execution. This interface allows stubbing out on tests, or potentially custom
// setups down the line.
type Commander interface {
Run() error
Output() ([]byte, error)
String() string
}
// CommanderProvider is a function that provides a struct meeting the Commander
// interface requirements.
type CommanderProvider func(ctx context.Context, name string, args ...string) Commander
// CfgOption is a configuration option callback for the Disk.
type CfgOption func(d *Disk) *Disk
// Disk represents the underlying virtual disk for the instance.
type Disk struct {
mu sync.RWMutex
// The total size of the disk allowed in bytes.
size int64
// The path where the disk image should be created.
diskPath string
// The point at which this disk should be made available on the system. This
// is where files can be read/written to.
mountAt string
fs afero.Fs
commander CommanderProvider
}
// DiskPath returns the underlying path that contains the virtual disk for the server
// identified by its UUID.
func DiskPath(uuid string) string {
return filepath.Join(config.Get().System.Data, ".vhd/", uuid+".img")
}
// Enabled returns true when VHD support is enabled on the instance.
func Enabled() bool {
return config.Get().Servers.Filesystem.Driver == config.FSDriverVHD
}
// New returns a new Disk instance. The "size" parameter should be provided in
// bytes of space allowed for the disk. An additional slice of option callbacks
// can be provided to programatically swap out the underlying filesystem
// implementation or the underlying command exection engine.
func New(size int64, diskPath string, mountAt string, opts ...func(*Disk)) *Disk {
if diskPath == "" || mountAt == "" {
panic("vhd: cannot specify an empty disk or mount path")
}
d := Disk{
size: size,
diskPath: diskPath,
mountAt: mountAt,
fs: afero.NewOsFs(),
commander: func(ctx context.Context, name string, args ...string) Commander {
return exec.CommandContext(ctx, name, args...)
},
}
for _, opt := range opts {
opt(&d)
}
return &d
}
// WithFs allows for a different underlying filesystem to be provided to the
// virtual disk manager.
func WithFs(fs afero.Fs) func(*Disk) {
return func(d *Disk) {
d.fs = fs
}
}
// WithCommander allows a different Commander provider to be provided.
func WithCommander(c CommanderProvider) func(*Disk) {
return func(d *Disk) {
d.commander = c
}
}
func (d *Disk) Path() string {
return d.diskPath
}
func (d *Disk) MountPath() string {
return d.mountAt
}
// Exists reports if the disk exists on the system yet or not. This only verifies
// the presence of the disk image, not the validity of it. An error is returned
// if the path exists but the destination is not a file or is a symlink.
func (d *Disk) Exists() (bool, error) {
d.mu.RLock()
defer d.mu.RUnlock()
st, err := d.fs.Stat(d.diskPath)
if err != nil && os.IsNotExist(err) {
return false, nil
} else if err != nil {
return false, errors.WithStack(err)
}
if !st.IsDir() && st.Mode()&os.ModeSymlink == 0 {
return true, nil
}
return false, errors.WithStack(ErrInvalidDiskPathTarget)
}
// IsMounted checks to see if the given disk is currently mounted.
func (d *Disk) IsMounted(ctx context.Context) (bool, error) {
find := d.mountAt + " ext4"
cmd := d.commander(ctx, "grep", "-qs", find, "/proc/mounts")
if err := cmd.Run(); err != nil {
if v, ok := err.(hasExitCode); ok {
if v.ExitCode() == 1 {
return false, nil
}
}
return false, errors.Wrap(err, "vhd: failed to execute grep for mount existence")
}
return true, nil
}
// Mount attempts to mount the disk as configured. If it does not exist or the
// mount command fails an error will be returned to the caller. This does not
// attempt to create the disk if it is missing from the filesystem.
//
// Attempting to mount a disk which does not exist will result in an error being
// returned to the caller. If the disk is already mounted an ErrFilesystemMounted
// error is returned to the caller.
func (d *Disk) Mount(ctx context.Context) error {
d.mu.Lock()
defer d.mu.Unlock()
return d.mount(ctx)
}
// Unmount attempts to unmount the disk from the system. If the disk is not
// currently mounted this function is a no-op and ErrFilesystemNotMounted is
// returned to the caller.
func (d *Disk) Unmount(ctx context.Context) error {
d.mu.Lock()
defer d.mu.Unlock()
return d.unmount(ctx)
}
// Allocate executes the "fallocate" command on the disk. This will first unmount
// the disk from the system before attempting to actually allocate the space. If
// this disk already exists on the machine it will be resized accordingly.
//
// DANGER! This will unmount the disk from the machine while performing this
// action, use caution when calling it during normal processes.
func (d *Disk) Allocate(ctx context.Context) error {
d.mu.Lock()
defer d.mu.Unlock()
if exists, err := d.Exists(); exists {
// If the disk currently exists attempt to unmount the mount point before
// allocating space.
if err := d.Unmount(ctx); err != nil {
return errors.WithStackIf(err)
}
} else if err != nil {
return errors.Wrap(err, "vhd: failed to check for existence of root disk")
}
trim := path.Base(d.diskPath)
if err := d.fs.MkdirAll(strings.TrimSuffix(d.diskPath, trim), 0700); err != nil {
return errors.Wrap(err, "vhd: failed to create base vhd disk directory")
}
cmd := d.allocationCmd(ctx)
if _, err := cmd.Output(); err != nil {
msg := "vhd: failed to execute space allocation command"
if v, ok := err.(*exec.ExitError); ok {
stderr := strings.Trim(string(v.Stderr), ".\n")
if !useDdAllocation && strings.HasSuffix(stderr, "not supported") {
// Try again: fallocate is not supported on some filesystems so we'll fall
// back to making use of dd for subsequent operations.
setDdAllocator.Do(func() {
useDdAllocation = true
})
return d.Allocate(ctx)
}
msg = msg + ": " + stderr
}
return errors.Wrap(err, msg)
}
return errors.WithStack(d.fs.Chmod(d.diskPath, 0600))
}
// Resize will change the internal disk size limit and then allocate the new
// space to the disk automatically.
func (d *Disk) Resize(ctx context.Context, size int64) error {
atomic.StoreInt64(&d.size, size)
return d.Allocate(ctx)
}
// Destroy removes the underlying allocated disk image and unmounts the disk.
func (d *Disk) Destroy(ctx context.Context) error {
d.mu.Lock()
defer d.mu.Unlock()
if err := d.unmount(ctx); err != nil {
return errors.WithStackIf(err)
}
return errors.WithStackIf(d.fs.RemoveAll(d.mountAt))
}
// MakeFilesystem will attempt to execute the "mkfs" command against the disk on
// the machine. If the disk has already been created this command will return an
// ErrFilesystemExists error to the caller. You should manually unmount the disk
// if it shouldn't be mounted at this point.
func (d *Disk) MakeFilesystem(ctx context.Context) error {
d.mu.Lock()
defer d.mu.Unlock()
// If no error is returned when mounting DO NOT execute this command as it will
// completely destroy the data stored at that location.
err := d.Mount(ctx)
if err == nil || errors.Is(err, ErrFilesystemMounted) {
// If it wasn't already mounted try to clean up at this point and unmount
// the disk. If this fails just ignore it for now.
if err != nil {
_ = d.Unmount(ctx)
}
return ErrFilesystemExists
}
if !strings.Contains(err.Error(), "can't find in /etc/fstab") && !strings.Contains(err.Error(), "exit status 32") {
return errors.WrapIf(err, "vhd: unexpected error from mount command")
}
// As long as we got an error back that was because we couldn't find thedisk
// in the /etc/fstab file we're good. Otherwise it means the disk probably exists
// or something else went wrong.
//
// Because this is a destructive command and non-tty based exection of it implies
// "-F" (force), we need to only run it when we can guarantee it doesn't already
// exist. No vague "maybe that error is expected" allowed here.
cmd := d.commander(ctx, "mkfs", "-t", "ext4", d.diskPath)
if err := cmd.Run(); err != nil {
return errors.Wrap(err, "vhd: failed to make filesystem for disk")
}
return nil
}
func (d *Disk) mount(ctx context.Context) error {
if isMounted, err := d.IsMounted(ctx); err != nil {
return errors.WithStackIf(err)
} else if isMounted {
return ErrFilesystemMounted
}
if st, err := d.fs.Stat(d.mountAt); err != nil && !os.IsNotExist(err) {
return errors.Wrap(err, "vhd: failed to stat mount path")
} else if os.IsNotExist(err) {
if err := d.fs.MkdirAll(d.mountAt, 0700); err != nil {
return errors.Wrap(err, "vhd: failed to create mount path")
}
} else if !st.IsDir() {
return errors.WithStack(ErrMountPathNotDirectory)
}
u := config.Get().System.User
if err := d.fs.Chown(d.mountAt, u.Uid, u.Gid); err != nil {
return errors.Wrap(err, "vhd: failed to chown mount point")
}
cmd := d.commander(ctx, "mount", "-t", "auto", "-o", "loop", d.diskPath, d.mountAt)
if _, err := cmd.Output(); err != nil {
msg := "vhd: failed to mount disk"
if v, ok := err.(*exec.ExitError); ok {
msg = msg + ": " + strings.Trim(string(v.Stderr), ".\n")
}
return errors.Wrap(err, msg)
}
return nil
}
func (d *Disk) unmount(ctx context.Context) error {
cmd := d.commander(ctx, "umount", d.mountAt)
if err := cmd.Run(); err != nil {
v, ok := err.(hasExitCode)
if ok && v.ExitCode() == 32 {
return ErrFilesystemNotMounted
}
return errors.Wrap(err, "vhd: failed to execute unmount command for disk")
}
return nil
}
// allocationCmd returns the command to allocate the disk image. This will attempt to
// use the fallocate command if available, otherwise it will fall back to dd if the
// fallocate command has previously failed.
//
// We use 1024 as the multiplier for all of the disk space logic within the application.
// Passing "K" (/1024) is the same as "KiB" for fallocate, but is different than "KB" (/1000).
func (d *Disk) allocationCmd(ctx context.Context) Commander {
s := atomic.LoadInt64(&d.size) / 1024
if useDdAllocation {
return d.commander(ctx, "dd", "if=/dev/zero", fmt.Sprintf("of=%s", d.diskPath), fmt.Sprintf("bs=%dk", s), "count=1")
}
return d.commander(ctx, "fallocate", "-l", fmt.Sprintf("%dK", s), d.diskPath)
}

View File

@@ -1,476 +0,0 @@
package vhd
import (
"context"
"errors"
"fmt"
"os"
"os/exec"
"testing"
"github.com/pterodactyl/wings/config"
"github.com/spf13/afero"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func init() {
config.Set(&config.Configuration{
AuthenticationToken: "token123",
System: config.SystemConfiguration{
User: struct {
Uid int
Gid int
}{Uid: 10, Gid: 10},
},
})
}
type mockCmd struct {
run func() error
output func() ([]byte, error)
string func() string
}
func (m *mockCmd) Run() error {
if m.run != nil {
return m.run()
}
return nil
}
func (m *mockCmd) Output() ([]byte, error) {
if m.output != nil {
return m.output()
}
return nil, nil
}
func (m *mockCmd) String() string {
if m.string != nil {
return m.string()
}
return ""
}
var _ Commander = (*mockCmd)(nil)
type mockedExitCode struct {
code int
}
func (m *mockedExitCode) ExitCode() int {
return m.code
}
func (m *mockedExitCode) Error() string {
return fmt.Sprintf("mocked exit code: code %d", m.code)
}
func newMockDisk(c CommanderProvider) *Disk {
commander := func(ctx context.Context, name string, args ...string) Commander {
return &mockCmd{}
}
w := commander
if c != nil {
w = c
}
return New(100 * 1024 * 1024, "/disk.img", "/mnt", WithFs(afero.NewMemMapFs()), WithCommander(w))
}
func Test_New(t *testing.T) {
t.Run("creates expected struct", func(t *testing.T) {
d := New(100 * 1024 * 1024, "/disk.img", "/mnt")
assert.NotNil(t, d)
assert.Equal(t, int64(100 * 1024 * 1024), d.size)
assert.Equal(t, "/disk.img", d.diskPath)
assert.Equal(t, "/mnt", d.mountAt)
// Ensure by default we get a commander interface returned and that it
// returns an *exec.Cmd.
o := d.commander(context.TODO(), "foo", "-bar")
assert.NotNil(t, o)
_, ok := o.(Commander)
assert.True(t, ok)
_, ok = o.(*exec.Cmd)
assert.True(t, ok)
})
t.Run("creates an instance with custom options", func(t *testing.T) {
fs := afero.NewMemMapFs()
cprov := struct {
Commander
}{}
c := func(ctx context.Context, name string, args ...string) Commander {
return &cprov
}
d := New(100, "/disk.img", "/mnt", WithFs(fs), WithCommander(c))
assert.NotNil(t, d)
assert.Same(t, fs, d.fs)
assert.Same(t, &cprov, d.commander(context.TODO(), ""))
})
t.Run("panics if either path is empty", func(t *testing.T) {
assert.Panics(t, func() {
_ = New(100, "", "/bar")
})
assert.Panics(t, func() {
_ = New(100, "/foo", "")
})
})
}
func TestDisk_Exists(t *testing.T) {
t.Run("it exists", func(t *testing.T) {
d := newMockDisk(nil)
f, err := d.fs.Create("/disk.img")
require.NoError(t, err)
_ = f.Close()
exists, err := d.Exists()
assert.NoError(t, err)
assert.True(t, exists)
})
t.Run("it does not exist", func(t *testing.T) {
d := newMockDisk(nil)
exists, err := d.Exists()
assert.NoError(t, err)
assert.False(t, exists)
})
t.Run("it reports errors", func(t *testing.T) {
d := newMockDisk(nil)
err := d.fs.Mkdir("/disk.img", 0600)
require.NoError(t, err)
exists, err := d.Exists()
assert.Error(t, err)
assert.False(t, exists)
assert.EqualError(t, err, ErrInvalidDiskPathTarget.Error())
})
}
func TestDisk_IsMounted(t *testing.T) {
t.Run("executes command and finds mounted disk", func(t *testing.T) {
is := assert.New(t)
var called bool
pctx := context.TODO()
var cmd CommanderProvider = func(ctx context.Context, name string, args ...string) Commander {
called = true
is.Same(pctx, ctx)
is.Equal("grep", name)
is.Len(args, 3)
is.Equal([]string{"-qs", "/mnt ext4", "/proc/mounts"}, args)
return &mockCmd{}
}
disk := newMockDisk(cmd)
mnt, err := disk.IsMounted(pctx)
is.NoError(err)
is.True(mnt)
is.True(called)
})
t.Run("handles exit code 1 gracefully", func(t *testing.T) {
var called bool
var cmd CommanderProvider = func(ctx context.Context, name string, args ...string) Commander {
called = true
return &mockCmd{
run: func() error {
return &mockedExitCode{code: 1}
},
}
}
disk := newMockDisk(cmd)
mnt, err := disk.IsMounted(context.TODO())
assert.NoError(t, err)
assert.False(t, mnt)
assert.True(t, called)
})
t.Run("handles unexpected errors successfully", func(t *testing.T) {
var cmd CommanderProvider = func(ctx context.Context, name string, args ...string) Commander {
return &mockCmd{
run: func() error {
return &mockedExitCode{code: 3}
},
}
}
disk := newMockDisk(cmd)
mnt, err := disk.IsMounted(context.TODO())
assert.Error(t, err)
assert.False(t, mnt)
})
}
func TestDisk_Mount(t *testing.T) {
failedCmd := func(ctx context.Context, name string, args ...string) Commander {
return &mockCmd{run: func() error {
return &mockedExitCode{code: 1}
}}
}
t.Run("error is returned if mount point is not a directory", func(t *testing.T) {
disk := newMockDisk(failedCmd)
_, err := disk.fs.Create("/mnt")
require.NoError(t, err)
err = disk.Mount(context.TODO())
assert.Error(t, err)
assert.EqualError(t, err, ErrMountPathNotDirectory.Error())
})
t.Run("error is returned if mount point cannot be created", func(t *testing.T) {
disk := newMockDisk(failedCmd)
disk.fs = afero.NewReadOnlyFs(disk.fs)
err := disk.Mount(context.TODO())
assert.Error(t, err)
assert.EqualError(t, err, "vhd: failed to create mount path: operation not permitted")
})
t.Run("error is returned if already mounted", func(t *testing.T) {
disk := newMockDisk(nil)
err := disk.Mount(context.TODO())
assert.Error(t, err)
assert.EqualError(t, err, ErrFilesystemMounted.Error())
})
t.Run("error is returned if mount command fails", func(t *testing.T) {
var called bool
var cmd CommanderProvider = func(ctx context.Context, name string, args ...string) Commander {
return &mockCmd{
run: func() error {
return &mockedExitCode{code: 1}
},
output: func() ([]byte, error) {
called = true
assert.Equal(t, "mount", name)
assert.Equal(t, []string{"-t", "auto", "-o", "loop", "/disk.img", "/mnt"}, args)
return nil, &exec.ExitError{
ProcessState: &os.ProcessState{},
Stderr: []byte("foo bar.\n"),
}
},
}
}
disk := newMockDisk(cmd)
err := disk.Mount(context.TODO())
assert.Error(t, err)
assert.EqualError(t, err, "vhd: failed to mount disk: foo bar: exit status 0")
assert.True(t, called)
})
t.Run("disk can be mounted at existing path", func(t *testing.T) {
disk := newMockDisk(failedCmd)
require.NoError(t, disk.fs.Mkdir("/mnt", 0600))
err := disk.Mount(context.TODO())
assert.NoError(t, err)
})
t.Run("disk can be mounted at non-existing path", func(t *testing.T) {
disk := newMockDisk(failedCmd)
err := disk.Mount(context.TODO())
assert.NoError(t, err)
st, err := disk.fs.Stat("/mnt")
assert.NoError(t, err)
assert.True(t, st.IsDir())
})
}
func TestDisk_Unmount(t *testing.T) {
t.Run("can unmount a disk", func(t *testing.T) {
is := assert.New(t)
pctx := context.TODO()
var called bool
var cmd CommanderProvider = func(ctx context.Context, name string, args ...string) Commander {
called = true
is.Same(pctx, ctx)
is.Equal("umount", name)
is.Equal([]string{"/mnt"}, args)
return &mockCmd{}
}
disk := newMockDisk(cmd)
err := disk.Unmount(pctx)
is.NoError(err)
is.True(called)
})
t.Run("handles exit code 32 correctly", func(t *testing.T) {
var cmd CommanderProvider = func(ctx context.Context, name string, args ...string) Commander {
return &mockCmd{
run: func() error {
return &mockedExitCode{code: 32}
},
}
}
disk := newMockDisk(cmd)
err := disk.Unmount(context.TODO())
assert.NoError(t, err)
})
t.Run("non code 32 errors are returned as error", func(t *testing.T) {
var cmd CommanderProvider = func(ctx context.Context, name string, args ...string) Commander {
return &mockCmd{
run: func() error {
return &mockedExitCode{code: 1}
},
}
}
disk := newMockDisk(cmd)
err := disk.Unmount(context.TODO())
assert.Error(t, err)
})
t.Run("errors without ExitCode function are returned", func(t *testing.T) {
var cmd CommanderProvider = func(ctx context.Context, name string, args ...string) Commander {
return &mockCmd{
run: func() error {
return errors.New("foo bar")
},
}
}
disk := newMockDisk(cmd)
err := disk.Unmount(context.TODO())
assert.Error(t, err)
})
}
func TestDisk_Allocate(t *testing.T) {
t.Run("disk is unmounted before allocating space", func(t *testing.T) {
var called bool
var cmd CommanderProvider = func(ctx context.Context, name string, args ...string) Commander {
return &mockCmd{
output: func() ([]byte, error) {
called = true
assert.Equal(t, "fallocate", name)
assert.Equal(t, []string{"-l", "102400K", "/disk.img"}, args)
return nil, nil
},
}
}
disk := newMockDisk(cmd)
err := disk.fs.Mkdir("/mnt", 0600)
require.NoError(t, err)
err = disk.Allocate(context.TODO())
assert.NoError(t, err)
assert.True(t, called)
})
t.Run("disk space is allocated even when not exists", func(t *testing.T) {
disk := newMockDisk(nil)
err := disk.Allocate(context.TODO())
assert.NoError(t, err)
})
t.Run("error is returned if command fails", func(t *testing.T) {
var cmd CommanderProvider = func(ctx context.Context, name string, args ...string) Commander {
return &mockCmd{
output: func() ([]byte, error) {
return nil, &exec.ExitError{
ProcessState: &os.ProcessState{},
Stderr: []byte("foo bar.\n"),
}
},
}
}
disk := newMockDisk(cmd)
_, err := disk.fs.Create("/disk.img")
require.NoError(t, err)
err = disk.Allocate(context.TODO())
assert.Error(t, err)
assert.EqualError(t, err, "vhd: failed to execute fallocate command: foo bar: exit status 0")
})
}
func TestDisk_MakeFilesystem(t *testing.T) {
t.Run("filesystem is created if not found in /etc/fstab", func(t *testing.T) {
var called bool
var cmd CommanderProvider = func(ctx context.Context, name string, args ...string) Commander {
return &mockCmd{
run: func() error {
// Expect the call from IsMounted here and just return what we need
// to indicate that nothing is currently mounted.
if name == "grep" {
return &mockedExitCode{code: 1}
}
called = true
assert.Equal(t, "mkfs", name)
assert.Equal(t, []string{"-t", "ext4", "/disk.img"}, args)
return nil
},
output: func() ([]byte, error) {
return nil, errors.New("error: can't find in /etc/fstab foo bar testing")
},
}
}
disk := newMockDisk(cmd)
err := disk.MakeFilesystem(context.TODO())
assert.NoError(t, err)
assert.True(t, called)
})
t.Run("filesystem is created if error is returned from mount command", func(t *testing.T) {
var called bool
var cmd CommanderProvider = func(ctx context.Context, name string, args ...string) Commander {
return &mockCmd{
run: func() error {
// Expect the call from IsMounted here and just return what we need
// to indicate that nothing is currently mounted.
if name == "grep" {
return &mockedExitCode{code: 1}
}
called = true
assert.Equal(t, "mkfs", name)
assert.Equal(t, []string{"-t", "ext4", "/disk.img"}, args)
return nil
},
output: func() ([]byte, error) {
if name == "mount" {
return nil, &exec.ExitError{
Stderr: []byte("foo bar: exit status 32\n"),
}
}
return nil, nil
},
}
}
disk := newMockDisk(cmd)
err := disk.MakeFilesystem(context.TODO())
assert.NoError(t, err)
assert.True(t, called)
})
t.Run("error is returned if currently mounted", func(t *testing.T) {
disk := newMockDisk(nil)
err := disk.MakeFilesystem(context.TODO())
assert.Error(t, err)
assert.EqualError(t, err, ErrFilesystemExists.Error())
})
}

View File

@@ -17,7 +17,6 @@ import (
"github.com/cenkalti/backoff/v4"
"github.com/goccy/go-json"
"github.com/pterodactyl/wings/config"
"github.com/pterodactyl/wings/system"
)
@@ -30,7 +29,7 @@ type Client interface {
SetArchiveStatus(ctx context.Context, uuid string, successful bool) error
SetBackupStatus(ctx context.Context, backup string, data BackupRequest) error
SendRestorationStatus(ctx context.Context, backup string, successful bool) error
SetInstallationStatus(ctx context.Context, uuid string, successful bool) error
SetInstallationStatus(ctx context.Context, uuid string, data InstallStatusRequest) error
SetTransferStatus(ctx context.Context, uuid string, successful bool) error
ValidateSftpCredentials(ctx context.Context, request SftpAuthRequest) (SftpAuthResponse, error)
SendActivityLogs(ctx context.Context, activity []models.Activity) error
@@ -60,18 +59,6 @@ func New(base string, opts ...ClientOption) Client {
return &c
}
// NewFromConfig returns a new Client using the configuration passed through
// by the caller.
func NewFromConfig(cfg *config.Configuration, opts ...ClientOption) Client {
passOpts := []ClientOption{
WithCredentials(cfg.AuthenticationTokenId, cfg.AuthenticationToken),
WithHttpClient(&http.Client{
Timeout: time.Second * time.Duration(cfg.RemoteQuery.Timeout),
}),
}
return New(cfg.PanelLocation, append(passOpts, opts...)...)
}
// WithCredentials sets the credentials to use when making request to the remote
// API endpoint.
func WithCredentials(id, token string) ClientOption {

View File

@@ -19,7 +19,7 @@ const (
ProcessStopNativeStop = "stop"
)
// GetServers returns all of the servers that are present on the Panel making
// GetServers returns all the servers that are present on the Panel making
// parallel API calls to the endpoint if more than one page of servers is
// returned.
func (c *client) GetServers(ctx context.Context, limit int) ([]RawServerData, error) {
@@ -58,7 +58,7 @@ func (c *client) GetServers(ctx context.Context, limit int) ([]RawServerData, er
//
// This handles Wings exiting during either of these processes which will leave
// things in a bad state within the Panel. This API call is executed once Wings
// has fully booted all of the servers.
// has fully booted all the servers.
func (c *client) ResetServersState(ctx context.Context) error {
res, err := c.Post(ctx, "/servers/reset", nil)
if err != nil {
@@ -92,8 +92,8 @@ func (c *client) GetInstallationScript(ctx context.Context, uuid string) (Instal
return config, err
}
func (c *client) SetInstallationStatus(ctx context.Context, uuid string, successful bool) error {
resp, err := c.Post(ctx, fmt.Sprintf("/servers/%s/install", uuid), d{"successful": successful})
func (c *client) SetInstallationStatus(ctx context.Context, uuid string, data InstallStatusRequest) error {
resp, err := c.Post(ctx, fmt.Sprintf("/servers/%s/install", uuid), data)
if err != nil {
return err
}
@@ -127,7 +127,7 @@ func (c *client) SetTransferStatus(ctx context.Context, uuid string, successful
// password combination provided is associated with a valid server on the instance
// using the Panel's authentication control mechanisms. This will get itself
// throttled if too many requests are made, allowing us to completely offload
// all of the authorization security logic to the Panel.
// all the authorization security logic to the Panel.
func (c *client) ValidateSftpCredentials(ctx context.Context, request SftpAuthRequest) (SftpAuthResponse, error) {
var auth SftpAuthResponse
res, err := c.Post(ctx, "/sftp/auth", request)

View File

@@ -92,8 +92,8 @@ type SftpAuthResponse struct {
}
type OutputLineMatcher struct {
// The raw string to match against. This may or may not be prefixed with
// regex: which indicates we want to match against the regex expression.
// raw string to match against. This may or may not be prefixed with
// `regex:` which indicates we want to match against the regex expression.
raw []byte
reg *regexp.Regexp
}
@@ -139,9 +139,9 @@ type ProcessStopConfiguration struct {
}
// ProcessConfiguration defines the process configuration for a given server
// instance. This sets what Wings is looking for to mark a server as done starting
// what to do when stopping, and what changes to make to the configuration file
// for a server.
// instance. This sets what Wings is looking for to mark a server as done
// starting what to do when stopping, and what changes to make to the
// configuration file for a server.
type ProcessConfiguration struct {
Startup struct {
Done []*OutputLineMatcher `json:"done"`
@@ -169,3 +169,8 @@ type BackupRequest struct {
Successful bool `json:"successful"`
Parts []BackupPart `json:"parts"`
}
type InstallStatusRequest struct {
Successful bool `json:"successful"`
Reinstall bool `json:"reinstall"`
}

View File

@@ -1,157 +0,0 @@
package router
import (
"fmt"
"net/http"
"os"
"strings"
"emperror.dev/errors"
"github.com/apex/log"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/pterodactyl/wings/server"
"github.com/pterodactyl/wings/server/filesystem"
)
type RequestError struct {
err error
uuid string
message string
server *server.Server
}
// Attaches an error to the gin.Context object for the request and ensures that it
// has a proper stacktrace associated with it when doing so.
//
// If you just call c.Error(err) without using this function you'll likely end up
// with an error that has no annotated stack on it.
func WithError(c *gin.Context, err error) error {
return c.Error(errors.WithStackDepthIf(err, 1))
}
// Generates a new tracked error, which simply tracks the specific error that
// is being passed in, and also assigned a UUID to the error so that it can be
// cross referenced in the logs.
func NewTrackedError(err error) *RequestError {
return &RequestError{
err: err,
uuid: uuid.Must(uuid.NewRandom()).String(),
}
}
// Same as NewTrackedError, except this will also attach the server instance that
// generated this server for the purposes of logging.
func NewServerError(err error, s *server.Server) *RequestError {
return &RequestError{
err: err,
uuid: uuid.Must(uuid.NewRandom()).String(),
server: s,
}
}
func (e *RequestError) logger() *log.Entry {
if e.server != nil {
return e.server.Log().WithField("error_id", e.uuid).WithField("error", e.err)
}
return log.WithField("error_id", e.uuid).WithField("error", e.err)
}
// Sets the output message to display to the user in the error.
func (e *RequestError) SetMessage(msg string) *RequestError {
e.message = msg
return e
}
// Aborts the request with the given status code, and responds with the error. This
// will also include the error UUID in the output so that the user can report that
// and link the response to a specific error in the logs.
func (e *RequestError) AbortWithStatus(status int, c *gin.Context) {
// In instances where the status has already been set just use that existing status
// since we cannot change it at this point, and trying to do so will emit a gin warning
// into the program output.
if c.Writer.Status() != 200 {
status = c.Writer.Status()
}
// If this error is because the resource does not exist, we likely do not need to log
// the error anywhere, just return a 404 and move on with our lives.
if errors.Is(e.err, os.ErrNotExist) {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{
"error": "The requested resource was not found on the system.",
})
return
}
if strings.HasPrefix(e.err.Error(), "invalid URL escape") {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{
"error": "Some of the data provided in the request appears to be escaped improperly.",
})
return
}
// If this is a Filesystem error just return it without all of the tracking code nonsense
// since we don't need to be logging it into the logs or anything, its just a normal error
// that the user can solve on their end.
if st, msg := e.getAsFilesystemError(); st != 0 {
c.AbortWithStatusJSON(st, gin.H{"error": msg})
return
}
// Otherwise, log the error to zap, and then report the error back to the user.
if status >= 500 {
e.logger().Error("unexpected error while handling HTTP request")
} else {
e.logger().Debug("non-server error encountered while handling HTTP request")
}
if e.message == "" {
e.message = "An unexpected error was encountered while processing this request."
}
c.AbortWithStatusJSON(status, gin.H{"error": e.message, "error_id": e.uuid})
}
// Helper function to just abort with an internal server error. This is generally the response
// from most errors encountered by the API.
func (e *RequestError) Abort(c *gin.Context) {
e.AbortWithStatus(http.StatusInternalServerError, c)
}
// Looks at the given RequestError and determines if it is a specific filesystem error that
// we can process and return differently for the user.
func (e *RequestError) getAsFilesystemError() (int, string) {
// Some external things end up calling fmt.Errorf() on our filesystem errors
// which ends up just unleashing chaos on the system. For the sake of this
// fallback to using text checks...
if filesystem.IsErrorCode(e.err, filesystem.ErrCodeDenylistFile) || strings.Contains(e.err.Error(), "filesystem: file access prohibited") {
return http.StatusForbidden, "This file cannot be modified: present in egg denylist."
}
if filesystem.IsErrorCode(e.err, filesystem.ErrCodePathResolution) || strings.Contains(e.err.Error(), "resolves to a location outside the server root") {
return http.StatusNotFound, "The requested resource was not found on the system."
}
if filesystem.IsErrorCode(e.err, filesystem.ErrCodeIsDirectory) || strings.Contains(e.err.Error(), "filesystem: is a directory") {
return http.StatusBadRequest, "Cannot perform that action: file is a directory."
}
if filesystem.IsErrorCode(e.err, filesystem.ErrCodeDiskSpace) || strings.Contains(e.err.Error(), "filesystem: not enough disk space") {
return http.StatusBadRequest, "Cannot perform that action: not enough disk space available."
}
if strings.HasSuffix(e.err.Error(), "file name too long") {
return http.StatusBadRequest, "Cannot perform that action: file name is too long."
}
if e, ok := e.err.(*os.SyscallError); ok && e.Syscall == "readdirent" {
return http.StatusNotFound, "The requested directory does not exist."
}
return 0, ""
}
// Handle specific filesystem errors for a server.
func (e *RequestError) AbortFilesystemError(c *gin.Context) {
e.Abort(c)
}
// Format the error to a string and include the UUID.
func (e *RequestError) Error() string {
return fmt.Sprintf("%v (uuid: %s)", e.err, e.uuid)
}

View File

@@ -1,11 +1,9 @@
package middleware
import (
"context"
"crypto/subtle"
"io"
"net/http"
"os"
"strings"
"emperror.dev/errors"
@@ -16,133 +14,8 @@ import (
"github.com/pterodactyl/wings/config"
"github.com/pterodactyl/wings/remote"
"github.com/pterodactyl/wings/server"
"github.com/pterodactyl/wings/server/filesystem"
)
// RequestError is a custom error type returned when something goes wrong with
// any of the HTTP endpoints.
type RequestError struct {
err error
status int
msg string
}
// NewError returns a new RequestError for the provided error.
func NewError(err error) *RequestError {
return &RequestError{
// Attach a stacktrace to the error if it is missing at this point and mark it
// as originating from the location where NewError was called, rather than this
// specific point in the code.
err: errors.WithStackDepthIf(err, 1),
}
}
// SetMessage allows for a custom error message to be set on an existing
// RequestError instance.
func (re *RequestError) SetMessage(m string) {
re.msg = m
}
// SetStatus sets the HTTP status code for the error response. By default this
// is a HTTP-500 error.
func (re *RequestError) SetStatus(s int) {
re.status = s
}
// Abort aborts the given HTTP request with the specified status code and then
// logs the event into the logs. The error that is output will include the unique
// request ID if it is present.
func (re *RequestError) Abort(c *gin.Context, status int) {
reqId := c.Writer.Header().Get("X-Request-Id")
// Generate the base logger instance, attaching the unique request ID and
// the URL that was requested.
event := log.WithField("request_id", reqId).WithField("url", c.Request.URL.String())
// If there is a server present in the gin.Context stack go ahead and pull it
// and attach that server UUID to the logs as well so that we can see what specific
// server triggered this error.
if s, ok := c.Get("server"); ok {
if s, ok := s.(*server.Server); ok {
event = event.WithField("server_id", s.ID())
}
}
if c.Writer.Status() == 200 {
// Handle context deadlines being exceeded a little differently since we want
// to report a more user-friendly error and a proper error code. The "context
// canceled" error is generally when a request is terminated before all of the
// logic is finished running.
if errors.Is(re.err, context.DeadlineExceeded) {
re.SetStatus(http.StatusGatewayTimeout)
re.SetMessage("The server could not process this request in time, please try again.")
} else if strings.Contains(re.Cause().Error(), "context canceled") {
re.SetStatus(http.StatusBadRequest)
re.SetMessage("Request aborted by client.")
}
}
// c.Writer.Status() will be a non-200 value if the headers have already been sent
// to the requester but an error is encountered. This can happen if there is an issue
// marshaling a struct placed into a c.JSON() call (or c.AbortWithJSON() call).
if status >= 500 || c.Writer.Status() != 200 {
event.WithField("status", status).WithField("error", re.err).Error("error while handling HTTP request")
} else {
event.WithField("status", status).WithField("error", re.err).Debug("error handling HTTP request (not a server error)")
}
if re.msg == "" {
re.msg = "An unexpected error was encountered while processing this request"
}
// Now abort the request with the error message and include the unique request
// ID that was present to make things super easy on people who don't know how
// or cannot view the response headers (where X-Request-Id would be present).
c.AbortWithStatusJSON(status, gin.H{"error": re.msg, "request_id": reqId})
}
// Cause returns the underlying error.
func (re *RequestError) Cause() error {
return re.err
}
// Error returns the underlying error message for this request.
func (re *RequestError) Error() string {
return re.err.Error()
}
// Looks at the given RequestError and determines if it is a specific filesystem
// error that we can process and return differently for the user.
//
// Some external things end up calling fmt.Errorf() on our filesystem errors
// which ends up just unleashing chaos on the system. For the sake of this,
// fallback to using text checks.
//
// If the error passed into this call is nil or does not match empty values will
// be returned to the caller.
func (re *RequestError) asFilesystemError() (int, string) {
err := re.Cause()
if err == nil {
return 0, ""
}
if filesystem.IsErrorCode(err, filesystem.ErrCodeDenylistFile) || strings.Contains(err.Error(), "filesystem: file access prohibited") {
return http.StatusForbidden, "This file cannot be modified: present in egg denylist."
}
if filesystem.IsErrorCode(err, filesystem.ErrCodePathResolution) || strings.Contains(err.Error(), "resolves to a location outside the server root") {
return http.StatusNotFound, "The requested resource was not found on the system."
}
if filesystem.IsErrorCode(err, filesystem.ErrCodeIsDirectory) || strings.Contains(err.Error(), "filesystem: is a directory") {
return http.StatusBadRequest, "Cannot perform that action: file is a directory."
}
if filesystem.IsErrorCode(err, filesystem.ErrCodeDiskSpace) || strings.Contains(err.Error(), "filesystem: not enough disk space") {
return http.StatusBadRequest, "There is not enough disk space available to perform that action."
}
if strings.HasSuffix(err.Error(), "file name too long") {
return http.StatusBadRequest, "Cannot perform that action: file name is too long."
}
if e, ok := err.(*os.SyscallError); ok && e.Syscall == "readdirent" {
return http.StatusNotFound, "The requested directory does not exist."
}
return 0, ""
}
// AttachRequestID attaches a unique ID to the incoming HTTP request so that any
// errors that are generated or returned to the client will include this reference
// allowing for an easier time identifying the specific request that failed for
@@ -180,7 +53,7 @@ func AttachApiClient(client remote.Client) gin.HandlerFunc {
}
// CaptureAndAbort aborts the request and attaches the provided error to the gin
// context so it can be reported properly. If the error is missing a stacktrace
// context, so it can be reported properly. If the error is missing a stacktrace
// at the time it is called the stack will be attached.
func CaptureAndAbort(c *gin.Context, err error) {
c.Abort()

View File

@@ -0,0 +1,141 @@
package middleware
import (
"context"
"net/http"
"os"
"strings"
"emperror.dev/errors"
"github.com/apex/log"
"github.com/gin-gonic/gin"
"github.com/pterodactyl/wings/server"
"github.com/pterodactyl/wings/server/filesystem"
)
// RequestError is a custom error type returned when something goes wrong with
// any of the HTTP endpoints.
type RequestError struct {
err error
status int
msg string
}
// NewError returns a new RequestError for the provided error.
func NewError(err error) *RequestError {
return &RequestError{
// Attach a stacktrace to the error if it is missing at this point and mark it
// as originating from the location where NewError was called, rather than this
// specific point in the code.
err: errors.WithStackDepthIf(err, 1),
}
}
// SetMessage allows for a custom error message to be set on an existing
// RequestError instance.
func (re *RequestError) SetMessage(m string) {
re.msg = m
}
// SetStatus sets the HTTP status code for the error response. By default this
// is a HTTP-500 error.
func (re *RequestError) SetStatus(s int) {
re.status = s
}
// Abort aborts the given HTTP request with the specified status code and then
// logs the event into the logs. The error that is output will include the unique
// request ID if it is present.
func (re *RequestError) Abort(c *gin.Context, status int) {
reqId := c.Writer.Header().Get("X-Request-Id")
// Generate the base logger instance, attaching the unique request ID and
// the URL that was requested.
event := log.WithField("request_id", reqId).WithField("url", c.Request.URL.String())
// If there is a server present in the gin.Context stack go ahead and pull it
// and attach that server UUID to the logs as well so that we can see what specific
// server triggered this error.
if s, ok := c.Get("server"); ok {
if s, ok := s.(*server.Server); ok {
event = event.WithField("server_id", s.ID())
}
}
if c.Writer.Status() == 200 {
// Handle context deadlines being exceeded a little differently since we want
// to report a more user-friendly error and a proper error code. The "context
// canceled" error is generally when a request is terminated before all of the
// logic is finished running.
if errors.Is(re.err, context.DeadlineExceeded) {
re.SetStatus(http.StatusGatewayTimeout)
re.SetMessage("The server could not process this request in time, please try again.")
} else if strings.Contains(re.Cause().Error(), "context canceled") {
re.SetStatus(http.StatusBadRequest)
re.SetMessage("Request aborted by client.")
}
}
// c.Writer.Status() will be a non-200 value if the headers have already been sent
// to the requester but an error is encountered. This can happen if there is an issue
// marshaling a struct placed into a c.JSON() call (or c.AbortWithJSON() call).
if status >= 500 || c.Writer.Status() != 200 {
event.WithField("status", status).WithField("error", re.err).Error("error while handling HTTP request")
} else {
event.WithField("status", status).WithField("error", re.err).Debug("error handling HTTP request (not a server error)")
}
if re.msg == "" {
re.msg = "An unexpected error was encountered while processing this request"
}
// Now abort the request with the error message and include the unique request
// ID that was present to make things super easy on people who don't know how
// or cannot view the response headers (where X-Request-Id would be present).
c.AbortWithStatusJSON(status, gin.H{"error": re.msg, "request_id": reqId})
}
// Cause returns the underlying error.
func (re *RequestError) Cause() error {
return re.err
}
// Error returns the underlying error message for this request.
func (re *RequestError) Error() string {
return re.err.Error()
}
// Looks at the given RequestError and determines if it is a specific filesystem
// error that we can process and return differently for the user.
//
// Some external things end up calling fmt.Errorf() on our filesystem errors
// which ends up just unleashing chaos on the system. For the sake of this,
// fallback to using text checks.
//
// If the error passed into this call is nil or does not match empty values will
// be returned to the caller.
func (re *RequestError) asFilesystemError() (int, string) {
err := re.Cause()
if err == nil {
return 0, ""
}
if filesystem.IsErrorCode(err, filesystem.ErrNotExist) ||
filesystem.IsErrorCode(err, filesystem.ErrCodePathResolution) ||
strings.Contains(err.Error(), "resolves to a location outside the server root") {
return http.StatusNotFound, "The requested resources was not found on the system."
}
if filesystem.IsErrorCode(err, filesystem.ErrCodeDenylistFile) || strings.Contains(err.Error(), "filesystem: file access prohibited") {
return http.StatusForbidden, "This file cannot be modified: present in egg denylist."
}
if filesystem.IsErrorCode(err, filesystem.ErrCodeIsDirectory) || strings.Contains(err.Error(), "filesystem: is a directory") {
return http.StatusBadRequest, "Cannot perform that action: file is a directory."
}
if filesystem.IsErrorCode(err, filesystem.ErrCodeDiskSpace) || strings.Contains(err.Error(), "filesystem: not enough disk space") {
return http.StatusBadRequest, "There is not enough disk space available to perform that action."
}
if strings.HasSuffix(err.Error(), "file name too long") {
return http.StatusBadRequest, "Cannot perform that action: file name is too long."
}
if e, ok := err.(*os.SyscallError); ok && e.Syscall == "readdirent" {
return http.StatusNotFound, "The requested directory does not exist."
}
return 0, ""
}

View File

@@ -1,6 +1,7 @@
package router
import (
"emperror.dev/errors"
"github.com/apex/log"
"github.com/gin-gonic/gin"
@@ -16,7 +17,10 @@ func Configure(m *wserver.Manager, client remote.Client) *gin.Engine {
router := gin.New()
router.Use(gin.Recovery())
router.SetTrustedProxies(config.Get().Api.TrustedProxies)
if err := router.SetTrustedProxies(config.Get().Api.TrustedProxies); err != nil {
panic(errors.WithStack(err))
return nil
}
router.Use(middleware.AttachRequestID(), middleware.CaptureErrors(), middleware.SetAccessControlHeaders())
router.Use(middleware.AttachServerManager(m), middleware.AttachApiClient(client))
// @todo log this into a different file so you can setup IP blocking for abusive requests and such.
@@ -40,7 +44,7 @@ func Configure(m *wserver.Manager, client remote.Client) *gin.Engine {
router.GET("/download/file", getDownloadFile)
router.POST("/upload/file", postServerUploadFiles)
// This route is special it sits above all of the other requests because we are
// This route is special it sits above all the other requests because we are
// using a JWT to authorize access to it, therefore it needs to be publicly
// accessible.
router.GET("/api/servers/:server/ws", middleware.ServerExists(), getServerWebsocket)
@@ -48,16 +52,16 @@ func Configure(m *wserver.Manager, client remote.Client) *gin.Engine {
// This request is called by another daemon when a server is going to be transferred out.
// This request does not need the AuthorizationMiddleware as the panel should never call it
// and requests are authenticated through a JWT the panel issues to the other daemon.
router.GET("/api/servers/:server/archive", middleware.ServerExists(), getServerArchive)
router.POST("/api/transfers", postTransfers)
// All of the routes beyond this mount will use an authorization middleware
// All the routes beyond this mount will use an authorization middleware
// and will not be accessible without the correct Authorization header provided.
protected := router.Use(middleware.RequireAuthorization())
protected.POST("/api/update", postUpdateConfiguration)
protected.GET("/api/system", getSystemInformation)
protected.GET("/api/servers", getAllServers)
protected.POST("/api/servers", postCreateServer)
protected.POST("/api/transfer", postTransfer)
protected.DELETE("/api/transfers/:server", deleteTransfer)
// These are server specific routes, and require that the request be authorized, and
// that the server exist on the Daemon.
@@ -77,7 +81,8 @@ func Configure(m *wserver.Manager, client remote.Client) *gin.Engine {
// This archive request causes the archive to start being created
// this should only be triggered by the panel.
server.POST("/archive", postServerArchive)
server.POST("/transfer", postServerTransfer)
server.DELETE("/transfer", deleteServerTransfer)
files := server.Group("/files")
{

View File

@@ -21,12 +21,11 @@ func getDownloadBackup(c *gin.Context) {
token := tokens.BackupPayload{}
if err := tokens.ParseToken([]byte(c.Query("token")), &token); err != nil {
NewTrackedError(err).Abort(c)
middleware.CaptureAndAbort(c, err)
return
}
s, ok := manager.Get(token.ServerUuid)
if !ok || !token.IsUniqueRequest() {
if _, ok := manager.Get(token.ServerUuid); !ok || !token.IsUniqueRequest() {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{
"error": "The requested resource was not found on this server.",
})
@@ -42,13 +41,13 @@ func getDownloadBackup(c *gin.Context) {
return
}
NewServerError(err, s).Abort(c)
middleware.CaptureAndAbort(c, err)
return
}
f, err := os.Open(b.Path())
if err != nil {
NewServerError(err, s).Abort(c)
middleware.CaptureAndAbort(c, err)
return
}
defer f.Close()
@@ -57,7 +56,7 @@ func getDownloadBackup(c *gin.Context) {
c.Header("Content-Disposition", "attachment; filename="+strconv.Quote(st.Name()))
c.Header("Content-Type", "application/octet-stream")
bufio.NewReader(f).WriteTo(c.Writer)
_, _ = bufio.NewReader(f).WriteTo(c.Writer)
}
// Handles downloading a specific file for a server.
@@ -65,7 +64,7 @@ func getDownloadFile(c *gin.Context) {
manager := middleware.ExtractManager(c)
token := tokens.FilePayload{}
if err := tokens.ParseToken([]byte(c.Query("token")), &token); err != nil {
NewTrackedError(err).Abort(c)
middleware.CaptureAndAbort(c, err)
return
}
@@ -82,7 +81,7 @@ func getDownloadFile(c *gin.Context) {
// If there is an error or we're somehow trying to download a directory, just
// respond with the appropriate error.
if err != nil {
NewServerError(err, s).Abort(c)
middleware.CaptureAndAbort(c, err)
return
} else if st.IsDir() {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{
@@ -93,7 +92,7 @@ func getDownloadFile(c *gin.Context) {
f, err := os.Open(p)
if err != nil {
NewServerError(err, s).Abort(c)
middleware.CaptureAndAbort(c, err)
return
}
@@ -101,5 +100,5 @@ func getDownloadFile(c *gin.Context) {
c.Header("Content-Disposition", "attachment; filename="+strconv.Quote(st.Name()))
c.Header("Content-Type", "application/octet-stream")
bufio.NewReader(f).WriteTo(c.Writer)
_, _ = bufio.NewReader(f).WriteTo(c.Writer)
}

View File

@@ -14,6 +14,7 @@ import (
"github.com/pterodactyl/wings/router/middleware"
"github.com/pterodactyl/wings/router/tokens"
"github.com/pterodactyl/wings/server"
"github.com/pterodactyl/wings/server/transfer"
)
// Returns a single server from the collection of servers.
@@ -34,7 +35,7 @@ func getServerLogs(c *gin.Context) {
out, err := s.ReadLogfile(l)
if err != nil {
NewServerError(err, s).Abort(c)
middleware.CaptureAndAbort(c, err)
return
}
@@ -108,7 +109,7 @@ func postServerCommands(c *gin.Context) {
s := ExtractServer(c)
if running, err := s.Environment.IsRunning(c.Request.Context()); err != nil {
NewServerError(err, s).Abort(c)
middleware.CaptureAndAbort(c, err)
return
} else if !running {
c.AbortWithStatusJSON(http.StatusBadGateway, gin.H{
@@ -142,7 +143,7 @@ func postServerSync(c *gin.Context) {
s := ExtractServer(c)
if err := s.Sync(); err != nil {
WithError(c, err)
middleware.CaptureAndAbort(c, err)
} else {
c.Status(http.StatusNoContent)
}
@@ -152,9 +153,15 @@ func postServerSync(c *gin.Context) {
func postServerInstall(c *gin.Context) {
s := ExtractServer(c)
go func(serv *server.Server) {
if err := serv.Install(true); err != nil {
serv.Log().WithField("error", err).Error("failed to execute server installation process")
go func(s *server.Server) {
s.Log().Info("syncing server state with remote source before executing installation process")
if err := s.Sync(); err != nil {
s.Log().WithField("error", err).Error("failed to sync server state with Panel")
return
}
if err := s.Install(); err != nil {
s.Log().WithField("error", err).Error("failed to execute server installation process")
}
}(s)
@@ -188,6 +195,17 @@ func deleteServer(c *gin.Context) {
// Immediately suspend the server to prevent a user from attempting
// to start it while this process is running.
s.Config().SetSuspended(true)
// Notify all websocket clients that the server is being deleted.
// This is useful for two reasons, one to tell clients not to bother
// retrying to connect to the websocket. And two, for transfers when
// the server has been successfully transferred to another node, and
// the client needs to switch to the new node.
if s.IsTransferring() {
s.Events().Publish(server.TransferStatusEvent, transfer.StatusCompleted)
}
s.Events().Publish(server.DeletedEvent, nil)
s.CleanupForDestroy()
// Remove any pending remote file downloads for the server.
@@ -199,7 +217,7 @@ func deleteServer(c *gin.Context) {
// forcibly terminate it before removing the container, so we do not need to handle
// that here.
if err := s.Environment.Destroy(); err != nil {
WithError(c, err)
middleware.CaptureAndAbort(c, err)
return
}
@@ -207,7 +225,7 @@ func deleteServer(c *gin.Context) {
// done in a separate process since failure is not the end of the world and can be
// manually cleaned up after the fact.
//
// In addition, servers with large amounts of files can take some time to finish deleting
// In addition, servers with large amounts of files can take some time to finish deleting,
// so we don't want to block the HTTP call while waiting on this.
go func(p string) {
if err := os.RemoveAll(p); err != nil {

View File

@@ -13,15 +13,13 @@ import (
"strconv"
"strings"
"github.com/pterodactyl/wings/internal/models"
"github.com/pterodactyl/wings/config"
"emperror.dev/errors"
"github.com/apex/log"
"github.com/gin-gonic/gin"
"golang.org/x/sync/errgroup"
"github.com/pterodactyl/wings/config"
"github.com/pterodactyl/wings/internal/models"
"github.com/pterodactyl/wings/router/downloader"
"github.com/pterodactyl/wings/router/middleware"
"github.com/pterodactyl/wings/router/tokens"
@@ -81,7 +79,7 @@ func getServerListDirectory(c *gin.Context) {
s := ExtractServer(c)
dir := c.Query("directory")
if stats, err := s.Filesystem().ListDirectory(dir); err != nil {
WithError(c, err)
middleware.CaptureAndAbort(c, err)
} else {
c.JSON(http.StatusOK, stats)
}
@@ -154,7 +152,7 @@ func putServerRenameFiles(c *gin.Context) {
return
}
NewServerError(err, s).AbortFilesystemError(c)
middleware.CaptureAndAbort(c, err)
return
}
@@ -174,11 +172,11 @@ func postServerCopyFile(c *gin.Context) {
}
if err := s.Filesystem().IsIgnored(data.Location); err != nil {
NewServerError(err, s).Abort(c)
middleware.CaptureAndAbort(c, err)
return
}
if err := s.Filesystem().Copy(data.Location); err != nil {
NewServerError(err, s).AbortFilesystemError(c)
middleware.CaptureAndAbort(c, err)
return
}
@@ -223,7 +221,7 @@ func postServerDeleteFiles(c *gin.Context) {
}
if err := g.Wait(); err != nil {
NewServerError(err, s).Abort(c)
middleware.CaptureAndAbort(c, err)
return
}
@@ -238,7 +236,7 @@ func postServerWriteFile(c *gin.Context) {
f = "/" + strings.TrimLeft(f, "/")
if err := s.Filesystem().IsIgnored(f); err != nil {
NewServerError(err, s).Abort(c)
middleware.CaptureAndAbort(c, err)
return
}
if err := s.Filesystem().Writefile(f, c.Request.Body); err != nil {
@@ -249,7 +247,7 @@ func postServerWriteFile(c *gin.Context) {
return
}
NewServerError(err, s).AbortFilesystemError(c)
middleware.CaptureAndAbort(c, err)
return
}
@@ -296,12 +294,12 @@ func postServerPullRemoteFile(c *gin.Context) {
})
return
}
WithError(c, err)
middleware.CaptureAndAbort(c, err)
return
}
if err := s.Filesystem().HasSpaceErr(true); err != nil {
WithError(c, err)
middleware.CaptureAndAbort(c, err)
return
}
// Do not allow more than three simultaneous remote file downloads at one time.
@@ -340,13 +338,13 @@ func postServerPullRemoteFile(c *gin.Context) {
}
if err := download(); err != nil {
NewServerError(err, s).Abort(c)
middleware.CaptureAndAbort(c, err)
return
}
st, err := s.Filesystem().Stat(dl.Path())
if err != nil {
NewServerError(err, s).AbortFilesystemError(c)
middleware.CaptureAndAbort(c, err)
return
}
c.JSON(http.StatusOK, &st)
@@ -382,7 +380,7 @@ func postServerCreateDirectory(c *gin.Context) {
return
}
NewServerError(err, s).Abort(c)
middleware.CaptureAndAbort(c, err)
return
}
@@ -417,7 +415,7 @@ func postServerCompressFiles(c *gin.Context) {
f, err := s.Filesystem().CompressFiles(data.RootPath, data.Files)
if err != nil {
NewServerError(err, s).AbortFilesystemError(c)
middleware.CaptureAndAbort(c, err)
return
}
@@ -442,7 +440,7 @@ func postServerDecompressFiles(c *gin.Context) {
s := middleware.ExtractServer(c)
lg := middleware.ExtractLogger(c).WithFields(log.Fields{"root_path": data.RootPath, "file": data.File})
lg.Debug("checking if space is available for file decompression")
err := s.Filesystem().SpaceAvailableForDecompression(data.RootPath, data.File)
err := s.Filesystem().SpaceAvailableForDecompression(context.Background(), data.RootPath, data.File)
if err != nil {
if filesystem.IsErrorCode(err, filesystem.ErrCodeUnknownArchive) {
lg.WithField("error", err).Warn("failed to decompress file: unknown archive format")
@@ -454,7 +452,7 @@ func postServerDecompressFiles(c *gin.Context) {
}
lg.Info("starting file decompression")
if err := s.Filesystem().DecompressFile(data.RootPath, data.File); err != nil {
if err := s.Filesystem().DecompressFile(context.Background(), data.RootPath, data.File); err != nil {
// If the file is busy for some reason just return a nicer error to the user since there is not
// much we specifically can do. They'll need to stop the running server process in order to overwrite
// a file like this.
@@ -535,7 +533,7 @@ func postServerChmodFile(c *gin.Context) {
return
}
NewServerError(err, s).AbortFilesystemError(c)
middleware.CaptureAndAbort(c, err)
return
}
@@ -547,7 +545,7 @@ func postServerUploadFiles(c *gin.Context) {
token := tokens.UploadPayload{}
if err := tokens.ParseToken([]byte(c.Query("token")), &token); err != nil {
NewTrackedError(err).Abort(c)
middleware.CaptureAndAbort(c, err)
return
}
@@ -593,14 +591,14 @@ func postServerUploadFiles(c *gin.Context) {
for _, header := range headers {
p, err := s.Filesystem().SafePath(filepath.Join(directory, header.Filename))
if err != nil {
NewServerError(err, s).Abort(c)
middleware.CaptureAndAbort(c, err)
return
}
// We run this in a different method so I can use defer without any of
// the consequences caused by calling it in a loop.
if err := handleFileUpload(p, s, header); err != nil {
NewServerError(err, s).Abort(c)
middleware.CaptureAndAbort(c, err)
return
} else {
s.SaveActivity(s.NewRequestActivity(token.UserUuid, c.ClientIP()), server.ActivityFileUploaded, models.ActivityMeta{

View File

@@ -0,0 +1,129 @@
package router
import (
"context"
"net/http"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/pterodactyl/wings/environment"
"github.com/pterodactyl/wings/router/middleware"
"github.com/pterodactyl/wings/server"
"github.com/pterodactyl/wings/server/installer"
"github.com/pterodactyl/wings/server/transfer"
)
// Data passed over to initiate a server transfer.
type serverTransferRequest struct {
URL string `binding:"required" json:"url"`
Token string `binding:"required" json:"token"`
Server installer.ServerDetails `json:"server"`
}
// postServerTransfer handles the start of a transfer for a server.
func postServerTransfer(c *gin.Context) {
var data serverTransferRequest
if err := c.BindJSON(&data); err != nil {
return
}
s := ExtractServer(c)
// Check if the server is already being transferred.
// There will be another endpoint for resetting this value either by deleting the
// server, or by canceling the transfer.
if s.IsTransferring() {
c.AbortWithStatusJSON(http.StatusConflict, gin.H{
"error": "A transfer is already in progress for this server.",
})
return
}
manager := middleware.ExtractManager(c)
notifyPanelOfFailure := func() {
if err := manager.Client().SetTransferStatus(context.Background(), s.ID(), false); err != nil {
s.Log().WithField("subsystem", "transfer").
WithField("status", false).
WithError(err).
Error("failed to set transfer status")
}
s.Events().Publish(server.TransferStatusEvent, "failure")
s.SetTransferring(false)
}
// Block the server from starting while we are transferring it.
s.SetTransferring(true)
// Ensure the server is offline. Sometimes a "No such container" error gets through
// which means the server is already stopped. We can ignore that.
if s.Environment.State() != environment.ProcessOfflineState {
if err := s.Environment.WaitForStop(
s.Context(),
time.Minute,
false,
); err != nil && !strings.Contains(strings.ToLower(err.Error()), "no such container") {
notifyPanelOfFailure()
s.Log().WithError(err).Error("failed to stop server for transfer")
return
}
}
// Create a new transfer instance for this server.
trnsfr := transfer.New(context.Background(), s)
transfer.Outgoing().Add(trnsfr)
go func() {
defer transfer.Outgoing().Remove(trnsfr)
if _, err := trnsfr.PushArchiveToTarget(data.URL, data.Token); err != nil {
notifyPanelOfFailure()
if err == context.Canceled {
trnsfr.Log().Debug("canceled")
trnsfr.SendMessage("Canceled.")
return
}
trnsfr.Log().WithError(err).Error("failed to push archive to target")
return
}
// DO NOT NOTIFY THE PANEL OF SUCCESS HERE. The only node that should send
// a success status is the destination node. When we send a failure status,
// the panel will automatically cancel the transfer and attempt to reset
// the server state on the destination node, we just need to make sure
// we clean up our statuses for failure.
trnsfr.Log().Debug("transfer complete")
}()
c.Status(http.StatusAccepted)
}
// deleteServerTransfer cancels an outgoing transfer for a server.
func deleteServerTransfer(c *gin.Context) {
s := ExtractServer(c)
if !s.IsTransferring() {
c.AbortWithStatusJSON(http.StatusConflict, gin.H{
"error": "Server is not currently being transferred.",
})
return
}
trnsfr := transfer.Outgoing().Get(s.ID())
if trnsfr == nil {
c.AbortWithStatusJSON(http.StatusConflict, gin.H{
"error": "Server is not currently being transferred.",
})
return
}
trnsfr.Cancel()
c.Status(http.StatusAccepted)
}

View File

@@ -34,7 +34,7 @@ func getServerWebsocket(c *gin.Context) {
handler, err := websocket.GetHandler(s, c.Writer, c.Request, c)
if err != nil {
NewServerError(err, s).Abort(c)
middleware.CaptureAndAbort(c, err)
return
}
defer handler.Connection.Close()

View File

@@ -10,9 +10,9 @@ import (
"github.com/gin-gonic/gin"
"github.com/pterodactyl/wings/config"
"github.com/pterodactyl/wings/installer"
"github.com/pterodactyl/wings/router/middleware"
"github.com/pterodactyl/wings/server"
"github.com/pterodactyl/wings/server/installer"
"github.com/pterodactyl/wings/system"
)
@@ -20,15 +20,31 @@ import (
func getSystemInformation(c *gin.Context) {
i, err := system.GetSystemInformation()
if err != nil {
NewTrackedError(err).Abort(c)
middleware.CaptureAndAbort(c, err)
return
}
if c.Query("v") == "2" {
c.JSON(http.StatusOK, i)
return
}
c.JSON(http.StatusOK, struct {
Architecture string `json:"architecture"`
CPUCount int `json:"cpu_count"`
KernelVersion string `json:"kernel_version"`
OS string `json:"os"`
Version string `json:"version"`
}{
Architecture: i.System.Architecture,
CPUCount: i.System.CPUThreads,
KernelVersion: i.System.KernelVersion,
OS: i.System.OSType,
Version: i.Version,
})
}
// Returns all of the servers that are registered and configured correctly on
// Returns all the servers that are registered and configured correctly on
// this wings instance.
func getAllServers(c *gin.Context) {
servers := middleware.ExtractManager(c).All()
@@ -75,7 +91,7 @@ func postCreateServer(c *gin.Context) {
return
}
if err := i.Server().Install(false); err != nil {
if err := i.Server().Install(); err != nil {
log.WithFields(log.Fields{"server": i.Server().ID(), "error": err}).Error("failed to run install process for server")
return
}
@@ -117,7 +133,7 @@ func postUpdateConfiguration(c *gin.Context) {
// Try to write this new configuration to the disk before updating our global
// state with it.
if err := config.WriteToDisk(cfg); err != nil {
WithError(c, err)
middleware.CaptureAndAbort(c, err)
return
}
// Since we wrote it to the disk successfully now update the global configuration

View File

@@ -1,54 +1,33 @@
package router
import (
"bufio"
"bytes"
"context"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"io"
"mime"
"mime/multipart"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"emperror.dev/errors"
"github.com/apex/log"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/juju/ratelimit"
"github.com/mholt/archiver/v3"
"github.com/mitchellh/colorstring"
"github.com/pterodactyl/wings/config"
"github.com/pterodactyl/wings/installer"
"github.com/pterodactyl/wings/remote"
"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/server/installer"
"github.com/pterodactyl/wings/server/transfer"
)
const progressWidth = 25
// Data passed over to initiate a server transfer.
type serverTransferRequest struct {
ServerID string `binding:"required" json:"server_id"`
URL string `binding:"required" json:"url"`
Token string `binding:"required" json:"token"`
Server installer.ServerDetails `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) {
// postTransfers .
func postTransfers(c *gin.Context) {
auth := strings.SplitN(c.GetHeader("Authorization"), " ", 2)
if len(auth) != 2 || auth[0] != "Bearer" {
c.Header("WWW-Authenticate", "Bearer")
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
@@ -59,461 +38,236 @@ func getServerArchive(c *gin.Context) {
token := tokens.TransferPayload{}
if err := tokens.ParseToken([]byte(auth[1]), &token); err != nil {
NewTrackedError(err).Abort(c)
middleware.CaptureAndAbort(c, err)
return
}
s := ExtractServer(c)
if token.Subject != s.ID() {
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{
"error": "Missing required token subject, or subject is not valid for the requested server.",
})
return
}
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)
return
}
c.AbortWithStatus(http.StatusNotFound)
return
}
// Compute sha256 checksum.
h := sha256.New()
f, err := os.Open(archivePath)
if err != nil {
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))
// Stream the file to the client.
f, err = os.Open(archivePath)
if err != nil {
_ = WithError(c, err)
return
}
defer f.Close()
c.Header("X-Checksum", checksum)
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.ID()+".tar.gz"))
c.Header("Content-Type", "application/octet-stream")
_, _ = bufio.NewReader(f).WriteTo(c.Writer)
}
func postServerArchive(c *gin.Context) {
s := middleware.ExtractServer(c)
manager := middleware.ExtractManager(c)
go func(s *server.Server) {
l := log.WithField("server", s.ID())
// This function automatically adds the Source Node prefix and Timestamp to the log
// output before sending it over the websocket.
sendTransferLog := func(data string) {
output := colorstring.Color(fmt.Sprintf("[yellow][bold]%s [Pterodactyl Transfer System] [Source Node]:[default] %s", time.Now().Format(time.RFC1123), data))
s.Events().Publish(server.TransferLogsEvent, output)
}
s.Events().Publish(server.TransferStatusEvent, "starting")
sendTransferLog("Attempting to archive server...")
hasError := true
defer func() {
if !hasError {
return
}
// Mark the server as not being transferred so it can actually be used.
s.SetTransferring(false)
s.Events().Publish(server.TransferStatusEvent, "failure")
sendTransferLog("Attempting to notify panel of archive failure..")
if err := manager.Client().SetArchiveStatus(s.Context(), s.ID(), false); err != nil {
if !remote.IsRequestError(err) {
sendTransferLog("Failed to notify panel of archive failure: " + err.Error())
l.WithField("error", err).Error("failed to notify panel of failed archive status")
return
}
sendTransferLog("Panel returned an error while notifying it of a failed archive: " + err.Error())
l.WithField("error", err.Error()).Error("panel returned an error when notifying it of a failed archive status")
return
}
sendTransferLog("Successfully notified panel of failed archive status")
l.Info("successfully notified panel of failed archive status")
}()
// Mark the server as transferring to prevent problems.
s.SetTransferring(true)
// Ensure the server is offline. Sometimes a "No such container" error gets through
// which means the server is already stopped. We can ignore that.
if err := s.Environment.WaitForStop(s.Context(), time.Minute, false); err != nil && !strings.Contains(strings.ToLower(err.Error()), "no such container") {
sendTransferLog("Failed to stop server, aborting transfer..")
l.WithField("error", err).Error("failed to stop server")
return
}
// Get the disk usage of the server (used to calculate the progress of the archive process)
rawSize, err := s.Filesystem().DiskUsage(true)
u, err := uuid.Parse(token.Subject)
if err != nil {
sendTransferLog("Failed to get disk usage for server, aborting transfer..")
l.WithField("error", err).Error("failed to get disk usage for server")
middleware.CaptureAndAbort(c, err)
return
}
// Create an archive of the entire server's data directory.
a := &filesystem.Archive{
BasePath: s.Filesystem().Path(),
Progress: filesystem.NewProgress(rawSize),
}
// Get or create a new transfer instance for this server.
var (
ctx context.Context
cancel context.CancelFunc
)
trnsfr := transfer.Incoming().Get(u.String())
if trnsfr == nil {
// TODO: should this use the request context?
trnsfr = transfer.New(c, nil)
// Send the archive progress to the websocket every 3 seconds.
ctx, cancel := context.WithCancel(s.Context())
ctx, cancel = context.WithCancel(trnsfr.Context())
defer cancel()
go func(ctx context.Context, p *filesystem.Progress, t *time.Ticker) {
defer t.Stop()
for {
select {
case <-ctx.Done():
return
case <-t.C:
sendTransferLog("Archiving " + p.Progress(progressWidth))
}
}
}(ctx, a.Progress, time.NewTicker(5*time.Second))
// Attempt to get an archive of the server.
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
}
// Cancel the progress ticker.
cancel()
// Show 100% completion.
sendTransferLog("Archiving " + a.Progress.Progress(progressWidth))
sendTransferLog("Successfully created archive, attempting to notify panel..")
l.Info("successfully created server transfer archive, notifying panel..")
if err := manager.Client().SetArchiveStatus(s.Context(), s.ID(), true); err != nil {
if !remote.IsRequestError(err) {
sendTransferLog("Failed to notify panel of archive success: " + err.Error())
l.WithField("error", err).Error("failed to notify panel of successful archive status")
return
}
sendTransferLog("Panel returned an error while notifying it of a successful archive: " + err.Error())
l.WithField("error", err.Error()).Error("panel returned an error when notifying it of a successful archive status")
return
}
hasError = false
// This log may not be displayed by the client due to the status event being sent before or at the same time.
sendTransferLog("Successfully notified panel of successful archive status")
l.Info("successfully notified panel of successful transfer archive status")
s.Events().Publish(server.TransferStatusEvent, "archived")
}(s)
c.Status(http.StatusAccepted)
}
// Log helper function to attach all errors and info output to a consistently formatted
// log string for easier querying.
func (str serverTransferRequest) log() *log.Entry {
return log.WithField("subsystem", "transfers").WithField("server_id", str.ServerID)
}
// Downloads an archive from the machine that the server currently lives on.
func (str serverTransferRequest) downloadArchive() (*http.Response, error) {
client := http.Client{Timeout: 0}
req, err := http.NewRequest(http.MethodGet, str.URL, nil)
i, err := installer.New(ctx, manager, installer.ServerDetails{
UUID: u.String(),
StartOnCompletion: false,
})
if err != nil {
return nil, err
if err := manager.Client().SetTransferStatus(context.Background(), trnsfr.Server.ID(), false); err != nil {
trnsfr.Log().WithField("status", false).WithError(err).Error("failed to set transfer status")
}
req.Header.Set("Authorization", str.Token)
res, err := client.Do(req) // lgtm [go/request-forgery]
if err != nil {
return nil, err
}
return res, nil
}
// Returns the path to the local archive on the system.
func (str serverTransferRequest) path() string {
return getArchivePath(str.ServerID)
}
// Creates the archive location on this machine by first checking that the required file
// does not already exist. If it does exist, the file is deleted and then re-created as
// an empty file.
func (str serverTransferRequest) createArchiveFile() (*os.File, error) {
p := str.path()
if _, err := os.Stat(p); err != nil {
if !os.IsNotExist(err) {
return nil, err
}
} else if err := os.Remove(p); err != nil {
return nil, err
}
return os.Create(p)
}
// Deletes the archive from the local filesystem. This is executed as a deferred function.
func (str serverTransferRequest) removeArchivePath() {
p := str.path()
str.log().Debug("deleting temporary transfer archive")
if err := os.Remove(p); err != nil && !os.IsNotExist(err) {
str.log().WithField("path", p).WithField("error", err).Error("failed to delete temporary transfer archive file")
return
}
str.log().Debug("deleted temporary transfer archive successfully")
}
// Verifies that the SHA-256 checksum of the file on the local filesystem matches the
// 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) {
f, err := os.Open(str.path())
if err != nil {
return false, "", err
}
defer f.Close()
h := sha256.New()
if _, err := io.Copy(h, bufio.NewReader(f)); err != nil {
return false, "", err
}
checksum := hex.EncodeToString(h.Sum(nil))
return checksum == matches, checksum, nil
}
// Sends a notification to the Panel letting it know what the status of this transfer is.
func (str serverTransferRequest) sendTransferStatus(client remote.Client, successful bool) error {
lg := str.log().WithField("transfer_successful", successful)
lg.Info("notifying Panel of server transfer state")
if err := client.SetTransferStatus(context.Background(), str.ServerID, successful); err != nil {
lg.WithField("error", err).Error("error notifying panel of transfer state")
return err
}
lg.Debug("notified panel of transfer state")
return nil
}
// Initiates a transfer between two nodes for a server by downloading an archive from the
// remote node and then applying the server details to this machine.
func postTransfer(c *gin.Context) {
var data serverTransferRequest
if err := c.BindJSON(&data); err != nil {
middleware.CaptureAndAbort(c, err)
return
}
manager := middleware.ExtractManager(c)
u, err := uuid.Parse(data.ServerID)
if err != nil {
_ = WithError(c, err)
return
}
// Force the server ID to be a valid UUID string at this point. If it is not an error
// is returned to the caller. This limits injection vulnerabilities that would cause
// the str.path() function to return a location not within the server archive directory.
data.ServerID = u.String()
data.log().Info("handling incoming server transfer request")
go func(data *serverTransferRequest) {
ctx := context.Background()
hasError := true
// Create a new server installer. This will only configure the environment and not
// run the installer scripts.
i, err := installer.New(ctx, manager, data.Server)
if err != nil {
_ = data.sendTransferStatus(manager.Client(), false)
data.log().WithField("error", err).Error("failed to validate received server data")
return
}
// This function automatically adds the Target Node prefix and Timestamp to the log output before sending it
// over the websocket.
sendTransferLog := func(data string) {
output := colorstring.Color(fmt.Sprintf("[yellow][bold]%s [Pterodactyl Transfer System] [Target Node]:[default] %s", time.Now().Format(time.RFC1123), data))
i.Server().Events().Publish(server.TransferLogsEvent, output)
}
// Mark the server as transferring to prevent problems later on during the process and
// then push the server into the global server collection for this instance.
i.Server().SetTransferring(true)
manager.Add(i.Server())
defer func(s *server.Server) {
// In the event that this transfer call fails, remove the server from the global
// server tracking so that we don't have a dangling instance.
if err := data.sendTransferStatus(manager.Client(), !hasError); hasError || err != nil {
sendTransferLog("Server transfer failed, check Wings logs for additional information.")
s.Events().Publish(server.TransferStatusEvent, "failure")
manager.Remove(func(match *server.Server) bool {
return match.ID() == s.ID()
})
// If the transfer status was successful but the request failed, act like the transfer failed.
if !hasError && err != nil {
// Delete all extracted files.
if err := os.RemoveAll(s.Filesystem().Path()); err != nil && !os.IsNotExist(err) {
data.log().WithField("error", err).Warn("failed to delete local server files directory")
}
}
// We add the transfer to the list of transfers once we have a server instance to use.
trnsfr.Server = i.Server()
transfer.Incoming().Add(trnsfr)
} else {
s.SetTransferring(false)
s.Events().Publish(server.TransferStatusEvent, "success")
sendTransferLog("Transfer completed.")
}
}(i.Server())
data.log().Info("downloading server archive from current server node")
sendTransferLog("Received incoming transfer from Panel, attempting to download archive from source node...")
res, err := data.downloadArchive()
if err != nil {
sendTransferLog("Failed to retrieve server archive from remote node: " + err.Error())
data.log().WithField("error", err).Error("failed to download archive for server transfer")
return
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
data.log().WithField("error", err).WithField("status", res.StatusCode).Error("unexpected error response from transfer endpoint")
return
}
size := res.ContentLength
if size == 0 {
data.log().WithField("error", err).Error("received an archive response with Content-Length of 0")
return
}
sendTransferLog("Got server archive response from remote node. (Content-Length: " + strconv.Itoa(int(size)) + ")")
sendTransferLog("Creating local archive file...")
file, err := data.createArchiveFile()
if err != nil {
data.log().WithField("error", err).Error("failed to create archive file on local filesystem")
return
}
sendTransferLog("Writing archive to disk...")
data.log().Info("writing transfer archive to disk...")
progress := filesystem.NewProgress(size)
// Send the archive progress to the websocket every 3 seconds.
ctx, cancel := context.WithCancel(ctx)
ctx, cancel = context.WithCancel(trnsfr.Context())
defer cancel()
go func(ctx context.Context, p *filesystem.Progress, t *time.Ticker) {
defer t.Stop()
}
// Any errors past this point (until the transfer is complete) will abort
// the transfer.
successful := false
defer func(ctx context.Context, trnsfr *transfer.Transfer) {
// Remove the transfer from the list of incoming transfers.
transfer.Incoming().Remove(trnsfr)
if !successful {
trnsfr.Server.Events().Publish(server.TransferStatusEvent, "failure")
manager.Remove(func(match *server.Server) bool {
return match.ID() == trnsfr.Server.ID()
})
}
if err := manager.Client().SetTransferStatus(context.Background(), trnsfr.Server.ID(), successful); err != nil {
// Only delete the files if the transfer actually failed, otherwise we could have
// unrecoverable data-loss.
if !successful && err != nil {
// Delete all extracted files.
go func(trnsfr *transfer.Transfer) {
if err := os.RemoveAll(trnsfr.Server.Filesystem().Path()); err != nil && !os.IsNotExist(err) {
trnsfr.Log().WithError(err).Warn("failed to delete local server files")
}
}(trnsfr)
}
trnsfr.Log().WithField("status", successful).WithError(err).Error("failed to set transfer status on panel")
return
}
trnsfr.Server.SetTransferring(false)
trnsfr.Server.Events().Publish(server.TransferStatusEvent, "success")
}(ctx, trnsfr)
mediaType, params, err := mime.ParseMediaType(c.GetHeader("Content-Type"))
if err != nil {
trnsfr.Log().Debug("failed to parse content type header")
middleware.CaptureAndAbort(c, err)
return
}
if !strings.HasPrefix(mediaType, "multipart/") {
trnsfr.Log().Debug("invalid content type")
middleware.CaptureAndAbort(c, fmt.Errorf("invalid content type \"%s\", expected \"multipart/form-data\"", mediaType))
return
}
// Used to calculate the hash of the file as it is being uploaded.
h := sha256.New()
// Used to read the file and checksum from the request body.
mr := multipart.NewReader(c.Request.Body, params["boundary"])
// Loop through the parts of the request body and process them.
var (
hasArchive bool
hasChecksum bool
checksumVerified bool
)
out:
for {
select {
case <-ctx.Done():
return
case <-t.C:
sendTransferLog("Downloading " + p.Progress(progressWidth))
break out
default:
p, err := mr.NextPart()
if err == io.EOF {
break out
}
}
}(ctx, progress, time.NewTicker(5*time.Second))
var reader io.Reader
downloadLimit := float64(config.Get().System.Transfers.DownloadLimit) * 1024 * 1024
if downloadLimit > 0 {
// Wrap the body with a reader that is limited to the defined download limit speed.
reader = ratelimit.Reader(res.Body, ratelimit.NewBucketWithRate(downloadLimit, int64(downloadLimit)))
} else {
reader = res.Body
}
buf := make([]byte, 1024*4)
if _, err := io.CopyBuffer(file, io.TeeReader(reader, progress), buf); err != nil {
_ = file.Close()
sendTransferLog("Failed while writing archive file to disk: " + err.Error())
data.log().WithField("error", err).Error("failed to copy archive file to disk")
return
}
cancel()
// Show 100% completion.
sendTransferLog("Downloading " + progress.Progress(progressWidth))
if err := file.Close(); err != nil {
data.log().WithField("error", err).Error("unable to close archive file on local filesystem")
return
}
data.log().Info("finished writing transfer archive to disk")
sendTransferLog("Successfully wrote archive to disk.")
// Whenever the transfer fails or succeeds, delete the temporary transfer archive that
// was created on the disk.
defer data.removeArchivePath()
sendTransferLog("Verifying checksum of downloaded archive...")
data.log().Info("computing checksum of downloaded archive file")
expected := res.Header.Get("X-Checksum")
if matches, computed, err := data.verifyChecksum(expected); err != nil {
data.log().WithField("error", err).Error("encountered an error while calculating local filesystem archive checksum")
return
} else if !matches {
sendTransferLog("@@@@@ CHECKSUM VERIFICATION FAILED @@@@@")
sendTransferLog(" - Source Checksum: " + expected)
sendTransferLog(" - Computed Checksum: " + computed)
data.log().WithField("expected_sum", expected).WithField("computed_checksum", computed).Error("checksum mismatch when verifying integrity of local archive")
if err != nil {
middleware.CaptureAndAbort(c, err)
return
}
// Create the server's environment.
sendTransferLog("Creating server environment, this could take a while..")
data.log().Info("creating server environment")
if err := i.Server().CreateEnvironment(); err != nil {
data.log().WithField("error", err).Error("failed to create server environment")
name := p.FormName()
switch name {
case "archive":
trnsfr.Log().Debug("received archive")
if err := trnsfr.Server.EnsureDataDirectoryExists(); err != nil {
middleware.CaptureAndAbort(c, err)
return
}
sendTransferLog("Server environment has been created, extracting transfer archive..")
data.log().Info("server environment configured, extracting transfer archive")
if err := archiver.NewTarGz().Unarchive(data.path(), i.Server().Filesystem().Path()); err != nil {
// Un-archiving failed, delete the server's data directory.
if err := os.RemoveAll(i.Server().Filesystem().Path()); err != nil && !os.IsNotExist(err) {
data.log().WithField("error", err).Warn("failed to delete local server files directory")
}
data.log().WithField("error", err).Error("failed to extract server archive")
tee := io.TeeReader(p, h)
if err := trnsfr.Server.Filesystem().ExtractStreamUnsafe(ctx, "/", tee); err != nil {
middleware.CaptureAndAbort(c, err)
return
}
// We mark the process as being successful here as if we fail to send a transfer success,
// then a transfer failure won't probably be successful either.
//
// It may be useful to retry sending the transfer success every so often just in case of a small
// hiccup or the fix of whatever error causing the success request to fail.
hasError = false
hasArchive = true
case "checksum":
trnsfr.Log().Debug("received checksum")
data.log().Info("archive extracted successfully, notifying Panel of status")
sendTransferLog("Archive extracted successfully.")
}(&data)
if !hasArchive {
middleware.CaptureAndAbort(c, errors.New("archive must be sent before the checksum"))
return
}
hasChecksum = true
v, err := io.ReadAll(p)
if err != nil {
middleware.CaptureAndAbort(c, err)
return
}
expected := make([]byte, hex.DecodedLen(len(v)))
n, err := hex.Decode(expected, v)
if err != nil {
middleware.CaptureAndAbort(c, err)
return
}
actual := h.Sum(nil)
trnsfr.Log().WithFields(log.Fields{
"expected": hex.EncodeToString(expected),
"actual": hex.EncodeToString(actual),
}).Debug("checksums")
if !bytes.Equal(expected[:n], actual) {
middleware.CaptureAndAbort(c, errors.New("checksums don't match"))
return
}
trnsfr.Log().Debug("checksums match")
checksumVerified = true
default:
continue
}
}
}
if !hasArchive || !hasChecksum {
middleware.CaptureAndAbort(c, errors.New("missing archive or checksum"))
return
}
if !checksumVerified {
middleware.CaptureAndAbort(c, errors.New("checksums don't match"))
return
}
// Transfer is almost complete, we just want to ensure the environment is
// configured correctly. We might want to not fail the transfer at this
// stage, but we will just to be safe.
// Ensure the server environment gets configured.
if err := trnsfr.Server.CreateEnvironment(); err != nil {
middleware.CaptureAndAbort(c, err)
return
}
// Changing this causes us to notify the panel about a successful transfer,
// rather than failing the transfer like we do by default.
successful = true
// The rest of the logic for ensuring the server is unlocked and everything
// is handled in the deferred function above.
trnsfr.Log().Debug("done!")
}
// deleteTransfer cancels an incoming transfer for a server.
func deleteTransfer(c *gin.Context) {
s := ExtractServer(c)
if !s.IsTransferring() {
c.AbortWithStatusJSON(http.StatusConflict, gin.H{
"error": "Server is not currently being transferred.",
})
return
}
trnsfr := transfer.Incoming().Get(s.ID())
if trnsfr == nil {
c.AbortWithStatusJSON(http.StatusConflict, gin.H{
"error": "Server is not currently being transferred.",
})
return
}
trnsfr.Cancel()
c.Status(http.StatusAccepted)
}

View File

@@ -1,124 +0,0 @@
Name: ptero-wings
Version: 1.7.0
Release: 1%{?dist}
Summary: The server control plane for Pterodactyl Panel. Written from the ground-up with security, speed, and stability in mind.
BuildArch: x86_64
License: MIT
URL: https://github.com/pterodactyl/wings
Source0: https://github.com/pterodactyl/wings/releases/download/v%{version}/wings_linux_amd64
%if 0%{?rhel} && 0%{?rhel} <= 8
BuildRequires: systemd
%else
BuildRequires: systemd-rpm-macros
%endif
%description
Wings is Pterodactyl's server control plane, built for the rapidly
changing gaming industry and designed to be highly performant and
secure. Wings provides an HTTP API allowing you to interface directly
with running server instances, fetch server logs, generate backups,
and control all aspects of the server lifecycle.
In addition, Wings ships with a built-in SFTP server allowing your
system to remain free of Pterodactyl specific dependencies, and
allowing users to authenticate with the same credentials they would
normally use to access the Panel.
%prep
%build
#nothing required
%install
mkdir -p %{buildroot}%{_bindir}
mkdir -p %{buildroot}%{_unitdir}
cp %{_sourcedir}/wings_linux_amd64 %{buildroot}%{_bindir}/wings
cat > %{buildroot}%{_unitdir}/wings.service << EOF
[Unit]
Description=Pterodactyl Wings Daemon
After=docker.service
Requires=docker.service
PartOf=docker.service
StartLimitIntervalSec=600
[Service]
WorkingDirectory=/etc/pterodactyl
ExecStart=/usr/bin/wings
ExecReload=/bin/kill -HUP $MAINPID
Restart=on-failure
StartLimitInterval=180
StartLimitBurst=30
RestartSec=5s
[Install]
WantedBy=multi-user.target
EOF
%files
%attr(0755, root, root) %{_prefix}/bin/wings
%attr(0644, root, root) %{_unitdir}/wings.service
%post
# Reload systemd
systemctl daemon-reload
# Create the required directory structure
mkdir -p /etc/pterodactyl
mkdir -p /var/lib/pterodactyl/{archives,backups,volumes}
mkdir -p /var/log/pterodactyl/install
%preun
systemctl is-active %{name} >/dev/null 2>&1
if [ $? -eq 0 ]; then
systemctl stop %{name}
fi
systemctl is-enabled %{name} >/dev/null 2>&1
if [ $? -eq 0 ]; then
systemctl disable %{name}
fi
%postun
rm -rf /var/log/pterodactyl
%verifyscript
wings --version
%changelog
* Wed Sep 14 2022 Chance Callahan <ccallaha@redhat.com> - 1.7.0-1
- Updating specfile to match stable release.
* Wed Oct 27 2021 Capitol Hosting Solutions Systems Engineering <syseng@chs.gg> - 1.5.3-1
- specfile by Capitol Hosting Solutions, Upstream by Pterodactyl
- Rebased for https://github.com/pterodactyl/wings/releases/tag/v1.5.3
- Fixes improper event registration and error handling during socket authentication that would cause the incorrect error message to be returned to the client, or no error in some scenarios. Event registration is now delayed until the socket is fully authenticated to ensure needless listeners are not registed.
- Fixes dollar signs always being evaluated as environment variables with no way to escape them. They can now be escaped as $$ which will transform into a single dollar sign.
- A websocket connection to a server will be closed by Wings if there is a send error encountered and the client will be left to handle reconnections, rather than simply logging the error and continuing to listen for new events.
* Sun Sep 12 2021 Capitol Hosting Solutions Systems Engineering <syseng@chs.gg> - 1.5.0-1
- specfile by Capitol Hosting Solutions, Upstream by Pterodactyl
- Rebased for https://github.com/pterodactyl/wings/releases/tag/v1.5.0
- Fixes a race condition when setting the application name in the console output for a server.
- Fixes a server being reinstalled causing the file_denylist parameter for an Egg to be ignored until Wings is restarted.
- Fixes YAML file parser not correctly setting boolean values.
- Fixes potential issue where the underlying websocket connection is closed but the parent request context is not yet canceled causing a write over a closed connection.
- Fixes race condition when closing all active websocket connections when a server is deleted.
- Fixes logic to determine if a server's context is closed out and send a websocket close message to connected clients. Previously this fired off whenever the request itself was closed, and not when the server context was closed.
- Exposes 8080 in the wings Dockerfile to better support reverse proxy tools.
- Releases are now built using Go 1.17 the minimum version required to build Wings remains Go 1.16.
- Simplifed the logic powering server updates to only pull information from the Panel rather than trying to accept updated values. All parts of Wings needing the most up-to-date server details should call Server#Sync() to fetch the latest stored build information.
- Installer#New() no longer requires passing all of the server data as a byte slice, rather a new Installer#ServerDetails struct is exposed which can be passed and accepts a UUID and if the server should be started after the installer finishes.
- Removes complicated (and unused) logic during the server installation process that was a hold-over from legacy Wings architectures.
- Removes the PATCH /api/servers/:server endpoint if you were previously using this API call it should be replaced with POST /api/servers/:server/sync.
* Wed Aug 25 2021 Capitol Hosting Solutions Systems Engineering <syseng@chs.gg> - 1.4.7-1
- specfile by Capitol Hosting Solutions, Upstream by Pterodactyl
- Rebased for https://github.com/pterodactyl/wings/releases/tag/v1.4.7
- SFTP access is now properly denied if a server is suspended.
- Correctly uses start_on_completion and crash_detection_enabled for servers.

View File

@@ -24,7 +24,6 @@ func (s *Server) notifyPanelOfBackup(uuid string, ad *backup.ArchiveDetails, suc
"backup": uuid,
"error": err,
}).Error("failed to notify panel of backup status due to wings error")
return err
}
@@ -127,7 +126,7 @@ func (s *Server) RestoreBackup(b backup.BackupInterface, reader io.ReadCloser) (
defer func() {
s.Config().SetSuspended(false)
if reader != nil {
reader.Close()
_ = reader.Close()
}
}()
// Send an API call to the Panel as soon as this function is done running so that
@@ -142,7 +141,7 @@ func (s *Server) RestoreBackup(b backup.BackupInterface, reader io.ReadCloser) (
// instance, otherwise you'll likely hit all types of write errors due to the
// server being suspended.
if s.Environment.State() != environment.ProcessOfflineState {
if err = s.Environment.WaitForStop(s.Context(), time.Minute*2, false); err != nil {
if err = s.Environment.WaitForStop(s.Context(), 2*time.Minute, false); err != nil {
if !client.IsErrNotFound(err) {
return errors.WrapIf(err, "server/backup: restore: failed to wait for container stop")
}
@@ -152,14 +151,19 @@ func (s *Server) RestoreBackup(b backup.BackupInterface, reader io.ReadCloser) (
// Attempt to restore the backup to the server by running through each entry
// in the file one at a time and writing them to the disk.
s.Log().Debug("starting file writing process for backup restoration")
err = b.Restore(s.Context(), reader, func(file string, r io.Reader, mode fs.FileMode, atime, mtime time.Time) error {
err = b.Restore(s.Context(), reader, func(file string, info fs.FileInfo, r io.ReadCloser) error {
defer r.Close()
s.Events().Publish(DaemonMessageEvent, "(restoring): "+file)
if err := s.Filesystem().Writefile(file, r); err != nil {
return err
}
if err := s.Filesystem().Chmod(file, mode); err != nil {
if err := s.Filesystem().Chmod(file, info.Mode()); err != nil {
return err
}
atime := info.ModTime()
mtime := atime
return s.Filesystem().Chtimes(file, atime, mtime)
})

View File

@@ -8,16 +8,21 @@ import (
"io/fs"
"os"
"path"
"time"
"emperror.dev/errors"
"github.com/apex/log"
"github.com/mholt/archiver/v4"
"golang.org/x/sync/errgroup"
"github.com/pterodactyl/wings/config"
"github.com/pterodactyl/wings/remote"
)
var format = archiver.CompressedArchive{
Compression: archiver.Gz{},
Archival: archiver.Tar{},
}
type AdapterType string
const (
@@ -27,7 +32,7 @@ const (
// RestoreCallback is a generic restoration callback that exists for both local
// and remote backups allowing the files to be restored.
type RestoreCallback func(file string, r io.Reader, mode fs.FileMode, atime, mtime time.Time) error
type RestoreCallback func(file string, info fs.FileInfo, r io.ReadCloser) error
// noinspection GoNameStartsWithPackageName
type BackupInterface interface {

View File

@@ -6,8 +6,10 @@ import (
"os"
"emperror.dev/errors"
"github.com/mholt/archiver/v3"
"github.com/juju/ratelimit"
"github.com/mholt/archiver/v4"
"github.com/pterodactyl/wings/config"
"github.com/pterodactyl/wings/remote"
"github.com/pterodactyl/wings/server/filesystem"
)
@@ -64,7 +66,7 @@ func (b *LocalBackup) Generate(ctx context.Context, basePath, ignore string) (*A
}
b.log().WithField("path", b.Path()).Info("creating backup for server")
if err := a.Create(b.Path()); err != nil {
if err := a.Create(ctx, b.Path()); err != nil {
return nil, err
}
b.log().Info("created backup successfully")
@@ -79,16 +81,27 @@ func (b *LocalBackup) Generate(ctx context.Context, basePath, ignore string) (*A
// Restore will walk over the archive and call the callback function for each
// file encountered.
func (b *LocalBackup) Restore(ctx context.Context, _ io.Reader, callback RestoreCallback) error {
return archiver.Walk(b.Path(), func(f archiver.File) error {
select {
case <-ctx.Done():
// Stop walking if the context is canceled.
return archiver.ErrStopWalk
default:
if f.IsDir() {
f, err := os.Open(b.Path())
if err != nil {
return err
}
var reader io.Reader = f
// Steal the logic we use for making backups which will be applied when restoring
// this specific backup. This allows us to prevent overloading the disk unintentionally.
if writeLimit := int64(config.Get().System.Backups.WriteLimit * 1024 * 1024); writeLimit > 0 {
reader = ratelimit.Reader(f, ratelimit.NewBucketWithRate(float64(writeLimit), writeLimit))
}
if err := format.Extract(ctx, reader, nil, func(ctx context.Context, f archiver.File) error {
r, err := f.Open()
if err != nil {
return err
}
defer r.Close()
return callback(filesystem.ExtractNameFromArchive(f), f.FileInfo, r)
}); err != nil {
return err
}
return nil
}
return callback(filesystem.ExtractNameFromArchive(f), f, f.Mode(), f.ModTime(), f.ModTime())
}
})
}

View File

@@ -1,8 +1,6 @@
package backup
import (
"archive/tar"
"compress/gzip"
"context"
"fmt"
"io"
@@ -13,13 +11,12 @@ import (
"emperror.dev/errors"
"github.com/cenkalti/backoff/v4"
"github.com/pterodactyl/wings/server/filesystem"
"github.com/juju/ratelimit"
"github.com/mholt/archiver/v4"
"github.com/pterodactyl/wings/config"
"github.com/pterodactyl/wings/remote"
"github.com/pterodactyl/wings/server/filesystem"
)
type S3Backup struct {
@@ -60,7 +57,7 @@ func (s *S3Backup) Generate(ctx context.Context, basePath, ignore string) (*Arch
}
s.log().WithField("path", s.Path()).Info("creating backup for server")
if err := a.Create(s.Path()); err != nil {
if err := a.Create(ctx, s.Path()); err != nil {
return nil, err
}
s.log().Info("created backup successfully")
@@ -96,32 +93,17 @@ func (s *S3Backup) Restore(ctx context.Context, r io.Reader, callback RestoreCal
if writeLimit := int64(config.Get().System.Backups.WriteLimit * 1024 * 1024); writeLimit > 0 {
reader = ratelimit.Reader(r, ratelimit.NewBucketWithRate(float64(writeLimit), writeLimit))
}
gr, err := gzip.NewReader(reader)
if err := format.Extract(ctx, reader, nil, func(ctx context.Context, f archiver.File) error {
r, err := f.Open()
if err != nil {
return err
}
defer gr.Close()
tr := tar.NewReader(gr)
for {
select {
case <-ctx.Done():
return nil
default:
// Do nothing, fall through to the next block of code in this loop.
}
header, err := tr.Next()
if err != nil {
if err == io.EOF {
break
}
defer r.Close()
return callback(filesystem.ExtractNameFromArchive(f), f.FileInfo, r)
}); err != nil {
return err
}
if header.Typeflag == tar.TypeReg {
if err := callback(header.Name, tr, header.FileInfo().Mode(), header.AccessTime, header.ModTime); err != nil {
return err
}
}
}
return nil
}

View File

@@ -46,6 +46,9 @@ type Configuration struct {
// server process.
EnvVars environment.Variables `json:"environment"`
// Labels is a map of container labels that should be applied to the running server process.
Labels map[string]string `json:"labels"`
Allocations environment.Allocations `json:"allocations"`
Build environment.Limits `json:"build"`
CrashDetectionEnabled bool `json:"crash_detection_enabled"`

View File

@@ -5,8 +5,7 @@ import (
"github.com/pterodactyl/wings/system"
)
// Defines all of the possible output events for a server.
// noinspection GoNameStartsWithPackageName
// Defines all the possible output events for a server.
const (
DaemonMessageEvent = "daemon message"
InstallOutputEvent = "install output"
@@ -19,6 +18,7 @@ const (
BackupCompletedEvent = "backup completed"
TransferLogsEvent = "transfer logs"
TransferStatusEvent = "transfer status"
DeletedEvent = "deleted"
)
// Events returns the server's emitter instance.

View File

@@ -2,13 +2,13 @@ package filesystem
import (
"archive/tar"
"context"
"io"
"io/fs"
"os"
"path/filepath"
"strings"
"sync"
"sync/atomic"
"emperror.dev/errors"
"github.com/apex/log"
@@ -18,7 +18,7 @@ import (
ignore "github.com/sabhiram/go-gitignore"
"github.com/pterodactyl/wings/config"
"github.com/pterodactyl/wings/system"
"github.com/pterodactyl/wings/internal/progress"
)
const memory = 4 * 1024
@@ -30,60 +30,29 @@ var pool = sync.Pool{
},
}
// Progress is used to track the progress of any I/O operation that are being
// performed.
type Progress struct {
// written is the total size of the files that have been written to the writer.
written int64
// Total is the total size of the archive in bytes.
total int64
// w .
w io.Writer
// TarProgress .
type TarProgress struct {
*tar.Writer
p *progress.Progress
}
// NewProgress .
func NewProgress(total int64) *Progress {
return &Progress{total: total}
}
// Written returns the total number of bytes written.
// This function should be used when the progress is tracking data being written.
func (p *Progress) Written() int64 {
return atomic.LoadInt64(&p.written)
}
// Total returns the total size in bytes.
func (p *Progress) Total() int64 {
return atomic.LoadInt64(&p.total)
}
// Write totals the number of bytes that have been written to the writer.
func (p *Progress) Write(v []byte) (int, error) {
n := len(v)
atomic.AddInt64(&p.written, int64(n))
if p.w != nil {
return p.w.Write(v)
// NewTarProgress .
func NewTarProgress(w *tar.Writer, p *progress.Progress) *TarProgress {
if p != nil {
p.Writer = w
}
return &TarProgress{
Writer: w,
p: p,
}
return n, nil
}
// Progress returns a formatted progress string for the current progress.
func (p *Progress) Progress(width int) string {
current := p.Written()
total := p.Total()
// v = 100 (Progress)
// size = 1000 (Content-Length)
// p / size = 0.1
// * 100 = 10% (Multiply by 100 to get a percentage of the download)
// 10% / tickPercentage = (10% / (100 / 25)) (Divide by tick percentage to get the number of ticks)
// 2.5 (Number of ticks as a float64)
// 2 (convert to an integer)
// We have to cast these numbers to float in order to get a float result from the division.
ticks := ((float64(current) / float64(total)) * 100) / (float64(100) / float64(width))
bar := strings.Repeat("=", int(ticks)) + strings.Repeat(" ", width-int(ticks))
return "[" + bar + "] " + system.FormatBytes(current) + " / " + system.FormatBytes(total)
// Write .
func (p *TarProgress) Write(v []byte) (int, error) {
if p.p == nil {
return p.Writer.Write(v)
}
return p.p.Write(v)
}
type Archive struct {
@@ -100,12 +69,12 @@ type Archive struct {
Files []string
// Progress wraps the writer of the archive to pass through the progress tracker.
Progress *Progress
Progress *progress.Progress
}
// Create creates an archive at dst with all the files defined in the
// included Files array.
func (a *Archive) Create(dst string) error {
func (a *Archive) Create(ctx context.Context, dst string) error {
f, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600)
if err != nil {
return err
@@ -123,6 +92,11 @@ func (a *Archive) Create(dst string) error {
writer = f
}
return a.Stream(ctx, writer)
}
// Stream .
func (a *Archive) Stream(ctx context.Context, w io.Writer) error {
// Choose which compression level to use based on the compression_level configuration option
var compressionLevel int
switch config.Get().System.Backups.CompressionLevel {
@@ -137,36 +111,30 @@ func (a *Archive) Create(dst string) error {
}
// Create a new gzip writer around the file.
gw, _ := pgzip.NewWriterLevel(writer, compressionLevel)
gw, _ := pgzip.NewWriterLevel(w, compressionLevel)
_ = gw.SetConcurrency(1<<20, 1)
defer gw.Close()
var pw io.Writer
if a.Progress != nil {
a.Progress.w = gw
pw = a.Progress
} else {
pw = gw
}
// Create a new tar writer around the gzip writer.
tw := tar.NewWriter(pw)
tw := tar.NewWriter(gw)
defer tw.Close()
pw := NewTarProgress(tw, a.Progress)
// Configure godirwalk.
options := &godirwalk.Options{
FollowSymbolicLinks: false,
Unsorted: true,
Callback: a.callback(tw),
}
// If we're specifically looking for only certain files, or have requested
// that certain files be ignored we'll update the callback function to reflect
// that request.
var callback godirwalk.WalkFunc
if len(a.Files) == 0 && len(a.Ignore) > 0 {
i := ignore.CompileIgnoreLines(strings.Split(a.Ignore, "\n")...)
options.Callback = a.callback(tw, func(_ string, rp string) error {
callback = a.callback(pw, func(_ string, rp string) error {
if i.MatchesPath(rp) {
return godirwalk.SkipThis
}
@@ -174,7 +142,19 @@ func (a *Archive) Create(dst string) error {
return nil
})
} else if len(a.Files) > 0 {
options.Callback = a.withFilesCallback(tw)
callback = a.withFilesCallback(pw)
} else {
callback = a.callback(pw)
}
// Set the callback function, wrapped with support for context cancellation.
options.Callback = func(path string, de *godirwalk.Dirent) error {
select {
case <-ctx.Done():
return ctx.Err()
default:
return callback(path, de)
}
}
// Recursively walk the path we are archiving.
@@ -183,7 +163,7 @@ func (a *Archive) Create(dst string) error {
// Callback function used to determine if a given file should be included in the archive
// being generated.
func (a *Archive) callback(tw *tar.Writer, opts ...func(path string, relative string) error) func(path string, de *godirwalk.Dirent) error {
func (a *Archive) callback(tw *TarProgress, opts ...func(path string, relative string) error) func(path string, de *godirwalk.Dirent) error {
return func(path string, de *godirwalk.Dirent) error {
// Skip directories because we are walking them recursively.
if de.IsDir() {
@@ -207,7 +187,7 @@ func (a *Archive) callback(tw *tar.Writer, opts ...func(path string, relative st
}
// Pushes only files defined in the Files key to the final archive.
func (a *Archive) withFilesCallback(tw *tar.Writer) func(path string, de *godirwalk.Dirent) error {
func (a *Archive) withFilesCallback(tw *TarProgress) func(path string, de *godirwalk.Dirent) error {
return a.callback(tw, func(p string, rp string) error {
for _, f := range a.Files {
// If the given doesn't match, or doesn't have the same prefix continue
@@ -228,7 +208,7 @@ func (a *Archive) withFilesCallback(tw *tar.Writer) func(path string, de *godirw
}
// Adds a given file path to the final archive being created.
func (a *Archive) addToArchive(p string, rp string, w *tar.Writer) error {
func (a *Archive) addToArchive(p string, rp string, w *TarProgress) error {
// Lstat the file, this will give us the same information as Stat except that it will not
// follow a symlink to its target automatically. This is important to avoid including
// files that exist outside the server root unintentionally in the backup.
@@ -255,7 +235,7 @@ func (a *Archive) addToArchive(p string, rp string, w *tar.Writer) error {
// it doesn't work.
target, err = os.Readlink(s.Name())
if err != nil {
// Ignore the not exist errors specifically, since theres nothing important about that.
// Ignore the not exist errors specifically, since there is nothing important about that.
if !os.IsNotExist(err) {
log.WithField("path", rp).WithField("readlink_err", err.Error()).Warn("failed reading symlink for target path; skipping...")
}

View File

@@ -4,7 +4,10 @@ import (
"archive/tar"
"archive/zip"
"compress/gzip"
"context"
"fmt"
"io"
iofs "io/fs"
"os"
"path"
"path/filepath"
@@ -13,14 +16,13 @@ import (
"sync/atomic"
"time"
"emperror.dev/errors"
gzip2 "github.com/klauspost/compress/gzip"
zip2 "github.com/klauspost/compress/zip"
"emperror.dev/errors"
"github.com/mholt/archiver/v3"
"github.com/mholt/archiver/v4"
)
// CompressFiles compresses all of the files matching the given paths in the
// CompressFiles compresses all the files matching the given paths in the
// specified directory. This function also supports passing nested paths to only
// compress certain files and folders when working in a larger directory. This
// effectively creates a local backup, but rather than ignoring specific files
@@ -35,7 +37,7 @@ func (fs *Filesystem) CompressFiles(dir string, paths []string) (os.FileInfo, er
return nil, err
}
// Take all of the paths passed in and merge them together with the root directory we've gotten.
// Take all the paths passed in and merge them together with the root directory we've gotten.
for i, p := range paths {
paths[i] = filepath.Join(cleanedRootDir, p)
}
@@ -51,7 +53,7 @@ func (fs *Filesystem) CompressFiles(dir string, paths []string) (os.FileInfo, er
fmt.Sprintf("archive-%s.tar.gz", strings.ReplaceAll(time.Now().Format(time.RFC3339), ":", "")),
)
if err := a.Create(d); err != nil {
if err := a.Create(context.Background(), d); err != nil {
return nil, err
}
@@ -73,7 +75,7 @@ func (fs *Filesystem) CompressFiles(dir string, paths []string) (os.FileInfo, er
// SpaceAvailableForDecompression looks through a given archive and determines
// if decompressing it would put the server over its allocated disk space limit.
func (fs *Filesystem) SpaceAvailableForDecompression(dir string, file string) error {
func (fs *Filesystem) SpaceAvailableForDecompression(ctx context.Context, dir string, file string) error {
// Don't waste time trying to determine this if we know the server will have the space for
// it since there is no limit.
if fs.MaxDisk() <= 0 {
@@ -89,68 +91,139 @@ func (fs *Filesystem) SpaceAvailableForDecompression(dir string, file string) er
// waiting an unnecessary amount of time on this call.
dirSize, err := fs.DiskUsage(false)
var size int64
// Walk over the archive and figure out just how large the final output would be from unarchiving it.
err = archiver.Walk(source, func(f archiver.File) error {
if atomic.AddInt64(&size, f.Size())+dirSize > fs.MaxDisk() {
return newFilesystemError(ErrCodeDiskSpace, nil)
}
return nil
})
fsys, err := archiver.FileSystem(source)
if err != nil {
if IsUnknownArchiveFormatError(err) {
if errors.Is(err, archiver.ErrNoMatch) {
return newFilesystemError(ErrCodeUnknownArchive, err)
}
return err
}
var size int64
return iofs.WalkDir(fsys, ".", func(path string, d iofs.DirEntry, err error) error {
if err != nil {
return err
}
select {
case <-ctx.Done():
// Stop walking if the context is canceled.
return ctx.Err()
default:
info, err := d.Info()
if err != nil {
return err
}
if atomic.AddInt64(&size, info.Size())+dirSize > fs.MaxDisk() {
return newFilesystemError(ErrCodeDiskSpace, nil)
}
return nil
}
})
}
// DecompressFile will decompress a file in a given directory by using the
// archiver tool to infer the file type and go from there. This will walk over
// all of the files within the given archive and ensure that there is not a
// all the files within the given archive and ensure that there is not a
// zip-slip attack being attempted by validating that the final path is within
// the server data directory.
func (fs *Filesystem) DecompressFile(dir string, file string) error {
func (fs *Filesystem) DecompressFile(ctx context.Context, dir string, file string) error {
source, err := fs.SafePath(filepath.Join(dir, file))
if err != nil {
return err
}
// Ensure that the source archive actually exists on the system.
if _, err := os.Stat(source); err != nil {
return fs.DecompressFileUnsafe(ctx, dir, source)
}
// DecompressFileUnsafe will decompress any file on the local disk without checking
// if it is owned by the server. The file will be SAFELY decompressed and extracted
// into the server's directory.
func (fs *Filesystem) DecompressFileUnsafe(ctx context.Context, dir string, file string) error {
// Ensure that the archive actually exists on the system.
if _, err := os.Stat(file); err != nil {
return errors.WithStack(err)
}
// Walk all of the files in the archiver file and write them to the disk. If any
// directory is encountered it will be skipped since we handle creating any missing
// directories automatically when writing files.
err = archiver.Walk(source, func(f archiver.File) error {
f, err := os.Open(file)
if err != nil {
return err
}
// TODO: defer file close?
// Identify the type of archive we are dealing with.
format, input, err := archiver.Identify(filepath.Base(file), f)
if err != nil {
if errors.Is(err, archiver.ErrNoMatch) {
return newFilesystemError(ErrCodeUnknownArchive, err)
}
return err
}
return fs.extractStream(ctx, extractStreamOptions{
Directory: dir,
Format: format,
Reader: input,
})
}
// ExtractStreamUnsafe .
func (fs *Filesystem) ExtractStreamUnsafe(ctx context.Context, dir string, r io.Reader) error {
format, input, err := archiver.Identify("archive.tar.gz", r)
if err != nil {
if errors.Is(err, archiver.ErrNoMatch) {
return newFilesystemError(ErrCodeUnknownArchive, err)
}
return err
}
return fs.extractStream(ctx, extractStreamOptions{
Directory: dir,
Format: format,
Reader: input,
})
}
type extractStreamOptions struct {
// The directory to extract the archive to.
Directory string
// File name of the archive.
FileName string
// Format of the archive.
Format archiver.Format
// Reader for the archive.
Reader io.Reader
}
func (fs *Filesystem) extractStream(ctx context.Context, opts extractStreamOptions) error {
// Decompress and extract archive
if ex, ok := opts.Format.(archiver.Extractor); ok {
return ex.Extract(ctx, opts.Reader, nil, func(ctx context.Context, f archiver.File) error {
if f.IsDir() {
return nil
}
p := filepath.Join(dir, ExtractNameFromArchive(f))
p := filepath.Join(opts.Directory, ExtractNameFromArchive(f))
// If it is ignored, just don't do anything with the file and skip over it.
if err := fs.IsIgnored(p); err != nil {
return nil
}
if err := fs.Writefile(p, f); err != nil {
return wrapError(err, source)
r, err := f.Open()
if err != nil {
return err
}
defer r.Close()
if err := fs.Writefile(p, r); err != nil {
return wrapError(err, opts.FileName)
}
// Update the file permissions to the one set in the archive.
if err := fs.Chmod(p, f.Mode()); err != nil {
return wrapError(err, source)
return wrapError(err, opts.FileName)
}
// Update the file modification time to the one set in the archive.
if err := fs.Chtimes(p, f.ModTime(), f.ModTime()); err != nil {
return wrapError(err, source)
return wrapError(err, opts.FileName)
}
return nil
})
if err != nil {
if IsUnknownArchiveFormatError(err) {
return newFilesystemError(ErrCodeUnknownArchive, err)
}
return err
}
return nil
}

View File

@@ -1,6 +1,7 @@
package filesystem
import (
"context"
"os"
"sync/atomic"
"testing"
@@ -28,7 +29,7 @@ func TestFilesystem_DecompressFile(t *testing.T) {
g.Assert(err).IsNil()
// decompress
err = fs.DecompressFile("/", "test."+ext)
err = fs.DecompressFile(context.Background(), "/", "test."+ext)
g.Assert(err).IsNil()
// make sure everything is where it is supposed to be

View File

@@ -1,8 +1,6 @@
package filesystem
import (
"context"
"github.com/pterodactyl/wings/internal/vhd"
"sync"
"sync/atomic"
"syscall"
@@ -37,46 +35,18 @@ func (ult *usageLookupTime) Get() time.Time {
return ult.value
}
// MaxDisk returns the maximum amount of disk space that this Filesystem
// instance is allowed to use.
// Returns the maximum amount of disk space that this Filesystem instance is allowed to use.
func (fs *Filesystem) MaxDisk() int64 {
return atomic.LoadInt64(&fs.diskLimit)
}
// SetDiskLimit sets the disk space limit for this Filesystem instance. This
// logic will also handle mounting or unmounting a virtual disk if it is being
// used currently.
func (fs *Filesystem) SetDiskLimit(ctx context.Context, i int64) error {
// Do nothing if this method is called but the limit is not changing.
if atomic.LoadInt64(&fs.diskLimit) == i {
return nil
}
if vhd.Enabled() {
if i == 0 && fs.IsVirtual() {
fs.log().Debug("disk limit changed to 0, destroying virtual disk")
// Remove the VHD if it is mounted so that we're just storing files directly on the system
// since we cannot have a virtual disk with a space limit enforced like that.
if err := fs.vhd.Destroy(ctx); err != nil {
return errors.WithStackIf(err)
}
fs.vhd = nil
}
// If we're setting a disk size go ahead and mount the VHD if it isn't already mounted,
// and then allocate the new space to the disk.
if i > 0 {
fs.log().Debug("disk limit updated, allocating new space to virtual disk")
if err := fs.ConfigureDisk(ctx, i); err != nil {
return errors.WithStackIf(err)
}
}
}
fs.log().WithField("limit", i).Debug("disk limit updated")
atomic.StoreInt64(&fs.diskLimit, i)
return nil
// Sets the disk space limit for this Filesystem instance.
func (fs *Filesystem) SetDiskLimit(i int64) {
atomic.SwapInt64(&fs.diskLimit, i)
}
// HasSpaceErr is the same concept as HasSpaceAvailable however this will return
// an error if there is no space, rather than a boolean value.
// The same concept as HasSpaceAvailable however this will return an error if there is
// no space, rather than a boolean value.
func (fs *Filesystem) HasSpaceErr(allowStaleValue bool) error {
if !fs.HasSpaceAvailable(allowStaleValue) {
return newFilesystemError(ErrCodeDiskSpace, nil)
@@ -84,77 +54,67 @@ func (fs *Filesystem) HasSpaceErr(allowStaleValue bool) error {
return nil
}
// HasSpaceAvailable determines if the directory a file is trying to be added to
// has enough space available for the file to be written to.
// Determines if the directory a file is trying to be added to has enough space available
// for the file to be written to.
//
// Because determining the amount of space being used by a server is a taxing
// operation we will load it all up into a cache and pull from that as long as
// the key is not expired. This operation will potentially block unless
// allowStaleValue is set to true. See the documentation on DiskUsage for how
// this affects the call.
// Because determining the amount of space being used by a server is a taxing operation we
// will load it all up into a cache and pull from that as long as the key is not expired.
//
// If the current size of the disk is larger than the maximum allowed size this
// function will return false, in all other cases it will return true. We do
// not check the existence of a virtual disk at this point since this logic is
// used to return friendly error messages to users, and also prevent us wasting
// time on more taxing operations when we know the result will end up failing due
// to space limits.
//
// If the servers disk limit is set to 0 it means there is no limit, however the
// DiskUsage method is still called to keep the cache warm. This function will
// always return true for a server with no limit set.
// This operation will potentially block unless allowStaleValue is set to true. See the
// documentation on DiskUsage for how this affects the call.
func (fs *Filesystem) HasSpaceAvailable(allowStaleValue bool) bool {
size, err := fs.DiskUsage(allowStaleValue)
if err != nil {
fs.log().WithField("error", err).Warn("failed to determine root fs directory size")
log.WithField("root", fs.root).WithField("error", err).Warn("failed to determine root fs directory size")
}
return fs.MaxDisk() == 0 || size <= fs.MaxDisk()
// 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 its limit but because this method caches the disk usage it would be best
// to calculate the disk usage and always return true.
if fs.MaxDisk() == 0 {
return true
}
return size <= fs.MaxDisk()
}
// CachedUsage returns the cached value for the amount of disk space used by the
// filesystem. Do not rely on this function for critical logical checks. It
// should only be used in areas where the actual disk usage does not need to be
// perfect, e.g. API responses for server resource usage.
// Returns the cached value for the amount of disk space used by the filesystem. Do not rely on this
// function for critical logical checks. It should only be used in areas where the actual disk usage
// does not need to be perfect, e.g. API responses for server resource usage.
func (fs *Filesystem) CachedUsage() int64 {
return atomic.LoadInt64(&fs.diskUsed)
}
// DiskUsage is an internal helper function to allow other parts of the codebase
// to check the total used disk space as needed without overly taxing the system.
// This will prioritize the value from the cache to avoid excessive IO usage. We
// will only walk the filesystem and determine the size of the directory if there
// Internal helper function to allow other parts of the codebase to check the total used disk space
// as needed without overly taxing the system. This will prioritize the value from the cache to avoid
// excessive IO usage. We will only walk the filesystem and determine the size of the directory if there
// is no longer a cached value.
//
// If "allowStaleValue" is set to true, a stale value MAY be returned to the
// caller if there is an expired cache value AND there is currently another
// lookup in progress. If there is no cached value but no other lookup is in
// progress, a fresh disk space response will be returned to the caller.
// If "allowStaleValue" is set to true, a stale value MAY be returned to the caller if there is an
// expired cache value AND there is currently another lookup in progress. If there is no cached value but
// no other lookup is in progress, a fresh disk space response will be returned to the caller.
//
// This is primarily to avoid a bunch of I/O operations from piling up on the
// server, especially on servers with a large amount of files.
// This is primarily to avoid a bunch of I/O operations from piling up on the server, especially on servers
// with a large amount of files.
func (fs *Filesystem) DiskUsage(allowStaleValue bool) (int64, error) {
// A disk check interval of 0 means this functionality is completely disabled.
if fs.diskCheckInterval == 0 {
return 0, nil
}
since := time.Now().Add(time.Second * fs.diskCheckInterval * -1)
// If the last lookup time was before our calculated limit we will re-execute this
// checking logic. If the lookup time was after the oldest possible timestamp we will
// continue returning the cached value.
if fs.lastLookupTime.Get().Before(since) {
if !fs.lastLookupTime.Get().After(time.Now().Add(time.Second * fs.diskCheckInterval * -1)) {
// If we are now allowing a stale response go ahead and perform the lookup and return the fresh
// value. This is a blocking operation to the calling process.
if !allowStaleValue {
return fs.updateCachedDiskUsage()
}
} else if !fs.lookupInProgress.Load() {
// Otherwise, if we allow a stale value and there isn't a valid item in the cache and we aren't
// currently performing a lookup, just do the disk usage calculation in the background.
if !fs.lookupInProgress.Load() {
go func(fs *Filesystem) {
if _, err := fs.updateCachedDiskUsage(); err != nil {
fs.log().WithField("error", err).Warn("failed to update fs disk usage from within routine")
log.WithField("root", fs.root).WithField("error", err).Warn("failed to update fs disk usage from within routine")
}
}(fs)
}
@@ -234,14 +194,11 @@ func (fs *Filesystem) DirectorySize(dir string) (int64, error) {
return size, errors.WrapIf(err, "server/filesystem: directorysize: failed to walk directory")
}
// HasSpaceFor is a function to determine if a server has space available for a
// file of a given size. If space is available, no error will be returned,
// otherwise an ErrNotEnoughSpace error will be raised. If this filesystem is
// configured as a virtual disk this function is a no-op as we will fall through
// to the native implementation to throw back an error if there is not disk
// space available.
// Helper function to determine if a server has space available for a file of a given size.
// If space is available, no error will be returned, otherwise an ErrNotEnoughSpace error
// will be raised.
func (fs *Filesystem) HasSpaceFor(size int64) error {
if fs.IsVirtual() || fs.MaxDisk() == 0 {
if fs.MaxDisk() == 0 {
return nil
}
s, err := fs.DiskUsage(true)
@@ -277,7 +234,3 @@ func (fs *Filesystem) addDisk(i int64) int64 {
return atomic.AddInt64(&fs.diskUsed, i)
}
func (fs *Filesystem) log() *log.Entry {
return log.WithField("server", fs.uuid).WithField("root", fs.root)
}

View File

@@ -4,7 +4,6 @@ import (
"fmt"
"os"
"path/filepath"
"strings"
"emperror.dev/errors"
"github.com/apex/log"
@@ -19,6 +18,7 @@ const (
ErrCodePathResolution ErrorCode = "E_BADPATH"
ErrCodeDenylistFile ErrorCode = "E_DENYLIST"
ErrCodeUnknownError ErrorCode = "E_UNKNOWN"
ErrNotExist ErrorCode = "E_NOTEXIST"
)
type Error struct {
@@ -69,6 +69,8 @@ func (e *Error) Error() string {
r = "<empty>"
}
return fmt.Sprintf("filesystem: server path [%s] resolves to a location outside the server root: %s", e.path, r)
case ErrNotExist:
return "filesystem: does not exist"
case ErrCodeUnknownError:
fallthrough
default:
@@ -122,15 +124,6 @@ func IsErrorCode(err error, code ErrorCode) bool {
return false
}
// IsUnknownArchiveFormatError checks if the error is due to the archive being
// in an unexpected file format.
func IsUnknownArchiveFormatError(err error) bool {
if err != nil && strings.HasPrefix(err.Error(), "format ") {
return true
}
return false
}
// NewBadPathResolution returns a new BadPathResolution error.
func NewBadPathResolution(path string, resolved string) error {
return errors.WithStackDepth(&Error{code: ErrCodePathResolution, path: path, resolved: resolved}, 1)

View File

@@ -20,7 +20,6 @@ import (
ignore "github.com/sabhiram/go-gitignore"
"github.com/pterodactyl/wings/config"
"github.com/pterodactyl/wings/internal/vhd"
"github.com/pterodactyl/wings/system"
)
@@ -31,23 +30,19 @@ type Filesystem struct {
diskUsed int64
diskCheckInterval time.Duration
denylist *ignore.GitIgnore
vhd *vhd.Disk
// The maximum amount of disk space (in bytes) that this Filesystem instance can use.
diskLimit int64
// The root data directory path for this Filesystem instance.
root string
uuid string
isTest bool
}
// New creates a new Filesystem instance for a given server.
func New(uuid string, size int64, denylist []string) *Filesystem {
root := filepath.Join(config.Get().System.Data, uuid)
fs := Filesystem{
uuid: uuid,
func New(root string, size int64, denylist []string) *Filesystem {
return &Filesystem{
root: root,
diskLimit: size,
diskCheckInterval: time.Duration(config.Get().System.DiskCheckInterval),
@@ -55,15 +50,6 @@ func New(uuid string, size int64, denylist []string) *Filesystem {
lookupInProgress: system.NewAtomicBool(false),
denylist: ignore.CompileIgnoreLines(denylist...),
}
// If VHD support is enabled but this server is configured with no disk size
// limit we cannot actually use a virtual disk. In that case fall back to using
// the default driver.
if vhd.Enabled() && size > 0 {
fs.vhd = vhd.New(size, vhd.DiskPath(uuid), fs.root)
}
return &fs
}
// Path returns the root path for the Filesystem instance.
@@ -75,25 +61,28 @@ func (fs *Filesystem) Path() string {
func (fs *Filesystem) File(p string) (*os.File, Stat, error) {
cleaned, err := fs.SafePath(p)
if err != nil {
return nil, Stat{}, err
return nil, Stat{}, errors.WithStackIf(err)
}
st, err := fs.Stat(cleaned)
if err != nil {
return nil, Stat{}, err
if errors.Is(err, os.ErrNotExist) {
return nil, Stat{}, newFilesystemError(ErrNotExist, err)
}
return nil, Stat{}, errors.WithStackIf(err)
}
if st.IsDir() {
return nil, Stat{}, newFilesystemError(ErrCodeIsDirectory, nil)
}
f, err := os.Open(cleaned)
if err != nil {
return nil, Stat{}, err
return nil, Stat{}, errors.WithStackIf(err)
}
return f, st, nil
}
// Touch acts by creating the given file and path on the disk if it is not present
// already. If it is present, the file is opened using the defaults which will
// truncate the contents. The opened file is then returned to the caller.
// already. If it is present, the file is opened using the defaults which will truncate
// the contents. The opened file is then returned to the caller.
func (fs *Filesystem) Touch(p string, flag int) (*os.File, error) {
cleaned, err := fs.SafePath(p)
if err != nil {
@@ -169,12 +158,6 @@ func (fs *Filesystem) Writefile(p string, r io.Reader) error {
buf := make([]byte, 1024*4)
sz, err := io.CopyBuffer(file, r, buf)
if err != nil {
if strings.Contains(err.Error(), "no space left on device") {
return newFilesystemError(ErrCodeDiskSpace, err)
}
return errors.WrapIf(err, "filesystem: failed to copy buffer for file write")
}
// Adjust the disk usage to account for the old size and the new size of the file.
fs.addDisk(sz - currentSize)
@@ -332,9 +315,8 @@ func (fs *Filesystem) findCopySuffix(dir string, name string, extension string)
return name + suffix + extension, nil
}
// Copy takes a given input file path and creates a copy of the file at the same
// location, appending a unique number to the end. For example, a copy of "test.txt"
// would create "test 2.txt" as the copy, then "test 3.txt" and so on.
// Copies a given file to the same location and appends a suffix to the file to indicate that
// it has been copied.
func (fs *Filesystem) Copy(p string) error {
cleaned, err := fs.SafePath(p)
if err != nil {

View File

@@ -84,6 +84,35 @@ func (rfs *rootFs) reset() {
}
}
func TestFilesystem_Openfile(t *testing.T) {
g := Goblin(t)
fs, rfs := NewFs()
g.Describe("File", func() {
g.It("returns custom error when file does not exist", func() {
_, _, err := fs.File("foo/bar.txt")
g.Assert(err).IsNotNil()
g.Assert(IsErrorCode(err, ErrNotExist)).IsTrue()
})
g.It("returns file stat information", func() {
_ = rfs.CreateServerFile("foo.txt", []byte("hello world"))
f, st, err := fs.File("foo.txt")
g.Assert(err).IsNil()
g.Assert(st.Name()).Equal("foo.txt")
g.Assert(f).IsNotNil()
_ = f.Close()
})
g.AfterEach(func() {
rfs.reset()
})
})
}
func TestFilesystem_Writefile(t *testing.T) {
g := Goblin(t)
fs, rfs := NewFs()

View File

@@ -1,42 +0,0 @@
package filesystem
import (
"context"
"emperror.dev/errors"
"github.com/pterodactyl/wings/internal/vhd"
)
// IsVirtual returns true if the filesystem is currently using a virtual disk.
func (fs *Filesystem) IsVirtual() bool {
return fs.vhd != nil
}
// ConfigureDisk will attempt to create a new VHD if there is not one already
// created for the filesystem. If there is this method will attempt to resize
// the underlying data volume. Passing a size of 0 or less will panic.
func (fs *Filesystem) ConfigureDisk(ctx context.Context, size int64) error {
if size <= 0 {
panic("filesystem: attempt to configure disk with empty size")
}
if fs.vhd == nil {
fs.vhd = vhd.New(size, vhd.DiskPath(fs.uuid), fs.root)
if err := fs.MountDisk(ctx); err != nil {
return errors.WithStackIf(err)
}
}
// Resize the disk now that it is for sure mounted and exists on the system.
if err := fs.vhd.Resize(ctx, size); err != nil {
return errors.WithStackIf(err)
}
return nil
}
// MountDisk will attempt to mount the underlying virtual disk for the server.
// If the disk is already mounted this is a no-op function.
func (fs *Filesystem) MountDisk(ctx context.Context) error {
err := fs.vhd.Mount(ctx)
if errors.Is(err, vhd.ErrFilesystemMounted) {
return nil
}
return errors.WrapIf(err, "filesystem: failed to mount VHD")
}

View File

@@ -4,6 +4,7 @@ import (
"bufio"
"bytes"
"context"
"fmt"
"html/template"
"io"
"os"
@@ -31,19 +32,17 @@ import (
//
// Pass true as the first argument in order to execute a server sync before the
// process to ensure the latest information is used.
func (s *Server) Install(sync bool) error {
if sync {
s.Log().Info("syncing server state with remote source before executing installation process")
if err := s.Sync(); err != nil {
return errors.WrapIf(err, "install: failed to sync server state with Panel")
}
}
func (s *Server) Install() error {
return s.install(false)
}
func (s *Server) install(reinstall bool) error {
var err error
if !s.Config().SkipEggScripts {
// Send the start event so the Panel can automatically update. We don't send this unless the process
// is actually going to run, otherwise all sorts of weird rapid UI behavior happens since there isn't
// an actual install process being executed.
// Send the start event so the Panel can automatically update. We don't
// send this unless the process is actually going to run, otherwise all
// sorts of weird rapid UI behavior happens since there isn't an actual
// install process being executed.
s.Events().Publish(InstallStartedEvent, "")
err = s.internalInstall()
@@ -52,12 +51,13 @@ func (s *Server) Install(sync bool) error {
}
s.Log().WithField("was_successful", err == nil).Debug("notifying panel of server install state")
if serr := s.SyncInstallState(err == nil); serr != nil {
if serr := s.SyncInstallState(err == nil, reinstall); serr != nil {
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 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", err)
}
@@ -65,19 +65,20 @@ func (s *Server) Install(sync bool) error {
l.Warn("failed to notify panel of server install state")
}
// Ensure that the server is marked as offline at this point, otherwise you end up
// with a blank value which is a bit confusing.
// Ensure that the server is marked as offline at this point, otherwise you
// end up with a blank value which is a bit confusing.
s.Environment.SetState(environment.ProcessOfflineState)
// Push an event to the websocket so we can auto-refresh the information in the panel once
// the install is completed.
// Push an event to the websocket, so we can auto-refresh the information in
// the panel once the installation is completed.
s.Events().Publish(InstallCompletedEvent, "")
return errors.WithStackIf(err)
}
// Reinstalls a server's software by utilizing the install script for the server egg. This
// does not touch any existing files for the server, other than what the script modifies.
// Reinstall reinstalls a server's software by utilizing the installation script
// for the server egg. This does not touch any existing files for the server,
// other than what the script modifies.
func (s *Server) Reinstall() error {
if s.Environment.State() != environment.ProcessOfflineState {
s.Log().Debug("waiting for server instance to enter a stopped state")
@@ -86,7 +87,12 @@ func (s *Server) Reinstall() error {
}
}
return s.Install(true)
s.Log().Info("syncing server state with remote source before executing re-installation process")
if err := s.Sync(); err != nil {
return errors.WrapIf(err, "install: failed to sync server state with Panel")
}
return s.install(true)
}
// Internal installation function used to simplify reporting back to the Panel.
@@ -115,8 +121,9 @@ type InstallationProcess struct {
client *client.Client
}
// Generates a new installation process struct that will be used to create containers,
// and otherwise perform installation commands for a server.
// NewInstallationProcess returns a new installation process struct that will be
// used to create containers and otherwise perform installation commands for a
// server.
func NewInstallationProcess(s *Server, script *remote.InstallationScript) (*InstallationProcess, error) {
proc := &InstallationProcess{
Script: script,
@@ -132,8 +139,8 @@ func NewInstallationProcess(s *Server, script *remote.InstallationScript) (*Inst
return proc, nil
}
// Determines if the server is actively running the installation process by checking the status
// of the installer lock.
// IsInstalling returns if the server is actively running the installation
// process by checking the status of the installer lock.
func (s *Server) IsInstalling() bool {
return s.installing.Load()
}
@@ -154,7 +161,7 @@ func (s *Server) SetRestoring(state bool) {
s.restoring.Store(state)
}
// Removes the installer container for the server.
// RemoveContainer removes the installation container for the server.
func (ip *InstallationProcess) RemoveContainer() error {
err := ip.client.ContainerRemove(ip.Server.Context(), ip.Server.ID()+"_installer", types.ContainerRemoveOptions{
RemoveVolumes: true,
@@ -327,14 +334,14 @@ func (ip *InstallationProcess) BeforeExecute() error {
return nil
}
// Returns the log path for the installation process.
// GetLogPath returns the log path for the installation process.
func (ip *InstallationProcess) GetLogPath() string {
return filepath.Join(config.Get().System.LogDirectory, "/install", ip.Server.ID()+".log")
}
// Cleans up after the execution of the installation process. This grabs the logs from the
// process to store in the server configuration directory, and then destroys the associated
// installation container.
// AfterExecute cleans up after the execution of the installation process.
// This grabs the logs from the process to store in the server configuration
// directory, and then destroys the associated installation container.
func (ip *InstallationProcess) AfterExecute(containerId string) error {
defer ip.RemoveContainer()
@@ -419,7 +426,12 @@ func (ip *InstallationProcess) Execute() (string, error) {
},
}
tmpfsSize := strconv.Itoa(int(config.Get().Docker.TmpfsSize))
cfg := config.Get()
if cfg.System.User.Rootless.Enabled {
conf.User = fmt.Sprintf("%d:%d", cfg.System.User.Rootless.ContainerUID, cfg.System.User.Rootless.ContainerGID)
}
tmpfsSize := strconv.Itoa(int(cfg.Docker.TmpfsSize))
hostConf := &container.HostConfig{
Mounts: []mount.Mount{
{
@@ -439,18 +451,11 @@ func (ip *InstallationProcess) Execute() (string, error) {
Tmpfs: map[string]string{
"/tmp": "rw,exec,nosuid,size=" + tmpfsSize + "M",
},
DNS: config.Get().Docker.Network.Dns,
LogConfig: container.LogConfig{
Type: "local",
Config: map[string]string{
"max-size": "5m",
"max-file": "1",
"compress": "false",
},
},
DNS: cfg.Docker.Network.Dns,
LogConfig: cfg.Docker.ContainerLogConfig(),
Privileged: true,
NetworkMode: container.NetworkMode(config.Get().Docker.Network.Mode),
UsernsMode: container.UsernsMode(config.Get().Docker.UsernsMode),
NetworkMode: container.NetworkMode(cfg.Docker.Network.Mode),
UsernsMode: container.UsernsMode(cfg.Docker.UsernsMode),
}
// Ensure the root directory for the server exists properly before attempting
@@ -526,7 +531,7 @@ func (ip *InstallationProcess) StreamOutput(ctx context.Context, id string) erro
return nil
}
// resourceLimits returns the install container specific resource limits. This
// resourceLimits returns resource limits for the installation container. This
// looks at the globally defined install container limits and attempts to use
// the higher of the two (defined limits & server limits). This allows for servers
// with super low limits (e.g. Discord bots with 128Mb of memory) to perform more
@@ -538,8 +543,8 @@ func (ip *InstallationProcess) StreamOutput(ctx context.Context, id string) erro
func (ip *InstallationProcess) resourceLimits() container.Resources {
limits := config.Get().Docker.InstallerLimits
// Create a copy of the configuration so we're not accidentally making changes
// to the underlying server build data.
// Create a copy of the configuration, so we're not accidentally making
// changes to the underlying server build data.
c := *ip.Server.Config()
cfg := c.Build
if cfg.MemoryLimit < limits.Memory {
@@ -563,10 +568,12 @@ func (ip *InstallationProcess) resourceLimits() container.Resources {
return resources
}
// SyncInstallState makes a HTTP request to the Panel instance notifying it that
// SyncInstallState makes an HTTP request to the Panel instance notifying it that
// the server has completed the installation process, and what the state of the
// server is. A boolean value of "true" means everything was successful, "false"
// means something went wrong and the server must be deleted and re-created.
func (s *Server) SyncInstallState(successful bool) error {
return s.client.SetInstallationStatus(s.Context(), s.ID(), successful)
// server is.
func (s *Server) SyncInstallState(successful, reinstall bool) error {
return s.client.SetInstallationStatus(s.Context(), s.ID(), remote.InstallStatusRequest{
Successful: successful,
Reinstall: reinstall,
})
}

View File

@@ -38,7 +38,7 @@ func New(ctx context.Context, manager *server.Manager, details ServerDetails) (*
// Create a new server instance using the configuration we wrote to the disk
// so that everything gets instantiated correctly on the struct.
s, err := manager.InitServer(ctx, c)
s, err := manager.InitServer(c)
if err != nil {
return nil, errors.WrapIf(err, "installer: could not init server instance")
}

View File

@@ -5,6 +5,7 @@ import (
"fmt"
"io"
"os"
"path/filepath"
"runtime"
"sync"
"time"
@@ -24,16 +25,14 @@ import (
type Manager struct {
mu sync.RWMutex
client remote.Client
skipVhdInitialization bool
servers []*Server
}
// NewManager returns a new server manager instance. This will boot up all the
// servers that are currently present on the filesystem and set them into the
// manager.
func NewManager(ctx context.Context, client remote.Client, skipVhdInit bool) (*Manager, error) {
func NewManager(ctx context.Context, client remote.Client) (*Manager, error) {
m := NewEmptyManager(client)
m.skipVhdInitialization = skipVhdInit
if err := m.init(ctx); err != nil {
return nil, err
}
@@ -185,7 +184,7 @@ func (m *Manager) ReadStates() (map[string]string, error) {
// InitServer initializes a server using a data byte array. This will be
// marshaled into the given struct using a YAML marshaler. This will also
// configure the given environment for a server.
func (m *Manager) InitServer(ctx context.Context, data remote.ServerConfigurationResponse) (*Server, error) {
func (m *Manager) InitServer(data remote.ServerConfigurationResponse) (*Server, error) {
s, err := New(m.client)
if err != nil {
return nil, err
@@ -197,15 +196,7 @@ func (m *Manager) InitServer(ctx context.Context, data remote.ServerConfiguratio
return nil, errors.WithStackIf(err)
}
s.fs = filesystem.New(s.ID(), s.DiskSpace(), s.Config().Egg.FileDenylist)
// If this is a virtual filesystem we need to go ahead and mount the disk
// so that everything is accessible.
if s.fs.IsVirtual() && !m.skipVhdInitialization {
log.WithField("server", s.ID()).Info("mounting virtual disk for server")
if err := s.fs.MountDisk(ctx); err != nil {
return nil, err
}
}
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
// this logic in. When we're ready to support other environment we'll need to make
@@ -214,6 +205,7 @@ func (m *Manager) InitServer(ctx context.Context, data remote.ServerConfiguratio
Mounts: s.Mounts(),
Allocations: s.cfg.Allocations,
Limits: s.cfg.Build,
Labels: s.cfg.Labels,
}
envCfg := environment.NewConfiguration(settings, s.GetEnvironmentVariables())
@@ -267,7 +259,7 @@ func (m *Manager) init(ctx context.Context) error {
log.WithField("server", data.Uuid).WithField("error", err).Error("failed to parse server configuration from API response, skipping...")
return
}
s, err := m.InitServer(ctx, d)
s, err := m.InitServer(d)
if err != nil {
log.WithField("server", data.Uuid).WithField("error", err).Error("failed to load server, skipping...")
return

View File

@@ -179,8 +179,6 @@ func (s *Server) Log() *log.Entry {
//
// This also means mass actions can be performed against servers on the Panel
// and they will automatically sync with Wings when the server is started.
//
// TODO: accept a context value rather than using the server's context.
func (s *Server) Sync() error {
cfg, err := s.client.GetServerConfiguration(s.Context(), s.ID())
if err != nil {
@@ -196,9 +194,7 @@ func (s *Server) Sync() error {
// Update the disk space limits for the server whenever the configuration for
// it changes.
if err := s.fs.SetDiskLimit(s.Context(), s.DiskSpace()); err != nil {
return errors.WrapIf(err, "server: failed to sync server configuration from API")
}
s.fs.SetDiskLimit(s.DiskSpace())
s.SyncWithEnvironment()

View File

@@ -0,0 +1,52 @@
package transfer
import (
"context"
"fmt"
"io"
"github.com/pterodactyl/wings/internal/progress"
"github.com/pterodactyl/wings/server/filesystem"
)
// Archive returns an archive that can be used to stream the contents of the
// contents of a server.
func (t *Transfer) Archive() (*Archive, error) {
if t.archive == nil {
// Get the disk usage of the server (used to calculate the progress of the archive process)
rawSize, err := t.Server.Filesystem().DiskUsage(true)
if err != nil {
return nil, fmt.Errorf("transfer: failed to get server disk usage: %w", err)
}
// Create a new archive instance and assign it to the transfer.
t.archive = NewArchive(t, uint64(rawSize))
}
return t.archive, nil
}
// Archive represents an archive used to transfer the contents of a server.
type Archive struct {
archive *filesystem.Archive
}
// NewArchive returns a new archive associated with the given transfer.
func NewArchive(t *Transfer, size uint64) *Archive {
return &Archive{
archive: &filesystem.Archive{
BasePath: t.Server.Filesystem().Path(),
Progress: progress.NewProgress(size),
},
}
}
// Stream returns a reader that can be used to stream the contents of the archive.
func (a *Archive) Stream(ctx context.Context, w io.Writer) error {
return a.archive.Stream(ctx, w)
}
// Progress returns the current progress of the archive.
func (a *Archive) Progress() *progress.Progress {
return a.archive.Progress
}

4
server/transfer/doc.go Normal file
View File

@@ -0,0 +1,4 @@
// Package transfer handles all logic related to transferring servers between
// two nodes. This includes the logic for archiving a server on the source node
// and logic for importing a server from the source node into the target node.
package transfer

View File

@@ -0,0 +1,57 @@
package transfer
import (
"sync"
)
var (
incomingTransfers = NewManager()
outgoingTransfers = NewManager()
)
// Incoming returns a transfer manager for incoming transfers.
func Incoming() *Manager {
return incomingTransfers
}
// Outgoing returns a transfer manager for outgoing transfers.
func Outgoing() *Manager {
return outgoingTransfers
}
// Manager manages transfers.
type Manager struct {
mu sync.RWMutex
transfers map[string]*Transfer
}
// NewManager returns a new transfer manager.
func NewManager() *Manager {
return &Manager{
transfers: make(map[string]*Transfer),
}
}
// Add adds a transfer to the manager.
func (m *Manager) Add(transfer *Transfer) {
m.mu.Lock()
defer m.mu.Unlock()
m.transfers[transfer.Server.ID()] = transfer
}
// Remove removes a transfer from the manager.
func (m *Manager) Remove(transfer *Transfer) {
m.mu.Lock()
defer m.mu.Unlock()
delete(m.transfers, transfer.Server.ID())
}
// Get gets a transfer from the manager using a server ID.
func (m *Manager) Get(id string) *Transfer {
m.mu.RLock()
defer m.mu.RUnlock()
return m.transfers[id]
}

159
server/transfer/source.go Normal file
View File

@@ -0,0 +1,159 @@
package transfer
import (
"context"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"io"
"mime/multipart"
"net/http"
"time"
"github.com/pterodactyl/wings/internal/progress"
)
// PushArchiveToTarget POSTs the archive to the target node and returns the
// response body.
func (t *Transfer) PushArchiveToTarget(url, token string) ([]byte, error) {
ctx, cancel := context.WithCancel(t.ctx)
defer cancel()
t.SendMessage("Preparing to stream server data to destination...")
t.SetStatus(StatusProcessing)
a, err := t.Archive()
if err != nil {
t.Error(err, "Failed to get archive for transfer.")
return nil, errors.New("failed to get archive for transfer")
}
t.SendMessage("Streaming archive to destination...")
// Send the upload progress to the websocket every 5 seconds.
ctx2, cancel2 := context.WithCancel(ctx)
defer cancel2()
go func(ctx context.Context, p *progress.Progress, tc *time.Ticker) {
defer tc.Stop()
for {
select {
case <-ctx.Done():
return
case <-tc.C:
t.SendMessage("Uploading " + p.Progress(25))
}
}
}(ctx2, a.Progress(), time.NewTicker(5*time.Second))
// Create a new request using the pipe as the body.
body, writer := io.Pipe()
defer body.Close()
defer writer.Close()
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, body)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", token)
// Create a new multipart writer that writes the archive to the pipe.
mp := multipart.NewWriter(writer)
defer mp.Close()
req.Header.Set("Content-Type", mp.FormDataContentType())
// Create a new goroutine to write the archive to the pipe used by the
// multipart writer.
errChan := make(chan error)
go func() {
defer close(errChan)
defer writer.Close()
defer mp.Close()
src, pw := io.Pipe()
defer src.Close()
defer pw.Close()
h := sha256.New()
tee := io.TeeReader(src, h)
dest, err := mp.CreateFormFile("archive", "archive.tar.gz")
if err != nil {
errChan <- errors.New("failed to create form file")
return
}
ch := make(chan error)
go func() {
defer close(ch)
if _, err := io.Copy(dest, tee); err != nil {
ch <- fmt.Errorf("failed to stream archive to destination: %w", err)
return
}
t.Log().Debug("finished copying dest to tee")
}()
if err := a.Stream(ctx, pw); err != nil {
errChan <- errors.New("failed to stream archive to pipe")
return
}
t.Log().Debug("finished streaming archive to pipe")
// Close the pipe writer early to release resources and ensure that the data gets flushed.
_ = pw.Close()
// Wait for the copy to finish before we continue.
t.Log().Debug("waiting on copy to finish")
if err := <-ch; err != nil {
errChan <- err
return
}
if err := mp.WriteField("checksum", hex.EncodeToString(h.Sum(nil))); err != nil {
errChan <- errors.New("failed to stream checksum")
return
}
cancel2()
t.SendMessage("Finished streaming archive to destination.")
if err := mp.Close(); err != nil {
t.Log().WithError(err).Error("error while closing multipart writer")
}
t.Log().Debug("closed multipart writer")
}()
t.Log().Debug("sending archive to destination")
client := http.Client{Timeout: 0}
res, err := client.Do(req)
t.Log().Debug("waiting for stream to complete")
select {
case <-ctx.Done():
return nil, ctx.Err()
case err2 := <-errChan:
t.Log().Debug("stream completed")
if err != nil || err2 != nil {
if err == context.Canceled {
return nil, err
}
t.Log().WithError(err).Debug("failed to send archive to destination")
return nil, fmt.Errorf("http error: %w, multipart error: %v", err, err2)
}
defer res.Body.Close()
t.Log().Debug("received response from destination")
v, err := io.ReadAll(res.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response body: %w", err)
}
if res.StatusCode != http.StatusOK {
return nil, errors.New(string(v))
}
return v, nil
}
}

128
server/transfer/transfer.go Normal file
View File

@@ -0,0 +1,128 @@
package transfer
import (
"context"
"time"
"github.com/apex/log"
"github.com/mitchellh/colorstring"
"github.com/pterodactyl/wings/server"
"github.com/pterodactyl/wings/system"
)
// Status represents the current status of a transfer.
type Status string
// String satisfies the fmt.Stringer interface.
func (s Status) String() string {
return string(s)
}
const (
// StatusPending is the status of a transfer when it is first created.
StatusPending Status = "pending"
// StatusProcessing is the status of a transfer when it is currently in
// progress, such as when the archive is being streamed to the target node.
StatusProcessing Status = "processing"
// StatusCancelling is the status of a transfer when it is in the process of
// being cancelled.
StatusCancelling Status = "cancelling"
// StatusCancelled is the final status of a transfer when it has been
// cancelled.
StatusCancelled Status = "cancelled"
// StatusFailed is the final status of a transfer when it has failed.
StatusFailed Status = "failed"
// StatusCompleted is the final status of a transfer when it has completed.
StatusCompleted Status = "completed"
)
// Transfer represents a transfer of a server from one node to another.
type Transfer struct {
// ctx is the context for the transfer.
ctx context.Context
// cancel is used to cancel all ongoing transfer operations for the server.
cancel *context.CancelFunc
// Server associated with the transfer.
Server *server.Server
// status of the transfer.
status *system.Atomic[Status]
// archive is the archive that is being created for the transfer.
archive *Archive
}
// New returns a new transfer instance for the given server.
func New(ctx context.Context, s *server.Server) *Transfer {
ctx, cancel := context.WithCancel(ctx)
return &Transfer{
ctx: ctx,
cancel: &cancel,
Server: s,
status: system.NewAtomic(StatusPending),
}
}
// Context returns the context for the transfer.
func (t *Transfer) Context() context.Context {
return t.ctx
}
// Cancel cancels the transfer.
func (t *Transfer) Cancel() {
status := t.Status()
if status == StatusCancelling ||
status == StatusCancelled ||
status == StatusCompleted ||
status == StatusFailed {
return
}
if t.cancel == nil {
return
}
t.SetStatus(StatusCancelling)
(*t.cancel)()
}
// Status returns the current status of the transfer.
func (t *Transfer) Status() Status {
return t.status.Load()
}
// SetStatus sets the status of the transfer.
func (t *Transfer) SetStatus(s Status) {
// TODO: prevent certain status changes from happening.
// If we are cancelling, then we can't go back to processing.
t.status.Store(s)
t.Server.Events().Publish(server.TransferStatusEvent, s)
}
// SendMessage sends a message to the server's console.
func (t *Transfer) SendMessage(v string) {
t.Server.Events().Publish(
server.TransferLogsEvent,
colorstring.Color("[yellow][bold]"+time.Now().Format(time.RFC1123)+" [Transfer System] [Source Node]:[default] "+v),
)
}
// Error logs an error that occurred on the source node.
func (t *Transfer) Error(err error, v string) {
t.Log().WithError(err).Error(v)
t.SendMessage(v)
}
// Log returns a logger for the transfer.
func (t *Transfer) Log() *log.Entry {
if t.Server == nil {
return log.WithField("subsystem", "transfer")
}
return t.Server.Log().WithField("subsystem", "transfer")
}

View File

@@ -1,3 +1,3 @@
package system
var Version = "develop"
var Version = "1.11.0-rc.2"

View File

@@ -1,17 +1,57 @@
package system
import (
"context"
"runtime"
"github.com/acobaugh/osrelease"
"github.com/docker/docker/api/types"
"github.com/docker/docker/client"
"github.com/docker/docker/pkg/parsers/kernel"
)
type Information struct {
Version string `json:"version"`
KernelVersion string `json:"kernel_version"`
Docker DockerInformation `json:"docker"`
System System `json:"system"`
}
type DockerInformation struct {
Version string `json:"version"`
Cgroups DockerCgroups `json:"cgroups"`
Containers DockerContainers `json:"containers"`
Storage DockerStorage `json:"storage"`
Runc DockerRunc `json:"runc"`
}
type DockerCgroups struct {
Driver string `json:"driver"`
Version string `json:"version"`
}
type DockerContainers struct {
Total int `json:"total"`
Running int `json:"running"`
Paused int `json:"paused"`
Stopped int `json:"stopped"`
}
type DockerStorage struct {
Driver string `json:"driver"`
Filesystem string `json:"filesystem"`
}
type DockerRunc struct {
Version string `json:"version"`
}
type System struct {
Architecture string `json:"architecture"`
CPUThreads int `json:"cpu_threads"`
MemoryBytes int64 `json:"memory_bytes"`
KernelVersion string `json:"kernel_version"`
OS string `json:"os"`
CpuCount int `json:"cpu_count"`
OSType string `json:"os_type"`
}
func GetSystemInformation() (*Information, error) {
@@ -20,13 +60,83 @@ func GetSystemInformation() (*Information, error) {
return nil, err
}
s := &Information{
Version: Version,
KernelVersion: k.String(),
Architecture: runtime.GOARCH,
OS: runtime.GOOS,
CpuCount: runtime.NumCPU(),
version, info, err := GetDockerInfo(context.Background())
if err != nil {
return nil, err
}
return s, nil
release, err := osrelease.Read()
if err != nil {
return nil, err
}
var os string
if release["PRETTY_NAME"] != "" {
os = release["PRETTY_NAME"]
} else if release["NAME"] != "" {
os = release["NAME"]
} else {
os = info.OperatingSystem
}
var filesystem string
for _, v := range info.DriverStatus {
if v[0] != "Backing Filesystem" {
continue
}
filesystem = v[1]
break
}
return &Information{
Version: Version,
Docker: DockerInformation{
Version: version.Version,
Cgroups: DockerCgroups{
Driver: info.CgroupDriver,
Version: info.CgroupVersion,
},
Containers: DockerContainers{
Total: info.Containers,
Running: info.ContainersRunning,
Paused: info.ContainersPaused,
Stopped: info.ContainersStopped,
},
Storage: DockerStorage{
Driver: info.Driver,
Filesystem: filesystem,
},
Runc: DockerRunc{
Version: info.RuncCommit.ID,
},
},
System: System{
Architecture: runtime.GOARCH,
CPUThreads: runtime.NumCPU(),
MemoryBytes: info.MemTotal,
KernelVersion: k.String(),
OS: os,
OSType: runtime.GOOS,
},
}, nil
}
func GetDockerInfo(ctx context.Context) (types.Version, types.Info, error) {
// TODO: find a way to re-use the client from the docker environment.
c, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
if err != nil {
return types.Version{}, types.Info{}, err
}
dockerVersion, err := c.ServerVersion(ctx)
if err != nil {
return types.Version{}, types.Info{}, err
}
dockerInfo, err := c.Info(ctx)
if err != nil {
return types.Version{}, types.Info{}, err
}
return dockerVersion, dockerInfo, nil
}

View File

@@ -120,7 +120,7 @@ func ScanReader(r io.Reader, callback func(line []byte)) error {
return nil
}
func FormatBytes(b int64) string {
func FormatBytes[T int | int16 | int32 | int64 | uint | uint16 | uint32 | uint64](b T) string {
if b < 1024 {
return fmt.Sprintf("%d B", b)
}
@@ -188,14 +188,14 @@ func NewAtomicString(v string) *AtomicString {
return &AtomicString{v: v}
}
// Stores the string value passed atomically.
// Store stores the string value passed atomically.
func (as *AtomicString) Store(v string) {
as.mu.Lock()
as.v = v
as.mu.Unlock()
}
// Loads the string value and returns it.
// Load loads the string value and returns it.
func (as *AtomicString) Load() string {
as.mu.RLock()
defer as.mu.RUnlock()
@@ -211,3 +211,41 @@ func (as *AtomicString) UnmarshalJSON(b []byte) error {
func (as *AtomicString) MarshalJSON() ([]byte, error) {
return json.Marshal(as.Load())
}
type Atomic[T any] struct {
v T
mu sync.RWMutex
}
func NewAtomic[T any](v T) *Atomic[T] {
return &Atomic[T]{v: v}
}
// Store stores the string value passed atomically.
func (a *Atomic[T]) Store(v T) {
a.mu.Lock()
defer a.mu.Unlock()
a.v = v
}
// Load loads the string value and returns it.
func (a *Atomic[T]) Load() T {
a.mu.RLock()
defer a.mu.RUnlock()
return a.v
}
// UnmarshalJSON unmarshals the JSON value into the Atomic[T] value.
func (a *Atomic[T]) UnmarshalJSON(b []byte) error {
a.mu.Lock()
defer a.mu.Unlock()
return json.Unmarshal(b, &a.v)
}
// MarshalJSON marshals the Atomic[T] value into JSON.
func (a *Atomic[T]) MarshalJSON() ([]byte, error) {
return json.Marshal(a.Load())
}