From b8b0702f84483e1fa74fdfb5c5795abd8c0e2701 Mon Sep 17 00:00:00 2001 From: Jakob Schrettenbrunner Date: Wed, 14 Mar 2018 10:33:23 +0100 Subject: [PATCH] switch to moby/moby in the docker_environment --- Gopkg.lock | 85 +++++-------- Gopkg.toml | 8 ++ constants/constants.go | 3 + control/console_handler.go | 33 +++++ control/docker_environment.go | 190 ++++++++++++---------------- control/docker_environment_test.go | 196 ++++++++++++++--------------- wings-api.paw | Bin 0 -> 16154 bytes 7 files changed, 250 insertions(+), 265 deletions(-) create mode 100644 control/console_handler.go create mode 100644 wings-api.paw diff --git a/Gopkg.lock b/Gopkg.lock index 1265fb3..115549b 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -7,79 +7,63 @@ revision = "af5e0ef38369dfb5819b56d27d593142841e4600" version = "0.1.2" -[[projects]] - branch = "master" - name = "github.com/Azure/go-ansiterm" - packages = [ - ".", - "winterm" - ] - revision = "d6e3b3328b783f23731bc4d058875b0371ff8109" - [[projects]] name = "github.com/Microsoft/go-winio" packages = ["."] revision = "7da180ee92d8bd8bb8c37fc560e673e6557c392f" version = "v0.4.7" -[[projects]] - branch = "master" - name = "github.com/Nvveen/Gotty" - packages = ["."] - revision = "cd527374f1e5bff4938207604a14f2e38a9cf512" - [[projects]] branch = "master" name = "github.com/StackExchange/wmi" packages = ["."] revision = "5d049714c4a64225c3c79a7cf7d02f7fb5b96338" -[[projects]] - branch = "master" - name = "github.com/containerd/continuity" - packages = ["pathdriver"] - revision = "d8fb8589b0e8e85b8c8bbaa8840226d0dfeb7371" - [[projects]] name = "github.com/davecgh/go-spew" packages = ["spew"] revision = "346938d642f2ec3594ed81d874461961cd0faa76" version = "v1.1.0" +[[projects]] + branch = "master" + name = "github.com/docker/distribution" + packages = [ + "digestset", + "reference" + ] + revision = "6664ec703991875e14419ff4960921cce7678bab" + [[projects]] name = "github.com/docker/docker" packages = [ + "api", "api/types", "api/types/blkiodev", "api/types/container", + "api/types/events", "api/types/filters", + "api/types/image", "api/types/mount", "api/types/network", "api/types/registry", "api/types/strslice", "api/types/swarm", "api/types/swarm/runtime", + "api/types/time", "api/types/versions", - "opts", - "pkg/archive", - "pkg/fileutils", - "pkg/homedir", - "pkg/idtools", - "pkg/ioutils", - "pkg/jsonmessage", - "pkg/longpath", - "pkg/mount", - "pkg/pools", - "pkg/stdcopy", - "pkg/system", - "pkg/term", - "pkg/term/windows" + "api/types/volume", + "client" ] - revision = "fe8aac6f5ae413a967adb0adad0b54abdfb825c4" + revision = "e3831a62a3052472d7252049bc59835d5d7dc8bd" [[projects]] name = "github.com/docker/go-connections" - packages = ["nat"] + packages = [ + "nat", + "sockets", + "tlsconfig" + ] revision = "3ede32e2033de7505e6500d6c868c2b9ed9f169d" version = "v0.3.0" @@ -95,12 +79,6 @@ revision = "c2828203cd70a50dcccfb2761f8b1f8ceef9a8e9" version = "v1.4.7" -[[projects]] - name = "github.com/fsouza/go-dockerclient" - packages = ["."] - revision = "2ff310040c161b75fa19fb9b287a90a6e03c0012" - version = "1.1" - [[projects]] name = "github.com/gin-gonic/gin" packages = [ @@ -211,15 +189,6 @@ revision = "d60099175f88c47cd379c4738d158884749ed235" version = "v1.0.1" -[[projects]] - name = "github.com/opencontainers/runc" - packages = [ - "libcontainer/system", - "libcontainer/user" - ] - revision = "baf6536d6259209c3edfa2b22237af82942d3dfa" - version = "v0.1.1" - [[projects]] name = "github.com/pelletier/go-toml" packages = ["."] @@ -251,11 +220,18 @@ "host", "internal/common", "mem", + "net", "process" ] revision = "c432be29ccce470088d07eea25b3ea7e68a8afbb" version = "v2.18.01" +[[projects]] + branch = "master" + name = "github.com/shirou/w32" + packages = ["."] + revision = "bb4de0191aa41b5507caa14b0650cdbddcd9280b" + [[projects]] name = "github.com/sirupsen/logrus" packages = ["."] @@ -318,7 +294,8 @@ name = "golang.org/x/net" packages = [ "context", - "context/ctxhttp" + "context/ctxhttp", + "proxy" ] revision = "cbe0f9307d0156177f9dd5dc85da1a31abc5f2fb" @@ -359,6 +336,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "983bf3264628787237fa4445a0aa430be028dbfb056756d99a30e583d14e8514" + inputs-digest = "c81145698e213e2c8f26c3d5b7e52033c4a2438cfb8eda51b6864770fac71fe4" solver-name = "gps-cdcl" solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml index 31785ab..99b9f5a 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -60,3 +60,11 @@ [prune] go-tests = true unused-packages = true + +[[constraint]] + name = "github.com/docker/docker" + revision = "e3831a62a3052472d7252049bc59835d5d7dc8bd" + +[[override]] + name = "github.com/docker/distribution" + branch = "master" diff --git a/constants/constants.go b/constants/constants.go index d1918e4..614262e 100644 --- a/constants/constants.go +++ b/constants/constants.go @@ -30,3 +30,6 @@ const JSONIndent = " " // DockerContainerPrefix is the prefix used for naming Docker containers. // It's also used to prefix the hostnames of the docker containers. const DockerContainerPrefix = "ptdl-" + +// WSMaxMessages is the maximum number of messages that are sent in one transfer. +const WSMaxMessages = 10 diff --git a/control/console_handler.go b/control/console_handler.go new file mode 100644 index 0000000..cc88ea3 --- /dev/null +++ b/control/console_handler.go @@ -0,0 +1,33 @@ +package control + +import ( + "io" + + "github.com/pterodactyl/wings/api/websockets" +) + +type ConsoleHandler struct { + Websockets *websockets.Collection + HandlerFunc *func(string) +} + +var _ io.Writer = ConsoleHandler{} + +func (c ConsoleHandler) Write(b []byte) (n int, e error) { + l := make([]byte, len(b)) + copy(l, b) + line := string(l) + m := websockets.Message{ + Type: websockets.MessageTypeConsole, + Payload: websockets.ConsolePayload{ + Line: line, + Level: websockets.ConsoleLevelPlain, + Source: websockets.ConsoleSourceServer, + }, + } + c.Websockets.Broadcast <- m + if c.HandlerFunc != nil { + (*c.HandlerFunc)(line) + } + return len(b), nil +} diff --git a/control/docker_environment.go b/control/docker_environment.go index 397606b..9c0b2b1 100644 --- a/control/docker_environment.go +++ b/control/docker_environment.go @@ -5,25 +5,21 @@ import ( "io" "os" "strings" + "time" + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/container" + "github.com/docker/docker/client" "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 + client *client.Client + hires types.HijackedResponse + attached bool server *ServerStruct } @@ -39,16 +35,20 @@ func NewDockerEnvironment(server *ServerStruct) (Environment, error) { env := dockerEnvironment{} env.server = server + env.attached = false + + cli, err := client.NewEnvClient() + env.client = cli + ctx := context.TODO() + cli.NegotiateAPIVersion(ctx) - 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 { + if err := env.inspectContainer(ctx); err != nil { log.WithError(err).Error("Failed to find the container with stored id, removing id.") env.server.DockerContainer.ID = "" env.server.Save() @@ -58,68 +58,28 @@ func NewDockerEnvironment(server *ServerStruct) (Environment, error) { 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) inspectContainer(ctx context.Context) error { + _, err := env.client.ContainerInspect(ctx, env.server.DockerContainer.ID) + return err } 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 + return nil } // 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") + ctx := context.TODO() + + if err := env.pullImage(ctx); err != nil { + log.WithError(err).WithField("image", env.server.GetService().DockerImage).WithField("server", env.server.ID).Error("Failed to pull docker image.") return err } @@ -129,27 +89,31 @@ func (env *dockerEnvironment) Create() error { // 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(), + + containerConfig := &container.Config{ + Image: env.server.GetService().DockerImage, + Cmd: strings.Split(env.server.StartupCommand, " "), + AttachStdin: true, + OpenStdin: true, + AttachStdout: true, + AttachStderr: true, + Tty: true, + Hostname: constants.DockerContainerPrefix + env.server.UUIDShort(), } - containerHostConfig := &docker.HostConfig{ - Memory: env.server.Settings.Memory, - MemorySwap: env.server.Settings.Swap, + + containerHostConfig := &container.HostConfig{ + Resources: container.Resources{ + 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) + + containerHostConfig.Memory = 0 + + container, err := env.client.ContainerCreate(ctx, containerConfig, containerHostConfig, nil, constants.DockerContainerPrefix+env.server.UUIDShort()) if err != nil { log.WithError(err).WithField("server", env.server.ID).Error("Failed to create docker container") return err @@ -157,11 +121,6 @@ func (env *dockerEnvironment) Create() error { 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 @@ -170,36 +129,29 @@ func (env *dockerEnvironment) Create() error { // 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 + + ctx := context.TODO() + + if err := env.inspectContainer(ctx); err != nil { + log.WithError(err).Debug("Container not found error") + log.WithField("server", env.server.ID).Debug("Container not found, docker environment destroyed already.") + return nil } - err := env.client.RemoveContainer(docker.RemoveContainerOptions{ - ID: env.server.DockerContainer.ID, - }) - if err != nil { + + if err := env.client.ContainerRemove(ctx, env.server.DockerContainer.ID, types.ContainerRemoveOptions{}); 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 + if err := env.inspectContainer(context.TODO()); err != nil { + return false } - env.checkContainerExists() - return env.container != nil + return true } // Start starts the environment's docker container @@ -208,7 +160,8 @@ func (env *dockerEnvironment) Start() error { 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 { + + if err := env.client.ContainerStart(context.TODO(), env.server.DockerContainer.ID, types.ContainerStartOptions{}); err != nil { log.WithError(err).Error("Failed to start docker container") return err } @@ -218,7 +171,10 @@ func (env *dockerEnvironment) Start() error { // 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 { + + // TODO: Decide after what timeout to kill the container, currently 10min + timeout := time.Minute * 10 + if err := env.client.ContainerStop(context.TODO(), env.server.DockerContainer.ID, &timeout); err != nil { log.WithError(err).Error("Failed to stop docker container") return err } @@ -227,9 +183,8 @@ func (env *dockerEnvironment) Stop() error { 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 { + + if err := env.client.ContainerKill(context.TODO(), env.server.DockerContainer.ID, "SIGKILL"); err != nil { log.WithError(err).Error("Failed to kill docker container") return err } @@ -238,7 +193,24 @@ func (env *dockerEnvironment) Kill() error { // 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")) + //log.Debug("Command: " + command) + //_, err := env.containerInput.Write([]byte(command + "\n")) + //return err + return nil +} + +func (env *dockerEnvironment) pullImage(ctx context.Context) error { + // 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 + log.WithField("image", env.server.GetService().DockerImage).Debug("Pulling docker image") + + rc, err := env.client.ImagePull(ctx, env.server.GetService().DockerImage, types.ImagePullOptions{}) + defer rc.Close() return err } diff --git a/control/docker_environment_test.go b/control/docker_environment_test.go index 4564ac3..9d8f438 100644 --- a/control/docker_environment_test.go +++ b/control/docker_environment_test.go @@ -1,133 +1,125 @@ package control -import ( - "fmt" - "testing" +// func testServer() *ServerStruct { +// return &ServerStruct{ +// ID: "testuuid-something-something", +// service: &service{ +// DockerImage: "alpine:latest", +// }, +// } +// } - docker "github.com/fsouza/go-dockerclient" - "github.com/stretchr/testify/assert" -) +// func TestNewDockerEnvironment(t *testing.T) { +// env, err := createTestDockerEnv(nil) -func testServer() *ServerStruct { - return &ServerStruct{ - ID: "testuuid-something-something", - service: &service{ - DockerImage: "alpine:latest", - }, - } -} +// assert.Nil(t, err) +// assert.NotNil(t, env) +// assert.NotNil(t, env.client) +// } -func TestNewDockerEnvironment(t *testing.T) { - env, err := createTestDockerEnv(nil) +// func TestNewDockerEnvironmentExisting(t *testing.T) { +// eenv, _ := createTestDockerEnv(nil) +// eenv.Create() - assert.Nil(t, err) - assert.NotNil(t, env) - assert.NotNil(t, env.client) -} +// env, err := createTestDockerEnv(eenv.server) -func TestNewDockerEnvironmentExisting(t *testing.T) { - eenv, _ := createTestDockerEnv(nil) - eenv.Create() +// assert.Nil(t, err) +// assert.NotNil(t, env) +// assert.NotNil(t, env.container) - env, err := createTestDockerEnv(eenv.server) +// eenv.Destroy() +// } - assert.Nil(t, err) - assert.NotNil(t, env) - assert.NotNil(t, env.container) +// func TestCreateDockerEnvironment(t *testing.T) { +// env, _ := createTestDockerEnv(nil) - eenv.Destroy() -} +// err := env.Create() -func TestCreateDockerEnvironment(t *testing.T) { - env, _ := createTestDockerEnv(nil) +// a := assert.New(t) +// a.Nil(err) +// a.NotNil(env.container) +// a.Equal(env.container.Name, "ptdl_testuuid") - err := env.Create() +// if err := env.client.RemoveContainer(docker.RemoveContainerOptions{ +// ID: env.container.ID, +// }); err != nil { +// fmt.Println(err) +// } +// } - a := assert.New(t) - a.Nil(err) - a.NotNil(env.container) - a.Equal(env.container.Name, "ptdl_testuuid") +// func TestDestroyDockerEnvironment(t *testing.T) { +// env, _ := createTestDockerEnv(nil) +// env.Create() - if err := env.client.RemoveContainer(docker.RemoveContainerOptions{ - ID: env.container.ID, - }); err != nil { - fmt.Println(err) - } -} +// err := env.Destroy() -func TestDestroyDockerEnvironment(t *testing.T) { - env, _ := createTestDockerEnv(nil) - env.Create() +// _, ierr := env.client.InspectContainer(env.container.ID) - err := env.Destroy() +// assert.Nil(t, err) +// assert.IsType(t, ierr, &docker.NoSuchContainer{}) +// } - _, ierr := env.client.InspectContainer(env.container.ID) +// func TestStartDockerEnvironment(t *testing.T) { +// env, _ := createTestDockerEnv(nil) +// env.Create() +// err := env.Start() - assert.Nil(t, err) - assert.IsType(t, ierr, &docker.NoSuchContainer{}) -} +// i, ierr := env.client.InspectContainer(env.container.ID) -func TestStartDockerEnvironment(t *testing.T) { - env, _ := createTestDockerEnv(nil) - env.Create() - err := env.Start() +// assert.Nil(t, err) +// assert.Nil(t, ierr) +// assert.True(t, i.State.Running) - i, ierr := env.client.InspectContainer(env.container.ID) +// env.client.KillContainer(docker.KillContainerOptions{ +// ID: env.container.ID, +// }) +// env.Destroy() +// } - assert.Nil(t, err) - assert.Nil(t, ierr) - assert.True(t, i.State.Running) +// func TestStopDockerEnvironment(t *testing.T) { +// env, _ := createTestDockerEnv(nil) +// env.Create() +// env.Start() +// err := env.Stop() - env.client.KillContainer(docker.KillContainerOptions{ - ID: env.container.ID, - }) - env.Destroy() -} +// i, ierr := env.client.InspectContainer(env.container.ID) -func TestStopDockerEnvironment(t *testing.T) { - env, _ := createTestDockerEnv(nil) - env.Create() - env.Start() - err := env.Stop() +// assert.Nil(t, err) +// assert.Nil(t, ierr) +// assert.False(t, i.State.Running) - i, ierr := env.client.InspectContainer(env.container.ID) +// env.client.KillContainer(docker.KillContainerOptions{ +// ID: env.container.ID, +// }) +// env.Destroy() +// } - assert.Nil(t, err) - assert.Nil(t, ierr) - assert.False(t, i.State.Running) +// func TestKillDockerEnvironment(t *testing.T) { +// env, _ := createTestDockerEnv(nil) +// env.Create() +// env.Start() +// err := env.Kill() - env.client.KillContainer(docker.KillContainerOptions{ - ID: env.container.ID, - }) - env.Destroy() -} +// i, ierr := env.client.InspectContainer(env.container.ID) -func TestKillDockerEnvironment(t *testing.T) { - env, _ := createTestDockerEnv(nil) - env.Create() - env.Start() - err := env.Kill() +// assert.Nil(t, err) +// assert.Nil(t, ierr) +// assert.False(t, i.State.Running) - i, ierr := env.client.InspectContainer(env.container.ID) +// env.client.KillContainer(docker.KillContainerOptions{ +// ID: env.container.ID, +// }) +// env.Destroy() +// } - assert.Nil(t, err) - assert.Nil(t, ierr) - assert.False(t, i.State.Running) +// func TestExecDockerEnvironment(t *testing.T) { - env.client.KillContainer(docker.KillContainerOptions{ - ID: env.container.ID, - }) - env.Destroy() -} +// } -func TestExecDockerEnvironment(t *testing.T) { - -} - -func createTestDockerEnv(s *ServerStruct) (*dockerEnvironment, error) { - if s == nil { - s = testServer() - } - env, err := NewDockerEnvironment(s) - return env.(*dockerEnvironment), err -} +// func createTestDockerEnv(s *ServerStruct) (*dockerEnvironment, error) { +// if s == nil { +// s = testServer() +// } +// env, err := NewDockerEnvironment(s) +// return env.(*dockerEnvironment), err +// } diff --git a/wings-api.paw b/wings-api.paw new file mode 100644 index 0000000000000000000000000000000000000000..78945495cc7d01c72af52ac24589d082fb9d8c46 GIT binary patch literal 16154 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 C7wM`1 literal 0 HcmV?d00001