wings/control/docker_environment.go

245 lines
6.9 KiB
Go
Raw Normal View History

package control
import (
"context"
"io"
"os"
"strings"
"github.com/pterodactyl/wings/constants"
"github.com/fsouza/go-dockerclient"
"github.com/pterodactyl/wings/api/websockets"
log "github.com/sirupsen/logrus"
)
type dockerEnvironment struct {
baseEnvironment
client *docker.Client
container *docker.Container
context context.Context
attached bool
containerInput io.Writer
containerOutput io.Writer
closeWaiter docker.CloseWaiter
server *ServerStruct
}
// Ensure DockerEnvironment implements Environment
var _ Environment = &dockerEnvironment{}
// NewDockerEnvironment creates a new docker enviornment
// instance and connects to the docker client on the host system
// If the container is already running it will try to reattach
// to the running container
func NewDockerEnvironment(server *ServerStruct) (Environment, error) {
env := dockerEnvironment{}
env.server = server
client, err := docker.NewClient("unix:///var/run/docker.sock")
env.client = client
if err != nil {
log.WithError(err).Fatal("Failed to connect to docker.")
return nil, err
}
if env.server.DockerContainer.ID != "" {
if err := env.checkContainerExists(); err != nil {
log.WithError(err).Error("Failed to find the container with stored id, removing id.")
env.server.DockerContainer.ID = ""
env.server.Save()
}
}
return &env, nil
}
func (env *dockerEnvironment) checkContainerExists() error {
container, err := env.client.InspectContainer(env.server.DockerContainer.ID)
if err != nil {
return err
}
env.container = container
return nil
}
func (env *dockerEnvironment) attach() error {
if env.attached {
return nil
}
pr, pw := io.Pipe()
env.containerInput = pw
cw := websockets.ConsoleWriter{
Hub: env.server.websockets,
}
success := make(chan struct{})
w, err := env.client.AttachToContainerNonBlocking(docker.AttachToContainerOptions{
Container: env.server.DockerContainer.ID,
InputStream: pr,
OutputStream: cw,
ErrorStream: cw,
Stdin: true,
Stdout: true,
Stderr: true,
Stream: true,
Success: success,
})
env.closeWaiter = w
<-success
close(success)
env.attached = true
return err
}
// Create creates the docker container for the environment and applies all
// settings to it
func (env *dockerEnvironment) Create() error {
log.WithField("server", env.server.ID).Debug("Creating docker environment")
// Split image repository and tag
imageParts := strings.Split(env.server.GetService().DockerImage, ":")
imageRepoParts := strings.Split(imageParts[0], "/")
if len(imageRepoParts) >= 3 {
// TODO: Handle possibly required authentication
}
// Pull docker image
var pullImageOpts = docker.PullImageOptions{
Repository: imageParts[0],
}
if len(imageParts) >= 2 {
pullImageOpts.Tag = imageParts[1]
}
log.WithField("image", env.server.GetService().DockerImage).Debug("Pulling docker image")
err := env.client.PullImage(pullImageOpts, docker.AuthConfiguration{})
if err != nil {
log.WithError(err).WithField("server", env.server.ID).Error("Failed to create docker environment")
return err
}
if err := os.MkdirAll(env.server.dataPath(), constants.DefaultFolderPerms); err != nil {
return err
}
// Create docker container
// TODO: apply cpu, io, disk limits.
containerConfig := &docker.Config{
Image: env.server.GetService().DockerImage,
Cmd: strings.Split(env.server.StartupCommand, " "),
OpenStdin: true,
ArgsEscaped: false,
Hostname: constants.DockerContainerPrefix + env.server.UUIDShort(),
}
containerHostConfig := &docker.HostConfig{
Memory: env.server.Settings.Memory,
MemorySwap: env.server.Settings.Swap,
// TODO: Allow custom binds via some kind of settings in the service
Binds: []string{env.server.dataPath() + ":/home/container"},
// TODO: Add port bindings
}
createContainerOpts := docker.CreateContainerOptions{
Name: constants.DockerContainerPrefix + env.server.UUIDShort(),
Config: containerConfig,
HostConfig: containerHostConfig,
Context: env.context,
}
container, err := env.client.CreateContainer(createContainerOpts)
if err != nil {
log.WithError(err).WithField("server", env.server.ID).Error("Failed to create docker container")
return err
}
env.server.DockerContainer.ID = container.ID
env.server.Save()
env.container = container
if env.closeWaiter != nil {
env.closeWaiter.Close()
}
env.attached = false
log.WithField("server", env.server.ID).Debug("Docker environment created")
return nil
}
// Destroy removes the environment's docker container
func (env *dockerEnvironment) Destroy() error {
log.WithField("server", env.server.ID).Debug("Destroying docker environment")
if _, err := env.client.InspectContainer(env.server.DockerContainer.ID); err != nil {
if _, ok := err.(*docker.NoSuchContainer); ok {
log.WithField("server", env.server.ID).Debug("Container not found, docker environment destroyed already.")
return nil
}
log.WithError(err).WithField("server", env.server.ID).Error("Could not destroy docker environment")
return err
}
err := env.client.RemoveContainer(docker.RemoveContainerOptions{
ID: env.server.DockerContainer.ID,
})
if err != nil {
log.WithError(err).WithField("server", env.server.ID).Error("Failed to destroy docker environment")
return err
}
if env.closeWaiter != nil {
env.closeWaiter.Close()
}
env.attached = false
log.WithField("server", env.server.ID).Debug("Docker environment destroyed")
return nil
}
func (env *dockerEnvironment) Exists() bool {
if env.container != nil {
return true
}
env.checkContainerExists()
return env.container != nil
}
// Start starts the environment's docker container
func (env *dockerEnvironment) Start() error {
log.WithField("server", env.server.ID).Debug("Starting service in docker environment")
if err := env.attach(); err != nil {
log.WithError(err).Error("Failed to attach to docker container")
}
if err := env.client.StartContainer(env.container.ID, nil); err != nil {
log.WithError(err).Error("Failed to start docker container")
return err
}
return nil
}
// Stop stops the environment's docker container
func (env *dockerEnvironment) Stop() error {
log.WithField("server", env.server.ID).Debug("Stopping service in docker environment")
if err := env.client.StopContainer(env.container.ID, 20000); err != nil {
log.WithError(err).Error("Failed to stop docker container")
return err
}
return nil
}
func (env *dockerEnvironment) Kill() error {
log.WithField("server", env.server.ID).Debug("Killing service in docker environment")
if err := env.client.KillContainer(docker.KillContainerOptions{
ID: env.container.ID,
}); err != nil {
log.WithError(err).Error("Failed to kill docker container")
return err
}
return nil
}
// Exec sends commands to the standard input of the docker container
func (env *dockerEnvironment) Exec(command string) error {
log.Debug("Command: " + command)
_, err := env.containerInput.Write([]byte(command + "\n"))
return err
}