From 636e75ae1f4a6962290db69b9a36ddc7afa242e1 Mon Sep 17 00:00:00 2001 From: Jakob Schrettenbrunner Date: Sun, 30 Jul 2017 20:05:06 +0200 Subject: [PATCH] first implementation of docker environment --- control/docker_environment.go | 157 ++++++++++++++++++++++++++++ control/docker_environment_test.go | 133 +++++++++++++++++++++++ control/environment.go | 72 +++++++++++++ control/environments/docker.go | 21 ---- control/environments/environment.go | 50 --------- control/server.go | 140 ++++++++++++++++++++++--- control/service.go | 9 +- glide.lock | 86 ++++++++++++++- glide.yaml | 12 +++ 9 files changed, 590 insertions(+), 90 deletions(-) create mode 100644 control/docker_environment.go create mode 100644 control/docker_environment_test.go create mode 100644 control/environment.go delete mode 100644 control/environments/docker.go delete mode 100644 control/environments/environment.go diff --git a/control/docker_environment.go b/control/docker_environment.go new file mode 100644 index 0000000..c139392 --- /dev/null +++ b/control/docker_environment.go @@ -0,0 +1,157 @@ +package control + +import ( + "context" + "strings" + + "github.com/fsouza/go-dockerclient" + log "github.com/sirupsen/logrus" +) + +type dockerEnvironment struct { + baseEnvironment + + client *docker.Client + container *docker.Container + context context.Context + + server *server +} + +// 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 *server) (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.reattach(); err != nil { + log.WithError(err).Error("Failed to reattach to existing container.") + return nil, err + } + } + + return &env, nil +} + +func (env *dockerEnvironment) reattach() error { + container, err := env.client.InspectContainer(env.server.DockerContainer.ID) + if err != nil { + return err + } + env.container = container + return nil +} + +// Create creates the docker container for the environment and applies all +// settings to it +func (env *dockerEnvironment) Create() error { + log.WithField("serverID", env.server.UUID).Debug("Creating docker environment") + // Split image repository and tag to feed it to the library + imageParts := strings.Split(env.server.Service().DockerImage, ":") + imageRepoParts := strings.Split(imageParts[0], "/") + if len(imageRepoParts) >= 3 { + // 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.service.DockerImage).Debug("Pulling docker image") + err := env.client.PullImage(pullImageOpts, docker.AuthConfiguration{}) + if err != nil { + log.WithError(err).WithField("serverID", env.server.UUID).Error("Failed to create docker environment") + return err + } + + // Create docker container + // TODO: apply cpu, io, disk limits. + containerConfig := &docker.Config{ + Image: env.server.Service().DockerImage, + } + containerHostConfig := &docker.HostConfig{ + Memory: env.server.Settings.Memory, + MemorySwap: env.server.Settings.Swap, + } + createContainerOpts := docker.CreateContainerOptions{ + Name: "ptdl_" + env.server.UUIDShort(), + Config: containerConfig, + HostConfig: containerHostConfig, + Context: env.context, + } + container, err := env.client.CreateContainer(createContainerOpts) + if err != nil { + log.WithError(err).WithField("serverID", env.server.UUID).Error("Failed to create docker container") + return err + } + env.server.DockerContainer.ID = container.ID + env.container = container + + return nil +} + +// Destroy removes the environment's docker container +func (env *dockerEnvironment) Destroy() error { + log.WithField("serverID", env.server.UUID).Debug("Destroying docker environment") + err := env.client.RemoveContainer(docker.RemoveContainerOptions{ + ID: env.server.DockerContainer.ID, + }) + if err != nil { + log.WithError(err).WithField("serverID", env.server.UUID).Error("Failed to destroy docker environment") + return err + } + return nil +} + +// Start starts the environment's docker container +func (env *dockerEnvironment) Start() error { + log.WithField("serverID", env.server.UUID).Debug("Starting service in docker environment") + 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("serverID", env.server.UUID).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("serverID", env.server.UUID).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 { + return nil +} diff --git a/control/docker_environment_test.go b/control/docker_environment_test.go new file mode 100644 index 0000000..8bb2090 --- /dev/null +++ b/control/docker_environment_test.go @@ -0,0 +1,133 @@ +package control + +import ( + "fmt" + "testing" + + docker "github.com/fsouza/go-dockerclient" + "github.com/stretchr/testify/assert" +) + +func testServer() *server { + return &server{ + UUID: "testuuid-something-something", + service: &service{ + DockerImage: "alpine:latest", + }, + } +} + +func TestNewDockerEnvironment(t *testing.T) { + env, err := createTestDockerEnv(nil) + + assert.Nil(t, err) + assert.NotNil(t, env) + assert.NotNil(t, env.client) +} + +func TestNewDockerEnvironmentExisting(t *testing.T) { + eenv, _ := createTestDockerEnv(nil) + eenv.Create() + + env, err := createTestDockerEnv(eenv.server) + + assert.Nil(t, err) + assert.NotNil(t, env) + assert.NotNil(t, env.container) + + eenv.Destroy() +} + +func TestCreateDockerEnvironment(t *testing.T) { + env, _ := createTestDockerEnv(nil) + + err := env.Create() + + a := assert.New(t) + a.Nil(err) + a.NotNil(env.container) + a.Equal(env.container.Name, "ptdl_testuuid") + + if err := env.client.RemoveContainer(docker.RemoveContainerOptions{ + ID: env.container.ID, + }); err != nil { + fmt.Println(err) + } +} + +func TestDestroyDockerEnvironment(t *testing.T) { + env, _ := createTestDockerEnv(nil) + env.Create() + + err := env.Destroy() + + _, ierr := env.client.InspectContainer(env.container.ID) + + assert.Nil(t, err) + assert.IsType(t, ierr, &docker.NoSuchContainer{}) +} + +func TestStartDockerEnvironment(t *testing.T) { + env, _ := createTestDockerEnv(nil) + env.Create() + err := env.Start() + + i, ierr := env.client.InspectContainer(env.container.ID) + + assert.Nil(t, err) + assert.Nil(t, ierr) + assert.True(t, i.State.Running) + + 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() + + i, ierr := env.client.InspectContainer(env.container.ID) + + assert.Nil(t, err) + assert.Nil(t, ierr) + assert.False(t, i.State.Running) + + 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() + + i, ierr := env.client.InspectContainer(env.container.ID) + + assert.Nil(t, err) + assert.Nil(t, ierr) + assert.False(t, i.State.Running) + + env.client.KillContainer(docker.KillContainerOptions{ + ID: env.container.ID, + }) + env.Destroy() +} + +func TestExecDockerEnvironment(t *testing.T) { + +} + +func createTestDockerEnv(s *server) (*dockerEnvironment, error) { + if s == nil { + s = testServer() + } + env, err := NewDockerEnvironment(s) + return env.(*dockerEnvironment), err +} diff --git a/control/environment.go b/control/environment.go new file mode 100644 index 0000000..061bdeb --- /dev/null +++ b/control/environment.go @@ -0,0 +1,72 @@ +package control + +// Environment provides abstraction of different environments +type Environment interface { + // Create creates the environment + Create() error + + // Destroy destroys the environment + Destroy() error + + // Start starts the service in the environment + Start() error + + // Stop stops the service in the environment + Stop() error + + // Kill kills the service in the environment + Kill() error + + // Execute a command in the environment + // This sends the command to the standard input of the environment + Exec(command string) error + + // Exists checks wether the Environment exists or not + Exists() bool + + // ReCreate recreates the environment by first Destroying and then Creating + ReCreate() error +} + +type baseEnvironment struct { +} + +// Ensure BaseEnvironment implements Environment +var _ Environment = &baseEnvironment{} + +func (env *baseEnvironment) Create() error { + return nil +} + +func (env *baseEnvironment) Destroy() error { + return nil +} + +func (env *baseEnvironment) Start() error { + return nil +} + +func (env *baseEnvironment) Stop() error { + return nil +} + +func (env *baseEnvironment) Kill() error { + return nil +} + +func (env *baseEnvironment) Exists() bool { + return false +} + +func (env *baseEnvironment) ReCreate() error { + if env.Exists() { + if err := env.Destroy(); err != nil { + return err + } + } + return env.Create() +} + +func (env *baseEnvironment) Exec(command string) error { + return nil +} diff --git a/control/environments/docker.go b/control/environments/docker.go deleted file mode 100644 index 25c4694..0000000 --- a/control/environments/docker.go +++ /dev/null @@ -1,21 +0,0 @@ -package environments - -type DockerEnvironment struct { - BaseEnvironment -} - -// Ensure DockerEnvironment implements Environment -var _ Environment = &DockerEnvironment{} - -func NewDockerEnvironment() *DockerEnvironment { - return &DockerEnvironment{} -} - -func (env *DockerEnvironment) Exec() error { - return nil -} - -func (env *DockerEnvironment) Create() error { - - return nil -} diff --git a/control/environments/environment.go b/control/environments/environment.go deleted file mode 100644 index 3beff97..0000000 --- a/control/environments/environment.go +++ /dev/null @@ -1,50 +0,0 @@ -package environments - -// Environment provides abstraction of different environments -type Environment interface { - // Execute a command in the environment - Exec() error - - // Create creates the environment - Create() error - - // Destroy destroys the environment - Destroy() error - - // Exists checks wether the Environment exists or not - Exists() bool - - // ReCreate recreates the environment by first Destroying and then Creating - ReCreate() error -} - -type BaseEnvironment struct { -} - -// Ensure BaseEnvironment implements Environment -var _ Environment = &BaseEnvironment{} - -func (env *BaseEnvironment) Create() error { - return nil -} - -func (env *BaseEnvironment) Destroy() error { - return nil -} - -func (env *BaseEnvironment) Exists() bool { - return false -} - -func (env *BaseEnvironment) ReCreate() error { - if env.Exists() { - if err := env.Destroy(); err != nil { - return err - } - } - return env.Create() -} - -func (env *BaseEnvironment) Exec() error { - return nil -} diff --git a/control/server.go b/control/server.go index 5f2678a..c0b553f 100644 --- a/control/server.go +++ b/control/server.go @@ -2,26 +2,79 @@ package control import ( "encoding/json" + "errors" "fmt" "io/ioutil" + "strings" + + log "github.com/sirupsen/logrus" ) +// Server is a Server +type Server interface { + Start() error + Stop() error + Exec(command string) error + Rebuild() error +} + // Server is a single instance of a Service managed by the panel -type Server struct { +type server struct { // UUID is the unique identifier of the server UUID string `json:"uuid"` - // Service is the service the server is an instance of - Service *Service `json:"-"` - // ServiceName is the name of the service. It is mainly used to allow storing the service // in the config ServiceName string `json:"serviceName"` + service *service + environment Environment + // StartupCommand is the command executed in the environment to start the server + StartupCommand string `json:"startupCommand"` + + // DockerContainer holds information regarding the docker container when the server + // is running in a docker environment + DockerContainer dockerContainer `json:"dockerContainer"` + + // EnvironmentVariables are set in the Environment the server is running in + EnvironmentVariables map[string]string `json:"env"` + + // Allocations contains the ports and ip addresses assigned to the server + Allocations allocations `json:"allocation"` + + // Settings are the environment settings and limitations for the server + Settings settings `json:"settings"` + + // Keys are some auth keys we will hopefully replace by something better. Keys map[string][]string `json:"keys"` } -var servers map[string]*Server +type allocations struct { + Ports []int16 `json:"ports"` + MainIP string `json:"ip"` + MainPort int16 `json:"port"` +} + +type settings struct { + Memory int64 `json:"memory"` + Swap int64 `json:"swap"` + IO int64 `json:"io"` + CPU int16 `json:"cpu"` + Disk int64 `json:"disk"` + Image string `json:"image"` + User string `json:"user"` + UserID int16 `json:"userID"` +} + +type dockerContainer struct { + ID string `json:"id"` + Image string `json:"image"` +} + +// ensure server implements Server +var _ Server = &server{} + +var servers map[string]*server // LoadServerConfigurations loads the configured servers from a specified path func LoadServerConfigurations(path string) error { @@ -29,7 +82,7 @@ func LoadServerConfigurations(path string) error { if err != nil { return err } - servers = make(map[string]*Server) + servers = make(map[string]*server) for _, file := range serverFiles { if !file.IsDir() { @@ -44,14 +97,14 @@ func LoadServerConfigurations(path string) error { return nil } -func loadServerConfiguration(path string) (*Server, error) { +func loadServerConfiguration(path string) (*server, error) { file, err := ioutil.ReadFile(path) if err != nil { return nil, err } - server := NewServer() + server := &server{} if err := json.Unmarshal(file, server); err != nil { return nil, err } @@ -60,17 +113,80 @@ func loadServerConfiguration(path string) (*Server, error) { } // GetServer returns the server identified by the provided uuid -func GetServer(uuid string) *Server { +func GetServer(uuid string) Server { return servers[uuid] } // NewServer creates a new Server -func NewServer() *Server { - return new(Server) +func NewServer() Server { + return new(server) +} + +func (s *server) Start() error { + /*if err := s.Environment().Create(); err != nil { + return err + } + if err := s.Environment().Start(); err != nil { + return err + }*/ + return nil +} + +func (s *server) Stop() error { + /*if err := s.Environment().Stop(); err != nil { + return err + }*/ + return nil +} + +func (s *server) Exec(command string) error { + /*if err := s.Environment().Exec(command); err != nil { + return err + }*/ + return nil +} + +func (s *server) Rebuild() error { + /*if err := s.Environment().ReCreate(); err != nil { + return err + }*/ + return nil +} + +// Service returns the server's service configuration +func (s *server) Service() *service { + if s.service == nil { + // TODO: Properly use the correct service, mock for now. + s.service = &service{ + DockerImage: "quay.io/pterodactyl/core:java", + EnvironmentName: "docker", + } + } + return s.service +} + +// UUIDShort returns the first block of the UUID +func (s *server) UUIDShort() string { + return s.UUID[0:strings.Index(s.UUID, "-")] +} + +// Environment returns the servers environment +func (s *server) Environment() (Environment, error) { + var err error + if s.environment == nil { + switch s.Service().EnvironmentName { + case "docker": + s.environment, err = NewDockerEnvironment(s) + default: + log.WithField("service", s.ServiceName).Error("Invalid environment name") + return nil, errors.New("Invalid environment name") + } + } + return s.environment, err } // HasPermission checks wether a provided token has a specific permission -func (s *Server) HasPermission(token string, permission string) bool { +func (s *server) HasPermission(token string, permission string) bool { for key, perms := range s.Keys { if key == token { for _, perm := range perms { diff --git a/control/service.go b/control/service.go index 8a65632..e4eb90c 100644 --- a/control/service.go +++ b/control/service.go @@ -1,7 +1,10 @@ package control -import "github.com/Pterodactyl/wings/control/environments" +type service struct { + server *Server -type Service struct { - Environment environments.Environment + // EnvironmentName is the name of the environment used by the service + EnvironmentName string `json:"environmentName"` + + DockerImage string `json:"dockerImage"` } diff --git a/glide.lock b/glide.lock index 2d1c2f5..58f8bba 100644 --- a/glide.lock +++ b/glide.lock @@ -1,19 +1,65 @@ -hash: 59d5a95695f3a40439b123ab6c4eac97e6454f594f89df61f5b93a7d467b86c4 -updated: 2017-07-06T20:22:40.308152778+02:00 +hash: e20502ccc280db349be5bb38269606eb6b84533b70ff81d7735ecc0e03256ead +updated: 2017-07-24T20:38:57.261980868+02:00 imports: - name: bitbucket.org/tebeka/strftime version: 2194253a23c090a4d5953b152a3c63fb5da4f5a4 +- name: github.com/Azure/go-ansiterm + version: 19f72df4d05d31cbe1c56bfc8045c96babff6c7e + subpackages: + - winterm +- name: github.com/docker/docker + version: 90d35abf7b3535c1c319c872900fbd76374e521c + subpackages: + - api/types + - api/types/blkiodev + - api/types/container + - api/types/filters + - api/types/mount + - api/types/network + - api/types/registry + - api/types/strslice + - api/types/swarm + - api/types/versions + - opts + - pkg/archive + - pkg/fileutils + - pkg/homedir + - pkg/idtools + - pkg/ioutils + - pkg/jsonlog + - pkg/jsonmessage + - pkg/longpath + - pkg/pools + - pkg/promise + - pkg/stdcopy + - pkg/system + - pkg/term + - pkg/term/windows +- name: github.com/docker/go-connections + version: 3ede32e2033de7505e6500d6c868c2b9ed9f169d + subpackages: + - nat +- name: github.com/docker/go-units + version: 0dadbb0345b35ec7ef35e228dabb8de89a65bf52 - name: github.com/fsnotify/fsnotify version: 4da3e2cfbabc9f751898f250b49f2439785783a1 +- name: github.com/fsouza/go-dockerclient + version: 4df4873b288c855e4186534280c3a3a1af403e67 - name: github.com/gin-gonic/gin version: e2212d40c62a98b388a5eb48ecbdcf88534688ba subpackages: - binding - render +- name: github.com/go-ole/go-ole + version: 085abb85892dc1949567b726dff00fa226c60c45 + subpackages: + - oleutil - name: github.com/golang/protobuf version: 2402d76f3d41f928c7902a765dfc872356dd3aad subpackages: - proto +- name: github.com/hashicorp/go-cleanhttp + version: 3573b8b52aa7b37b9358d966a898feb387f62437 - name: github.com/hashicorp/hcl version: 392dba7d905ed5d04a5794ba89f558b27e2ba1ca subpackages: @@ -35,16 +81,44 @@ imports: version: ee05b128a739a0fb76c7ebd3ae4810c1de808d6d - name: github.com/mattn/go-isatty version: fc9e8d8ef48496124e79ae0df75490096eccf6fe +- name: github.com/Microsoft/go-winio + version: c4dc1301f1dc0307acd38e611aa375a64dfe0642 - name: github.com/mitchellh/mapstructure version: d0303fe809921458f417bcf828397a65db30a7e4 +- name: github.com/moby/moby + version: 90d35abf7b3535c1c319c872900fbd76374e521c + subpackages: + - client +- name: github.com/Nvveen/Gotty + version: cd527374f1e5bff4938207604a14f2e38a9cf512 +- name: github.com/opencontainers/runc + version: 6ca8b741bb67839b7170d96257dde5c246f8b784 + subpackages: + - libcontainer/system + - libcontainer/user - name: github.com/pelletier/go-buffruneio version: c37440a7cf42ac63b919c752ca73a85067e05992 - name: github.com/pelletier/go-toml version: 4a000a21a414d139727f616a8bb97f847b1b310b - name: github.com/rifflock/lfshook version: 6844c808343cb8fa357d7f141b1b990e05d24e41 +- name: github.com/shirou/gopsutil + version: aa0a3bce9d1f4efc710ed812f19f77851da2eedd + subpackages: + - cpu + - host + - internal/common + - mem + - net + - process +- name: github.com/shirou/w32 + version: bb4de0191aa41b5507caa14b0650cdbddcd9280b - name: github.com/sirupsen/logrus - version: 202f25545ea4cf9b191ff7f846df5d87c9382c2b + version: a3f95b5c423586578a4e099b11a46c2479628cac +- name: github.com/Sirupsen/logrus + version: a3f95b5c423586578a4e099b11a46c2479628cac + repo: https://github.com/sirupsen/logrus.git + vcs: git - name: github.com/spf13/afero version: 9be650865eab0c12963d8753212f4f9c66cdcf12 subpackages: @@ -52,21 +126,25 @@ imports: - name: github.com/spf13/cast version: acbeb36b902d72a7a4c18e8f3241075e7ab763e4 - name: github.com/spf13/cobra - version: 8c6fa02d2225de0f9bdcb7ca912556f68d172d8c + version: 90fc11bbc0a789c29272c21b5ff9e93db183f8dc - name: github.com/spf13/jwalterweatherman version: 0efa5202c04663c757d84f90f5219c1250baf94f - name: github.com/spf13/pflag version: e57e3eeb33f795204c1ca35f56c44f83227c6e66 - name: github.com/spf13/viper version: c1de95864d73a5465492829d7cb2dd422b19ac96 +- name: github.com/StackExchange/wmi + version: ea383cf3ba6ec950874b8486cd72356d007c768f - name: golang.org/x/net version: f315505cf3349909cdf013ea56690da34e96a451 subpackages: - context + - context/ctxhttp - name: golang.org/x/sys version: f7928cfef4d09d1b080aa2b6fd3ca9ba1567c733 subpackages: - unix + - windows - name: golang.org/x/text version: 5a2c30c33799f1e813f7f3259000d594a5ed493a subpackages: diff --git a/glide.yaml b/glide.yaml index 57d3f38..2d49599 100644 --- a/glide.yaml +++ b/glide.yaml @@ -8,8 +8,20 @@ import: version: ~1.7.0 - package: github.com/sirupsen/logrus version: ~1.0.0 +- package: github.com/Sirupsen/logrus + version: ~1.0.0 + repo: https://github.com/sirupsen/logrus.git + vcs: git - package: github.com/spf13/viper - package: github.com/spf13/cobra +- package: github.com/shirou/gopsutil + version: ^2.17.6 +- package: github.com/moby/moby + version: ~17.5.0-ce-rc3 + subpackages: + - client +- package: github.com/fsouza/go-dockerclient +- package: github.com/hashicorp/go-cleanhttp testImport: - package: github.com/stretchr/testify version: ~1.1.4