current state of api & docker env implementation

This commit is contained in:
Jakob Schrettenbrunner 2017-10-01 20:42:17 +02:00
parent d1fdc713f5
commit c979285eb9
14 changed files with 565 additions and 60 deletions

View File

@ -29,6 +29,15 @@ func (api *API) Listen() {
api.router = gin.Default() 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() api.registerRoutes()
listenString := fmt.Sprintf("%s:%d", viper.GetString(config.APIHost), viper.GetInt(config.APIPort)) listenString := fmt.Sprintf("%s:%d", viper.GetString(config.APIHost), viper.GetInt(config.APIPort))

View File

@ -55,7 +55,7 @@ func (a *authorizationManager) HasPermission(permission string) bool {
return config.ContainsAuthKey(a.token) return config.ContainsAuthKey(a.token)
} }
if prefix == "s" { if prefix == "s" {
return a.server.HasPermission(a.token, permission) return a.server.HasPermission(a.token, permission) || config.ContainsAuthKey(a.token)
} }
return false return false
} }

View File

@ -2,6 +2,7 @@ package api
import ( import (
"net/http" "net/http"
"runtime"
"github.com/Pterodactyl/wings/constants" "github.com/Pterodactyl/wings/constants"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@ -35,16 +36,18 @@ func handleGetIndex(c *gin.Context) {
System struct { System struct {
SystemType string `json:"type"` SystemType string `json:"type"`
Platform string `json:"platform"` Platform string `json:"platform"`
Arch string `json:"arch"`
Release string `json:"release"` Release string `json:"release"`
Cpus int32 `json:"cpus"` Cpus int32 `json:"cpus"`
Freemem uint64 `json:"freemem"` Freemem uint64 `json:"freemem"`
} `json:"os"` } `json:"system"`
}{ }{
Name: "Pterodactyl wings", Name: "Pterodactyl wings",
Version: constants.Version, Version: constants.Version,
} }
info.System.SystemType = hostInfo.OS info.System.SystemType = hostInfo.OS
info.System.Platform = hostInfo.Platform info.System.Platform = hostInfo.Platform
info.System.Arch = runtime.GOARCH
info.System.Release = hostInfo.KernelVersion info.System.Release = hostInfo.KernelVersion
info.System.Cpus = cpuInfo[0].Cores info.System.Cpus = cpuInfo[0].Cores
info.System.Freemem = memInfo.Free info.System.Freemem = memInfo.Free
@ -57,7 +60,65 @@ func handleGetIndex(c *gin.Context) {
c.String(http.StatusOK, constants.IndexPage) 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 // handlePatchConfig handles PATCH /config
func handlePatchConfig(c *gin.Context) { 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.")
}
} }

View File

@ -1,27 +1,94 @@
package api 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) { 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 handlePostServers(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.")
func handleDeleteServers(c *gin.Context) { 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) { 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) { 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) { 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
} }
func handlePostServerCommand(c *gin.Context) { 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) { func handleGetServerLog(c *gin.Context) {

103
api/handlers_server_test.go Normal file
View File

@ -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
}

View File

@ -12,7 +12,7 @@ func (api *API) registerServerRoutes() {
api.router.POST("/servers/:server/reinstall", AuthHandler("s:install-server"), handlePostServerReinstall) 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/rebuild", AuthHandler("g:server:rebuild"), handlePostServerRebuild)
api.router.POST("/servers/:server/password", AuthHandler(""), handlePostServerPassword) 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.POST("/servers/:server/command", AuthHandler("s:command"), handlePostServerCommand)
api.router.GET("/servers/:server/log", AuthHandler("s:console"), handleGetServerLog) api.router.GET("/servers/:server/log", AuthHandler("s:console"), handleGetServerLog)
api.router.POST("/servers/:server/suspend", AuthHandler(""), handlePostServerSuspend) api.router.POST("/servers/:server/suspend", AuthHandler(""), handlePostServerSuspend)

10
api/utils.go Normal file
View File

@ -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"))
}

View File

@ -1,10 +1,16 @@
package command package command
import ( import (
"path/filepath"
"strconv"
"github.com/spf13/viper"
"github.com/Pterodactyl/wings/api" "github.com/Pterodactyl/wings/api"
"github.com/Pterodactyl/wings/config" "github.com/Pterodactyl/wings/config"
"github.com/Pterodactyl/wings/constants" "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" log "github.com/sirupsen/logrus"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -30,7 +36,7 @@ func Execute() {
func run(cmd *cobra.Command, args []string) { func run(cmd *cobra.Command, args []string) {
tools.InitLogging() tools.InitLogging()
log.Info("Loading configuration") log.Info("Loading configuration...")
if err := config.LoadConfiguration(configPath); err != nil { if err := config.LoadConfiguration(configPath); err != nil {
log.WithError(err).Fatal("Failed to find configuration file") 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("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 := api.NewAPI()
api.Listen() api.Listen()
} }

View File

@ -1,6 +1,6 @@
{ {
"debug": false, "debug": false,
"dataPath": "/srv/daemon-data", "dataPath": "/srv/wings",
"api": { "api": {
"host": "0.0.0.0", "host": "0.0.0.0",
"port": 8080, "port": 8080,
@ -30,7 +30,7 @@
}, },
"remote": "https://pterodactyl.app", "remote": "https://pterodactyl.app",
"log": { "log": {
"path": "/srv/daemon-data/logs/", "path": "/srv/wings/logs/",
"level": "info", "level": "info",
"deleteAfterDays": 100 "deleteAfterDays": 100
}, },

View File

@ -1,4 +1,28 @@
package constants package constants
// Version is the current wings version import "os"
// Version is the current wings version.
const Version = "0.0.1-alpha" 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 = " "

View File

@ -2,8 +2,12 @@ package control
import ( import (
"context" "context"
"io"
"os"
"strings" "strings"
"github.com/Pterodactyl/wings/constants"
"github.com/fsouza/go-dockerclient" "github.com/fsouza/go-dockerclient"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
@ -15,6 +19,10 @@ type dockerEnvironment struct {
container *docker.Container container *docker.Container
context context.Context context context.Context
containerInput io.Writer
containerOutput io.Writer
closeWaiter docker.CloseWaiter
server *ServerStruct server *ServerStruct
} }
@ -38,16 +46,17 @@ func NewDockerEnvironment(server *ServerStruct) (Environment, error) {
} }
if env.server.DockerContainer.ID != "" { if env.server.DockerContainer.ID != "" {
if err := env.reattach(); err != nil { if err := env.checkContainerExists(); err != nil {
log.WithError(err).Error("Failed to reattach to existing container.") log.WithError(err).Error("Failed to find the container with stored id, removing id.")
return nil, err env.server.DockerContainer.ID = ""
env.server.Save()
} }
} }
return &env, nil return &env, nil
} }
func (env *dockerEnvironment) reattach() error { func (env *dockerEnvironment) checkContainerExists() error {
container, err := env.client.InspectContainer(env.server.DockerContainer.ID) container, err := env.client.InspectContainer(env.server.DockerContainer.ID)
if err != nil { if err != nil {
return err return err
@ -56,10 +65,31 @@ func (env *dockerEnvironment) reattach() error {
return nil 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 // Create creates the docker container for the environment and applies all
// settings to it // settings to it
func (env *dockerEnvironment) Create() error { 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 // Split image repository and tag to feed it to the library
imageParts := strings.Split(env.server.Service().DockerImage, ":") imageParts := strings.Split(env.server.Service().DockerImage, ":")
imageRepoParts := strings.Split(imageParts[0], "/") imageRepoParts := strings.Split(imageParts[0], "/")
@ -77,7 +107,11 @@ func (env *dockerEnvironment) Create() error {
log.WithField("image", env.server.service.DockerImage).Debug("Pulling docker image") log.WithField("image", env.server.service.DockerImage).Debug("Pulling docker image")
err := env.client.PullImage(pullImageOpts, docker.AuthConfiguration{}) err := env.client.PullImage(pullImageOpts, docker.AuthConfiguration{})
if err != nil { 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 return err
} }
@ -85,10 +119,13 @@ func (env *dockerEnvironment) Create() error {
// TODO: apply cpu, io, disk limits. // TODO: apply cpu, io, disk limits.
containerConfig := &docker.Config{ containerConfig := &docker.Config{
Image: env.server.Service().DockerImage, Image: env.server.Service().DockerImage,
Cmd: strings.Split(env.server.StartupCommand, " "),
OpenStdin: true,
} }
containerHostConfig := &docker.HostConfig{ containerHostConfig := &docker.HostConfig{
Memory: env.server.Settings.Memory, Memory: env.server.Settings.Memory,
MemorySwap: env.server.Settings.Swap, MemorySwap: env.server.Settings.Swap,
Binds: []string{env.server.dataPath() + ":/home/container"},
} }
createContainerOpts := docker.CreateContainerOptions{ createContainerOpts := docker.CreateContainerOptions{
Name: "ptdl-" + env.server.UUIDShort(), Name: "ptdl-" + env.server.UUIDShort(),
@ -98,31 +135,55 @@ func (env *dockerEnvironment) Create() error {
} }
container, err := env.client.CreateContainer(createContainerOpts) container, err := env.client.CreateContainer(createContainerOpts)
if err != nil { 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 return err
} }
env.server.DockerContainer.ID = container.ID env.server.DockerContainer.ID = container.ID
env.server.Save()
env.container = container env.container = container
log.WithField("server", env.server.ID).Debug("Docker environment created")
return nil return nil
} }
// Destroy removes the environment's docker container // Destroy removes the environment's docker container
func (env *dockerEnvironment) Destroy() error { 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{ err := env.client.RemoveContainer(docker.RemoveContainerOptions{
ID: env.server.DockerContainer.ID, ID: env.server.DockerContainer.ID,
}) })
if err != nil { 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 return err
} }
log.WithField("server", env.server.ID).Debug("Docker environment destroyed")
return nil 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 // Start starts the environment's docker container
func (env *dockerEnvironment) Start() error { 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 { if err := env.client.StartContainer(env.container.ID, nil); err != nil {
log.WithError(err).Error("Failed to start docker container") log.WithError(err).Error("Failed to start docker container")
return err return err
@ -132,7 +193,7 @@ func (env *dockerEnvironment) Start() error {
// Stop stops the environment's docker container // Stop stops the environment's docker container
func (env *dockerEnvironment) Stop() error { 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 { if err := env.client.StopContainer(env.container.ID, 20000); err != nil {
log.WithError(err).Error("Failed to stop docker container") log.WithError(err).Error("Failed to stop docker container")
return err return err
@ -141,7 +202,7 @@ func (env *dockerEnvironment) Stop() error {
} }
func (env *dockerEnvironment) Kill() 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{ if err := env.client.KillContainer(docker.KillContainerOptions{
ID: env.container.ID, ID: env.container.ID,
}); err != nil { }); err != nil {
@ -153,5 +214,7 @@ func (env *dockerEnvironment) Kill() error {
// Exec sends commands to the standard input of the docker container // Exec sends commands to the standard input of the docker container
func (env *dockerEnvironment) Exec(command string) error { func (env *dockerEnvironment) Exec(command string) error {
return nil log.Debug("Command: " + command)
_, err := env.containerInput.Write([]byte(command + "\n"))
return err
} }

View File

@ -4,22 +4,42 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"io/ioutil" "io/ioutil"
"os"
"path/filepath"
"strings" "strings"
"github.com/Pterodactyl/wings/config"
"github.com/Pterodactyl/wings/constants"
log "github.com/sirupsen/logrus" 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 // Server is a Server
type Server interface { type Server interface {
Start() error Start() error
Stop() error Stop() error
Restart() error
Kill() error
Exec(command string) error Exec(command string) error
Rebuild() error Rebuild() error
Save() error
Environment() (Environment, error)
HasPermission(string, string) bool 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 { type ServerStruct struct {
// ID is the unique identifier of the server // ID is the unique identifier of the server
ID string `json:"uuid"` 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 // ServiceName is the name of the service. It is mainly used to allow storing the service
// in the config // in the config
ServiceName string `json:"serviceName"` ServiceName string `json:"serviceName"`
service *service service *Service
environment Environment environment Environment
// StartupCommand is the command executed in the environment to start the server // StartupCommand is the command executed in the environment to start the server
@ -88,8 +108,8 @@ func LoadServerConfigurations(path string) error {
servers = make(serversMap) servers = make(serversMap)
for _, file := range serverFiles { for _, file := range serverFiles {
if !file.IsDir() { if file.IsDir() {
server, err := loadServerConfiguration(path + file.Name()) server, err := loadServerConfiguration(filepath.Join(path, file.Name(), constants.ServerConfigFile))
if err != nil { if err != nil {
return err return err
} }
@ -114,6 +134,38 @@ func loadServerConfiguration(path string) (*ServerStruct, error) {
return server, nil 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 // GetServers returns an array of all servers the daemon manages
func GetServers() []Server { func GetServers() []Server {
serverArray := make([]Server, len(servers)) serverArray := make([]Server, len(servers))
@ -136,53 +188,83 @@ func GetServer(id string) Server {
// CreateServer creates a new server // CreateServer creates a new server
func CreateServer(server *ServerStruct) (Server, error) { func CreateServer(server *ServerStruct) (Server, error) {
if servers[server.ID] != nil {
return nil, ErrServerExists{server.ID}
}
servers[server.ID] = server servers[server.ID] = server
if err := server.Save(); err != nil {
return nil, err
}
return server, nil return server, nil
} }
// DeleteServer deletes a server and all related files // DeleteServer deletes a server and all related files
// NOTE: This is not reversible. // NOTE: This is not reversible.
func DeleteServer(uuid string) error { func DeleteServer(id string) error {
delete(servers, uuid) if err := deleteServerFolder(id); err != nil {
log.WithField("server", id).WithError(err).Error("Failed to delete server.")
}
delete(servers, id)
return nil return nil
} }
func (s *ServerStruct) Start() error { func (s *ServerStruct) Start() error {
/*if err := s.Environment().Create(); err != nil { env, err := s.Environment()
if err != nil {
return err return err
} }
if err := s.Environment().Start(); err != nil { if !env.Exists() {
if err := env.Create(); err != nil {
return err return err
}*/ }
return nil }
return env.Start()
} }
func (s *ServerStruct) Stop() error { func (s *ServerStruct) Stop() error {
/*if err := s.Environment().Stop(); err != nil { env, err := s.Environment()
if err != nil {
return err 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 { 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 err
}*/ }
return nil return env.Exec(command)
} }
func (s *ServerStruct) Rebuild() error { func (s *ServerStruct) Rebuild() error {
/*if err := s.Environment().ReCreate(); err != nil { env, err := s.Environment()
if err != nil {
return err return err
}*/ }
return nil return env.ReCreate()
} }
// Service returns the server's service configuration // Service returns the server's service configuration
func (s *ServerStruct) Service() *service { func (s *ServerStruct) Service() *Service {
if s.service == nil { if s.service == nil {
// TODO: Properly use the correct service, mock for now. // TODO: Properly use the correct service, mock for now.
s.service = &service{ s.service = &Service{
DockerImage: "quay.io/pterodactyl/core:java", DockerImage: "quay.io/pterodactyl/core:java",
EnvironmentName: "docker", EnvironmentName: "docker",
} }
@ -225,6 +307,22 @@ func (s *ServerStruct) HasPermission(token string, permission string) bool {
return false 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)
} }

View File

@ -1,6 +1,6 @@
package control package control
type service struct { type Service struct {
server *Server server *Server
// EnvironmentName is the name of the environment used by the service // EnvironmentName is the name of the environment used by the service

View File

@ -26,7 +26,6 @@ func InitLogging() {
// ConfigureLogging applies the configuration to the logging library. // ConfigureLogging applies the configuration to the logging library.
func ConfigureLogging() error { func ConfigureLogging() error {
path := filepath.Clean(viper.GetString(config.LogPath)) path := filepath.Clean(viper.GetString(config.LogPath))
if err := os.MkdirAll(path, constants.DefaultFolderPerms); err != nil { if err := os.MkdirAll(path, constants.DefaultFolderPerms); err != nil {
return err return err