Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
941 changes: 540 additions & 401 deletions client.go

Large diffs are not rendered by default.

56 changes: 0 additions & 56 deletions client_http.go

This file was deleted.

142 changes: 112 additions & 30 deletions client_monitor.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,17 @@ package linodego

import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
"path"

"github.com/go-resty/resty/v2"
"path/filepath"
"strings"
)

const (
Expand All @@ -24,25 +28,36 @@ const (
MonitorAPIEnvVar = "MONITOR_API_TOKEN"
)

// MonitorClient is a wrapper around the Resty client
// MonitorClient is a wrapper around the http client
type MonitorClient struct {
resty *resty.Client
httpClient *http.Client
debug bool
apiBaseURL string
apiProtocol string
apiVersion string
hostURL string
userAgent string
header http.Header
logger Logger
}

// NewMonitorClient is the entry point for user to create a new MonitorClient
// It utilizes default values and looks for environment variables to initialize a MonitorClient.
func NewMonitorClient(hc *http.Client) (mClient MonitorClient) {
if hc != nil {
mClient.resty = resty.NewWithClient(hc)
mClient.httpClient = hc
} else {
mClient.resty = resty.New()
mClient.httpClient = &http.Client{}
}

// Ensure transport is initialized so SetRootCertificate can configure TLS
if mClient.httpClient.Transport == nil {
mClient.httpClient.Transport = &http.Transport{}
}

mClient.header = make(http.Header)
mClient.logger = createLogger()

mClient.SetUserAgent(DefaultUserAgent)

baseURL, baseURLExists := os.LookupEnv(MonitorAPIHostVar)
Expand Down Expand Up @@ -72,32 +87,22 @@ func NewMonitorClient(hc *http.Client) (mClient MonitorClient) {
// SetUserAgent sets a custom user-agent for HTTP requests
func (mc *MonitorClient) SetUserAgent(ua string) *MonitorClient {
mc.userAgent = ua
mc.resty.SetHeader("User-Agent", mc.userAgent)
mc.header.Set("User-Agent", ua)

return mc
}

// R wraps resty's R method
func (mc *MonitorClient) R(ctx context.Context) *resty.Request {
return mc.resty.R().
ExpectContentType("application/json").
SetHeader("Content-Type", "application/json").
SetContext(ctx).
SetError(APIError{})
}

// SetDebug sets the debug on resty's client
// SetDebug sets the debug on the client
func (mc *MonitorClient) SetDebug(debug bool) *MonitorClient {
mc.debug = debug
mc.resty.SetDebug(debug)

return mc
}

// SetLogger allows the user to override the output
// logger for debug logs.
func (mc *MonitorClient) SetLogger(logger Logger) *MonitorClient {
mc.resty.SetLogger(logger)
mc.logger = logger

return mc
}
Expand All @@ -124,21 +129,44 @@ func (mc *MonitorClient) SetAPIVersion(apiVersion string) *MonitorClient {
}

// SetRootCertificate adds a root certificate to the underlying TLS client config
func (mc *MonitorClient) SetRootCertificate(path string) *MonitorClient {
mc.resty.SetRootCertificate(path)
func (mc *MonitorClient) SetRootCertificate(certPath string) *MonitorClient {
transport, ok := mc.httpClient.Transport.(*http.Transport)
if !ok {
mc.logger.Errorf("current transport is not an *http.Transport instance")
return mc
}

if transport.TLSClientConfig == nil {
transport.TLSClientConfig = &tls.Config{
MinVersion: tls.VersionTLS12,
}
}

if transport.TLSClientConfig.RootCAs == nil {
transport.TLSClientConfig.RootCAs = x509.NewCertPool()
}

pem, err := os.ReadFile(filepath.Clean(certPath))
if err != nil {
mc.logger.Errorf("Failed to read root certificate at %s: %s", certPath, err.Error())
return mc
}

transport.TLSClientConfig.RootCAs.AppendCertsFromPEM(pem)

return mc
}

// SetToken sets the API token for all requests from this client
func (mc *MonitorClient) SetToken(token string) *MonitorClient {
mc.resty.SetHeader("Authorization", fmt.Sprintf("Bearer %s", token))
mc.header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
return mc
}

// SetHeader sets a custom header to be used in all API requests made with the current client.
// NOTE: Some headers may be overridden by the individual request functions.
func (mc *MonitorClient) SetHeader(name, value string) {
mc.resty.SetHeader(name, value)
mc.header.Set(name, value)
}

func (mc *MonitorClient) updateMonitorHostURL() {
Expand All @@ -158,12 +186,66 @@ func (mc *MonitorClient) updateMonitorHostURL() {
apiProto = mc.apiProtocol
}

mc.resty.SetBaseURL(
fmt.Sprintf(
"%s://%s/%s",
apiProto,
baseURL,
url.PathEscape(apiVersion),
),
mc.hostURL = fmt.Sprintf(
"%s://%s/%s",
apiProto,
baseURL,
url.PathEscape(apiVersion),
)
}

// doRequest is a generic helper to execute HTTP requests for the MonitorClient
func (mc *MonitorClient) doRequest(ctx context.Context, method, endpoint string, params requestParams) error {
var bodyReader io.Reader

if params.Body != nil {
if _, err := params.Body.Seek(0, io.SeekStart); err != nil {
return fmt.Errorf("failed to seek body: %w", err)
}

bodyReader = params.Body
}

reqURL := fmt.Sprintf("%s/%s", strings.TrimRight(mc.hostURL, "/"), strings.TrimLeft(endpoint, "/"))

req, err := http.NewRequestWithContext(ctx, method, reqURL, bodyReader)
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
}

req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")

for name, values := range mc.header {
for _, value := range values {
req.Header.Set(name, value)
}
}

if mc.debug && mc.logger != nil {
mc.logger.Debugf("Sending request: %s %s", method, reqURL)
}

resp, err := mc.httpClient.Do(req)
if err != nil {
return fmt.Errorf("failed to send request: %w", err)
}
defer resp.Body.Close()

_, err = coupleAPIErrors(resp, nil)
if err != nil {
return err
}

if mc.debug && mc.logger != nil {
mc.logger.Debugf("Received response: %s", resp.Status)
}

if params.Response != nil {
if err := json.NewDecoder(resp.Body).Decode(params.Response); err != nil {
return fmt.Errorf("failed to decode response: %w", err)
}
}

return nil
}
Loading
Loading