diff --git a/api/api.go b/api/api.go index 61cdb95..49c47b8 100644 --- a/api/api.go +++ b/api/api.go @@ -29,6 +29,15 @@ func (api *API) Listen() { api.router = gin.Default() + api.router.Use(func(c *gin.Context) { + c.Header("Access-Control-Allow-Origin", "*") + }) + + api.router.OPTIONS("/", func(c *gin.Context) { + c.Header("Access-Control-Allow-Methods", "POST, GET, PUT, DELETE, OPTIONS") + c.Header("Access-Control-Allow-Headers", "X-Access-Token") + }) + api.registerRoutes() listenString := fmt.Sprintf("%s:%d", viper.GetString(config.APIHost), viper.GetInt(config.APIPort)) diff --git a/api/auth.go b/api/auth.go index 4aa4571..00cde92 100644 --- a/api/auth.go +++ b/api/auth.go @@ -55,7 +55,7 @@ func (a *authorizationManager) HasPermission(permission string) bool { return config.ContainsAuthKey(a.token) } if prefix == "s" { - return a.server.HasPermission(a.token, permission) + return a.server.HasPermission(a.token, permission) || config.ContainsAuthKey(a.token) } return false } diff --git a/api/handlers.go b/api/handlers.go index 73b4c95..34c0ace 100644 --- a/api/handlers.go +++ b/api/handlers.go @@ -2,6 +2,7 @@ package api import ( "net/http" + "runtime" "github.com/Pterodactyl/wings/constants" "github.com/gin-gonic/gin" @@ -35,16 +36,18 @@ func handleGetIndex(c *gin.Context) { System struct { SystemType string `json:"type"` Platform string `json:"platform"` + Arch string `json:"arch"` Release string `json:"release"` Cpus int32 `json:"cpus"` Freemem uint64 `json:"freemem"` - } `json:"os"` + } `json:"system"` }{ Name: "Pterodactyl wings", Version: constants.Version, } info.System.SystemType = hostInfo.OS info.System.Platform = hostInfo.Platform + info.System.Arch = runtime.GOARCH info.System.Release = hostInfo.KernelVersion info.System.Cpus = cpuInfo[0].Cores info.System.Freemem = memInfo.Free @@ -57,7 +60,65 @@ func handleGetIndex(c *gin.Context) { c.String(http.StatusOK, constants.IndexPage) } +type incomingConfiguration struct { + Debug bool `mapstructure:"debug"` + Web struct { + ListenHost string `mapstructure:"host"` + ListenPort int16 `mapstructure:"port"` + SSL struct { + Enabled bool `mapstructure:"enabled"` + Certificate string `mapstructure:"certificate"` + Key string `mapstructure:"key"` + } `mapstructure:"ssl"` + + Uploads struct { + MaximumSize int64 `mapstructure:"maximumSize"` + } `mapstructure:"uploads"` + } `mapstructure:"web"` + + Docker struct { + Socket string `mapstructure:"socket"` + AutoupdateImages bool `mapstructure:"autoupdateImages"` + NetworkInterface string `mapstructure:"networkInterface"` + TimezonePath string `mapstructure:"timezonePath"` + } `mapstructure:"docker"` + + Sftp struct { + Path string `mapstructure:"path"` + Port int16 `mapstructure:"port"` + } `mapstructure:"sftp"` + + Query struct { + KillOnFail bool `mapstructure:"killOnFail"` + FailLimit bool `mapstructure:"failLimit"` + } `mapstructure:"query"` + + Remote string `mapstructure:"remote"` + + Log struct { + Path string `mapstructure:"path"` + Level string `mapstructure:"level"` + DeleteAfterDays int `mapstructure:"deleteAfterDays"` + } `mapstructure:"log"` + + AuthKeys []string `mapstructure:"authKeys"` +} + // handlePatchConfig handles PATCH /config func handlePatchConfig(c *gin.Context) { - + // reqBody, err := ioutil.ReadAll(c.Request.Body) + // if err != nil { + // log.WithError(err).Error("Failed to read input.") + // return + // } + // reqJSON := new(incomingConfiguration) + // err = json.Unmarshal(reqBody, reqJSON) + // if err != nil { + // log.WithError(err).Error("Failed to decode JSON.") + // return + // } + var json incomingConfiguration + if err := c.BindJSON(&json); err != nil { + log.WithError(err).Error("Failed to bind Json.") + } } diff --git a/api/handlers_server.go b/api/handlers_server.go index 5d4430e..608cb54 100644 --- a/api/handlers_server.go +++ b/api/handlers_server.go @@ -1,27 +1,94 @@ package api -import "github.com/gin-gonic/gin" +import ( + "net/http" + "github.com/Pterodactyl/wings/control" + "github.com/gin-gonic/gin" + log "github.com/sirupsen/logrus" +) + +// GET /servers +// TODO: make jsonapi compliant func handleGetServers(c *gin.Context) { - + servers := control.GetServers() + c.JSON(http.StatusOK, servers) } +// POST /servers +// TODO: make jsonapi compliant func handlePostServers(c *gin.Context) { - -} - -func handleDeleteServers(c *gin.Context) { - + server := control.ServerStruct{} + if err := c.BindJSON(&server); err != nil { + log.WithField("server", server).WithError(err).Error("Failed to parse server request.") + c.Status(http.StatusBadRequest) + return + } + var srv control.Server + var err error + if srv, err = control.CreateServer(&server); err != nil { + if _, ok := err.(control.ErrServerExists); ok { + log.WithError(err).Error("Cannot create server, it already exists.") + c.Status(http.StatusBadRequest) + return + } + log.WithField("server", server).WithError(err).Error("Failed to create server.") + c.Status(http.StatusInternalServerError) + return + } + go func() { + env, err := srv.Environment() + if err != nil { + log.WithField("server", srv).WithError(err).Error("Failed to get server environment.") + } + env.Create() + }() + c.JSON(http.StatusOK, srv) } +// GET /servers/:server +// TODO: make jsonapi compliant func handleGetServer(c *gin.Context) { - + id := c.Param("server") + server := control.GetServer(id) + if server == nil { + c.Status(http.StatusNotFound) + return + } + c.JSON(http.StatusOK, server) } +// PATCH /servers/:server func handlePatchServer(c *gin.Context) { } +// DELETE /servers/:server +// TODO: make jsonapi compliant +func handleDeleteServer(c *gin.Context) { + id := c.Param("server") + server := control.GetServer(id) + if server == nil { + c.Status(http.StatusNotFound) + return + } + + env, err := server.Environment() + if err != nil { + log.WithError(err).WithField("server", server).Error("Failed to delete server.") + } + if err := env.Destroy(); err != nil { + log.WithError(err).Error("Failed to delete server, the environment couldn't be destroyed.") + } + + if err := control.DeleteServer(id); err != nil { + log.WithError(err).Error("Failed to delete server.") + c.Status(http.StatusInternalServerError) + return + } + c.Status(http.StatusOK) +} + func handlePostServerReinstall(c *gin.Context) { } @@ -34,12 +101,67 @@ func handlePostServerRebuild(c *gin.Context) { } -func handlePutServerPower(c *gin.Context) { +// POST /servers/:server/power +// TODO: make jsonapi compliant +func handlePostServerPower(c *gin.Context) { + server := getServerFromContext(c) + if server == nil { + c.Status(http.StatusNotFound) + return + } + auth := GetContextAuthManager(c) + if auth == nil { + c.Status(http.StatusInternalServerError) + return + } + + switch c.Query("action") { + case "start": + { + if !auth.HasPermission("s:power:start") { + c.Status(http.StatusForbidden) + return + } + server.Start() + } + case "stop": + { + if !auth.HasPermission("s:power:stop") { + c.Status(http.StatusForbidden) + return + } + server.Stop() + } + case "restart": + { + if !auth.HasPermission("s:power:restart") { + c.Status(http.StatusForbidden) + return + } + server.Restart() + } + case "kill": + { + if !auth.HasPermission("s:power:kill") { + c.Status(http.StatusForbidden) + return + } + server.Kill() + } + default: + { + c.Status(http.StatusBadRequest) + } + } } +// POST /servers/:server/command +// TODO: make jsonapi compliant func handlePostServerCommand(c *gin.Context) { - + server := getServerFromContext(c) + cmd := c.Query("command") + server.Exec(cmd) } func handleGetServerLog(c *gin.Context) { diff --git a/api/handlers_server_test.go b/api/handlers_server_test.go new file mode 100644 index 0000000..25d15d9 --- /dev/null +++ b/api/handlers_server_test.go @@ -0,0 +1,103 @@ +package api + +import ( + "bytes" + "encoding/json" + "io/ioutil" + "net/http" + "net/http/httptest" + "testing" + + "github.com/Pterodactyl/wings/control" + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/assert" +) + +func TestHandleGetServers(t *testing.T) { + router := gin.New() + req, _ := http.NewRequest("GET", "/servers", nil) + + router.GET("/servers", handleGetServers) + + t.Run("returns an empty json array when no servers are configured", func(t *testing.T) { + rec := httptest.NewRecorder() + router.ServeHTTP(rec, req) + body, err := ioutil.ReadAll(rec.Body) + assert.Equal(t, http.StatusOK, rec.Code) + assert.Nil(t, err) + assert.Equal(t, "[]\n", string(body)) + }) + + t.Run("returns an array of servers", func(t *testing.T) { + + }) +} + +var testServer = control.ServerStruct{ + ID: "id1", +} + +func TestHandlePostServer(t *testing.T) { + // We need to decide how deep we want to go with testing here + // Should we just verify it works in general or test validation and + // minimal required config options as well? + // This will also vary depending on the Environment the server runs in +} + +func TestHandleGetServer(t *testing.T) { + router := gin.New() + router.GET("/servers/:server", handleGetServer) + + control.CreateServer(&testServer) + + t.Run("returns not found when the server doesn't exist", func(t *testing.T) { + req, _ := http.NewRequest("GET", "/servers/id0", nil) + rec := httptest.NewRecorder() + router.ServeHTTP(rec, req) + assert.Equal(t, http.StatusNotFound, rec.Code) + }) + + t.Run("returns the server as json", func(t *testing.T) { + req, _ := http.NewRequest("GET", "/servers/id1", nil) + rec := httptest.NewRecorder() + router.ServeHTTP(rec, req) + assert.Equal(t, http.StatusOK, rec.Code) + }) +} + +func TestHandlePatchServer(t *testing.T) { + +} + +func TestHandleDeleteServer(t *testing.T) { + router := gin.New() + router.DELETE("/servers/:server", handleDeleteServer) + + control.CreateServer(&testServer) + + t.Run("returns not found when the server doesn't exist", func(t *testing.T) { + req, _ := http.NewRequest("DELETE", "/servers/id0", nil) + rec := httptest.NewRecorder() + router.ServeHTTP(rec, req) + assert.Equal(t, http.StatusNotFound, rec.Code) + }) + + t.Run("deletes the server", func(t *testing.T) { + req, _ := http.NewRequest("DELETE", "/servers/id1", nil) + rec := httptest.NewRecorder() + router.ServeHTTP(rec, req) + assert.Equal(t, http.StatusOK, rec.Code) + }) +} + +func bodyToJSON(body *bytes.Buffer, v interface{}) error { + reqBody, err := ioutil.ReadAll(body) + if err != nil { + return err + } + err = json.Unmarshal(reqBody, v) + if err != nil { + return err + } + return nil +} diff --git a/api/routes_server.go b/api/routes_server.go index 955527a..9eb6e6d 100644 --- a/api/routes_server.go +++ b/api/routes_server.go @@ -12,7 +12,7 @@ func (api *API) registerServerRoutes() { api.router.POST("/servers/:server/reinstall", AuthHandler("s:install-server"), handlePostServerReinstall) api.router.POST("/servers/:server/rebuild", AuthHandler("g:server:rebuild"), handlePostServerRebuild) api.router.POST("/servers/:server/password", AuthHandler(""), handlePostServerPassword) - api.router.POST("/servers/:server/power", AuthHandler("s:power"), handlePutServerPower) + api.router.POST("/servers/:server/power", AuthHandler("s:power"), handlePostServerPower) api.router.POST("/servers/:server/command", AuthHandler("s:command"), handlePostServerCommand) api.router.GET("/servers/:server/log", AuthHandler("s:console"), handleGetServerLog) api.router.POST("/servers/:server/suspend", AuthHandler(""), handlePostServerSuspend) diff --git a/api/utils.go b/api/utils.go new file mode 100644 index 0000000..d9a972e --- /dev/null +++ b/api/utils.go @@ -0,0 +1,10 @@ +package api + +import ( + "github.com/Pterodactyl/wings/control" + "github.com/gin-gonic/gin" +) + +func getServerFromContext(context *gin.Context) control.Server { + return control.GetServer(context.Param("server")) +} diff --git a/command/root.go b/command/root.go index ba4d945..21cb0a3 100644 --- a/command/root.go +++ b/command/root.go @@ -1,10 +1,16 @@ package command import ( + "path/filepath" + "strconv" + + "github.com/spf13/viper" + "github.com/Pterodactyl/wings/api" "github.com/Pterodactyl/wings/config" "github.com/Pterodactyl/wings/constants" - "github.com/Pterodactyl/wings/tools" + "github.com/Pterodactyl/wings/control" + "github.com/Pterodactyl/wings/utils" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) @@ -30,7 +36,7 @@ func Execute() { func run(cmd *cobra.Command, args []string) { tools.InitLogging() - log.Info("Loading configuration") + log.Info("Loading configuration...") if err := config.LoadConfiguration(configPath); err != nil { log.WithError(err).Fatal("Failed to find configuration file") } @@ -46,7 +52,17 @@ func run(cmd *cobra.Command, args []string) { log.Info("Configuration loaded successfully.") - log.Info("Starting api webserver") + log.Info("Loading configured servers...") + if err := control.LoadServerConfigurations(filepath.Join(viper.GetString(config.DataPath), constants.ServersPath)); err != nil { + log.WithError(err).Error("Failed to load configured servers.") + } + if amount := len(control.GetServers()); amount == 1 { + log.Info("Loaded 1 server.") + } else { + log.Info("Loaded " + strconv.Itoa(amount) + " servers.") + } + + log.Info("Starting api webserver...") api := api.NewAPI() api.Listen() } diff --git a/config.example.json b/config.example.json index 3fad714..27dc704 100644 --- a/config.example.json +++ b/config.example.json @@ -1,6 +1,6 @@ { "debug": false, - "dataPath": "/srv/daemon-data", + "dataPath": "/srv/wings", "api": { "host": "0.0.0.0", "port": 8080, @@ -30,7 +30,7 @@ }, "remote": "https://pterodactyl.app", "log": { - "path": "/srv/daemon-data/logs/", + "path": "/srv/wings/logs/", "level": "info", "deleteAfterDays": 100 }, diff --git a/constants/constants.go b/constants/constants.go index c5918de..29c8dfb 100644 --- a/constants/constants.go +++ b/constants/constants.go @@ -1,4 +1,28 @@ package constants -// Version is the current wings version +import "os" + +// Version is the current wings version. const Version = "0.0.1-alpha" + +/* ---------- PATHS ---------- */ + +// DefaultFilePerms are the file perms used for created files. +const DefaultFilePerms os.FileMode = 0644 + +// DefaultFolderPerms are the file perms used for created folders. +const DefaultFolderPerms os.FileMode = 0744 + +// ServersPath is the path of the servers within the configured DataPath. +const ServersPath = "servers" + +// ServerConfigFile is the filename of the server config file. +const ServerConfigFile = "server.json" + +// ServerDataPath is the path of the data of a single server. +const ServerDataPath = "data" + +/* ---------- MISC ---------- */ + +// JSONIndent is the indent to use with the json.MarshalIndent() function. +const JSONIndent = " " diff --git a/control/docker_environment.go b/control/docker_environment.go index b4b7be6..f9dd19f 100644 --- a/control/docker_environment.go +++ b/control/docker_environment.go @@ -2,8 +2,12 @@ package control import ( "context" + "io" + "os" "strings" + "github.com/Pterodactyl/wings/constants" + "github.com/fsouza/go-dockerclient" log "github.com/sirupsen/logrus" ) @@ -15,6 +19,10 @@ type dockerEnvironment struct { container *docker.Container context context.Context + containerInput io.Writer + containerOutput io.Writer + closeWaiter docker.CloseWaiter + server *ServerStruct } @@ -38,16 +46,17 @@ func NewDockerEnvironment(server *ServerStruct) (Environment, error) { } if env.server.DockerContainer.ID != "" { - if err := env.reattach(); err != nil { - log.WithError(err).Error("Failed to reattach to existing container.") - return nil, err + 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) reattach() error { +func (env *dockerEnvironment) checkContainerExists() error { container, err := env.client.InspectContainer(env.server.DockerContainer.ID) if err != nil { return err @@ -56,10 +65,31 @@ func (env *dockerEnvironment) reattach() error { return nil } +func (env *dockerEnvironment) attach() error { + pr, pw := io.Pipe() + + success := make(chan struct{}) + w, err := env.client.AttachToContainerNonBlocking(docker.AttachToContainerOptions{ + Container: env.server.DockerContainer.ID, + InputStream: pr, + OutputStream: os.Stdout, + Stdin: true, + Stdout: true, + Stream: true, + Success: success, + }) + env.closeWaiter = w + env.containerInput = pw + + <-success + close(success) + return err +} + // Create creates the docker container for the environment and applies all // settings to it func (env *dockerEnvironment) Create() error { - log.WithField("serverID", env.server.ID).Debug("Creating docker environment") + 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, ":") imageRepoParts := strings.Split(imageParts[0], "/") @@ -77,18 +107,25 @@ func (env *dockerEnvironment) Create() error { 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.ID).Error("Failed to create docker environment") + 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.Service().DockerImage, + Image: env.server.Service().DockerImage, + Cmd: strings.Split(env.server.StartupCommand, " "), + OpenStdin: true, } containerHostConfig := &docker.HostConfig{ Memory: env.server.Settings.Memory, MemorySwap: env.server.Settings.Swap, + Binds: []string{env.server.dataPath() + ":/home/container"}, } createContainerOpts := docker.CreateContainerOptions{ Name: "ptdl-" + env.server.UUIDShort(), @@ -98,31 +135,55 @@ func (env *dockerEnvironment) Create() error { } container, err := env.client.CreateContainer(createContainerOpts) if err != nil { - log.WithError(err).WithField("serverID", env.server.ID).Error("Failed to create docker container") + 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 + 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("serverID", env.server.ID).Debug("Destroying docker environment") + 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("serverID", env.server.ID).Error("Failed to destroy docker environment") + log.WithError(err).WithField("server", env.server.ID).Error("Failed to destroy docker environment") return err } + + 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("serverID", env.server.ID).Debug("Starting service in docker environment") + 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 @@ -132,7 +193,7 @@ func (env *dockerEnvironment) Start() error { // Stop stops the environment's docker container func (env *dockerEnvironment) Stop() error { - log.WithField("serverID", env.server.ID).Debug("Stopping service in docker environment") + 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 @@ -141,7 +202,7 @@ func (env *dockerEnvironment) Stop() error { } func (env *dockerEnvironment) Kill() error { - log.WithField("serverID", env.server.ID).Debug("Killing service in docker environment") + 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 { @@ -153,5 +214,7 @@ func (env *dockerEnvironment) Kill() error { // Exec sends commands to the standard input of the docker container func (env *dockerEnvironment) Exec(command string) error { - return nil + log.Debug("Command: " + command) + _, err := env.containerInput.Write([]byte(command + "\n")) + return err } diff --git a/control/server.go b/control/server.go index cbc6f21..26fadbd 100644 --- a/control/server.go +++ b/control/server.go @@ -4,22 +4,42 @@ import ( "encoding/json" "errors" "io/ioutil" + "os" + "path/filepath" "strings" + "github.com/Pterodactyl/wings/config" + "github.com/Pterodactyl/wings/constants" log "github.com/sirupsen/logrus" + "github.com/spf13/viper" ) +// ErrServerExists is returned when a server already exists on creation. +type ErrServerExists struct { + id string +} + +func (e ErrServerExists) Error() string { + return "server " + e.id + " already exists" +} + // Server is a Server type Server interface { Start() error Stop() error + Restart() error + Kill() error Exec(command string) error Rebuild() error + Save() error + + Environment() (Environment, error) + HasPermission(string, string) bool } -// server is a single instance of a Service managed by the panel +// 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"` @@ -27,7 +47,7 @@ type ServerStruct struct { // 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 + service *Service environment Environment // StartupCommand is the command executed in the environment to start the server @@ -88,8 +108,8 @@ func LoadServerConfigurations(path string) error { servers = make(serversMap) for _, file := range serverFiles { - if !file.IsDir() { - server, err := loadServerConfiguration(path + file.Name()) + if file.IsDir() { + server, err := loadServerConfiguration(filepath.Join(path, file.Name(), constants.ServerConfigFile)) if err != nil { return err } @@ -114,6 +134,38 @@ func loadServerConfiguration(path string) (*ServerStruct, error) { 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)) @@ -136,53 +188,83 @@ func GetServer(id string) Server { // CreateServer creates a new server func CreateServer(server *ServerStruct) (Server, error) { + if servers[server.ID] != nil { + return nil, ErrServerExists{server.ID} + } servers[server.ID] = server + if err := server.Save(); err != nil { + return nil, err + } return server, nil } // DeleteServer deletes a server and all related files // NOTE: This is not reversible. -func DeleteServer(uuid string) error { - delete(servers, uuid) +func DeleteServer(id string) error { + if err := deleteServerFolder(id); err != nil { + log.WithField("server", id).WithError(err).Error("Failed to delete server.") + } + delete(servers, id) return nil } func (s *ServerStruct) Start() error { - /*if err := s.Environment().Create(); err != nil { + env, err := s.Environment() + if err != nil { return err } - if err := s.Environment().Start(); err != nil { - return err - }*/ - return nil + if !env.Exists() { + if err := env.Create(); err != nil { + return err + } + } + return env.Start() } func (s *ServerStruct) Stop() error { - /*if err := s.Environment().Stop(); err != nil { + env, err := s.Environment() + if err != nil { return err - }*/ - return nil + } + return env.Stop() +} + +func (s *ServerStruct) Restart() error { + if err := s.Stop(); err != nil { + return err + } + return s.Start() +} + +func (s *ServerStruct) Kill() error { + env, err := s.Environment() + if err != nil { + return err + } + return env.Kill() } func (s *ServerStruct) Exec(command string) error { - /*if err := s.Environment().Exec(command); err != nil { + env, err := s.Environment() + if err != nil { return err - }*/ - return nil + } + return env.Exec(command) } func (s *ServerStruct) Rebuild() error { - /*if err := s.Environment().ReCreate(); err != nil { + env, err := s.Environment() + if err != nil { return err - }*/ - return nil + } + return env.ReCreate() } // Service returns the server's service configuration -func (s *ServerStruct) Service() *service { +func (s *ServerStruct) Service() *Service { if s.service == nil { // TODO: Properly use the correct service, mock for now. - s.service = &service{ + s.service = &Service{ DockerImage: "quay.io/pterodactyl/core:java", EnvironmentName: "docker", } @@ -225,6 +307,22 @@ func (s *ServerStruct) HasPermission(token string, permission string) bool { return false } -func (s *ServerStruct) save() { - +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/service.go b/control/service.go index e4eb90c..e982e7c 100644 --- a/control/service.go +++ b/control/service.go @@ -1,6 +1,6 @@ package control -type service struct { +type Service struct { server *Server // EnvironmentName is the name of the environment used by the service diff --git a/utils/logging.go b/utils/logging.go index 08bad7f..ff05ebd 100644 --- a/utils/logging.go +++ b/utils/logging.go @@ -26,7 +26,6 @@ func InitLogging() { // ConfigureLogging applies the configuration to the logging library. func ConfigureLogging() error { - path := filepath.Clean(viper.GetString(config.LogPath)) if err := os.MkdirAll(path, constants.DefaultFolderPerms); err != nil { return err