some reorganisation etc. in control package

some minor stuff
just pushing so I can try to replace fsouza/go-dockerclient with moby/moby/client
This commit is contained in:
Jakob Schrettenbrunner 2018-02-20 23:38:29 +01:00
parent 184d7e0afe
commit 501409827e
8 changed files with 251 additions and 164 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

49
control/server_util.go Normal file
View File

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

View File

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

View File

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