Compare commits

..

16 Commits

Author SHA1 Message Date
Matthew Penner
d739948989
feat: add ability to mount generated passwd files to containers (#197)
This PR will add an option to mount:
- `/etc/group`
- `/etc/passwd`

Signed-off-by: Matthew Penner <me@matthewp.io>
2024-07-24 15:12:42 -06:00
Matthew Penner
ac260bd5ee
ci: update to go1.22.5, drop go1.21
Signed-off-by: Matthew Penner <me@matthewp.io>
2024-07-24 13:14:25 -06:00
Matthew Penner
2f4a0d7262
nix: update flake.lock
Flake lock file updates:

• Updated input 'flake-parts':
    'github:hercules-ci/flake-parts/b253292d9c0a5ead9bc98c4e9a26c6312e27d69f?narHash=sha256-a0NYyp%2Bh9hlb7ddVz4LUn1vT/PLwqfrWYcHMvFB1xYg%3D' (2024-02-01)
  → 'github:hercules-ci/flake-parts/9227223f6d922fee3c7b190b2cc238a99527bbb7?narHash=sha256-pQMhCCHyQGRzdfAkdJ4cIWiw%2BJNuWsTX7f0ZYSyz0VY%3D' (2024-07-03)
• Updated input 'flake-parts/nixpkgs-lib':
    'github:NixOS/nixpkgs/97b17f32362e475016f942bbdfda4a4a72a8a652?dir=lib&narHash=sha256-UcsnCG6wx%2B%2B23yeER4Hg18CXWbgNpqNXcHIo5/1Y%2Bhc%3D' (2024-01-29)
  → '5daf051448.tar.gz?narHash=sha256-Fm2rDDs86sHy0/1jxTOKB1118Q0O3Uc7EC0iXvXKpbI%3D' (2024-07-01)
• Updated input 'nixpkgs':
    'github:NixOS/nixpkgs/a4d4fe8c5002202493e87ec8dbc91335ff55552c?narHash=sha256-ZL2TrjVsiFNKOYwYQozpbvQSwvtV/3Me7Zwhmdsfyu4%3D' (2024-02-15)
  → 'github:NixOS/nixpkgs/68c9ed8bbed9dfce253cc91560bf9043297ef2fe?narHash=sha256-Tybxt65eyOARf285hMHIJ2uul8SULjFZbT9ZaEeUnP8%3D' (2024-07-21)
• Updated input 'treefmt-nix':
    'github:numtide/treefmt-nix/ac599dab59a66304eb511af07b3883114f061b9d?narHash=sha256-qQF0fEkHlnxHcrKIMRzOETnRBksUK048MXkX0SOmxvA%3D' (2024-02-07)
  → 'github:numtide/treefmt-nix/8db8970be1fb8be9c845af7ebec53b699fe7e009?narHash=sha256-6Pqa0bi5nV74IZcENKYRToRNM5obo1EQ%2B3ihtunJ014%3D' (2024-07-23)
2024-07-24 13:10:35 -06:00
EpicPlayerA10
1d8b383682
fix: only count hard-links once when calculating filesystem usage (#181) 2024-06-29 12:56:51 -06:00
Arnaud Lier
934bf2493d
fix: properly use base2 (1024, *bibyte) when calculating memory limits (#190) 2024-06-29 12:34:20 -06:00
Daniel Barton
29e4425e21
fix: overhaul docker container termination signals (#192)
Fixes https://github.com/pterodactyl/panel/issues/4783

Requires https://github.com/pterodactyl/panel/pull/5132 to work
2024-06-29 12:31:36 -06:00
Daniel Barton
5a15612754
chore: show the actual location in "config not found" error (#179) 2024-06-29 12:26:59 -06:00
Daniel Barton
ad1ae862a9
fix: user-defined labels not being passed to environment (#191) 2024-06-29 12:25:47 -06:00
Matthew Penner
3114a3b82e
Update README.md
Signed-off-by: Matthew Penner <me@matthewp.io>
2024-06-22 19:21:54 -06:00
Matthew Penner
500f217514
Update CHANGELOG.md 2024-05-13 16:01:33 -06:00
Daniel Barton
9ffbcdcdb1
cmd: handle relative paths to the config file (#180) 2024-05-13 15:53:33 -06:00
Daniel Barton
9b341db2db
server(filesystem): fix sort position of directories (#188) 2024-05-13 15:52:10 -06:00
Matthew Penner
71c5338549
Update CHANGELOG.md 2024-05-07 22:12:59 -06:00
Matthew Penner
326f115f5b
Update README.md 2024-05-07 22:12:55 -06:00
Geri
06614de99d
server(filesystem): handle individual compressed files (#184) 2024-05-07 22:06:15 -06:00
Matthew Penner
2b0e35360b
cmd(configure): fix panel url not being set
Fixes https://github.com/pterodactyl/panel/issues/5087
2024-05-07 22:01:12 -06:00
19 changed files with 272 additions and 88 deletions

View File

@ -16,7 +16,7 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
os: [ubuntu-22.04] os: [ubuntu-22.04]
go: ["1.21.9", "1.22.2"] go: ["1.22.5"]
goos: [linux] goos: [linux]
goarch: [amd64, arm64] goarch: [amd64, arm64]
@ -62,14 +62,14 @@ jobs:
- name: Upload Release Artifact - name: Upload Release Artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
if: ${{ (github.ref == 'refs/heads/develop' || github.event_name == 'pull_request') && matrix.go == '1.21.8' }} if: ${{ (github.ref == 'refs/heads/develop' || github.event_name == 'pull_request') && matrix.go == '1.22.5' }}
with: with:
name: wings_linux_${{ matrix.goarch }} name: wings_linux_${{ matrix.goarch }}
path: dist/wings path: dist/wings
- name: Upload Debug Artifact - name: Upload Debug Artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
if: ${{ (github.ref == 'refs/heads/develop' || github.event_name == 'pull_request') && matrix.go == '1.21.8' }} if: ${{ (github.ref == 'refs/heads/develop' || github.event_name == 'pull_request') && matrix.go == '1.22.5' }}
with: with:
name: wings_linux_${{ matrix.goarch }}_debug name: wings_linux_${{ matrix.goarch }}_debug
path: dist/wings_debug path: dist/wings_debug

View File

@ -17,7 +17,7 @@ jobs:
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: "1.21.9" go-version: "1.22.5"
- name: Build release binaries - name: Build release binaries
env: env:

View File

@ -1,5 +1,22 @@
# Changelog # Changelog
## v1.11.14
### Added
* Support relative file paths for the Wings config ([#180](https://github.com/pterodactyl/wings/pull/180))
### Fixed
* Folders not being sorted before files properly ([#5078](https://github.com/pterodactyl/panel/issues/5078)
## v1.11.13
### Fixed
* Auto-configure not working ([#5087](https://github.com/pterodactyl/panel/issues/5087))
* Individual files unable to be decompressed ([#5034](https://github.com/pterodactyl/panel/issues/5034))
## v1.11.12 ## v1.11.12
### Fixed ### Fixed

View File

@ -1,5 +1,5 @@
# Stage 1 (Build) # Stage 1 (Build)
FROM golang:1.21.9-alpine AS builder FROM golang:1.22.5-alpine AS builder
ARG VERSION ARG VERSION
RUN apk add --update --no-cache git make RUN apk add --update --no-cache git make

View File

@ -19,11 +19,11 @@ I would like to extend my sincere thanks to the following sponsors for helping f
[Interested in becoming a sponsor?](https://github.com/sponsors/matthewpi) [Interested in becoming a sponsor?](https://github.com/sponsors/matthewpi)
| Company | About | | Company | About |
|--------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| |--------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [**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. | | [**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. |
| [**CodeNode LLC**](https://codenode.gg/) | Looking for simplicity? Well, look no further! CodeNode has got you covered with everything you need at the rock-bottom price of $1.75 per GB, including dedicated IPs in Dallas, Texas, and Amsterdam, Netherlands. We're not just good, we're the best in the game! |
| [**BisectHosting**](https://www.bisecthosting.com/) | BisectHosting provides Minecraft, Valheim and other server hosting services with the highest reliability and lightning fast support since 2012. | | [**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! | | [**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! |
| [**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. |
| [**HostEZ**](https://hostez.io) | US & EU Rust & Minecraft Hosting. DDoS Protected bare metal, VPS and colocation with low latency, high uptime and maximum availability. EZ! | | [**HostEZ**](https://hostez.io) | US & EU Rust & Minecraft Hosting. DDoS Protected bare metal, VPS and colocation with low latency, high uptime and maximum availability. EZ! |
| [**Blueprint**](https://blueprint.zip/?pterodactyl=true) | Create and install Pterodactyl addons and themes with the growing Blueprint framework - the package-manager for Pterodactyl. Use multiple modifications at once without worrying about conflicts and make use of the large extension ecosystem. | | [**Blueprint**](https://blueprint.zip/?pterodactyl=true) | Create and install Pterodactyl addons and themes with the growing Blueprint framework - the package-manager for Pterodactyl. Use multiple modifications at once without worrying about conflicts and make use of the large extension ecosystem. |
| [**indifferent broccoli**](https://indifferentbroccoli.com/) | indifferent broccoli is a game server hosting and rental company. With us, you get top-notch computer power for your gaming sessions. We destroy lag, latency, and complexity--letting you focus on the fun stuff. | | [**indifferent broccoli**](https://indifferentbroccoli.com/) | indifferent broccoli is a game server hosting and rental company. With us, you get top-notch computer power for your gaming sessions. We destroy lag, latency, and complexity--letting you focus on the fun stuff. |

View File

@ -155,6 +155,9 @@ func configureCmdRun(cmd *cobra.Command, args []string) {
panic(err) panic(err)
} }
// Manually specify the Panel URL as it won't be decoded from JSON.
cfg.PanelLocation = configureArgs.PanelURL
if err = config.WriteToDisk(cfg); err != nil { if err = config.WriteToDisk(cfg); err != nil {
panic(err) panic(err)
} }

View File

@ -13,7 +13,6 @@ import (
"path/filepath" "path/filepath"
"runtime" "runtime"
"strconv" "strconv"
"strings"
"time" "time"
"github.com/NYTimes/logrotate" "github.com/NYTimes/logrotate"
@ -113,6 +112,9 @@ func rootCmdRun(cmd *cobra.Command, _ []string) {
if err := config.EnsurePterodactylUser(); err != nil { if err := config.EnsurePterodactylUser(); err != nil {
log.WithField("error", err).Fatal("failed to create pterodactyl system user") log.WithField("error", err).Fatal("failed to create pterodactyl system user")
} }
if err := config.ConfigurePasswd(); err != nil {
log.WithField("error", err).Fatal("failed to configure container passwd file")
}
log.WithFields(log.Fields{ log.WithFields(log.Fields{
"username": config.Get().System.Username, "username": config.Get().System.Username,
"uid": config.Get().System.User.Uid, "uid": config.Get().System.User.Uid,
@ -379,13 +381,14 @@ func rootCmdRun(cmd *cobra.Command, _ []string) {
// Reads the configuration from the disk and then sets up the global singleton // Reads the configuration from the disk and then sets up the global singleton
// with all the configuration values. // with all the configuration values.
func initConfig() { func initConfig() {
if !strings.HasPrefix(configPath, "/") { if !filepath.IsAbs(configPath) {
d, err := os.Getwd() d, err := filepath.Abs(configPath)
if err != nil { if err != nil {
log2.Fatalf("cmd/root: could not determine directory: %s", err) log2.Fatalf("cmd/root: failed to get path to config file: %s", err)
} }
configPath = path.Clean(path.Join(d, configPath)) configPath = d
} }
err := config.FromFile(configPath) err := config.FromFile(configPath)
if err != nil { if err != nil {
if errors.Is(err, os.ErrNotExist) { if errors.Is(err, os.ErrNotExist) {
@ -440,18 +443,18 @@ in all copies or substantial portions of the Software.%s`), system.Version, time
} }
func exitWithConfigurationNotice() { func exitWithConfigurationNotice() {
fmt.Print(colorstring.Color(` fmt.Printf(colorstring.Color(`
[_red_][white][bold]Error: Configuration File Not Found[reset] [_red_][white][bold]Error: Configuration File Not Found[reset]
Wings was not able to locate your configuration file, and therefore is not Wings was not able to locate your configuration file, and therefore is not
able to complete its boot process. Please ensure you have copied your instance able to complete its boot process. Please ensure you have copied your instance
configuration file into the default location below. configuration file into the default location below.
Default Location: /etc/pterodactyl/config.yml Default Location: %s
[yellow]This is not a bug with this software. Please do not make a bug report [yellow]This is not a bug with this software. Please do not make a bug report
for this issue, it will be closed.[reset] for this issue, it will be closed.[reset]
`)) `), config.DefaultLocation)
os.Exit(1) os.Exit(1)
} }

View File

@ -172,6 +172,25 @@ type SystemConfiguration struct {
Gid int `yaml:"gid"` Gid int `yaml:"gid"`
} `yaml:"user"` } `yaml:"user"`
// Passwd controls the mounting of a generated passwd files into containers started by Wings.
Passwd struct {
// Enable controls whether generated passwd files should be mounted into containers.
//
// By default this option is disabled and Wings will not mount any additional passwd
// files into containers.
Enable bool `yaml:"enabled" default:"false"`
// Directory is the directory on disk where the generated files will be stored.
// This directory may be temporary as it will be re-created whenever Wings is started.
//
// This path **WILL** be both written to by Wings and mounted into containers created by
// Wings. If you are running Wings itself in a container, this path will need to be mounted
// into the Wings container as the exact path on the host, which should match the value
// specified here. If you are using SELinux, you will need to make sure this file has the
// correct SELinux context in order for containers to use it.
Directory string `yaml:"directory" default:"/run/wings/etc"`
} `yaml:"passwd"`
// The amount of time in seconds that can elapse before a server's disk space calculation is // 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 // considered stale and a re-check should occur. DANGER: setting this value too low can seriously
// impact system performance and cause massive I/O bottlenecks and high CPU usage for the Wings // impact system performance and cause massive I/O bottlenecks and high CPU usage for the Wings
@ -497,6 +516,37 @@ func EnsurePterodactylUser() error {
return nil return nil
} }
// ConfigurePasswd generates required passwd files for use with containers started by Wings.
func ConfigurePasswd() error {
passwd := _config.System.Passwd
if !passwd.Enable {
return nil
}
v := []byte(fmt.Sprintf(
`root:x:0:
container:x:%d:
nogroup:x:65534:`,
_config.System.User.Gid,
))
if err := os.WriteFile(filepath.Join(passwd.Directory, "group"), v, 0o644); err != nil {
return fmt.Errorf("failed to write file to %s/group: %v", passwd.Directory, err)
}
v = []byte(fmt.Sprintf(
`root:x:0:0::/root:/bin/sh
container:x:%d:%d::/home/container:/bin/sh
nobody:x:65534:65534::/var/empty:/bin/sh
`,
_config.System.User.Uid,
_config.System.User.Gid,
))
if err := os.WriteFile(filepath.Join(passwd.Directory, "passwd"), v, 0o644); err != nil {
return fmt.Errorf("failed to write file to %s/passwd: %v", passwd.Directory, err)
}
return nil
}
// FromFile reads the configuration from the provided file and stores it in the // FromFile reads the configuration from the provided file and stores it in the
// global singleton for this instance. // global singleton for this instance.
func FromFile(path string) error { func FromFile(path string) error {
@ -561,6 +611,13 @@ func ConfigureDirectories() error {
return err return err
} }
if _config.System.Passwd.Enable {
log.WithField("path", _config.System.Passwd.Directory).Debug("ensuring passwd directory exists")
if err := os.MkdirAll(_config.System.Passwd.Directory, 0o755); err != nil {
return err
}
}
return nil return nil
} }

View File

@ -4,7 +4,6 @@ import (
"context" "context"
"os" "os"
"strings" "strings"
"syscall"
"time" "time"
"emperror.dev/errors" "emperror.dev/errors"
@ -143,42 +142,49 @@ func (e *Environment) Stop(ctx context.Context) error {
s := e.meta.Stop s := e.meta.Stop
e.mu.RUnlock() e.mu.RUnlock()
// A native "stop" as the Type field value will just skip over all of this
// logic and end up only executing the container stop command (which may or
// may not work as expected).
if s.Type == "" || s.Type == remote.ProcessStopSignal {
if s.Type == "" {
log.WithField("container_id", e.Id).Warn("no stop configuration detected for environment, using termination procedure")
}
signal := os.Kill
// Handle a few common cases, otherwise just fall through and just pass along
// the os.Kill signal to the process.
switch strings.ToUpper(s.Value) {
case "SIGABRT":
signal = syscall.SIGABRT
case "SIGINT":
signal = syscall.SIGINT
case "SIGTERM":
signal = syscall.SIGTERM
}
return e.Terminate(ctx, signal)
}
// If the process is already offline don't switch it back to stopping. Just leave it how // If the process is already offline don't switch it back to stopping. Just leave it how
// it is and continue through to the stop handling for the process. // it is and continue through to the stop handling for the process.
if e.st.Load() != environment.ProcessOfflineState { if e.st.Load() != environment.ProcessOfflineState {
e.SetState(environment.ProcessStoppingState) e.SetState(environment.ProcessStoppingState)
} }
// Handle signal based actions
if s.Type == remote.ProcessStopSignal {
log.WithField("signal_value", s.Value).Debug("stopping server using signal")
// Handle some common signals - Default to SIGKILL
signal := "SIGKILL"
switch strings.ToUpper(s.Value) {
case "SIGABRT":
signal = "SIGABRT"
case "SIGINT", "C":
signal = "SIGINT"
case "SIGTERM":
signal = "SIGTERM"
case "SIGKILL":
signal = "SIGKILL"
default:
log.Info("Unrecognised signal requested, defaulting to SIGKILL")
}
return e.SignalContainer(ctx, signal)
}
// Handle command based stops
// Only attempt to send the stop command to the instance if we are actually attached to // Only attempt to send the stop command to the instance if we are actually attached to
// the instance. If we are not for some reason, just send the container stop event. // the instance. If we are not for some reason, just send the container stop event.
if e.IsAttached() && s.Type == remote.ProcessStopCommand { if e.IsAttached() && s.Type == remote.ProcessStopCommand {
return e.SendCommand(s.Value) return e.SendCommand(s.Value)
} }
// Allow the stop action to run for however long it takes, similar to executing a command if s.Type == "" {
// and using a different logic pathway to wait for the container to stop successfully. log.WithField("container_id", e.Id).Warn("no stop configuration detected for environment, using native docker stop")
}
// Fallback to a native docker stop. As we aren't passing a signal to ContainerStop docker will
// attempt to stop the container using the default stop signal, SIGTERM, unless
// another signal was specified in the Dockerfile
// //
// Using a negative timeout here will allow the container to stop gracefully, // Using a negative timeout here will allow the container to stop gracefully,
// rather than forcefully terminating it. Value is in seconds, but -1 is // rather than forcefully terminating it. Value is in seconds, but -1 is
@ -224,7 +230,7 @@ func (e *Environment) WaitForStop(ctx context.Context, duration time.Duration, t
doTermination := func(s string) error { doTermination := func(s string) error {
e.log().WithField("step", s).WithField("duration", duration).Warn("container stop did not complete in time, terminating process...") e.log().WithField("step", s).WithField("duration", duration).Warn("container stop did not complete in time, terminating process...")
return e.Terminate(ctx, os.Kill) return e.Terminate(ctx, "SIGKILL")
} }
// We pass through the timed context for this stop action so that if one of the // We pass through the timed context for this stop action so that if one of the
@ -268,8 +274,8 @@ func (e *Environment) WaitForStop(ctx context.Context, duration time.Duration, t
return nil return nil
} }
// Terminate forcefully terminates the container using the signal provided. // Sends the specified signal to the container in an attempt to stop it.
func (e *Environment) Terminate(ctx context.Context, signal os.Signal) error { func (e *Environment) SignalContainer(ctx context.Context, signal string) error {
c, err := e.ContainerInspect(ctx) c, err := e.ContainerInspect(ctx)
if err != nil { if err != nil {
// Treat missing containers as an okay error state, means it is obviously // Treat missing containers as an okay error state, means it is obviously
@ -294,11 +300,27 @@ func (e *Environment) Terminate(ctx context.Context, signal os.Signal) error {
// We set it to stopping than offline to prevent crash detection from being triggered. // We set it to stopping than offline to prevent crash detection from being triggered.
e.SetState(environment.ProcessStoppingState) e.SetState(environment.ProcessStoppingState)
sig := strings.TrimSuffix(strings.TrimPrefix(signal.String(), "signal "), "ed") if err := e.client.ContainerKill(ctx, e.Id, signal); err != nil && !client.IsErrNotFound(err) {
if err := e.client.ContainerKill(ctx, e.Id, sig); err != nil && !client.IsErrNotFound(err) {
return errors.WithStack(err) return errors.WithStack(err)
} }
return nil
}
// Terminate forcefully terminates the container using the signal provided.
// then sets its state to stopped.
func (e *Environment) Terminate(ctx context.Context, signal string) error {
// Send the signal to the container to kill it
if err := e.SignalContainer(ctx, signal); err != nil {
return errors.WithStack(err)
}
// We expect Terminate to instantly kill the container
// so go ahead and mark it as dead and clean up
e.SetState(environment.ProcessOfflineState) e.SetState(environment.ProcessOfflineState)
return nil return nil
} }

View File

@ -2,7 +2,6 @@ package environment
import ( import (
"context" "context"
"os"
"time" "time"
"github.com/pterodactyl/wings/events" "github.com/pterodactyl/wings/events"
@ -72,7 +71,7 @@ type ProcessEnvironment interface {
// Terminate stops a running server instance using the provided signal. This function // Terminate stops a running server instance using the provided signal. This function
// is a no-op if the server is already stopped. // is a no-op if the server is already stopped.
Terminate(ctx context.Context, signal os.Signal) error Terminate(ctx context.Context, signal string) error
// Destroys the environment removing any containers that were created (in Docker // Destroys the environment removing any containers that were created (in Docker
// environments at least). // environments at least).

View File

@ -34,7 +34,7 @@ type Mount struct {
// Limits is the build settings for a given server that impact docker container // Limits is the build settings for a given server that impact docker container
// creation and resource limits for a server instance. // creation and resource limits for a server instance.
type Limits struct { type Limits struct {
// The total amount of memory in megabytes that this server is allowed to // The total amount of memory in mebibytes that this server is allowed to
// use on the host system. // use on the host system.
MemoryLimit int64 `json:"memory_limit"` MemoryLimit int64 `json:"memory_limit"`
@ -79,7 +79,7 @@ func (l Limits) MemoryOverheadMultiplier() float64 {
} }
func (l Limits) BoundedMemoryLimit() int64 { func (l Limits) BoundedMemoryLimit() int64 {
return int64(math.Round(float64(l.MemoryLimit) * l.MemoryOverheadMultiplier() * 1_000_000)) return int64(math.Round(float64(l.MemoryLimit) * l.MemoryOverheadMultiplier() * 1024 * 1024))
} }
// ConvertedSwap returns the amount of swap available as a total in bytes. This // ConvertedSwap returns the amount of swap available as a total in bytes. This
@ -90,7 +90,7 @@ func (l Limits) ConvertedSwap() int64 {
return -1 return -1
} }
return (l.Swap * 1_000_000) + l.BoundedMemoryLimit() return (l.Swap * 1024 * 1024) + l.BoundedMemoryLimit()
} }
// ProcessLimit returns the process limit for a container. This is currently // ProcessLimit returns the process limit for a container. This is currently
@ -105,7 +105,7 @@ func (l Limits) AsContainerResources() container.Resources {
pids := l.ProcessLimit() pids := l.ProcessLimit()
resources := container.Resources{ resources := container.Resources{
Memory: l.BoundedMemoryLimit(), Memory: l.BoundedMemoryLimit(),
MemoryReservation: l.MemoryLimit * 1_000_000, MemoryReservation: l.MemoryLimit * 1024 * 1024,
MemorySwap: l.ConvertedSwap(), MemorySwap: l.ConvertedSwap(),
BlkioWeight: l.IoWeight, BlkioWeight: l.IoWeight,
OomKillDisable: &l.OOMDisabled, OomKillDisable: &l.OOMDisabled,

View File

@ -5,11 +5,11 @@
"nixpkgs-lib": "nixpkgs-lib" "nixpkgs-lib": "nixpkgs-lib"
}, },
"locked": { "locked": {
"lastModified": 1706830856, "lastModified": 1719994518,
"narHash": "sha256-a0NYyp+h9hlb7ddVz4LUn1vT/PLwqfrWYcHMvFB1xYg=", "narHash": "sha256-pQMhCCHyQGRzdfAkdJ4cIWiw+JNuWsTX7f0ZYSyz0VY=",
"owner": "hercules-ci", "owner": "hercules-ci",
"repo": "flake-parts", "repo": "flake-parts",
"rev": "b253292d9c0a5ead9bc98c4e9a26c6312e27d69f", "rev": "9227223f6d922fee3c7b190b2cc238a99527bbb7",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -20,11 +20,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1707956935, "lastModified": 1721562059,
"narHash": "sha256-ZL2TrjVsiFNKOYwYQozpbvQSwvtV/3Me7Zwhmdsfyu4=", "narHash": "sha256-Tybxt65eyOARf285hMHIJ2uul8SULjFZbT9ZaEeUnP8=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "a4d4fe8c5002202493e87ec8dbc91335ff55552c", "rev": "68c9ed8bbed9dfce253cc91560bf9043297ef2fe",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -36,20 +36,14 @@
}, },
"nixpkgs-lib": { "nixpkgs-lib": {
"locked": { "locked": {
"dir": "lib", "lastModified": 1719876945,
"lastModified": 1706550542, "narHash": "sha256-Fm2rDDs86sHy0/1jxTOKB1118Q0O3Uc7EC0iXvXKpbI=",
"narHash": "sha256-UcsnCG6wx++23yeER4Hg18CXWbgNpqNXcHIo5/1Y+hc=", "type": "tarball",
"owner": "NixOS", "url": "https://github.com/NixOS/nixpkgs/archive/5daf0514482af3f97abaefc78a6606365c9108e2.tar.gz"
"repo": "nixpkgs",
"rev": "97b17f32362e475016f942bbdfda4a4a72a8a652",
"type": "github"
}, },
"original": { "original": {
"dir": "lib", "type": "tarball",
"owner": "NixOS", "url": "https://github.com/NixOS/nixpkgs/archive/5daf0514482af3f97abaefc78a6606365c9108e2.tar.gz"
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
} }
}, },
"root": { "root": {
@ -66,11 +60,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1707300477, "lastModified": 1721769617,
"narHash": "sha256-qQF0fEkHlnxHcrKIMRzOETnRBksUK048MXkX0SOmxvA=", "narHash": "sha256-6Pqa0bi5nV74IZcENKYRToRNM5obo1EQ+3ihtunJ014=",
"owner": "numtide", "owner": "numtide",
"repo": "treefmt-nix", "repo": "treefmt-nix",
"rev": "ac599dab59a66304eb511af07b3883114f061b9d", "rev": "8db8970be1fb8be9c845af7ebec53b699fe7e009",
"type": "github" "type": "github"
}, },
"original": { "original": {

View File

@ -156,6 +156,7 @@ func (fs *Filesystem) DecompressFile(ctx context.Context, dir string, file strin
} }
return fs.extractStream(ctx, extractStreamOptions{ return fs.extractStream(ctx, extractStreamOptions{
FileName: file,
Directory: dir, Directory: dir,
Format: format, Format: format,
Reader: input, Reader: input,
@ -190,11 +191,74 @@ type extractStreamOptions struct {
} }
func (fs *Filesystem) extractStream(ctx context.Context, opts extractStreamOptions) error { func (fs *Filesystem) extractStream(ctx context.Context, opts extractStreamOptions) error {
// Decompress and extract archive
// See if it's a compressed archive, such as TAR or a ZIP
ex, ok := opts.Format.(archiver.Extractor) ex, ok := opts.Format.(archiver.Extractor)
if !ok {
// If not, check if it's a single-file compression, such as
// .log.gz, .sql.gz, and so on
de, ok := opts.Format.(archiver.Decompressor)
if !ok { if !ok {
return nil return nil
} }
// Strip the compression suffix
p := filepath.Join(opts.Directory, strings.TrimSuffix(opts.FileName, opts.Format.Name()))
// Make sure it's not ignored
if err := fs.IsIgnored(p); err != nil {
return nil
}
reader, err := de.OpenReader(opts.Reader)
if err != nil {
return err
}
defer reader.Close()
// Open the file for creation/writing
f, err := fs.unixFS.OpenFile(p, ufs.O_WRONLY|ufs.O_CREATE, 0o644)
if err != nil {
return err
}
defer f.Close()
// Read in 4 KB chunks
buf := make([]byte, 4096)
for {
n, err := reader.Read(buf)
if n > 0 {
// Check quota before writing the chunk
if quotaErr := fs.HasSpaceFor(int64(n)); quotaErr != nil {
return quotaErr
}
// Write the chunk
if _, writeErr := f.Write(buf[:n]); writeErr != nil {
return writeErr
}
// Add to quota
fs.addDisk(int64(n))
}
if err != nil {
// EOF are expected
if err == io.EOF {
break
}
// Return any other
return err
}
}
return nil
}
// Decompress and extract archive
return ex.Extract(ctx, opts.Reader, nil, func(ctx context.Context, f archiver.File) error { return ex.Extract(ctx, opts.Reader, nil, func(ctx context.Context, f archiver.File) error {
if f.IsDir() { if f.IsDir() {
return nil return nil

View File

@ -1,6 +1,8 @@
package filesystem package filesystem
import ( import (
"golang.org/x/sys/unix"
"slices"
"sync" "sync"
"sync/atomic" "sync/atomic"
"time" "time"
@ -164,6 +166,8 @@ func (fs *Filesystem) DirectorySize(root string) (int64, error) {
return 0, err return 0, err
} }
var hardLinks []uint64
var size atomic.Int64 var size atomic.Int64
err = fs.unixFS.WalkDirat(dirfd, name, func(dirfd int, name, _ string, d ufs.DirEntry, err error) error { err = fs.unixFS.WalkDirat(dirfd, name, func(dirfd int, name, _ string, d ufs.DirEntry, err error) error {
if err != nil { if err != nil {
@ -180,8 +184,16 @@ func (fs *Filesystem) DirectorySize(root string) (int64, error) {
return errors.Wrap(err, "lstatat err") return errors.Wrap(err, "lstatat err")
} }
// TODO: detect if info is a hard-link and de-duplicate it. var sysFileInfo = info.Sys().(*unix.Stat_t)
// ref; https://github.com/pterodactyl/wings/pull/181/files if sysFileInfo.Nlink > 1 {
// Hard links have the same inode number
if slices.Contains(hardLinks, sysFileInfo.Ino) {
// Don't add hard links size twice
return nil
} else {
hardLinks = append(hardLinks, sysFileInfo.Ino)
}
}
size.Add(info.Size()) size.Add(info.Size())
return nil return nil

View File

@ -480,9 +480,9 @@ func (fs *Filesystem) ListDirectory(p string) ([]Stat, error) {
case a.IsDir() && b.IsDir(): case a.IsDir() && b.IsDir():
return 0 return 0
case a.IsDir(): case a.IsDir():
return 1
default:
return -1 return -1
default:
return 1
} }
}) })

View File

@ -29,6 +29,21 @@ func (s *Server) Mounts() []environment.Mount {
}, },
} }
// Handle mounting a generated `/etc/passwd` if the feature is enabled.
if passwd := config.Get().System.Passwd; passwd.Enable {
s.Log().WithFields(log.Fields{"source_path": passwd.Directory}).Info("mouting generated /etc/{group,passwd} to workaround UID/GID issues")
m = append(m, environment.Mount{
Source: filepath.Join(passwd.Directory, "group"),
Target: "/etc/group",
ReadOnly: true,
})
m = append(m, environment.Mount{
Source: filepath.Join(passwd.Directory, "passwd"),
Target: "/etc/passwd",
ReadOnly: true,
})
}
// Also include any of this server's custom mounts when returning them. // Also include any of this server's custom mounts when returning them.
return append(m, s.customMounts()...) return append(m, s.customMounts()...)
} }
@ -56,14 +71,12 @@ func (s *Server) customMounts() []environment.Mount {
if !strings.HasPrefix(source, filepath.Clean(allowed)) { if !strings.HasPrefix(source, filepath.Clean(allowed)) {
continue continue
} }
mounted = true mounted = true
mounts = append(mounts, environment.Mount{ mounts = append(mounts, environment.Mount{
Source: source, Source: source,
Target: target, Target: target,
ReadOnly: m.ReadOnly, ReadOnly: m.ReadOnly,
}) })
break break
} }

View File

@ -3,7 +3,6 @@ package server
import ( import (
"context" "context"
"fmt" "fmt"
"os"
"time" "time"
"emperror.dev/errors" "emperror.dev/errors"
@ -161,7 +160,7 @@ func (s *Server) HandlePowerAction(action PowerAction, waitSeconds ...int) error
return s.Environment.Start(s.Context()) return s.Environment.Start(s.Context())
case PowerActionTerminate: case PowerActionTerminate:
return s.Environment.Terminate(s.Context(), os.Kill) return s.Environment.Terminate(s.Context(), "SIGKILL")
} }
return errors.New("attempting to handle unknown power action") return errors.New("attempting to handle unknown power action")

View File

@ -28,6 +28,7 @@ func (s *Server) SyncWithEnvironment() {
Mounts: s.Mounts(), Mounts: s.Mounts(),
Allocations: cfg.Allocations, Allocations: cfg.Allocations,
Limits: cfg.Build, Limits: cfg.Build,
Labels: cfg.Labels,
}) })
// For Docker specific environments we also want to update the configured image // For Docker specific environments we also want to update the configured image

View File

@ -1,3 +1,3 @@
package system package system
var Version = "1.11.12" var Version = "develop"