From 5bba1c0d9015f77026a7af256f4d6fd5756ca288 Mon Sep 17 00:00:00 2001 From: Jakob Schrettenbrunner Date: Wed, 14 Mar 2018 13:59:57 +0100 Subject: [PATCH] readd docker environment tests implement Exec() --- control/environment_docker.go | 28 +++-- control/environment_docker_test.go | 196 +++++++++++++++-------------- control/server_persistance.go | 7 +- wings-api.paw | Bin 16154 -> 18283 bytes 4 files changed, 123 insertions(+), 108 deletions(-) diff --git a/control/environment_docker.go b/control/environment_docker.go index 08b1bcb..1d7daec 100644 --- a/control/environment_docker.go +++ b/control/environment_docker.go @@ -199,8 +199,8 @@ func (env *dockerEnvironment) Start() error { func (env *dockerEnvironment) Stop() error { log.WithField("server", env.server.ID).Debug("Stopping service in docker environment") - // TODO: Decide after what timeout to kill the container, currently 10min - timeout := time.Minute * 10 + // TODO: Decide after what timeout to kill the container, currently 30 seconds + timeout := 30 * time.Second 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 @@ -211,7 +211,7 @@ 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.ContainerKill(context.TODO(), env.server.DockerContainer.ID, "SIGKILL"); err != nil { + if err := env.client.ContainerKill(context.TODO(), env.server.DockerContainer.ID, "KILL"); err != nil { log.WithError(err).Error("Failed to kill docker container") return err } @@ -220,24 +220,26 @@ 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")) - //return err - return nil + log.Debug("Command: " + command) + _, err := env.hires.Conn.Write([]byte(command + "\n")) + return err } 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 - } + //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{}) + if err != nil { + return err + } defer rc.Close() - return err + return nil } diff --git a/control/environment_docker_test.go b/control/environment_docker_test.go index 9d8f438..68aeb10 100644 --- a/control/environment_docker_test.go +++ b/control/environment_docker_test.go @@ -1,125 +1,133 @@ package control -// func testServer() *ServerStruct { -// return &ServerStruct{ -// ID: "testuuid-something-something", -// service: &service{ -// DockerImage: "alpine:latest", -// }, -// } -// } +import ( + "context" + "fmt" + "testing" -// func TestNewDockerEnvironment(t *testing.T) { -// env, err := createTestDockerEnv(nil) + "github.com/pterodactyl/wings/api/websockets" -// assert.Nil(t, err) -// assert.NotNil(t, env) -// assert.NotNil(t, env.client) -// } + "github.com/docker/docker/api/types" + "github.com/docker/docker/client" + "github.com/pterodactyl/wings/config" + "github.com/spf13/viper" + "github.com/stretchr/testify/assert" +) -// func TestNewDockerEnvironmentExisting(t *testing.T) { -// eenv, _ := createTestDockerEnv(nil) -// eenv.Create() +func testServer() *ServerStruct { + viper.SetDefault(config.DataPath, "./data") + return &ServerStruct{ + ID: "testuuid-something-something", + Service: &Service{ + DockerImage: "alpine:latest", + }, + StartupCommand: "/bin/ash echo hello && sleep 100", + websockets: websockets.NewCollection(), + } +} -// env, err := createTestDockerEnv(eenv.server) +func TestNewDockerEnvironment(t *testing.T) { + env, err := createTestDockerEnv(nil) -// assert.Nil(t, err) -// assert.NotNil(t, env) -// assert.NotNil(t, env.container) + assert.Nil(t, err) + assert.NotNil(t, env) + assert.NotNil(t, env.client) +} -// eenv.Destroy() -// } +func TestNewDockerEnvironmentExisting(t *testing.T) { + eenv, _ := createTestDockerEnv(nil) + eenv.Create() -// func TestCreateDockerEnvironment(t *testing.T) { -// env, _ := createTestDockerEnv(nil) + env, err := createTestDockerEnv(eenv.server) -// err := env.Create() + assert.Nil(t, err) + assert.NotNil(t, env) + assert.NotNil(t, env.server.DockerContainer) -// a := assert.New(t) -// a.Nil(err) -// a.NotNil(env.container) -// a.Equal(env.container.Name, "ptdl_testuuid") + eenv.Destroy() +} -// if err := env.client.RemoveContainer(docker.RemoveContainerOptions{ -// ID: env.container.ID, -// }); err != nil { -// fmt.Println(err) -// } -// } +func TestCreateDockerEnvironment(t *testing.T) { + env, _ := createTestDockerEnv(nil) -// func TestDestroyDockerEnvironment(t *testing.T) { -// env, _ := createTestDockerEnv(nil) -// env.Create() + err := env.Create() -// err := env.Destroy() + a := assert.New(t) + a.Nil(err) + a.NotNil(env.server.DockerContainer.ID) -// _, ierr := env.client.InspectContainer(env.container.ID) + if err := env.client.ContainerRemove(context.TODO(), env.server.DockerContainer.ID, types.ContainerRemoveOptions{}); err != nil { + fmt.Println(err) + } +} -// assert.Nil(t, err) -// assert.IsType(t, ierr, &docker.NoSuchContainer{}) -// } +func TestDestroyDockerEnvironment(t *testing.T) { + env, _ := createTestDockerEnv(nil) + env.Create() -// func TestStartDockerEnvironment(t *testing.T) { -// env, _ := createTestDockerEnv(nil) -// env.Create() -// err := env.Start() + err := env.Destroy() -// i, ierr := env.client.InspectContainer(env.container.ID) + _, ierr := env.client.ContainerInspect(context.TODO(), env.server.DockerContainer.ID) -// assert.Nil(t, err) -// assert.Nil(t, ierr) -// assert.True(t, i.State.Running) + assert.Nil(t, err) + assert.True(t, client.IsErrNotFound(ierr)) +} -// env.client.KillContainer(docker.KillContainerOptions{ -// ID: env.container.ID, -// }) -// env.Destroy() -// } +func TestStartDockerEnvironment(t *testing.T) { + env, _ := createTestDockerEnv(nil) + env.Create() + err := env.Start() -// func TestStopDockerEnvironment(t *testing.T) { -// env, _ := createTestDockerEnv(nil) -// env.Create() -// env.Start() -// err := env.Stop() + i, ierr := env.client.ContainerInspect(context.TODO(), env.server.DockerContainer.ID) -// i, ierr := env.client.InspectContainer(env.container.ID) + assert.Nil(t, err) + assert.Nil(t, ierr) + assert.True(t, i.State.Running) -// assert.Nil(t, err) -// assert.Nil(t, ierr) -// assert.False(t, i.State.Running) + env.client.ContainerKill(context.TODO(), env.server.DockerContainer.ID, "KILL") + env.Destroy() +} -// env.client.KillContainer(docker.KillContainerOptions{ -// ID: env.container.ID, -// }) -// env.Destroy() -// } +func TestStopDockerEnvironment(t *testing.T) { + env, _ := createTestDockerEnv(nil) + env.Create() + env.Start() + err := env.Stop() -// func TestKillDockerEnvironment(t *testing.T) { -// env, _ := createTestDockerEnv(nil) -// env.Create() -// env.Start() -// err := env.Kill() + i, ierr := env.client.ContainerInspect(context.TODO(), env.server.DockerContainer.ID) -// i, ierr := env.client.InspectContainer(env.container.ID) + assert.Nil(t, err) + assert.Nil(t, ierr) + assert.False(t, i.State.Running) -// assert.Nil(t, err) -// assert.Nil(t, ierr) -// assert.False(t, i.State.Running) + env.client.ContainerKill(context.TODO(), env.server.DockerContainer.ID, "KILL") + env.Destroy() +} -// env.client.KillContainer(docker.KillContainerOptions{ -// ID: env.container.ID, -// }) -// env.Destroy() -// } +func TestKillDockerEnvironment(t *testing.T) { + env, _ := createTestDockerEnv(nil) + env.Create() + env.Start() + err := env.Kill() -// func TestExecDockerEnvironment(t *testing.T) { + i, ierr := env.client.ContainerInspect(context.TODO(), env.server.DockerContainer.ID) -// } + assert.Nil(t, err) + assert.Nil(t, ierr) + assert.False(t, i.State.Running) -// func createTestDockerEnv(s *ServerStruct) (*dockerEnvironment, error) { -// if s == nil { -// s = testServer() -// } -// env, err := NewDockerEnvironment(s) -// return env.(*dockerEnvironment), err -// } + env.client.ContainerKill(context.TODO(), env.server.DockerContainer.ID, "KILL") + 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 +} diff --git a/control/server_persistance.go b/control/server_persistance.go index 98c93bd..14c2520 100644 --- a/control/server_persistance.go +++ b/control/server_persistance.go @@ -91,7 +91,12 @@ func (s *ServerStruct) Save() error { } func (s *ServerStruct) path() string { - return filepath.Join(viper.GetString(config.DataPath), constants.ServersPath, s.ID) + p, err := filepath.Abs(viper.GetString(config.DataPath)) + if err != nil { + log.WithError(err).WithField("server", s.ID).Error("Failed to get absolute data path for server.") + p = viper.GetString(config.DataPath) + } + return filepath.Join(p, constants.ServersPath, s.ID) } func (s *ServerStruct) dataPath() string { diff --git a/wings-api.paw b/wings-api.paw index 78945495cc7d01c72af52ac24589d082fb9d8c46..0df8451d2e35917e905d70e0954081b69731de63 100644 GIT binary patch delta 8726 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 CHY6wj