Code cleanup for remote client

This commit is contained in:
Dane Everitt 2021-02-01 21:43:04 -08:00
parent e3b0b91912
commit 065da77afa
No known key found for this signature in database
GPG Key ID: EEA66103B3D71F53
9 changed files with 161 additions and 175 deletions

View File

@ -137,11 +137,12 @@ func rootCmdRun(cmd *cobra.Command, _ []string) {
"gid": config.Get().System.User.Gid, "gid": config.Get().System.User.Gid,
}).Info("configured system user successfully") }).Info("configured system user successfully")
pclient := remote.CreateClient( pclient := remote.New(
config.Get().PanelLocation, config.Get().PanelLocation,
config.Get().AuthenticationTokenId, remote.WithCredentials(config.Get().AuthenticationTokenId, config.Get().AuthenticationToken),
config.Get().AuthenticationToken, remote.WithHttpClient(&http.Client{
remote.WithTimeout(time.Second*time.Duration(config.Get().RemoteQuery.Timeout)), Timeout: time.Second * time.Duration(config.Get().RemoteQuery.Timeout),
}),
) )
manager, err := server.NewManager(cmd.Context(), pclient) manager, err := server.NewManager(cmd.Context(), pclient)

View File

@ -1,45 +0,0 @@
package remote
import (
"context"
"fmt"
"strconv"
)
func (c *client) GetBackupRemoteUploadURLs(ctx context.Context, backup string, size int64) (BackupRemoteUploadResponse, error) {
var data BackupRemoteUploadResponse
res, err := c.get(ctx, fmt.Sprintf("/backups/%s", backup), q{"size": strconv.FormatInt(size, 10)})
if err != nil {
return data, err
}
defer res.Body.Close()
if res.HasError() {
return data, res.Error()
}
err = res.BindJSON(&data)
return data, err
}
func (c *client) SetBackupStatus(ctx context.Context, backup string, data BackupRequest) error {
resp, err := c.post(ctx, fmt.Sprintf("/backups/%s", backup), data)
if err != nil {
return err
}
defer resp.Body.Close()
return resp.Error()
}
// SendRestorationStatus triggers a request to the Panel to notify it that a
// restoration has been completed and the server should be marked as being
// activated again.
func (c *client) SendRestorationStatus(ctx context.Context, backup string, successful bool) error {
resp, err := c.post(ctx, fmt.Sprintf("/backups/%s/restore", backup), d{"successful": successful})
if err != nil {
return err
}
defer resp.Body.Close()
return resp.Error()
}

View File

@ -1,55 +0,0 @@
package remote
import (
"context"
"net/http"
"strings"
"time"
)
type Client interface {
GetBackupRemoteUploadURLs(ctx context.Context, backup string, size int64) (BackupRemoteUploadResponse, error)
GetInstallationScript(ctx context.Context, uuid string) (InstallationScript, error)
GetServerConfiguration(ctx context.Context, uuid string) (ServerConfigurationResponse, error)
GetServers(context context.Context, perPage int) ([]RawServerData, error)
SetArchiveStatus(ctx context.Context, uuid string, successful bool) error
SetBackupStatus(ctx context.Context, backup string, data BackupRequest) error
SendRestorationStatus(ctx context.Context, backup string, successful bool) error
SetInstallationStatus(ctx context.Context, uuid string, successful bool) error
SetTransferStatus(ctx context.Context, uuid string, successful bool) error
ValidateSftpCredentials(ctx context.Context, request SftpAuthRequest) (SftpAuthResponse, error)
}
type client struct {
httpClient *http.Client
baseUrl string
tokenId string
token string
retries int
}
type ClientOption func(c *client)
func CreateClient(base, tokenId, token string, opts ...ClientOption) Client {
httpClient := &http.Client{
Timeout: time.Second * 15,
}
base = strings.TrimSuffix(base, "/")
c := &client{
baseUrl: base + "/api/remote",
tokenId: tokenId,
token: token,
httpClient: httpClient,
retries: 3,
}
for _, o := range opts {
o(c)
}
return c
}
func WithTimeout(timeout time.Duration) ClientOption {
return func(c *client) {
c.httpClient.Timeout = timeout
}
}

View File

@ -1,19 +0,0 @@
package remote
import (
"net/http"
"net/http/httptest"
)
func createTestClient(h http.HandlerFunc) (*client, *httptest.Server) {
s := httptest.NewServer(h)
c := &client{
httpClient: s.Client(),
baseUrl: s.URL,
retries: 1,
tokenId: "testid",
token: "testtoken",
}
return c, s
}

View File

@ -9,34 +9,65 @@ import (
"io" "io"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"strings"
"time"
"github.com/apex/log"
"github.com/pterodactyl/wings/system" "github.com/pterodactyl/wings/system"
) )
// A generic type allowing for easy binding use when making requests to API type Client interface {
// endpoints that only expect a singular argument or something that would not GetBackupRemoteUploadURLs(ctx context.Context, backup string, size int64) (BackupRemoteUploadResponse, error)
// benefit from being a typed struct. GetInstallationScript(ctx context.Context, uuid string) (InstallationScript, error)
// GetServerConfiguration(ctx context.Context, uuid string) (ServerConfigurationResponse, error)
// Inspired by gin.H, same concept. GetServers(context context.Context, perPage int) ([]RawServerData, error)
type d map[string]interface{} SetArchiveStatus(ctx context.Context, uuid string, successful bool) error
SetBackupStatus(ctx context.Context, backup string, data BackupRequest) error
// Same concept as d, but a map of strings, used for querying GET requests. SendRestorationStatus(ctx context.Context, backup string, successful bool) error
type q map[string]string SetInstallationStatus(ctx context.Context, uuid string, successful bool) error
SetTransferStatus(ctx context.Context, uuid string, successful bool) error
// Response is a custom response type that allows for commonly used error ValidateSftpCredentials(ctx context.Context, request SftpAuthRequest) (SftpAuthResponse, error)
// handling and response parsing from the Panel API. This just embeds the normal
// HTTP response from Go and we attach a few helper functions to it.
type Response struct {
*http.Response
} }
type Pagination struct { type client struct {
CurrentPage uint `json:"current_page"` httpClient *http.Client
From uint `json:"from"` baseUrl string
LastPage uint `json:"last_page"` tokenId string
PerPage uint `json:"per_page"` token string
To uint `json:"to"` retries int
Total uint `json:"total"` }
// New returns a new HTTP request client that is used for making authenticated
// requests to the Panel that this instance is running under.
func New(base string, opts ...ClientOption) Client {
c := client{
baseUrl: strings.TrimSuffix(base, "/") + "/api/remote",
httpClient: &http.Client{
Timeout: time.Second * 15,
},
retries: 3,
}
for _, opt := range opts {
opt(&c)
}
return &c
}
// WithCredentials sets the credentials to use when making request to the remote
// API endpoint.
func WithCredentials(id, token string) ClientOption {
return func(c *client) {
c.tokenId = id
c.token = token
}
}
// WithHttpClient sets the underlying HTTP client instance to use when making
// requests to the Panel API.
func WithHttpClient(httpClient *http.Client) ClientOption {
return func(c *client) {
c.httpClient = httpClient
}
} }
// requestOnce creates a http request and executes it once. Prefer request() // requestOnce creates a http request and executes it once. Prefer request()
@ -103,6 +134,13 @@ func (c *client) post(ctx context.Context, path string, data interface{}) (*Resp
return c.request(ctx, http.MethodPost, path, bytes.NewBuffer(b)) return c.request(ctx, http.MethodPost, path, bytes.NewBuffer(b))
} }
// Response is a custom response type that allows for commonly used error
// handling and response parsing from the Panel API. This just embeds the normal
// HTTP response from Go and we attach a few helper functions to it.
type Response struct {
*http.Response
}
// HasError determines if the API call encountered an error. If no request has // HasError determines if the API call encountered an error. If no request has
// been made the response will be false. This function will evaluate to true if // been made the response will be false. This function will evaluate to true if
// the response code is anything 300 or higher. // the response code is anything 300 or higher.
@ -165,3 +203,26 @@ func (r *Response) Error() error {
return e return e
} }
// Logs the request into the debug log with all of the important request bits.
// The authorization key will be cleaned up before being output.
func debugLogRequest(req *http.Request) {
if l, ok := log.Log.(*log.Logger); ok && l.Level != log.DebugLevel {
return
}
headers := make(map[string][]string)
for k, v := range req.Header {
if k != "Authorization" || len(v) == 0 || len(v[0]) == 0 {
headers[k] = v
continue
}
headers[k] = []string{"(redacted)"}
}
log.WithFields(log.Fields{
"method": req.Method,
"endpoint": req.URL.String(),
"headers": headers,
}).Debug("making request to external HTTP endpoint")
}

View File

@ -3,11 +3,25 @@ package remote
import ( import (
"context" "context"
"net/http" "net/http"
"net/http/httptest"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func createTestClient(h http.HandlerFunc) (*client, *httptest.Server) {
s := httptest.NewServer(h)
c := &client{
httpClient: s.Client(),
baseUrl: s.URL,
retries: 1,
tokenId: "testid",
token: "testtoken",
}
return c, s
}
func TestRequest(t *testing.T) { func TestRequest(t *testing.T) {
c, _ := createTestClient(func(rw http.ResponseWriter, r *http.Request) { c, _ := createTestClient(func(rw http.ResponseWriter, r *http.Request) {
assert.Equal(t, "application/vnd.pterodactyl.v1+json", r.Header.Get("Accept")) assert.Equal(t, "application/vnd.pterodactyl.v1+json", r.Header.Get("Accept"))

View File

@ -144,6 +144,44 @@ func (c *client) ValidateSftpCredentials(ctx context.Context, request SftpAuthRe
return auth, err return auth, err
} }
func (c *client) GetBackupRemoteUploadURLs(ctx context.Context, backup string, size int64) (BackupRemoteUploadResponse, error) {
var data BackupRemoteUploadResponse
res, err := c.get(ctx, fmt.Sprintf("/backups/%s", backup), q{"size": strconv.FormatInt(size, 10)})
if err != nil {
return data, err
}
defer res.Body.Close()
if res.HasError() {
return data, res.Error()
}
err = res.BindJSON(&data)
return data, err
}
func (c *client) SetBackupStatus(ctx context.Context, backup string, data BackupRequest) error {
resp, err := c.post(ctx, fmt.Sprintf("/backups/%s", backup), data)
if err != nil {
return err
}
defer resp.Body.Close()
return resp.Error()
}
// SendRestorationStatus triggers a request to the Panel to notify it that a
// restoration has been completed and the server should be marked as being
// activated again.
func (c *client) SendRestorationStatus(ctx context.Context, backup string, successful bool) error {
resp, err := c.post(ctx, fmt.Sprintf("/backups/%s/restore", backup), d{"successful": successful})
if err != nil {
return err
}
defer resp.Body.Close()
return resp.Error()
}
// getServersPaged returns a subset of servers from the Panel API using the // getServersPaged returns a subset of servers from the Panel API using the
// pagination query parameters. // pagination query parameters.
func (c *client) getServersPaged(ctx context.Context, page, limit int) ([]RawServerData, Pagination, error) { func (c *client) getServersPaged(ctx context.Context, page, limit int) ([]RawServerData, Pagination, error) {

View File

@ -9,6 +9,27 @@ import (
"github.com/pterodactyl/wings/parser" "github.com/pterodactyl/wings/parser"
) )
// A generic type allowing for easy binding use when making requests to API
// endpoints that only expect a singular argument or something that would not
// benefit from being a typed struct.
//
// Inspired by gin.H, same concept.
type d map[string]interface{}
// Same concept as d, but a map of strings, used for querying GET requests.
type q map[string]string
type ClientOption func(c *client)
type Pagination struct {
CurrentPage uint `json:"current_page"`
From uint `json:"from"`
LastPage uint `json:"last_page"`
PerPage uint `json:"per_page"`
To uint `json:"to"`
Total uint `json:"total"`
}
// ServerConfigurationResponse holds the server configuration data returned from // ServerConfigurationResponse holds the server configuration data returned from
// the Panel. When a server process is started, Wings communicates with the // the Panel. When a server process is started, Wings communicates with the
// Panel to fetch the latest build information as well as get all of the details // Panel to fetch the latest build information as well as get all of the details

View File

@ -1,30 +0,0 @@
package remote
import (
"net/http"
"github.com/apex/log"
)
// Logs the request into the debug log with all of the important request bits.
// The authorization key will be cleaned up before being output.
func debugLogRequest(req *http.Request) {
if l, ok := log.Log.(*log.Logger); ok && l.Level != log.DebugLevel {
return
}
headers := make(map[string][]string)
for k, v := range req.Header {
if k != "Authorization" || len(v) == 0 || len(v[0]) == 0 {
headers[k] = v
continue
}
headers[k] = []string{"(redacted)"}
}
log.WithFields(log.Fields{
"method": req.Method,
"endpoint": req.URL.String(),
"headers": headers,
}).Debug("making request to external HTTP endpoint")
}