diff --git a/config/keys.go b/config/keys.go index fcf6304..a7076da 100644 --- a/config/keys.go +++ b/config/keys.go @@ -6,7 +6,7 @@ const ( // DataPath is a string containing the path where data should // be stored on the system - DataPath = "datapath" + DataPath = "data" // APIHost is a string containing the interface ip address // on what the api should listen on diff --git a/constants/constants.go b/constants/constants.go index 29c8dfb..d1918e4 100644 --- a/constants/constants.go +++ b/constants/constants.go @@ -26,3 +26,7 @@ const ServerDataPath = "data" // JSONIndent is the indent to use with the json.MarshalIndent() function. 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-" diff --git a/control/docker_environment.go b/control/docker_environment.go index 8ae0051..397606b 100644 --- a/control/docker_environment.go +++ b/control/docker_environment.go @@ -20,6 +20,7 @@ type dockerEnvironment struct { container *docker.Container context context.Context + attached bool containerInput io.Writer containerOutput io.Writer closeWaiter docker.CloseWaiter @@ -67,23 +68,33 @@ func (env *dockerEnvironment) checkContainerExists() error { } 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: os.Stdout, + OutputStream: cw, + ErrorStream: cw, Stdin: true, Stdout: true, + Stderr: true, Stream: true, Success: success, }) env.closeWaiter = w - env.containerInput = pw <-success close(success) + env.attached = true return err } @@ -91,11 +102,11 @@ func (env *dockerEnvironment) attach() error { // settings to it func (env *dockerEnvironment) Create() error { log.WithField("server", env.server.ID).Debug("Creating docker environment") - // Split image repository and tag to feed it to the library - imageParts := strings.Split(env.server.Service().DockerImage, ":") + // Split image repository and tag + imageParts := strings.Split(env.server.GetService().DockerImage, ":") imageRepoParts := strings.Split(imageParts[0], "/") if len(imageRepoParts) >= 3 { - // Handle possibly required authentication + // TODO: Handle possibly required authentication } // Pull docker image @@ -105,7 +116,7 @@ func (env *dockerEnvironment) Create() error { if len(imageParts) >= 2 { pullImageOpts.Tag = imageParts[1] } - log.WithField("image", env.server.service.DockerImage).Debug("Pulling docker image") + 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") @@ -119,17 +130,21 @@ func (env *dockerEnvironment) Create() error { // Create docker container // TODO: apply cpu, io, disk limits. containerConfig := &docker.Config{ - Image: env.server.Service().DockerImage, - Cmd: strings.Split(env.server.StartupCommand, " "), - OpenStdin: true, + 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, - Binds: []string{env.server.dataPath() + ":/home/container"}, + // 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: "ptdl-" + env.server.UUIDShort(), + Name: constants.DockerContainerPrefix + env.server.UUIDShort(), Config: containerConfig, HostConfig: containerHostConfig, Context: env.context, @@ -139,12 +154,16 @@ func (env *dockerEnvironment) Create() error { 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 } @@ -167,6 +186,10 @@ func (env *dockerEnvironment) Destroy() error { return err } + if env.closeWaiter != nil { + env.closeWaiter.Close() + } + env.attached = false log.WithField("server", env.server.ID).Debug("Docker environment destroyed") return nil } diff --git a/control/server.go b/control/server.go index 715636c..bedc721 100644 --- a/control/server.go +++ b/control/server.go @@ -1,16 +1,19 @@ package control import ( - "encoding/json" "errors" - "io/ioutil" - "os" - "path/filepath" - "strings" "github.com/pterodactyl/wings/api/websockets" log "github.com/sirupsen/logrus" - "github.com/spf13/viper" +) + +type Status string + +const ( + StatusStopped Status = "stopped" + StatusStarting Status = "starting" + StatusRunning Status = "running" + StatusStopping Status = "stopping" ) // ErrServerExists is returned when a server already exists on creation. @@ -34,6 +37,7 @@ type Server interface { Save() error Environment() (Environment, error) + Websockets() *websockets.Hub HasPermission(string, string) bool } @@ -41,32 +45,36 @@ type Server interface { // ServerStruct is a single instance of a Service managed by the panel type ServerStruct struct { // ID is the unique identifier of the server - ID string `json:"uuid"` + ID string `json:"uuid" jsonapi:"primary,server"` // 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 + ServiceName string `json:"serviceName"` + Service *Service `json:"-" jsonapi:"relation,service"` environment Environment // StartupCommand is the command executed in the environment to start the server - StartupCommand string `json:"startupCommand"` + StartupCommand string `json:"startupCommand" jsonapi:"attr,startup_command"` // DockerContainer holds information regarding the docker container when the server // is running in a docker environment - DockerContainer dockerContainer `json:"dockerContainer"` + DockerContainer dockerContainer `json:"dockerContainer" jsonapi:"attr,docker_container"` // EnvironmentVariables are set in the Environment the server is running in - EnvironmentVariables map[string]string `json:"env"` + EnvironmentVariables map[string]string `json:"environmentVariables" jsonapi:"attr,environment_variables"` // Allocations contains the ports and ip addresses assigned to the server - Allocations allocations `json:"allocation"` + Allocations allocations `json:"allocation" jsonapi:"attr,allocations"` // Settings are the environment settings and limitations for the server - Settings settings `json:"settings"` + Settings settings `json:"settings" jsonapi:"attr,settings"` // Keys are some auth keys we will hopefully replace by something better. + // TODO remove Keys map[string][]string `json:"keys"` + + websockets *websockets.Hub + status Status } type allocations struct { @@ -98,73 +106,6 @@ type serversMap map[string]*ServerStruct var servers = make(serversMap) -// LoadServerConfigurations loads the configured servers from a specified path -func LoadServerConfigurations(path string) error { - serverFiles, err := ioutil.ReadDir(path) - if err != nil { - return err - } - servers = make(serversMap) - - for _, file := range serverFiles { - if file.IsDir() { - server, err := loadServerConfiguration(filepath.Join(path, file.Name(), constants.ServerConfigFile)) - if err != nil { - return err - } - servers[server.ID] = server - } - } - - return nil -} - -func loadServerConfiguration(path string) (*ServerStruct, error) { - file, err := ioutil.ReadFile(path) - - if err != nil { - return nil, err - } - - server := &ServerStruct{} - if err := json.Unmarshal(file, server); err != nil { - return nil, err - } - return server, nil -} - -func storeServerConfiguration(server *ServerStruct) error { - serverJSON, err := json.MarshalIndent(server, "", constants.JSONIndent) - if err != nil { - return err - } - if err := os.MkdirAll(server.path(), constants.DefaultFolderPerms); err != nil { - return err - } - if err := ioutil.WriteFile(server.configFilePath(), serverJSON, constants.DefaultFilePerms); err != nil { - return err - } - return nil -} - -func storeServerConfigurations() error { - for _, s := range servers { - if err := storeServerConfiguration(s); err != nil { - return err - } - } - return nil -} - -func deleteServerFolder(id string) error { - path := filepath.Join(viper.GetString(config.DataPath), constants.ServersPath, id) - folder, err := os.Stat(path) - if os.IsNotExist(err) || !folder.IsDir() { - return err - } - return os.RemoveAll(path) -} - // GetServers returns an array of all servers the daemon manages func GetServers() []Server { serverArray := make([]Server, len(servers)) @@ -192,6 +133,11 @@ func CreateServer(server *ServerStruct) (Server, error) { } servers[server.ID] = server if err := server.Save(); err != nil { + delete(servers, server.ID) + return nil, err + } + if err := server.init(); err != nil { + DeleteServer(server.ID) return nil, err } return server, nil @@ -207,13 +153,40 @@ func DeleteServer(id string) error { return nil } +func (s *ServerStruct) init() error { + // TODO: Properly use the correct service, mock for now. + s.Service = &Service{ + DockerImage: "quay.io/pterodactyl/core:java", + EnvironmentName: "docker", + } + s.status = StatusStopped + + s.websockets = websockets.NewHub() + go s.websockets.Run() + + var err error + if s.environment == nil { + switch s.GetService().EnvironmentName { + case "docker": + s.environment, err = NewDockerEnvironment(s) + default: + log.WithField("service", s.ServiceName).Error("Invalid environment name") + return errors.New("Invalid environment name") + } + } + return err +} + func (s *ServerStruct) Start() error { + s.SetStatus(StatusStarting) env, err := s.Environment() if err != nil { + s.SetStatus(StatusStopped) return err } if !env.Exists() { if err := env.Create(); err != nil { + s.SetStatus(StatusStopped) return err } } @@ -221,8 +194,10 @@ func (s *ServerStruct) Start() error { } func (s *ServerStruct) Stop() error { + s.SetStatus(StatusStopping) env, err := s.Environment() if err != nil { + s.SetStatus(StatusRunning) return err } return env.Stop() @@ -258,70 +233,3 @@ func (s *ServerStruct) Rebuild() error { } return env.ReCreate() } - -// Service returns the server's service configuration -func (s *ServerStruct) 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 *ServerStruct) UUIDShort() string { - return s.ID[0:strings.Index(s.ID, "-")] -} - -// Environment returns the servers environment -func (s *ServerStruct) 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 *ServerStruct) HasPermission(token string, permission string) bool { - for key, perms := range s.Keys { - if key == token { - for _, perm := range perms { - if perm == permission || perm == "s:*" { - return true - } - } - return false - } - } - return false -} - -func (s *ServerStruct) Save() error { - if err := storeServerConfiguration(s); err != nil { - log.WithField("server", s.ID).WithError(err).Error("Failed to store server configuration.") - return err - } - return nil -} - -func (s *ServerStruct) path() string { - return filepath.Join(viper.GetString(config.DataPath), constants.ServersPath, s.ID) -} - -func (s *ServerStruct) dataPath() string { - return filepath.Join(s.path(), constants.ServerDataPath) -} - -func (s *ServerStruct) configFilePath() string { - return filepath.Join(s.path(), constants.ServerConfigFile) -} diff --git a/control/server_persistance.go b/control/server_persistance.go new file mode 100644 index 0000000..98c93bd --- /dev/null +++ b/control/server_persistance.go @@ -0,0 +1,103 @@ +package control + +import ( + "encoding/json" + "io/ioutil" + "os" + "path/filepath" + + "github.com/pterodactyl/wings/config" + "github.com/pterodactyl/wings/constants" + log "github.com/sirupsen/logrus" + "github.com/spf13/viper" +) + +// LoadServerConfigurations loads the configured servers from a specified path +func LoadServerConfigurations(path string) error { + serverFiles, err := ioutil.ReadDir(path) + if err != nil { + return err + } + servers = make(serversMap) + + for _, file := range serverFiles { + if file.IsDir() { + server, err := loadServerConfiguration(filepath.Join(path, file.Name(), constants.ServerConfigFile)) + if err != nil { + return err + } + servers[server.ID] = server + } + } + + return nil +} + +func loadServerConfiguration(path string) (*ServerStruct, error) { + file, err := ioutil.ReadFile(path) + + if err != nil { + return nil, err + } + + server := &ServerStruct{} + if err := json.Unmarshal(file, server); err != nil { + return nil, err + } + if err := server.init(); err != nil { + return nil, err + } + return server, nil +} + +func storeServerConfiguration(server *ServerStruct) error { + serverJSON, err := json.MarshalIndent(server, "", constants.JSONIndent) + if err != nil { + return err + } + if err := os.MkdirAll(server.path(), constants.DefaultFolderPerms); err != nil { + return err + } + if err := ioutil.WriteFile(server.configFilePath(), serverJSON, constants.DefaultFilePerms); err != nil { + return err + } + return nil +} + +func storeServerConfigurations() error { + for _, s := range servers { + if err := storeServerConfiguration(s); err != nil { + return err + } + } + return nil +} + +func deleteServerFolder(id string) error { + path := filepath.Join(viper.GetString(config.DataPath), constants.ServersPath, id) + folder, err := os.Stat(path) + if os.IsNotExist(err) || !folder.IsDir() { + return err + } + return os.RemoveAll(path) +} + +func (s *ServerStruct) Save() error { + if err := storeServerConfiguration(s); err != nil { + log.WithField("server", s.ID).WithError(err).Error("Failed to store server configuration.") + return err + } + return nil +} + +func (s *ServerStruct) path() string { + return filepath.Join(viper.GetString(config.DataPath), constants.ServersPath, s.ID) +} + +func (s *ServerStruct) dataPath() string { + return filepath.Join(s.path(), constants.ServerDataPath) +} + +func (s *ServerStruct) configFilePath() string { + return filepath.Join(s.path(), constants.ServerConfigFile) +} diff --git a/control/server_util.go b/control/server_util.go new file mode 100644 index 0000000..03d20e5 --- /dev/null +++ b/control/server_util.go @@ -0,0 +1,49 @@ +package control + +import ( + "strings" + + "github.com/pterodactyl/wings/api/websockets" +) + +func (s *ServerStruct) SetStatus(st Status) { + s.status = st + s.websockets.Broadcast <- websockets.Message{ + Type: websockets.MessageTypeStatus, + Payload: s.status, + } +} + +// Service returns the server's service configuration +func (s *ServerStruct) GetService() *Service { + return s.Service +} + +// UUIDShort returns the first block of the UUID +func (s *ServerStruct) UUIDShort() string { + return s.ID[0:strings.Index(s.ID, "-")] +} + +// Environment returns the servers environment +func (s *ServerStruct) Environment() (Environment, error) { + return s.environment, nil +} + +func (s *ServerStruct) Websockets() *websockets.Hub { + return s.websockets +} + +// HasPermission checks wether a provided token has a specific permission +func (s *ServerStruct) HasPermission(token string, permission string) bool { + for key, perms := range s.Keys { + if key == token { + for _, perm := range perms { + if perm == permission || perm == "s:*" { + return true + } + } + return false + } + } + return false +} diff --git a/control/service.go b/control/service.go index e982e7c..8b7f06c 100644 --- a/control/service.go +++ b/control/service.go @@ -4,7 +4,7 @@ type Service struct { server *Server // EnvironmentName is the name of the environment used by the service - EnvironmentName string `json:"environmentName"` + EnvironmentName string `json:"environmentName" jsonapi:"primary,service"` - DockerImage string `json:"dockerImage"` + DockerImage string `json:"dockerImage" jsonapi:"attr,docker_image"` } diff --git a/utils/logging.go b/utils/logging.go index d1a6851..876c735 100644 --- a/utils/logging.go +++ b/utils/logging.go @@ -7,7 +7,7 @@ import ( "github.com/pterodactyl/wings/constants" - rotatelogs "github.com/lestrrat/go-file-rotatelogs" + "github.com/lestrrat/go-file-rotatelogs" "github.com/rifflock/lfshook" log "github.com/sirupsen/logrus" "github.com/spf13/viper"