230 lines
5.9 KiB
Go
230 lines
5.9 KiB
Go
package scaleset
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"log/slog"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/hashicorp/go-retryablehttp"
|
|
)
|
|
|
|
const (
|
|
headerActionsActivityID = "ActivityId"
|
|
headerGitHubRequestID = "X-GitHub-Request-Id"
|
|
)
|
|
|
|
type commonClient struct {
|
|
httpClient *http.Client
|
|
|
|
systemInfo SystemInfo // never set directly, use setSystemInfoUnlocked
|
|
|
|
userAgent string
|
|
|
|
httpClientOption
|
|
}
|
|
|
|
func newCommonClient(systemInfo SystemInfo, httpClientOption httpClientOption) *commonClient {
|
|
c := &commonClient{
|
|
httpClientOption: httpClientOption,
|
|
}
|
|
c.setSystemInfo(systemInfo)
|
|
|
|
retryableHTTPClient, err := httpClientOption.newRetryableHTTPClient()
|
|
if err != nil {
|
|
panic(fmt.Sprintf("failed to create retryable HTTP client: %v", err))
|
|
}
|
|
c.httpClient = retryableHTTPClient.StandardClient()
|
|
|
|
return c
|
|
}
|
|
|
|
func (c *commonClient) newRetryableHTTPClient() (*retryablehttp.Client, error) {
|
|
return c.httpClientOption.newRetryableHTTPClient()
|
|
}
|
|
|
|
func (c *commonClient) do(req *http.Request) (*http.Response, error) {
|
|
return sendRequest(c.httpClient, req)
|
|
}
|
|
|
|
// sendRequest ensures that the request is sent and the response body is fully read and closed.
|
|
// It trims the BOM when present in the response body.
|
|
//
|
|
// Make sure to use this function instead of http.Client.Do directly to avoid issues.
|
|
func sendRequest(c *http.Client, req *http.Request) (*http.Response, error) {
|
|
resp, err := c.Do(req)
|
|
if err != nil {
|
|
return nil, newRequestResponseError(req, resp, fmt.Errorf("failed to send request: %w", err))
|
|
}
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, newRequestResponseError(req, resp, fmt.Errorf("failed to read the response body: %w", err))
|
|
}
|
|
if err := resp.Body.Close(); err != nil {
|
|
return nil, newRequestResponseError(req, resp, fmt.Errorf("failed to close the response body: %w", err))
|
|
}
|
|
|
|
body = trimByteOrderMark(body)
|
|
resp.Body = io.NopCloser(bytes.NewReader(body))
|
|
return resp, nil
|
|
}
|
|
|
|
type httpClientOption struct {
|
|
logger *slog.Logger
|
|
|
|
// Options for built-in retryable HTTP client.
|
|
// Ignored if a custom retryable HTTP client is provided via WithRetryableHTTPClint.
|
|
retryMax int
|
|
retryWaitMax time.Duration
|
|
|
|
// fields added to the transport if specified
|
|
rootCAs *x509.CertPool
|
|
tlsInsecureSkipVerify bool
|
|
proxyFunc ProxyFunc
|
|
timeout time.Duration
|
|
|
|
retryableHTTPClient *retryablehttp.Client
|
|
}
|
|
|
|
func (o *httpClientOption) defaults() {
|
|
if o.logger == nil {
|
|
o.logger = slog.New(slog.DiscardHandler)
|
|
}
|
|
if o.retryMax == 0 {
|
|
o.retryMax = 4
|
|
}
|
|
if o.retryWaitMax == 0 {
|
|
o.retryWaitMax = 30 * time.Second
|
|
}
|
|
if o.timeout == 0 {
|
|
o.timeout = 5 * time.Minute
|
|
}
|
|
}
|
|
|
|
func (o *httpClientOption) newRetryableHTTPClient() (*retryablehttp.Client, error) {
|
|
var retryClient *retryablehttp.Client
|
|
if o.retryableHTTPClient != nil {
|
|
retryClient = o.retryableHTTPClient
|
|
} else {
|
|
retryClient = retryablehttp.NewClient()
|
|
retryClient.RetryMax = o.retryMax
|
|
retryClient.RetryWaitMax = o.retryWaitMax
|
|
}
|
|
|
|
if retryClient.HTTPClient.Timeout == 0 {
|
|
retryClient.HTTPClient.Timeout = o.timeout
|
|
}
|
|
|
|
retryClient.Logger = o.logger
|
|
|
|
transport, ok := retryClient.HTTPClient.Transport.(*http.Transport)
|
|
if !ok {
|
|
// this should always be true, because retryablehttp.NewClient() uses
|
|
// cleanhttp.DefaultPooledTransport()
|
|
return nil, fmt.Errorf("failed to get http transport from retryablehttp client")
|
|
}
|
|
if transport.TLSClientConfig == nil {
|
|
transport.TLSClientConfig = &tls.Config{}
|
|
}
|
|
|
|
if o.rootCAs != nil {
|
|
transport.TLSClientConfig.RootCAs = o.rootCAs
|
|
}
|
|
|
|
if o.tlsInsecureSkipVerify {
|
|
transport.TLSClientConfig.InsecureSkipVerify = true
|
|
}
|
|
|
|
if o.proxyFunc != nil {
|
|
transport.Proxy = o.proxyFunc
|
|
}
|
|
|
|
retryClient.HTTPClient.Transport = transport
|
|
|
|
return retryClient, nil
|
|
}
|
|
|
|
func (c *commonClient) setSystemInfo(info SystemInfo) {
|
|
c.systemInfo = info
|
|
c.setUserAgent()
|
|
}
|
|
|
|
func (c *commonClient) setUserAgent() {
|
|
b, _ := json.Marshal(userAgent{
|
|
SystemInfo: c.systemInfo,
|
|
BuildVersion: buildInfo.version,
|
|
BuildCommitSHA: buildInfo.commitSHA,
|
|
Kind: "scaleset",
|
|
})
|
|
c.userAgent = string(b)
|
|
}
|
|
|
|
// HTTPOption defines a functional option for configuring the Client.
|
|
type HTTPOption func(*httpClientOption)
|
|
|
|
// WithRetryableHTTPClint allows users to provide a custom retryable HTTP client.
|
|
// If not set, a default client will be used with the specified retry and timeout settings.
|
|
func WithRetryableHTTPClint(client *retryablehttp.Client) HTTPOption {
|
|
return func(c *httpClientOption) {
|
|
c.retryableHTTPClient = client
|
|
}
|
|
}
|
|
|
|
// WithLogger sets a custom logger for the Client.
|
|
// If nil is passed, a discard logger will be used.
|
|
func WithLogger(logger *slog.Logger) HTTPOption {
|
|
return func(c *httpClientOption) {
|
|
if logger == nil {
|
|
logger = slog.New(slog.DiscardHandler)
|
|
}
|
|
c.logger = logger
|
|
}
|
|
}
|
|
|
|
// WithRetryMax sets the maximum number of retries for the Client.
|
|
func WithRetryMax(retryMax int) HTTPOption {
|
|
return func(c *httpClientOption) {
|
|
c.retryMax = retryMax
|
|
}
|
|
}
|
|
|
|
// WithRetryWaitMax sets the maximum wait time between retries for the Client.
|
|
func WithRetryWaitMax(retryWaitMax time.Duration) HTTPOption {
|
|
return func(c *httpClientOption) {
|
|
c.retryWaitMax = retryWaitMax
|
|
}
|
|
}
|
|
|
|
// WithRootCAs sets custom root certificate authorities for the Client.
|
|
func WithRootCAs(rootCAs *x509.CertPool) HTTPOption {
|
|
return func(c *httpClientOption) {
|
|
c.rootCAs = rootCAs
|
|
}
|
|
}
|
|
|
|
// WithoutTLSVerify disables TLS certificate verification for the Client.
|
|
func WithoutTLSVerify() HTTPOption {
|
|
return func(c *httpClientOption) {
|
|
c.tlsInsecureSkipVerify = true
|
|
}
|
|
}
|
|
|
|
// WithProxy sets a custom proxy function for the Client.
|
|
func WithProxy(proxyFunc ProxyFunc) HTTPOption {
|
|
return func(c *httpClientOption) {
|
|
c.proxyFunc = proxyFunc
|
|
}
|
|
}
|
|
|
|
// WithTimeout sets a timeout for the Client.
|
|
func WithTimeout(duration time.Duration) HTTPOption {
|
|
return func(c *httpClientOption) {
|
|
c.timeout = duration
|
|
}
|
|
}
|