Add initial support for fetching egg configuration from panel for servers
This commit is contained in:
parent
2a745c5da1
commit
d7753d9c7f
114
api/api.go
Normal file
114
api/api.go
Normal file
|
@ -0,0 +1,114 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/buger/jsonparser"
|
||||||
|
"github.com/pterodactyl/wings/config"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Initalizes the requester instance.
|
||||||
|
func NewRequester() *PanelRequest {
|
||||||
|
return &PanelRequest{
|
||||||
|
Response: nil,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type PanelRequest struct {
|
||||||
|
Response *http.Response
|
||||||
|
}
|
||||||
|
|
||||||
|
// Builds the base request instance that can be used with the HTTP client.
|
||||||
|
func (r *PanelRequest) GetClient() *http.Client {
|
||||||
|
return &http.Client{Timeout: time.Second * 30}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *PanelRequest) SetHeaders(req *http.Request) *http.Request {
|
||||||
|
req.Header.Set("Accept", "application/vnd.pterodactyl.v1+json")
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("Authorization", "Bearer " + config.Get().AuthenticationToken)
|
||||||
|
|
||||||
|
return req
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *PanelRequest) GetEndpoint(endpoint string) string {
|
||||||
|
return fmt.Sprintf(
|
||||||
|
"%s/api/remote/%s",
|
||||||
|
strings.TrimSuffix(config.Get().PanelLocation, "/"),
|
||||||
|
strings.TrimPrefix(strings.TrimPrefix(endpoint, "/"), "api/remote/"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *PanelRequest) Get(url string) (*http.Response, error) {
|
||||||
|
c := r.GetClient()
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodGet, r.GetEndpoint(url), nil)
|
||||||
|
req = r.SetHeaders(req)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
zap.S().Debugw("GET request to endpoint", zap.String("endpoint", r.GetEndpoint(url)), zap.Any("headers", req.Header))
|
||||||
|
|
||||||
|
return c.Do(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determines if the API call encountered an error. If no request has been made
|
||||||
|
// the response will be false.
|
||||||
|
func (r *PanelRequest) HasError() bool {
|
||||||
|
if r.Response == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.Response.StatusCode >= 300 || r.Response.StatusCode < 200;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reads the body from the response and returns it, then replaces it on the response
|
||||||
|
// so that it can be read again later.
|
||||||
|
func (r *PanelRequest) ReadBody() ([]byte, error) {
|
||||||
|
var b []byte
|
||||||
|
if r.Response == nil {
|
||||||
|
return nil, errors.New("no response exists on interface")
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.Response.Body != nil {
|
||||||
|
b, _ = ioutil.ReadAll(r.Response.Body)
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Response.Body = ioutil.NopCloser(bytes.NewBuffer(b))
|
||||||
|
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
// Returns the error message from the API call as a string. The error message will be formatted
|
||||||
|
// similar to the below example:
|
||||||
|
//
|
||||||
|
// HttpNotFoundException: The requested resource does not exist. (HTTP/404)
|
||||||
|
func (r *PanelRequest) Error() (string, error) {
|
||||||
|
body, err := r.ReadBody()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
zap.S().Debugw("got body", zap.ByteString("b", body))
|
||||||
|
_, valueType, _, err := jsonparser.Get(body, "errors")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if valueType != jsonparser.Object {
|
||||||
|
return "no error object present on response", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
code, _ := jsonparser.GetString(body, "errors.0.code")
|
||||||
|
status, _ := jsonparser.GetString(body, "errors.0.status")
|
||||||
|
detail, _ := jsonparser.GetString(body, "errors.0.detail")
|
||||||
|
|
||||||
|
return fmt.Sprintf("%s: %s (HTTP/%s)", code, detail, status), nil
|
||||||
|
}
|
89
api/server_endpoints.go
Normal file
89
api/server_endpoints.go
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/buger/jsonparser"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Defines a single find/replace instance for a given server configuration file.
|
||||||
|
type ConfigurationFileReplacement struct {
|
||||||
|
Match string `json:"match"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
ValueType jsonparser.ValueType `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfr *ConfigurationFileReplacement) UnmarshalJSON(data []byte) error {
|
||||||
|
if m, err := jsonparser.GetString(data, "match"); err != nil {
|
||||||
|
return err
|
||||||
|
} else {
|
||||||
|
cfr.Match = m
|
||||||
|
}
|
||||||
|
|
||||||
|
if v, dt, _, err := jsonparser.Get(data, "value"); err != nil {
|
||||||
|
return err
|
||||||
|
} else {
|
||||||
|
if dt != jsonparser.String && dt != jsonparser.Number && dt != jsonparser.Boolean {
|
||||||
|
return errors.New(
|
||||||
|
fmt.Sprintf("cannot parse JSON: received unexpected replacement value type: %d", dt),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfr.Value = string(v)
|
||||||
|
cfr.ValueType = dt
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Defines a configuration file for the server startup. These will be looped over
|
||||||
|
// and modified before the server finishes booting.
|
||||||
|
type ConfigurationFile struct {
|
||||||
|
FileName string `json:"file"`
|
||||||
|
Parser string `json:"parser"`
|
||||||
|
Replace []ConfigurationFileReplacement `json:"replace"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Defines the process configuration for a given server instance. This sets what the
|
||||||
|
// daemon is looking for to mark a server as done starting, what to do when stopping,
|
||||||
|
// and what changes to make to the configuration file for a server.
|
||||||
|
type ServerConfiguration struct {
|
||||||
|
Startup struct {
|
||||||
|
Done string `json:"done"`
|
||||||
|
UserInteraction []string `json:"userInteraction"`
|
||||||
|
} `json:"startup"`
|
||||||
|
Stop struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
} `json:"stop"`
|
||||||
|
ConfigurationFiles []ConfigurationFile `json:"configs"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetches the server configuration and returns the struct for it.
|
||||||
|
func (r *PanelRequest) GetServerConfiguration(uuid string) (*ServerConfiguration, error) {
|
||||||
|
resp, err := r.Get(fmt.Sprintf("/servers/%s/configuration", uuid))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
r.Response = resp
|
||||||
|
|
||||||
|
if r.HasError() {
|
||||||
|
e, err := r.Error()
|
||||||
|
zap.S().Warnw("got error", zap.String("message", e), zap.Error(err))
|
||||||
|
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
res := &ServerConfiguration{}
|
||||||
|
b, _ := r.ReadBody()
|
||||||
|
|
||||||
|
if err := json.Unmarshal(b, res); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}
|
|
@ -14,6 +14,10 @@ type Environment interface {
|
||||||
// for this specific server instance.
|
// for this specific server instance.
|
||||||
IsRunning() (bool, error)
|
IsRunning() (bool, error)
|
||||||
|
|
||||||
|
// Runs before the environment is started. If an error is returned starting will
|
||||||
|
// not occur, otherwise proceeds as normal.
|
||||||
|
OnBeforeStart() error
|
||||||
|
|
||||||
// Starts a server instance. If the server instance is not in a state where it
|
// Starts a server instance. If the server instance is not in a state where it
|
||||||
// can be started an error should be returned.
|
// can be started an error should be returned.
|
||||||
Start() error
|
Start() error
|
||||||
|
|
|
@ -113,12 +113,35 @@ func (d *DockerEnvironment) IsRunning() (bool, error) {
|
||||||
return c.State.Running, nil
|
return c.State.Running, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Run before the container starts and get the process configuration from the Panel.
|
||||||
|
// This is important since we use this to check configuration files as well as ensure
|
||||||
|
// we always have the latest version of an egg available for server processes.
|
||||||
|
func (d *DockerEnvironment) OnBeforeStart() error {
|
||||||
|
c, err := d.Server.GetProcessConfiguration()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
d.Server.processConfiguration = c
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Checks if there is a container that already exists for the server. If so that
|
// Checks if there is a container that already exists for the server. If so that
|
||||||
// container is started. If there is no container, one is created and then started.
|
// container is started. If there is no container, one is created and then started.
|
||||||
func (d *DockerEnvironment) Start() error {
|
func (d *DockerEnvironment) Start() error {
|
||||||
|
sawError := false
|
||||||
|
// If sawError is set to true there was an error somewhere in the pipeline that
|
||||||
|
// got passed up, but we also want to ensure we set the server to be offline at
|
||||||
|
// that point.
|
||||||
|
defer func () {
|
||||||
|
if sawError {
|
||||||
|
d.Server.SetState(ProcessOfflineState)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
c, err := d.Client.ContainerInspect(context.Background(), d.Server.Uuid)
|
c, err := d.Client.ContainerInspect(context.Background(), d.Server.Uuid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// @todo what?
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -132,27 +155,36 @@ func (d *DockerEnvironment) Start() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
d.Server.SetState(ProcessStartingState)
|
||||||
|
// Set this to true for now, we will set it to false once we reach the
|
||||||
|
// end of this chain.
|
||||||
|
sawError = true
|
||||||
|
|
||||||
|
// Run the before start function and wait for it to finish.
|
||||||
|
if err := d.OnBeforeStart(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// Truncate the log file so we don't end up outputting a bunch of useless log information
|
// Truncate the log file so we don't end up outputting a bunch of useless log information
|
||||||
// to the websocket and whatnot.
|
// to the websocket and whatnot.
|
||||||
if err := os.Truncate(c.LogPath, 0); err != nil {
|
if err := os.Truncate(c.LogPath, 0); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
d.Server.SetState(ProcessStartingState)
|
|
||||||
|
|
||||||
// Reset the permissions on files for the server before actually trying
|
// Reset the permissions on files for the server before actually trying
|
||||||
// to start it.
|
// to start it.
|
||||||
if err := d.Server.Filesystem.Chown("/"); err != nil {
|
if err := d.Server.Filesystem.Chown("/"); err != nil {
|
||||||
d.Server.SetState(ProcessOfflineState)
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
opts := types.ContainerStartOptions{}
|
opts := types.ContainerStartOptions{}
|
||||||
if err := d.Client.ContainerStart(context.Background(), d.Server.Uuid, opts); err != nil {
|
if err := d.Client.ContainerStart(context.Background(), d.Server.Uuid, opts); err != nil {
|
||||||
d.Server.SetState(ProcessOfflineState)
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// No errors, good to continue through.
|
||||||
|
sawError = false
|
||||||
|
|
||||||
return d.Attach()
|
return d.Attach()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -246,6 +278,7 @@ func (d *DockerEnvironment) FollowConsoleOutput() error {
|
||||||
ShowStderr: true,
|
ShowStderr: true,
|
||||||
ShowStdout: true,
|
ShowStdout: true,
|
||||||
Follow: true,
|
Follow: true,
|
||||||
|
Since: time.Now().Format(time.RFC3339),
|
||||||
}
|
}
|
||||||
|
|
||||||
reader, err := d.Client.ContainerLogs(ctx, d.Server.Uuid, opts)
|
reader, err := d.Client.ContainerLogs(ctx, d.Server.Uuid, opts)
|
||||||
|
@ -270,9 +303,7 @@ func (d *DockerEnvironment) FollowConsoleOutput() error {
|
||||||
// information, instead just sit there with an async process that lets Docker stream all of this data
|
// information, instead just sit there with an async process that lets Docker stream all of this data
|
||||||
// to us automatically.
|
// to us automatically.
|
||||||
func (d *DockerEnvironment) EnableResourcePolling() error {
|
func (d *DockerEnvironment) EnableResourcePolling() error {
|
||||||
fmt.Println("called")
|
|
||||||
if d.Server.State == ProcessOfflineState {
|
if d.Server.State == ProcessOfflineState {
|
||||||
fmt.Println("not running")
|
|
||||||
return errors.New("cannot enable resource polling on a server that is not running")
|
return errors.New("cannot enable resource polling on a server that is not running")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/patrickmn/go-cache"
|
"github.com/patrickmn/go-cache"
|
||||||
|
"github.com/pterodactyl/wings/api"
|
||||||
"github.com/pterodactyl/wings/config"
|
"github.com/pterodactyl/wings/config"
|
||||||
"github.com/remeh/sizedwaitgroup"
|
"github.com/remeh/sizedwaitgroup"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
@ -56,6 +57,11 @@ type Server struct {
|
||||||
|
|
||||||
// All of the registered event listeners for this server instance.
|
// All of the registered event listeners for this server instance.
|
||||||
listeners EventListeners
|
listeners EventListeners
|
||||||
|
|
||||||
|
// Defines the process configuration for the server instance. This is dynamically
|
||||||
|
// fetched from the Pterodactyl Server instance each time the server process is
|
||||||
|
// started, and then cached here.
|
||||||
|
processConfiguration *api.ServerConfiguration
|
||||||
}
|
}
|
||||||
|
|
||||||
// The build settings for a given server that impact docker container creation and
|
// The build settings for a given server that impact docker container creation and
|
||||||
|
@ -183,6 +189,8 @@ func FromConfiguration(data []byte, cfg *config.SystemConfiguration) (*Server, e
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
s.AddEventListeners()
|
||||||
|
|
||||||
withConfiguration := func(e *DockerEnvironment) {
|
withConfiguration := func(e *DockerEnvironment) {
|
||||||
e.User = cfg.User.Uid
|
e.User = cfg.User.Uid
|
||||||
e.TimezonePath = cfg.TimezonePath
|
e.TimezonePath = cfg.TimezonePath
|
||||||
|
@ -207,6 +215,11 @@ func FromConfiguration(data []byte, cfg *config.SystemConfiguration) (*Server, e
|
||||||
}
|
}
|
||||||
s.Resources = &ResourceUsage{}
|
s.Resources = &ResourceUsage{}
|
||||||
|
|
||||||
|
// This is also done when the server is booted, however we need to account for instances
|
||||||
|
// where the server is already running and the Daemon reboots. In those cases this will
|
||||||
|
// allow us to you know, stop servers.
|
||||||
|
s.GetProcessConfiguration()
|
||||||
|
|
||||||
return s, nil
|
return s, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -245,6 +258,8 @@ func (s *Server) SetState(state string) error {
|
||||||
|
|
||||||
s.State = state
|
s.State = state
|
||||||
|
|
||||||
|
zap.S().Debugw("saw server status change event", zap.String("server", s.Uuid), zap.String("status", state))
|
||||||
|
|
||||||
// Emit the event to any listeners that are currently registered.
|
// Emit the event to any listeners that are currently registered.
|
||||||
s.Emit(StatusEvent, s.State)
|
s.Emit(StatusEvent, s.State)
|
||||||
|
|
||||||
|
@ -254,3 +269,8 @@ func (s *Server) SetState(state string) error {
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Gets the process configuration data for the server.
|
||||||
|
func (s *Server) GetProcessConfiguration() (*api.ServerConfiguration, error) {
|
||||||
|
return api.NewRequester().GetServerConfiguration(s.Uuid)
|
||||||
|
}
|
35
server/server_listeners.go
Normal file
35
server/server_listeners.go
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
// Adds all of the internal event listeners we want to use for a server.
|
||||||
|
func (s *Server) AddEventListeners() {
|
||||||
|
s.AddListener(ConsoleOutputEvent, s.onConsoleOutput())
|
||||||
|
}
|
||||||
|
|
||||||
|
var onConsoleOutputListener func(string)
|
||||||
|
|
||||||
|
// Custom listener for console output events that will check if the given line
|
||||||
|
// of output matches one that should mark the server as started or not.
|
||||||
|
func (s *Server) onConsoleOutput() *func(string) {
|
||||||
|
if onConsoleOutputListener == nil {
|
||||||
|
onConsoleOutputListener = func (data string) {
|
||||||
|
// If the specific line of output is one that would mark the server as started,
|
||||||
|
// set the server to that state. Only do this if the server is not currently stopped
|
||||||
|
// or stopping.
|
||||||
|
if s.State == ProcessStartingState && strings.Contains(data, s.processConfiguration.Startup.Done) {
|
||||||
|
zap.S().Debugw(
|
||||||
|
"detected server in running state based on line output", zap.String("match", s.processConfiguration.Startup.Done), zap.String("against", data),
|
||||||
|
)
|
||||||
|
|
||||||
|
s.SetState(ProcessRunningState)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &onConsoleOutputListener
|
||||||
|
}
|
|
@ -142,7 +142,14 @@ func (rt *Router) routeWebsocket(w http.ResponseWriter, r *http.Request, ps http
|
||||||
|
|
||||||
_, p, err := c.ReadMessage()
|
_, p, err := c.ReadMessage()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseGoingAway, websocket.CloseNoStatusReceived, websocket.CloseServiceRestart) {
|
if !websocket.IsCloseError(
|
||||||
|
err,
|
||||||
|
websocket.CloseNormalClosure,
|
||||||
|
websocket.CloseGoingAway,
|
||||||
|
websocket.CloseNoStatusReceived,
|
||||||
|
websocket.CloseServiceRestart,
|
||||||
|
websocket.CloseAbnormalClosure,
|
||||||
|
) {
|
||||||
zap.S().Errorw("error handling websocket message", zap.Error(err))
|
zap.S().Errorw("error handling websocket message", zap.Error(err))
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
|
|
Loading…
Reference in New Issue
Block a user