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.
|
||||
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
|
||||
// can be started an error should be returned.
|
||||
Start() error
|
||||
|
|
|
@ -113,12 +113,35 @@ func (d *DockerEnvironment) IsRunning() (bool, error) {
|
|||
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
|
||||
// container is started. If there is no container, one is created and then started.
|
||||
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)
|
||||
if err != nil {
|
||||
// @todo what?
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -132,27 +155,36 @@ func (d *DockerEnvironment) Start() error {
|
|||
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
|
||||
// to the websocket and whatnot.
|
||||
if err := os.Truncate(c.LogPath, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d.Server.SetState(ProcessStartingState)
|
||||
|
||||
// Reset the permissions on files for the server before actually trying
|
||||
// to start it.
|
||||
if err := d.Server.Filesystem.Chown("/"); err != nil {
|
||||
d.Server.SetState(ProcessOfflineState)
|
||||
return err
|
||||
}
|
||||
|
||||
opts := types.ContainerStartOptions{}
|
||||
if err := d.Client.ContainerStart(context.Background(), d.Server.Uuid, opts); err != nil {
|
||||
d.Server.SetState(ProcessOfflineState)
|
||||
return err
|
||||
}
|
||||
|
||||
// No errors, good to continue through.
|
||||
sawError = false
|
||||
|
||||
return d.Attach()
|
||||
}
|
||||
|
||||
|
@ -246,6 +278,7 @@ func (d *DockerEnvironment) FollowConsoleOutput() error {
|
|||
ShowStderr: true,
|
||||
ShowStdout: true,
|
||||
Follow: true,
|
||||
Since: time.Now().Format(time.RFC3339),
|
||||
}
|
||||
|
||||
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
|
||||
// to us automatically.
|
||||
func (d *DockerEnvironment) EnableResourcePolling() error {
|
||||
fmt.Println("called")
|
||||
if d.Server.State == ProcessOfflineState {
|
||||
fmt.Println("not running")
|
||||
return errors.New("cannot enable resource polling on a server that is not running")
|
||||
}
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"github.com/patrickmn/go-cache"
|
||||
"github.com/pterodactyl/wings/api"
|
||||
"github.com/pterodactyl/wings/config"
|
||||
"github.com/remeh/sizedwaitgroup"
|
||||
"go.uber.org/zap"
|
||||
|
@ -56,6 +57,11 @@ type Server struct {
|
|||
|
||||
// All of the registered event listeners for this server instance.
|
||||
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
|
||||
|
@ -183,6 +189,8 @@ func FromConfiguration(data []byte, cfg *config.SystemConfiguration) (*Server, e
|
|||
return nil, err
|
||||
}
|
||||
|
||||
s.AddEventListeners()
|
||||
|
||||
withConfiguration := func(e *DockerEnvironment) {
|
||||
e.User = cfg.User.Uid
|
||||
e.TimezonePath = cfg.TimezonePath
|
||||
|
@ -207,6 +215,11 @@ func FromConfiguration(data []byte, cfg *config.SystemConfiguration) (*Server, e
|
|||
}
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -245,6 +258,8 @@ func (s *Server) SetState(state string) error {
|
|||
|
||||
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.
|
||||
s.Emit(StatusEvent, s.State)
|
||||
|
||||
|
@ -254,3 +269,8 @@ func (s *Server) SetState(state string) error {
|
|||
|
||||
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()
|
||||
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))
|
||||
}
|
||||
break
|
||||
|
|
Loading…
Reference in New Issue
Block a user