Skip to content

Commit

Permalink
environment(docker): fix podman compatibility (#151)
Browse files Browse the repository at this point in the history
  • Loading branch information
matthewpi authored Nov 6, 2022
1 parent f577f55 commit 3337362
Show file tree
Hide file tree
Showing 10 changed files with 95 additions and 115 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
# ignore configuration file
/config.yml
/config*.yml
/config.yaml
/config*.yaml

# Ignore Vagrant stuff
/.vagrant
Expand Down
2 changes: 1 addition & 1 deletion cmd/diagnostics.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
8 changes: 3 additions & 5 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
Expand Down Expand Up @@ -111,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")
}
Expand Down Expand Up @@ -364,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 {
Expand Down
33 changes: 30 additions & 3 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -425,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.
Expand Down
17 changes: 17 additions & 0 deletions config/config_docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"sort"

"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/goccy/go-json"
)

Expand Down Expand Up @@ -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
Expand Down
113 changes: 23 additions & 90 deletions environment/docker/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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,
Expand Down Expand Up @@ -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
}
}()

Expand Down Expand Up @@ -163,14 +151,14 @@ 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
}
}

Expand All @@ -186,8 +174,7 @@ func (e *Environment) Create() error {

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,
Expand All @@ -199,7 +186,14 @@ func (e *Environment) Create() error {
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, "-", "")
Expand Down Expand Up @@ -238,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,
Expand All @@ -268,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 {
Expand Down Expand Up @@ -349,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.
Expand Down
2 changes: 1 addition & 1 deletion environment/docker/environment.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
4 changes: 2 additions & 2 deletions environment/docker/power.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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()

Expand Down
6 changes: 5 additions & 1 deletion environment/docker/stats.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading

0 comments on commit 3337362

Please sign in to comment.