Add initial support for fetching egg configuration from panel for servers

This commit is contained in:
Dane Everitt 2019-09-22 20:47:38 -07:00
parent 2a745c5da1
commit d7753d9c7f
No known key found for this signature in database
GPG Key ID: EEA66103B3D71F53
7 changed files with 308 additions and 8 deletions

114
api/api.go Normal file
View 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
View 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
}

View File

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

View File

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

View File

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

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

View File

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