170 lines
5.8 KiB
Go
170 lines
5.8 KiB
Go
package environment
|
|
|
|
import (
|
|
"fmt"
|
|
"math"
|
|
"strconv"
|
|
|
|
"github.com/apex/log"
|
|
"github.com/docker/docker/api/types/container"
|
|
|
|
"github.com/pterodactyl/wings/config"
|
|
)
|
|
|
|
type Mount struct {
|
|
// In Docker environments this makes no difference, however in a non-Docker environment you
|
|
// should treat the "Default" mount as the root directory for the server. All other mounts
|
|
// are just in addition to that one, and generally things like shared maps or timezone data.
|
|
Default bool `json:"-"`
|
|
|
|
// The target path on the system. This is "/home/container" for all server's Default mount
|
|
// but in non-container environments you can likely ignore the target and just work with the
|
|
// source.
|
|
Target string `json:"target"`
|
|
|
|
// The directory from which the files will be read. In Docker environments this is the directory
|
|
// that we're mounting into the container at the Target location.
|
|
Source string `json:"source"`
|
|
|
|
// Whether the directory is being mounted as read-only. It is up to the environment to
|
|
// handle this value correctly and ensure security expectations are met with its usage.
|
|
ReadOnly bool `json:"read_only"`
|
|
}
|
|
|
|
// Limits is the build settings for a given server that impact docker container
|
|
// creation and resource limits for a server instance.
|
|
type Limits struct {
|
|
// The total amount of memory in megabytes that this server is allowed to
|
|
// use on the host system.
|
|
MemoryLimit int64 `json:"memory_limit"`
|
|
|
|
// The amount of additional swap space to be provided to a container instance.
|
|
Swap int64 `json:"swap"`
|
|
|
|
// The relative weight for IO operations in a container. This is relative to other
|
|
// containers on the system and should be a value between 10 and 1000.
|
|
IoWeight uint16 `json:"io_weight"`
|
|
|
|
// The percentage of CPU that this instance is allowed to consume relative to
|
|
// the host. A value of 200% represents complete utilization of two cores. This
|
|
// should be a value between 1 and THREAD_COUNT * 100.
|
|
CpuLimit int64 `json:"cpu_limit"`
|
|
|
|
// The amount of disk space in megabytes that a server is allowed to use.
|
|
DiskSpace int64 `json:"disk_space"`
|
|
|
|
// Sets which CPU threads can be used by the docker instance.
|
|
Threads string `json:"threads"`
|
|
|
|
OOMDisabled bool `json:"oom_disabled"`
|
|
}
|
|
|
|
// ConvertedCpuLimit converts the CPU limit for a server build into a number
|
|
// that can be better understood by the Docker environment. If there is no limit
|
|
// set, return -1 which will indicate to Docker that it has unlimited CPU quota.
|
|
func (l Limits) ConvertedCpuLimit() int64 {
|
|
if l.CpuLimit == 0 {
|
|
return -1
|
|
}
|
|
|
|
return l.CpuLimit * 1000
|
|
}
|
|
|
|
// MemoryOverheadMultiplier sets the hard limit for memory usage to be 5% more
|
|
// than the amount of memory assigned to the server. If the memory limit for the
|
|
// server is < 4G, use 10%, if less than 2G use 15%. This avoids unexpected
|
|
// crashes from processes like Java which run over the limit.
|
|
func (l Limits) MemoryOverheadMultiplier() float64 {
|
|
return config.Get().Docker.Overhead.GetMultiplier(l.MemoryLimit)
|
|
}
|
|
|
|
func (l Limits) BoundedMemoryLimit() int64 {
|
|
return int64(math.Round(float64(l.MemoryLimit) * l.MemoryOverheadMultiplier() * 1_000_000))
|
|
}
|
|
|
|
// ConvertedSwap returns the amount of swap available as a total in bytes. This
|
|
// is returned as the amount of memory available to the server initially, PLUS
|
|
// the amount of additional swap to include which is the format used by Docker.
|
|
func (l Limits) ConvertedSwap() int64 {
|
|
if l.Swap < 0 {
|
|
return -1
|
|
}
|
|
|
|
return (l.Swap * 1_000_000) + l.BoundedMemoryLimit()
|
|
}
|
|
|
|
// ProcessLimit returns the process limit for a container. This is currently
|
|
// defined at a system level and not on a per-server basis.
|
|
func (l Limits) ProcessLimit() int64 {
|
|
return config.Get().Docker.ContainerPidLimit
|
|
}
|
|
|
|
// AsContainerResources returns the available resources for a container in a format
|
|
// that Docker understands.
|
|
func (l Limits) AsContainerResources() container.Resources {
|
|
pids := l.ProcessLimit()
|
|
resources := container.Resources{
|
|
Memory: l.BoundedMemoryLimit(),
|
|
MemoryReservation: l.MemoryLimit * 1_000_000,
|
|
MemorySwap: l.ConvertedSwap(),
|
|
BlkioWeight: l.IoWeight,
|
|
OomKillDisable: &l.OOMDisabled,
|
|
PidsLimit: &pids,
|
|
}
|
|
|
|
// If the CPU Limit is not set, don't send any of these fields through. Providing
|
|
// them seems to break some Java services that try to read the available processors.
|
|
//
|
|
// @see https://github.com/pterodactyl/panel/issues/3988
|
|
if l.CpuLimit > 0 {
|
|
resources.CPUQuota = l.CpuLimit * 1_000
|
|
resources.CPUPeriod = 100_000
|
|
resources.CPUShares = 1024
|
|
}
|
|
|
|
// Similar to above, don't set the specific assigned CPUs if we didn't actually limit
|
|
// the server to any of them.
|
|
if l.Threads != "" {
|
|
resources.CpusetCpus = l.Threads
|
|
}
|
|
|
|
return resources
|
|
}
|
|
|
|
type Variables map[string]interface{}
|
|
|
|
// Get is an ugly hacky function to handle environment variables that get passed
|
|
// through as not-a-string from the Panel. Ideally we'd just say only pass
|
|
// strings, but that is a fragile idea and if a string wasn't passed through
|
|
// you'd cause a crash or the server to become unavailable. For now try to
|
|
// handle the most likely values from the JSON and hope for the best.
|
|
func (v Variables) Get(key string) string {
|
|
val, ok := v[key]
|
|
if !ok {
|
|
return ""
|
|
}
|
|
|
|
switch val.(type) {
|
|
case int:
|
|
return strconv.Itoa(val.(int))
|
|
case int32:
|
|
return strconv.FormatInt(val.(int64), 10)
|
|
case int64:
|
|
return strconv.FormatInt(val.(int64), 10)
|
|
case float32:
|
|
return fmt.Sprintf("%f", val.(float32))
|
|
case float64:
|
|
return fmt.Sprintf("%f", val.(float64))
|
|
case bool:
|
|
return strconv.FormatBool(val.(bool))
|
|
case string:
|
|
return val.(string)
|
|
}
|
|
|
|
// TODO: I think we can add a check for val == nil and return an empty string for those
|
|
// and this warning should theoretically never happen?
|
|
log.Warn(fmt.Sprintf("failed to marshal environment variable \"%s\" of type %+v into string", key, val))
|
|
|
|
return ""
|
|
}
|