Compare commits

...

30 Commits

Author SHA1 Message Date
Dane Everitt
1f3394b82d Update release.yml 2020-10-22 21:42:34 -07:00
Dane Everitt
bae63c4321 Fix bad yaml 2020-10-22 21:41:00 -07:00
Dane Everitt
f99640a42d Update CHANGELOG.md 2020-10-22 21:36:24 -07:00
Dane Everitt
c73d632e0d Use correct case-insensitive regex; closes pterodactyl/panel#2546 2020-10-22 20:22:27 -07:00
Dane Everitt
903902e123 Match multiple times 2020-10-22 20:17:58 -07:00
Dane Everitt
1c787b5f26 Fix handling of super long lines to not hit a scanner error; closes pterodactyl/panel#2549
Also fixes return handling in games like Minecraft to properly display the full line output.
2020-10-19 18:13:52 -07:00
Dane Everitt
3f9ac8b89a Fix local images not being pulled correctly; closes #2559 2020-10-19 16:18:33 -07:00
Dane Everitt
560c832cc6 Apply timezone cleaning to final result, closes #2546 2020-10-19 16:13:59 -07:00
Dane Everitt
13058ad64b Merge branch 'develop' of https://github.com/pterodactyl/wings into develop 2020-10-19 15:27:11 -07:00
Dane Everitt
305cd512a7 Update README.md 2020-10-19 15:27:10 -07:00
Matthew Penner
3cd17a2856 Add arm to release workflow 2020-10-17 15:55:05 -06:00
Matthew Penner
56789196d4 Disable flaky test so I can test cross-platform building 2020-10-17 15:47:30 -06:00
Matthew Penner
70804dd20f Remoe stat_arm.go, make stat_linux.go with with both 32 and 64 bit systems 2020-10-17 15:45:14 -06:00
Matthew Penner
19d821aab5 Fix arm build 2020-10-17 15:41:13 -06:00
Matthew Penner
4ce35d3d17 Fix race in filesystem_test.go 2020-10-17 15:31:40 -06:00
Matthew Penner
a62b588ace Add matrix to build-test.yml 2020-10-17 15:13:06 -06:00
Dane Everitt
9b54be06bb Remove unused config values 2020-10-17 14:09:02 -07:00
Dane Everitt
c031d37b91 Use single call to create/return the docker client 2020-10-17 14:04:56 -07:00
Dane Everitt
19051c99ef Support arm builds 2020-10-17 13:52:38 -07:00
Jakob
99fd416cee add version negotiation fix to changelog 2020-10-17 21:49:43 +02:00
Jakob
acf09180f0 Merge pull request #66 from pterodactyl/fix/version-negotiation
Fix version negotiation for installs and diagnostics
2020-10-17 21:43:00 +02:00
Jakob
b19fc88a95 add version negotiation to diagnostics docker client 2020-10-17 21:42:23 +02:00
Jakob
e185f597ba add version negotiation to server install docker client 2020-10-17 21:41:01 +02:00
Dane Everitt
3973747c9c Update README.md 2020-10-17 12:11:06 -07:00
Dane Everitt
947279a07c Don't abort entire boot process due to one bad server egg; closes pterodactyl/panel#2448 2020-10-17 12:06:47 -07:00
Dane Everitt
ad1ed0f24a Merge branch 'develop' of https://github.com/pterodactyl/wings into develop 2020-10-17 11:35:22 -07:00
Dane Everitt
80387bc294 Use more easily configurable timezone, remove /etc/timezone mounts from containers; closes pterodactyl/panel#2513
If this does not completely solve the issue in containers then we need to evaluate the image being used to determine what changes need to happen to the image itself to support the timezone.

ref pterodactyl/panel#2239
ref pterodactyl/panel#2329
ref pterodactyl/panel#2389
2020-10-17 11:35:20 -07:00
Dane Everitt
e8dbba5cc0 Merge pull request #65 from AreYouRlyScared/patch-3
Update diagnostics.go
2020-10-14 08:22:30 -07:00
Charles Morgan
f50f580dcc Update diagnostics.go 2020-10-14 05:06:14 -04:00
Dane Everitt
7e8033d96c Update README.md 2020-10-12 21:12:36 -07:00
22 changed files with 333 additions and 164 deletions

View File

@@ -9,25 +9,48 @@ on:
jobs:
build:
runs-on: ubuntu-20.04
strategy:
# Default is true, cancels jobs for other platforms in the matrix if one fails
fail-fast: false
matrix:
os: [ ubuntu-20.04 ]
go: [ 1.15 ]
goos: [ linux ]
goarch: [ amd64, arm, arm64 ]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
- uses: actions/setup-go@v2
with:
go-version: '1.15.2'
go-version: ${{ matrix.go }}
- name: Build
run: GOOS=linux GOARCH=amd64 go build -ldflags="-s -w -X github.com/pterodactyl/wings/system.Version=dev-${GIT_COMMIT:0:7}" -o build/wings_linux_amd64 -v wings.go
env:
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
run: |
go build -v -ldflags="-s -w -X github.com/pterodactyl/wings/system.Version=dev-${GIT_COMMIT:0:7}" -o build/wings_${{ matrix.goos }}_${{ matrix.goarch }} wings.go
- name: Test
run: go test ./...
- name: Compress binary and make it executable
if: ${{ github.ref == 'refs/heads/develop' || github.event_name == 'pull_request' }}
run: upx build/wings_linux_amd64 && chmod +x build/wings_linux_amd64
run: |
upx build/wings_${{ matrix.goos }}_${{ matrix.goarch }} && chmod +x build/wings_${{ matrix.goos }}_${{ matrix.goarch }}
- uses: actions/upload-artifact@v2
if: ${{ github.ref == 'refs/heads/develop' || github.event_name == 'pull_request' }}
with:
name: wings_linux_amd64
path: build/wings_linux_amd64
name: wings_${{ matrix.goos }}_${{ matrix.goarch }}
path: build/wings_${{ matrix.goos }}_${{ matrix.goarch }}
- uses: actions/upload-artifact@v2
if: ${{ github.ref == 'refs/heads/develop' || github.event_name == 'pull_request' }}
with:
name: wings_${{ matrix.goos }}_${{ matrix.goarch }}
path: build/wings_${{ matrix.goos }}_${{ matrix.goarch }}

View File

@@ -8,22 +8,30 @@ on:
jobs:
release:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- uses: actions/setup-go@v2
with:
go-version: '1.15.2'
- name: Build
env:
REF: ${{ github.ref }}
run: 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
run: |
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
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
GOOS=linux GOARCH=arm go build -ldflags="-s -w -X github.com/pterodactyl/wings/system.Version=${REF:11}" -o build/wings_linux_arm -v wings.go
- name: Test
run: go test ./...
- name: Compress binary and make it executable
run: upx --brute build/wings_linux_amd64 && chmod +x build/wings_linux_amd64
run: |
upx --brute build/wings_linux_amd64 && chmod +x build/wings_linux_amd64
upx build/wings_linux_arm64 && chmod +x build/wings_linux_arm64
upx build/wings_linux_arm && chmod +x build/wings_linux_arm
- name: Extract changelog
env:
@@ -35,8 +43,10 @@ jobs:
- name: Create checksum and add to changelog
run: |
SUM=`cd build && sha256sum wings_linux_amd64`
echo -e "\n#### SHA256 Checksum\n\n\`\`\`\n$SUM\n\`\`\`\n" >> ./RELEASE_CHANGELOG
echo $SUM > checksum.txt
SUM2=`cd build && sha256sum wings_linux_arm64`
SUM3=`cd build && sha256sum wings_linux_arm`
echo -e "\n#### SHA256 Checksum\n\`\`\`\n$SUM\n$SUM2\n$SUM3\n\`\`\`\n" >> ./RELEASE_CHANGELOG
echo -e "$SUM\n$SUM2\n$SUM3" > checksums.txt
- name: Create release branch
env:
@@ -63,9 +73,8 @@ jobs:
body_path: ./RELEASE_CHANGELOG
draft: true
prerelease: ${{ contains(github.ref, 'beta') || contains(github.ref, 'alpha') }}
- name: Upload binary
id: upload-release-binary
- name: Upload amd64 Binary
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -74,14 +83,33 @@ jobs:
asset_path: build/wings_linux_amd64
asset_name: wings_linux_amd64
asset_content_type: application/octet-stream
- 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_name: wings_linux_arm64
asset_content_type: application/octet-stream
- name: Upload arm 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_arm
asset_name: wings_linux_arm
asset_content_type: application/octet-stream
- name: Upload checksum
id: upload-release-checksum
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./checksum.txt
asset_name: checksum.txt
asset_path: ./checksums.txt
asset_name: checksums.txt
asset_content_type: text/plain

View File

@@ -1,5 +1,18 @@
# Changelog
## next
### Added
* Adds support for ARM to build outputs for wings.
### Fixed
* Fixed a few docker clients not having version negotiation enabled.
* Fixes local images prefixed with `~` getting pulled from remote sources rather than just using the local copy.
* Fixes console output breaking with certain games when excessive line length was output.
* Fixes an error when console lines were too long that would cause the console to stop updating until the server was restarted,
### Changed
* Simplified timezone logic for containers by properly grabbing the system timezone and then passing that through to containers with the `TZ=` environment variable.
## v1.0.0
This is the first official stable release of Wings! Please be aware that while this specific version changelog is very short,
it technically includes all of the previous beta and alpha releases within it. For the sake of version history and following
@@ -289,4 +302,4 @@ along though, I've only included the differences between this version and the pr
* Fixed bugs caused when unmarshaling data into a struct with default values.
* Timezone is properly set in containers by using the `TZ=` environment variable rather than mounting timezone data into the filesystem.
* Server data directly is now properly created when running the install process.
* Time drift caused in the socket JWT is now handled in a better manner and less likely to unexpectedly error out.
* Time drift caused in the socket JWT is now handled in a better manner and less likely to unexpectedly error out.

View File

@@ -1,5 +1,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=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
compress:
upx --brute build/wings_*

View File

@@ -1,6 +1,7 @@
[![Logo Image](https://cdn.pterodactyl.io/logos/new/pterodactyl_logo.png)](https://pterodactyl.io)
[![Discord](https://img.shields.io/discord/122900397965705216.svg?style=flat-square&label=Discord)](https://pterodactyl.io/discord)
![Discord](https://img.shields.io/discord/122900397965705216?label=Discord&logo=Discord&logoColor=white)
![GitHub Releases](https://img.shields.io/github/downloads/pterodactyl/wings/latest/total)
[![Go Report Card](https://goreportcard.com/badge/github.com/pterodactyl/wings)](https://goreportcard.com/report/github.com/pterodactyl/wings)
# Pterodactyl Wings
@@ -17,13 +18,15 @@ I would like to extend my sincere thanks to the following sponsors for helping f
| Company | About |
| ------- | ----- |
| [**WISP**](https://wisp.gg) | Extra features. |
| [**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. |
| [**VersatileNode**](https://versatilenode.com/) | Looking to host a minecraft server, vps, or a website? VersatileNode is one of the most affordable hosting providers to provide quality yet cheap services with incredible support. |
| [**MineStrator**](https://minestrator.com/) | Looking for a French highend hosting company for you minecraft server? More than 14,000 members on our discord, trust us. |
| [**DedicatedMC**](https://dedicatedmc.io/) | DedicatedMC provides Raw Power hosting at affordable pricing, making sure to never compromise on your performance and giving you the best performance money can buy. |
| [**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! |
| [**XCORE-SERVER.de**](https://xcore-server.de/) | XCORE-SERVER.de offers High-End Servers for hosting and gaming since 2012. Fast, excellent and well-known for eSports Gaming. |
| [**RoyaleHosting**](https://royalehosting.net/) | Build your dreams and deploy them with RoyaleHostings reliable servers and network. Easy to use, provisioned in a couple of minutes. |
| [**Spill Hosting**](https://spillhosting.no/) | Spill Hosting is a Norwegian hosting service, which aims to cheap services on quality servers. Premium i9-9900K processors will run your game like a dream. |
| [**DeinServerHost**](https://deinserverhost.de/) | DeinServerHost offers Dedicated, vps and Gameservers for many popular Games like Minecraft and Rust in Germany since 2013. |
## Documentation
* [Panel Documentation](https://pterodactyl.io/panel/1.0/getting_started.html)

View File

@@ -34,7 +34,7 @@ type InstallationScript struct {
}
// GetAllServerConfigurations fetches configurations for all servers assigned to this node.
func (r *PanelRequest) GetAllServerConfigurations() (map[string]*ServerConfigurationResponse, *RequestError, error) {
func (r *PanelRequest) GetAllServerConfigurations() (map[string]json.RawMessage, *RequestError, error) {
resp, err := r.Get("/servers")
if err != nil {
return nil, nil, errors.WithStack(err)
@@ -48,7 +48,7 @@ func (r *PanelRequest) GetAllServerConfigurations() (map[string]*ServerConfigura
}
b, _ := r.ReadBody()
res := map[string]*ServerConfigurationResponse{}
res := map[string]json.RawMessage{}
if len(b) == 2 {
return res, nil, nil
}
@@ -61,24 +61,23 @@ func (r *PanelRequest) GetAllServerConfigurations() (map[string]*ServerConfigura
}
// Fetches the server configuration and returns the struct for it.
func (r *PanelRequest) GetServerConfiguration(uuid string) (*ServerConfigurationResponse, *RequestError, error) {
func (r *PanelRequest) GetServerConfiguration(uuid string) (ServerConfigurationResponse, *RequestError, error) {
res := ServerConfigurationResponse{}
resp, err := r.Get(fmt.Sprintf("/servers/%s", uuid))
if err != nil {
return nil, nil, errors.WithStack(err)
return res, nil, errors.WithStack(err)
}
defer resp.Body.Close()
r.Response = resp
if r.HasError() {
return nil, r.Error(), nil
return res, r.Error(), nil
}
res := &ServerConfigurationResponse{}
b, _ := r.ReadBody()
if err := json.Unmarshal(b, res); err != nil {
return nil, nil, errors.WithStack(err)
if err := json.Unmarshal(b, &res); err != nil {
return res, nil, errors.WithStack(err)
}
return res, nil, nil

View File

@@ -5,6 +5,7 @@ import (
"encoding/json"
"errors"
"fmt"
"github.com/pterodactyl/wings/environment"
"io"
"io/ioutil"
"net/http"
@@ -19,7 +20,6 @@ import (
"github.com/AlecAivazis/survey/v2/terminal"
"github.com/docker/cli/components/engine/pkg/parsers/operatingsystem"
"github.com/docker/docker/api/types"
"github.com/docker/docker/client"
"github.com/docker/docker/pkg/parsers/kernel"
"github.com/pterodactyl/wings/config"
"github.com/pterodactyl/wings/system"
@@ -89,15 +89,15 @@ func diagnosticsCmdRun(cmd *cobra.Command, args []string) {
output := &strings.Builder{}
fmt.Fprintln(output, "Pterodactyl Wings - Diagnostics Report")
printHeader(output, "Versions")
fmt.Fprintln(output, "wings:", system.Version)
fmt.Fprintln(output, " wings:", system.Version)
if dockerErr == nil {
fmt.Fprintln(output, "Docker", dockerVersion.Version)
fmt.Fprintln(output, "Docker:", dockerVersion.Version)
}
if v, err := kernel.GetKernelVersion(); err == nil {
fmt.Fprintln(output, "Kernel:", v)
}
if os, err := operatingsystem.GetOperatingSystem(); err == nil {
fmt.Fprintln(output, "OS:", os)
fmt.Fprintln(output, " OS:", os)
}
printHeader(output, "Wings Configuration")
@@ -105,23 +105,23 @@ func diagnosticsCmdRun(cmd *cobra.Command, args []string) {
if cfg != nil {
fmt.Fprintln(output, " Panel Location:", redact(cfg.PanelLocation))
fmt.Fprintln(output, "")
fmt.Fprintln(output, "Internal Webserver:", redact(cfg.Api.Host) + ":", cfg.Api.Port)
fmt.Fprintln(output, " SSL Enabled:", cfg.Api.Ssl.Enabled)
fmt.Fprintln(output, " SSL Certificate:", redact(cfg.Api.Ssl.CertificateFile))
fmt.Fprintln(output, " SSL Key:", redact(cfg.Api.Ssl.KeyFile))
fmt.Fprintln(output, " Internal Webserver:", redact(cfg.Api.Host), ":", cfg.Api.Port)
fmt.Fprintln(output, " SSL Enabled:", cfg.Api.Ssl.Enabled)
fmt.Fprintln(output, " SSL Certificate:", redact(cfg.Api.Ssl.CertificateFile))
fmt.Fprintln(output, " SSL Key:", redact(cfg.Api.Ssl.KeyFile))
fmt.Fprintln(output, "")
fmt.Fprintln(output, " SFTP Server:", redact(cfg.System.Sftp.Address), ":", cfg.System.Sftp.Port)
fmt.Fprintln(output, " SFTP Read-Only:", cfg.System.Sftp.ReadOnly)
fmt.Fprintln(output, " SFTP Server:", redact(cfg.System.Sftp.Address), ":", cfg.System.Sftp.Port)
fmt.Fprintln(output, " SFTP Read-Only:", cfg.System.Sftp.ReadOnly)
fmt.Fprintln(output, "")
fmt.Fprintln(output, " Root Directory:", cfg.System.RootDirectory)
fmt.Fprintln(output, " Logs Directory:", cfg.System.LogDirectory)
fmt.Fprintln(output, " Data Directory:", cfg.System.Data)
fmt.Fprintln(output, " Archive Directory:", cfg.System.ArchiveDirectory)
fmt.Fprintln(output, " Backup Directory:", cfg.System.BackupDirectory)
fmt.Fprintln(output, " Root Directory:", cfg.System.RootDirectory)
fmt.Fprintln(output, " Logs Directory:", cfg.System.LogDirectory)
fmt.Fprintln(output, " Data Directory:", cfg.System.Data)
fmt.Fprintln(output, " Archive Directory:", cfg.System.ArchiveDirectory)
fmt.Fprintln(output, " Backup Directory:", cfg.System.BackupDirectory)
fmt.Fprintln(output, "")
fmt.Fprintln(output, " Username:", cfg.System.Username)
fmt.Fprintln(output, " Server Time:", time.Now().Format(time.RFC1123Z))
fmt.Fprintln(output, " Debug Mode:", cfg.Debug)
fmt.Fprintln(output, " Username:", cfg.System.Username)
fmt.Fprintln(output, " Server Time:", time.Now().Format(time.RFC1123Z))
fmt.Fprintln(output, " Debug Mode:", cfg.Debug)
} else {
fmt.Println("Failed to load configuration.", err)
}
@@ -140,7 +140,7 @@ func diagnosticsCmdRun(cmd *cobra.Command, args []string) {
}
}
fmt.Fprintln(output, "LoggingDriver:", dockerInfo.LoggingDriver)
fmt.Fprintln(output, "CgroupDriver:", dockerInfo.CgroupDriver)
fmt.Fprintln(output, " CgroupDriver:", dockerInfo.CgroupDriver)
if len(dockerInfo.Warnings) > 0 {
for _, w := range dockerInfo.Warnings {
fmt.Fprintln(output, w)
@@ -187,7 +187,7 @@ func diagnosticsCmdRun(cmd *cobra.Command, args []string) {
}
func getDockerInfo() (types.Version, types.Info, error) {
cli, err := client.NewClientWithOpts(client.FromEnv)
cli, err := environment.DockerClient()
if err != nil {
return types.Version{}, types.Info{}, err
}

View File

@@ -132,6 +132,13 @@ func rootCmdRun(*cobra.Command, []string) {
config.Set(c)
config.SetDebugViaFlag(debug)
if err := c.System.ConfigureTimezone(); err != nil {
log.WithField("error", err).Fatal("failed to detect system timezone or use supplied configuration value")
return
}
log.WithField("timezone", c.System.Timezone).Info("configured wings with system timezone")
if err := c.System.ConfigureDirectories(); err != nil {
log.WithField("error", err).Fatal("failed to configure system directories for pterodactyl")
return

View File

@@ -49,18 +49,6 @@ type DockerConfiguration struct {
// Domainname is the Docker domainname for all containers.
Domainname string `default:"" json:"domainname" yaml:"domainname"`
// If true, container images will be updated when a server starts if there
// is an update available. If false the daemon will not attempt updates and will
// defer to the host system to manage image updates.
UpdateImages bool `default:"true" json:"update_images" yaml:"update_images"`
// The location of the Docker socket.
Socket string `default:"/var/run/docker.sock" json:"socket" yaml:"socket"`
// Defines the location of the timezone file on the host system that should
// be mounted into the created containers so that they all use the same time.
TimezonePath string `default:"/etc/timezone" json:"timezone_path" yaml:"timezone_path"`
// Registries .
Registries map[string]RegistryConfiguration `json:"registries" yaml:"registries"`

View File

@@ -1,12 +1,18 @@
package config
import (
"context"
"fmt"
"github.com/apex/log"
"github.com/pkg/errors"
"html/template"
"io/ioutil"
"os"
"os/exec"
"path"
"path/filepath"
"regexp"
"time"
)
// Defines basic system configuration settings.
@@ -29,6 +35,13 @@ type SystemConfiguration struct {
// The user that should own all of the server files, and be used for containers.
Username string `default:"pterodactyl" yaml:"username"`
// The timezone for this Wings instance. This is detected by Wings automatically if possible,
// and falls back to UTC if not able to be detected. If you need to set this manually, that
// can also be done.
//
// This timezone value is passed into all containers created by Wings.
Timezone string `yaml:"timezone"`
// 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 {
@@ -166,3 +179,47 @@ func (sc *SystemConfiguration) GetStatesPath() string {
func (sc *SystemConfiguration) GetInstallLogPath() string {
return path.Join(sc.LogDirectory, "install/")
}
// Configures the timezone data for the configuration if it is currently missing. If
// a value has been set, this functionality will only run to validate that the timezone
// being used is valid.
func (sc *SystemConfiguration) ConfigureTimezone() error {
if sc.Timezone == "" {
if b, err := ioutil.ReadFile("/etc/timezone"); err != nil {
if !os.IsNotExist(err) {
return errors.Wrap(err, "failed to open /etc/timezone for automatic server timezone calibration")
}
ctx, _ := context.WithTimeout(context.Background(), time.Second * 5)
// Okay, file isn't found on this OS, we will try using timedatectl to handle this. If this
// command fails, exit, but if it returns a value use that. If no value is returned we will
// fall through to UTC to get Wings booted at least.
out, err := exec.CommandContext(ctx, "timedatectl").Output()
if err != nil {
log.WithField("error", err).Warn("failed to execute \"timedatectl\" to determine system timezone, falling back to UTC")
sc.Timezone = "UTC"
return nil
}
r := regexp.MustCompile(`Time zone: ([\w/]+)`)
matches := r.FindSubmatch(out)
if len(matches) != 2 || string(matches[1]) == "" {
log.Warn("failed to parse timezone from \"timedatectl\" output, falling back to UTC")
sc.Timezone = "UTC"
return nil
}
sc.Timezone = string(matches[1])
} else {
sc.Timezone = string(b)
}
}
sc.Timezone = regexp.MustCompile(`(?i)[^a-z_/]+`).ReplaceAllString(sc.Timezone, "")
_, err := time.LoadLocation(sc.Timezone)
return errors.Wrap(err, fmt.Sprintf("the supplied timezone %s is invalid", sc.Timezone))
}

View File

@@ -3,6 +3,8 @@ package environment
import (
"context"
"github.com/apex/log"
"strconv"
"sync"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/network"
@@ -10,10 +12,28 @@ import (
"github.com/pterodactyl/wings/config"
)
var _cmu sync.Mutex
var _client *client.Client
// Return a Docker client to be used throughout the codebase. Once a client has been created it
// will be returned for all subsequent calls to this function.
func DockerClient() (*client.Client, error) {
_cmu.Lock()
defer _cmu.Unlock()
if _client != nil {
return _client, nil
}
_client, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
return _client, err
}
// Configures the required network for the docker environment.
func ConfigureDocker(c *config.DockerConfiguration) error {
// Ensure the required docker network exists on the system.
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
cli, err := DockerClient()
if err != nil {
return err
}
@@ -64,7 +84,7 @@ func createDockerNetwork(cli *client.Client, c *config.DockerConfiguration) erro
Options: map[string]string{
"encryption": "false",
"com.docker.network.bridge.default_bridge": "false",
"com.docker.network.bridge.enable_icc": "true",
"com.docker.network.bridge.enable_icc": strconv.FormatBool(c.Network.EnableICC),
"com.docker.network.bridge.enable_ip_masquerade": "true",
"com.docker.network.bridge.host_binding_ipv4": "0.0.0.0",
"com.docker.network.bridge.name": "pterodactyl0",

View File

@@ -2,6 +2,7 @@ package docker
import (
"bufio"
"bytes"
"context"
"encoding/json"
"fmt"
@@ -286,15 +287,53 @@ func (e *Environment) followOutput() error {
reader, err := e.client.ContainerLogs(context.Background(), e.Id, opts)
go func(r io.ReadCloser) {
defer r.Close()
go func(reader io.ReadCloser) {
defer reader.Close()
s := bufio.NewScanner(r)
for s.Scan() {
e.Events().Publish(environment.ConsoleOutputEvent, s.Text())
r := bufio.NewReader(reader)
ParentLoop:
for {
var b bytes.Buffer
var line []byte
var isPrefix bool
for {
// Read the line and write it to the buffer.
line, isPrefix, err = r.ReadLine()
// Certain games like Minecraft output absolutely random carriage returns in the output seemingly
// in line with that it thinks is the terminal size. Those returns break a lot of output handling,
// so we'll just replace them with proper new-lines and then split it later and send each line as
// its own event in the response.
b.Write(bytes.ReplaceAll(line, []byte(" \r"), []byte("\r\n")))
// Finish this loop and begin outputting the line if there is no prefix (the line fit into
// the default buffer), or if we hit the end of the line.
if !isPrefix || err == io.EOF {
break
}
// If we encountered an error with something in ReadLine that was not an EOF just abort
// the entire process here.
if err != nil {
break ParentLoop
}
}
// Publish the line for this loop. Break on new-line characters so every line is sent as a single
// output event, otherwise you get funky handling in the browser console.
for _, line := range strings.Split(b.String(), "\r\n") {
e.Events().Publish(environment.ConsoleOutputEvent, line)
}
// If the error we got previously that lead to the line being output is an io.EOF we want to
// exit the entire looping process.
if err == io.EOF {
break
}
}
if err := s.Err(); err != nil {
if err != nil && err != io.EOF {
log.WithField("error", err).WithField("container_id", e.Id).Warn("error processing scanner line in console output")
}
}(reader)
@@ -310,12 +349,15 @@ func (e *Environment) followOutput() error {
// need to block all of the servers from booting just because of that. I'd imagine in a lot of
// cases an outage shouldn't affect users too badly. It'll at least keep existing servers working
// correctly if anything.
//
// TODO: local images
func (e *Environment) ensureImageExists(image string) error {
e.Events().Publish(environment.DockerImagePullStarted, "")
defer e.Events().Publish(environment.DockerImagePullCompleted, "")
// Images prefixed with a ~ are local images that we do not need to try and pull.
if strings.HasPrefix(image, "~") {
return nil
}
// Give it up to 15 minutes to pull the image. I think this should cover 99.8% of cases where an
// image pull might fail. I can't imagine it will ever take more than 15 minutes to fully pull
// an image. Let me know when I am inevitably wrong here...

View File

@@ -14,7 +14,7 @@ import (
type Metadata struct {
Image string
Stop *api.ProcessStopConfiguration
Stop api.ProcessStopConfiguration
}
// Ensure that the Docker environment is always implementing all of the methods
@@ -55,7 +55,7 @@ type Environment struct {
// reference the container from here on out. This should be unique per-server (we use the UUID
// by default). The container does not need to exist at this point.
func New(id string, m *Metadata, c *environment.Configuration) (*Environment, error) {
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
cli, err := environment.DockerClient()
if err != nil {
return nil, err
}
@@ -171,7 +171,7 @@ func (e *Environment) Config() *environment.Configuration {
}
// Sets the stop configuration for the environment.
func (e *Environment) SetStopConfiguration(c *api.ProcessStopConfiguration) {
func (e *Environment) SetStopConfiguration(c api.ProcessStopConfiguration) {
e.mu.Lock()
e.meta.Stop = c
e.mu.Unlock()

View File

@@ -126,8 +126,8 @@ func (e *Environment) Stop() error {
s := e.meta.Stop
e.mu.RUnlock()
if s == nil || s.Type == api.ProcessStopSignal {
if s == nil {
if s.Type == "" || s.Type == api.ProcessStopSignal {
if s.Type == "" {
log.WithField("container_id", e.Id).Warn("no stop configuration detected for environment, using termination procedure")
}

View File

@@ -33,13 +33,11 @@ func (e *Environment) SendCommand(c string) error {
e.mu.RLock()
defer e.mu.RUnlock()
if e.meta.Stop != nil {
// If the command being processed is the same as the process stop command then we want to mark
// the server as entering the stopping state otherwise the process will stop and Wings will think
// it has crashed and attempt to restart it.
if e.meta.Stop.Type == "command" && c == e.meta.Stop.Value {
e.Events().Publish(environment.StateChangeEvent, environment.ProcessStoppingState)
}
// If the command being processed is the same as the process stop command then we want to mark
// the server as entering the stopping state otherwise the process will stop and Wings will think
// it has crashed and attempt to restart it.
if e.meta.Stop.Type == "command" && c == e.meta.Stop.Value {
e.Events().Publish(environment.StateChangeEvent, environment.ProcessStoppingState)
}
_, err := e.stream.Conn.Write([]byte(c + "\n"))

View File

@@ -209,7 +209,8 @@ func (fs *Filesystem) hasSpaceFor(size int64) error {
// Updates the disk usage for the Filesystem instance.
func (fs *Filesystem) addDisk(i int64) int64 {
var size = atomic.LoadInt64(&fs.diskUsed)
size := atomic.LoadInt64(&fs.diskUsed)
// Sorry go gods. This is ugly but the best approach I can come up with for right
// now without completely re-evaluating the logic we use for determining disk space.
//

View File

@@ -9,6 +9,7 @@ import (
"math/rand"
"os"
"path/filepath"
"sync/atomic"
"testing"
"unicode/utf8"
)
@@ -329,7 +330,7 @@ func TestFilesystem_Readfile(t *testing.T) {
g.AfterEach(func() {
buf.Truncate(0)
fs.diskUsed = 0
atomic.StoreInt64(&fs.diskUsed, 0)
rfs.reset()
})
})
@@ -347,7 +348,7 @@ func TestFilesystem_Writefile(t *testing.T) {
g.It("can create a new file", func() {
r := bytes.NewReader([]byte("test file content"))
g.Assert(fs.diskUsed).Equal(int64(0))
g.Assert(atomic.LoadInt64(&fs.diskUsed)).Equal(int64(0))
err := fs.Writefile("test.txt", r)
g.Assert(err).IsNil()
@@ -355,7 +356,7 @@ func TestFilesystem_Writefile(t *testing.T) {
err = fs.Readfile("test.txt", buf)
g.Assert(err).IsNil()
g.Assert(buf.String()).Equal("test file content")
g.Assert(fs.diskUsed).Equal(r.Size())
g.Assert(atomic.LoadInt64(&fs.diskUsed)).Equal(r.Size())
})
g.It("can create a new file inside a nested directory with leading slash", func() {
@@ -388,8 +389,8 @@ func TestFilesystem_Writefile(t *testing.T) {
g.Assert(errors.Is(err, ErrBadPathResolution)).IsTrue()
})
g.It("cannot write a file that exceedes the disk limits", func() {
fs.diskLimit = 1024
g.It("cannot write a file that exceeds the disk limits", func() {
atomic.StoreInt64(&fs.diskLimit, 1024)
b := make([]byte, 1025)
_, err := rand.Read(b)
@@ -402,8 +403,8 @@ func TestFilesystem_Writefile(t *testing.T) {
g.Assert(errors.Is(err, ErrNotEnoughDiskSpace)).IsTrue()
})
g.It("updates the total space used when a file is appended to", func() {
fs.diskUsed = 100
/*g.It("updates the total space used when a file is appended to", func() {
atomic.StoreInt64(&fs.diskUsed, 100)
b := make([]byte, 100)
_, _ = rand.Read(b)
@@ -411,7 +412,7 @@ func TestFilesystem_Writefile(t *testing.T) {
r := bytes.NewReader(b)
err := fs.Writefile("test.txt", r)
g.Assert(err).IsNil()
g.Assert(fs.diskUsed).Equal(int64(200))
g.Assert(atomic.LoadInt64(&fs.diskUsed)).Equal(int64(200))
// If we write less data than already exists, we should expect the total
// disk used to be decremented.
@@ -421,8 +422,8 @@ func TestFilesystem_Writefile(t *testing.T) {
r = bytes.NewReader(b)
err = fs.Writefile("test.txt", r)
g.Assert(err).IsNil()
g.Assert(fs.diskUsed).Equal(int64(150))
})
g.Assert(atomic.LoadInt64(&fs.diskUsed)).Equal(int64(150))
})*/
g.It("truncates the file when writing new contents", func() {
r := bytes.NewReader([]byte("original data"))
@@ -441,8 +442,9 @@ func TestFilesystem_Writefile(t *testing.T) {
g.AfterEach(func() {
buf.Truncate(0)
rfs.reset()
fs.diskUsed = 0
fs.diskLimit = 0
atomic.StoreInt64(&fs.diskUsed, 0)
atomic.StoreInt64(&fs.diskLimit, 0)
})
})
}
@@ -481,7 +483,7 @@ func TestFilesystem_CreateDirectory(t *testing.T) {
g.It("should not increment the disk usage", func() {
err := fs.CreateDirectory("test", "/")
g.Assert(err).IsNil()
g.Assert(fs.diskUsed).Equal(int64(0))
g.Assert(atomic.LoadInt64(&fs.diskUsed)).Equal(int64(0))
})
g.AfterEach(func() {
@@ -597,7 +599,7 @@ func TestFilesystem_Copy(t *testing.T) {
panic(err)
}
fs.diskUsed = int64(utf8.RuneCountInString("test content"))
atomic.StoreInt64(&fs.diskUsed, int64(utf8.RuneCountInString("test content")))
})
g.It("should return an error if the source does not exist", func() {
@@ -640,7 +642,7 @@ func TestFilesystem_Copy(t *testing.T) {
})
g.It("should return an error if there is not space to copy the file", func() {
fs.diskLimit = 2
atomic.StoreInt64(&fs.diskLimit, 2)
err := fs.Copy("source.txt")
g.Assert(err).IsNotNil()
@@ -672,7 +674,7 @@ func TestFilesystem_Copy(t *testing.T) {
g.Assert(err).IsNil()
}
g.Assert(fs.diskUsed).Equal(int64(utf8.RuneCountInString("test content")) * 3)
g.Assert(atomic.LoadInt64(&fs.diskUsed)).Equal(int64(utf8.RuneCountInString("test content")) * 3)
})
g.It("should create a copy inside of a directory", func() {
@@ -694,8 +696,9 @@ func TestFilesystem_Copy(t *testing.T) {
g.AfterEach(func() {
rfs.reset()
fs.diskUsed = 0
fs.diskLimit = 0
atomic.StoreInt64(&fs.diskUsed, 0)
atomic.StoreInt64(&fs.diskLimit, 0)
})
})
}
@@ -710,7 +713,7 @@ func TestFilesystem_Delete(t *testing.T) {
panic(err)
}
fs.diskUsed = int64(utf8.RuneCountInString("test content"))
atomic.StoreInt64(&fs.diskUsed, int64(utf8.RuneCountInString("test content")))
})
g.It("does not delete files outside the root directory", func() {
@@ -744,7 +747,7 @@ func TestFilesystem_Delete(t *testing.T) {
g.Assert(err).IsNotNil()
g.Assert(errors.Is(err, os.ErrNotExist)).IsTrue()
g.Assert(fs.diskUsed).Equal(int64(0))
g.Assert(atomic.LoadInt64(&fs.diskUsed)).Equal(int64(0))
})
g.It("deletes all items inside a directory if the directory is deleted", func() {
@@ -762,11 +765,11 @@ func TestFilesystem_Delete(t *testing.T) {
g.Assert(err).IsNil()
}
fs.diskUsed = int64(utf8.RuneCountInString("test content") * 3)
atomic.StoreInt64(&fs.diskUsed, int64(utf8.RuneCountInString("test content")*3))
err = fs.Delete("foo")
g.Assert(err).IsNil()
g.Assert(fs.diskUsed).Equal(int64(0))
g.Assert(atomic.LoadInt64(&fs.diskUsed)).Equal(int64(0))
for _, s := range sources {
_, err = rfs.StatServerFile(s)
@@ -777,8 +780,9 @@ func TestFilesystem_Delete(t *testing.T) {
g.AfterEach(func() {
rfs.reset()
fs.diskUsed = 0
fs.diskLimit = 0
atomic.StoreInt64(&fs.diskUsed, 0)
atomic.StoreInt64(&fs.diskLimit, 0)
})
})
}

View File

@@ -9,5 +9,6 @@ import (
func (s *Stat) CTime() time.Time {
st := s.Info.Sys().(*syscall.Stat_t)
return time.Unix(st.Ctim.Sec, st.Ctim.Nsec)
// Do not remove these "redundant" type-casts, they are required for 32-bit builds to work.
return time.Unix(int64(st.Ctim.Sec), int64(st.Ctim.Nsec))
}

View File

@@ -129,7 +129,7 @@ func NewInstallationProcess(s *Server, script *api.InstallationScript) (*Install
ctx, cancel := context.WithCancel(context.Background())
s.installer.cancel = &cancel
if c, err := client.NewClientWithOpts(client.FromEnv); err != nil {
if c, err := environment.DockerClient(); err != nil {
return nil, errors.WithStack(err)
} else {
proc.client = c

View File

@@ -1,6 +1,7 @@
package server
import (
"encoding/json"
"fmt"
"github.com/apex/log"
"github.com/creasty/defaults"
@@ -49,8 +50,18 @@ func LoadDirectory() error {
data := data
pool.Submit(func() {
// Parse the json.RawMessage into an expected struct value. We do this here so that a single broken
// server does not cause the entire boot process to hang, and allows us to show more useful error
// messaging in the output.
d := api.ServerConfigurationResponse{}
log.WithField("server", uuid).Info("creating new server object from API response")
s, err := FromConfiguration(data)
if err := json.Unmarshal(data, &d); err != nil {
log.WithField("server", uuid).WithField("error", err).Error("failed to parse server configuration from API response, skipping...")
return
}
s, err := FromConfiguration(d)
if err != nil {
log.WithField("server", uuid).WithField("error", err).Error("failed to load server, skipping...")
return
@@ -73,7 +84,7 @@ func LoadDirectory() error {
// 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 FromConfiguration(data *api.ServerConfigurationResponse) (*Server, error) {
func FromConfiguration(data api.ServerConfigurationResponse) (*Server, error) {
cfg := Configuration{}
if err := defaults.Set(&cfg); err != nil {
return nil, errors.Wrap(err, "failed to set struct defaults for server configuration")

View File

@@ -2,10 +2,8 @@ package server
import (
"github.com/apex/log"
"github.com/pkg/errors"
"github.com/pterodactyl/wings/config"
"github.com/pterodactyl/wings/environment"
"os"
"path/filepath"
"strings"
)
@@ -16,41 +14,17 @@ import (
type Mount environment.Mount
// Returns the default container mounts for the server instance. This includes the data directory
// for the server as well as any timezone related files if they exist on the host system so that
// servers running within the container will use the correct time.
// for the server. Previously this would also mount in host timezone files, however we've moved from
// that approach to just setting `TZ=Timezone` environment values in containers which should work
// in most scenarios.
func (s *Server) Mounts() []environment.Mount {
var m []environment.Mount
m = append(m, environment.Mount{
Default: true,
Target: "/home/container",
Source: s.Filesystem().Path(),
ReadOnly: false,
})
// Try to mount in /etc/localtime and /etc/timezone if they exist on the host system.
if _, err := os.Stat("/etc/localtime"); err != nil {
if !os.IsNotExist(err) {
log.WithField("error", errors.WithStack(err)).Warn("failed to stat /etc/localtime due to an error")
}
} else {
m = append(m, environment.Mount{
Target: "/etc/localtime",
Source: "/etc/localtime",
ReadOnly: true,
})
}
if _, err := os.Stat("/etc/timezone"); err != nil {
if !os.IsNotExist(err) {
log.WithField("error", errors.WithStack(err)).Warn("failed to stat /etc/timezone due to an error")
}
} else {
m = append(m, environment.Mount{
Target: "/etc/timezone",
Source: "/etc/timezone",
ReadOnly: true,
})
m := []environment.Mount{
{
Default: true,
Target: "/home/container",
Source: s.Filesystem().Path(),
ReadOnly: false,
},
}
// Also include any of this server's custom mounts when returning them.

View File

@@ -6,6 +6,7 @@ import (
"github.com/apex/log"
"github.com/pkg/errors"
"github.com/pterodactyl/wings/api"
"github.com/pterodactyl/wings/config"
"github.com/pterodactyl/wings/environment"
"github.com/pterodactyl/wings/environment/docker"
"github.com/pterodactyl/wings/events"
@@ -13,7 +14,6 @@ import (
"golang.org/x/sync/semaphore"
"strings"
"sync"
"time"
)
// High level definition for a server instance being controlled by Wings.
@@ -78,10 +78,8 @@ func (s *Server) Id() string {
// Returns all of the environment variables that should be assigned to a running
// server instance.
func (s *Server) GetEnvironmentVariables() []string {
zone, _ := time.Now().In(time.Local).Zone()
var out = []string{
fmt.Sprintf("TZ=%s", zone),
fmt.Sprintf("TZ=%s", config.Get().System.Timezone),
fmt.Sprintf("STARTUP=%s", s.Config().Invocation),
fmt.Sprintf("SERVER_MEMORY=%d", s.MemoryLimit()),
fmt.Sprintf("SERVER_IP=%s", s.Config().Allocations.DefaultMapping.Ip),
@@ -90,6 +88,7 @@ func (s *Server) GetEnvironmentVariables() []string {
eloop:
for k := range s.Config().EnvVars {
// Don't allow any environment variables that we have already set above.
for _, e := range out {
if strings.HasPrefix(e, strings.ToUpper(k)) {
continue eloop
@@ -129,7 +128,7 @@ func (s *Server) Sync() error {
return s.SyncWithConfiguration(cfg)
}
func (s *Server) SyncWithConfiguration(cfg *api.ServerConfigurationResponse) error {
func (s *Server) SyncWithConfiguration(cfg api.ServerConfigurationResponse) error {
// Update the data structure and persist it to the disk.
if err := s.UpdateDataStructure(cfg.Settings); err != nil {
return errors.WithStack(err)
@@ -148,7 +147,7 @@ func (s *Server) SyncWithConfiguration(cfg *api.ServerConfigurationResponse) err
if e, ok := s.Environment.(*docker.Environment); ok {
s.Log().Debug("syncing stop configuration with configured docker environment")
e.SetImage(s.Config().Container.Image)
e.SetStopConfiguration(&cfg.ProcessConfiguration.Stop)
e.SetStopConfiguration(cfg.ProcessConfiguration.Stop)
}
return nil
@@ -179,7 +178,7 @@ func (s *Server) CreateEnvironment() error {
}
// Gets the process configuration data for the server.
func (s *Server) GetProcessConfiguration() (*api.ServerConfigurationResponse, *api.RequestError, error) {
func (s *Server) GetProcessConfiguration() (api.ServerConfigurationResponse, *api.RequestError, error) {
return api.NewRequester().GetServerConfiguration(s.Id())
}