2019-04-04 05:01:11 +00:00
|
|
|
package server
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
2019-04-06 04:59:27 +00:00
|
|
|
"github.com/docker/docker/api/types"
|
2019-04-04 05:01:11 +00:00
|
|
|
"github.com/docker/docker/api/types/container"
|
2019-04-04 05:49:15 +00:00
|
|
|
"github.com/docker/docker/api/types/mount"
|
2019-04-04 05:01:11 +00:00
|
|
|
"github.com/docker/docker/client"
|
2019-04-04 05:49:15 +00:00
|
|
|
"github.com/docker/docker/daemon/logger/jsonfilelog"
|
2019-04-04 06:09:15 +00:00
|
|
|
"github.com/docker/go-connections/nat"
|
2019-04-04 05:01:11 +00:00
|
|
|
"golang.org/x/net/context"
|
|
|
|
"os"
|
2019-04-04 06:54:38 +00:00
|
|
|
"strconv"
|
2019-04-04 05:01:11 +00:00
|
|
|
"strings"
|
2019-04-06 04:59:27 +00:00
|
|
|
"time"
|
2019-04-04 05:01:11 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
// Defines the docker configuration used by the daemon when interacting with
|
|
|
|
// containers and networks on the system.
|
|
|
|
type DockerConfiguration struct {
|
|
|
|
Container struct {
|
|
|
|
User string
|
|
|
|
}
|
|
|
|
|
|
|
|
// Network configuration that should be used when creating a new network
|
|
|
|
// for containers run through the daemon.
|
|
|
|
Network struct {
|
|
|
|
// The interface that should be used to create the network. Must not conflict
|
|
|
|
// with any other interfaces in use by Docker or on the system.
|
|
|
|
Interface string
|
|
|
|
|
|
|
|
// The name of the network to use. If this network already exists it will not
|
|
|
|
// be created. If it is not found, a new network will be created using the interface
|
|
|
|
// defined.
|
|
|
|
Name string
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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 `yaml:"update_images"`
|
|
|
|
|
|
|
|
// The location of the Docker socket.
|
|
|
|
Socket string
|
|
|
|
|
|
|
|
// 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 `yaml:"timezone_path"`
|
|
|
|
}
|
|
|
|
|
2019-04-06 04:59:27 +00:00
|
|
|
// Defines the base environment for Docker instances running through Wings.
|
2019-04-04 05:01:11 +00:00
|
|
|
type DockerEnvironment struct {
|
|
|
|
Server *Server
|
|
|
|
|
|
|
|
// Defines the configuration for the Docker instance that will allow us to connect
|
|
|
|
// and create and modify containers.
|
|
|
|
Configuration DockerConfiguration
|
2019-04-06 04:59:27 +00:00
|
|
|
|
|
|
|
// The Docker client being used for this instance.
|
|
|
|
Client *client.Client
|
|
|
|
}
|
|
|
|
|
|
|
|
// Creates a new base Docker environment. A server must still be attached to it.
|
|
|
|
func NewDockerEnvironment(cfg DockerConfiguration) (*DockerEnvironment, error) {
|
|
|
|
cli, err := client.NewClientWithOpts(client.FromEnv)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return &DockerEnvironment{
|
|
|
|
Configuration: cfg,
|
|
|
|
Client: cli,
|
|
|
|
}, nil
|
2019-04-04 05:01:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Ensure that the Docker environment is always implementing all of the methods
|
|
|
|
// from the base environment interface.
|
|
|
|
var _ Environment = (*DockerEnvironment)(nil)
|
|
|
|
|
2019-04-06 04:59:27 +00:00
|
|
|
// Determines if the container exists in this environment.
|
2019-04-04 05:01:11 +00:00
|
|
|
func (d *DockerEnvironment) Exists() bool {
|
2019-04-06 04:59:27 +00:00
|
|
|
_, err := d.Client.ContainerInspect(context.Background(), d.Server.Uuid)
|
|
|
|
|
|
|
|
return err == nil
|
2019-04-04 05:01:11 +00:00
|
|
|
}
|
|
|
|
|
2019-04-06 04:59:27 +00:00
|
|
|
// Checks if there is a container that already exists for the server. If so that
|
|
|
|
// container is started. If there is no container, one is created and then started.
|
2019-04-04 05:01:11 +00:00
|
|
|
func (d *DockerEnvironment) Start() error {
|
2019-04-06 04:59:27 +00:00
|
|
|
c, err := d.Client.ContainerInspect(context.Background(), d.Server.Uuid)
|
|
|
|
if err != nil {
|
|
|
|
// @todo what?
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// No reason to try starting a container that is already running.
|
|
|
|
if c.State.Running {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
opts := types.ContainerStartOptions{}
|
|
|
|
|
|
|
|
return d.Client.ContainerStart(context.Background(), d.Server.Uuid, opts)
|
2019-04-04 05:01:11 +00:00
|
|
|
}
|
|
|
|
|
2019-04-06 04:59:27 +00:00
|
|
|
// Stops the container that the server is running in. This will allow up to 10
|
|
|
|
// seconds to pass before a failure occurs.
|
2019-04-04 05:01:11 +00:00
|
|
|
func (d *DockerEnvironment) Stop() error {
|
2019-04-06 04:59:27 +00:00
|
|
|
t := time.Second * 10
|
|
|
|
|
|
|
|
return d.Client.ContainerStop(context.Background(), d.Server.Uuid, &t)
|
2019-04-04 05:01:11 +00:00
|
|
|
}
|
|
|
|
|
2019-04-06 04:59:27 +00:00
|
|
|
// Forcefully terminates the container using the signal passed through.
|
2019-04-04 05:01:11 +00:00
|
|
|
func (d *DockerEnvironment) Terminate(signal os.Signal) error {
|
2019-04-06 04:59:27 +00:00
|
|
|
ctx := context.Background()
|
|
|
|
|
|
|
|
c, err := d.Client.ContainerInspect(ctx, d.Server.Uuid)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if !c.State.Running {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
return d.Client.ContainerKill(ctx, d.Server.Uuid, signal.String())
|
2019-04-04 05:01:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Creates a new container for the server using all of the data that is currently
|
|
|
|
// available for it. If the container already exists it will be returned.
|
|
|
|
func (d *DockerEnvironment) Create() error {
|
|
|
|
ctx := context.Background()
|
2019-04-04 06:58:45 +00:00
|
|
|
cli, err := client.NewClientWithOpts(client.FromEnv)
|
2019-04-04 05:01:11 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2019-04-04 05:49:15 +00:00
|
|
|
var oomDisabled = true
|
|
|
|
|
2019-04-04 05:01:11 +00:00
|
|
|
// If the container already exists don't hit the user with an error, just return
|
|
|
|
// the current information about it which is what we would do when creating the
|
|
|
|
// container anyways.
|
|
|
|
if _, err := cli.ContainerInspect(ctx, d.Server.Uuid); err == nil {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
conf := &container.Config{
|
2019-04-04 05:49:15 +00:00
|
|
|
Hostname: "container",
|
|
|
|
User: d.Configuration.Container.User,
|
|
|
|
AttachStdin: true,
|
2019-04-04 05:01:11 +00:00
|
|
|
AttachStdout: true,
|
|
|
|
AttachStderr: true,
|
2019-04-04 05:49:15 +00:00
|
|
|
OpenStdin: true,
|
|
|
|
Tty: true,
|
2019-04-04 05:01:11 +00:00
|
|
|
|
2019-04-04 06:09:15 +00:00
|
|
|
ExposedPorts: d.exposedPorts(),
|
|
|
|
|
2019-04-04 05:01:11 +00:00
|
|
|
Image: d.Server.Container.Image,
|
2019-04-04 05:49:15 +00:00
|
|
|
Env: d.environmentVariables(),
|
2019-04-04 05:01:11 +00:00
|
|
|
|
|
|
|
Labels: map[string]string{
|
|
|
|
"Service": "Pterodactyl",
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
hostConf := &container.HostConfig{
|
2019-04-04 06:09:15 +00:00
|
|
|
PortBindings: d.portBindings(),
|
|
|
|
|
2019-04-04 05:49:15 +00:00
|
|
|
// Configure the mounts for this container. First mount the server data directory
|
|
|
|
// into the container as a r/w bind. Additionally mount the host timezone data into
|
|
|
|
// the container as a readonly bind so that software running in the container uses
|
|
|
|
// the same time as the host system.
|
|
|
|
Mounts: []mount.Mount{
|
|
|
|
{
|
|
|
|
Target: "/home/container",
|
|
|
|
Source: d.Server.Filesystem().Path(),
|
|
|
|
Type: mount.TypeBind,
|
|
|
|
ReadOnly: false,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Target: d.Configuration.TimezonePath,
|
|
|
|
Source: d.Configuration.TimezonePath,
|
|
|
|
Type: mount.TypeBind,
|
|
|
|
ReadOnly: true,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
|
|
|
|
// 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=50M",
|
|
|
|
},
|
|
|
|
|
|
|
|
// Define resource limits for the container based on the data passed through
|
|
|
|
// from the Panel.
|
2019-04-04 05:01:11 +00:00
|
|
|
Resources: container.Resources{
|
2019-04-04 05:49:15 +00:00
|
|
|
// @todo memory limit should be slightly higher than the reservation
|
|
|
|
Memory: d.Server.Build.MemoryLimit * 1000000,
|
|
|
|
MemoryReservation: d.Server.Build.MemoryLimit * 1000000,
|
|
|
|
MemorySwap: d.Server.Build.ConvertedSwap(),
|
|
|
|
|
|
|
|
CPUQuota: d.Server.Build.ConvertedCpuLimit(),
|
|
|
|
CPUPeriod: 100000,
|
|
|
|
CPUShares: 1024,
|
|
|
|
|
|
|
|
BlkioWeight: d.Server.Build.IoWeight,
|
|
|
|
OomKillDisable: &oomDisabled,
|
|
|
|
},
|
|
|
|
|
|
|
|
// @todo make this configurable again
|
|
|
|
DNS: []string{"1.1.1.1", "8.8.8.8"},
|
|
|
|
|
|
|
|
// 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: jsonfilelog.Name,
|
|
|
|
Config: map[string]string{
|
|
|
|
"max-size": "5m",
|
|
|
|
"max-file": "1",
|
|
|
|
},
|
2019-04-04 05:01:11 +00:00
|
|
|
},
|
2019-04-04 05:49:15 +00:00
|
|
|
|
|
|
|
SecurityOpt: []string{"no-new-privileges"},
|
|
|
|
ReadonlyRootfs: true,
|
|
|
|
CapDrop: []string{
|
|
|
|
"setpcap", "mknod", "audit_write", "net_raw", "dac_override",
|
|
|
|
"fowner", "fsetid", "net_bind_service", "sys_chroot", "setfcap",
|
|
|
|
},
|
|
|
|
NetworkMode: "pterodactyl_nw",
|
2019-04-04 05:01:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if _, err := cli.ContainerCreate(ctx, conf, hostConf, nil, d.Server.Uuid); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Returns the environment variables for a server in KEY="VALUE" form.
|
|
|
|
func (d *DockerEnvironment) environmentVariables() []string {
|
2019-04-04 05:20:39 +00:00
|
|
|
var out = []string{
|
|
|
|
fmt.Sprintf("STARTUP=%s", d.Server.Invocation),
|
|
|
|
}
|
2019-04-04 05:01:11 +00:00
|
|
|
|
|
|
|
for k, v := range d.Server.EnvVars {
|
2019-04-04 05:20:39 +00:00
|
|
|
if strings.ToUpper(k) == "STARTUP" {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
out = append(out, fmt.Sprintf("%s=\"%s\"", strings.ToUpper(k), v))
|
2019-04-04 05:01:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return out
|
|
|
|
}
|
|
|
|
|
|
|
|
func (d *DockerEnvironment) volumes() map[string]struct{} {
|
|
|
|
return nil
|
2019-04-04 05:49:15 +00:00
|
|
|
}
|
2019-04-04 06:09:15 +00:00
|
|
|
|
|
|
|
// Converts the server allocation mappings into a format that can be understood
|
|
|
|
// by Docker.
|
|
|
|
func (d *DockerEnvironment) portBindings() nat.PortMap {
|
|
|
|
var out = nat.PortMap{}
|
|
|
|
|
|
|
|
for ip, ports := range d.Server.Allocations.Mappings {
|
|
|
|
for _, port := range ports {
|
|
|
|
// Skip over invalid ports.
|
|
|
|
if port < 0 || port > 65535 {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
binding := []nat.PortBinding{
|
|
|
|
{
|
|
|
|
HostIP: ip,
|
2019-04-04 06:54:38 +00:00
|
|
|
HostPort: strconv.Itoa(port),
|
2019-04-04 06:09:15 +00:00
|
|
|
},
|
|
|
|
}
|
|
|
|
|
2019-04-04 06:45:15 +00:00
|
|
|
out[nat.Port(fmt.Sprintf("%d/tcp", port))] = binding
|
|
|
|
out[nat.Port(fmt.Sprintf("%d/udp", port))] = binding
|
2019-04-04 06:09:15 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return out
|
|
|
|
}
|
|
|
|
|
|
|
|
// Converts the server allocation mappings into a PortSet that can be understood
|
|
|
|
// by Docker. This formatting is slightly different than portBindings as it should
|
|
|
|
// return an empty struct rather than a binding.
|
|
|
|
//
|
|
|
|
// To accomplish this, we'll just get the values from portBindings and then set them
|
|
|
|
// to empty structs. Because why not.
|
|
|
|
func (d *DockerEnvironment) exposedPorts() nat.PortSet {
|
|
|
|
var out = nat.PortSet{}
|
|
|
|
|
|
|
|
for port := range d.portBindings() {
|
|
|
|
out[port] = struct{}{}
|
|
|
|
}
|
|
|
|
|
|
|
|
return out
|
|
|
|
}
|