github.com/filecoin-project/bacalhau@v0.3.23-0.20230228154132-45c989550ace/pkg/publicapi/client.go (about)

     1  package publicapi
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"encoding/json"
     7  	"fmt"
     8  	"io"
     9  	"net/http"
    10  	"reflect"
    11  	"time"
    12  
    13  	"github.com/filecoin-project/bacalhau/pkg/bacerrors"
    14  	"github.com/filecoin-project/bacalhau/pkg/model"
    15  	"github.com/filecoin-project/bacalhau/pkg/system"
    16  	"github.com/filecoin-project/bacalhau/pkg/util/closer"
    17  	"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
    18  	"go.opentelemetry.io/otel/attribute"
    19  	"go.opentelemetry.io/otel/trace"
    20  )
    21  
    22  // APIClient is a utility for interacting with a node's API server.
    23  type APIClient struct {
    24  	BaseURI        string
    25  	DefaultHeaders map[string]string
    26  
    27  	Client *http.Client
    28  }
    29  
    30  // NewAPIClient returns a new client for a node's API server.
    31  func NewAPIClient(baseURI string) *APIClient {
    32  	return &APIClient{
    33  		BaseURI:        baseURI,
    34  		DefaultHeaders: map[string]string{},
    35  
    36  		Client: &http.Client{
    37  			Timeout: 300 * time.Second,
    38  			Transport: otelhttp.NewTransport(nil,
    39  				otelhttp.WithSpanOptions(
    40  					trace.WithAttributes(
    41  						attribute.String("clientID", system.GetClientID()),
    42  					),
    43  				),
    44  			),
    45  		},
    46  	}
    47  }
    48  
    49  // Alive calls the node's API server health check.
    50  func (apiClient *APIClient) Alive(ctx context.Context) (bool, error) {
    51  	ctx, span := system.NewSpan(ctx, system.GetTracer(), "pkg/publicapi.Client.Alive")
    52  	defer span.End()
    53  
    54  	var body io.Reader
    55  	req, err := http.NewRequestWithContext(ctx, http.MethodGet, apiClient.BaseURI+"/livez", body)
    56  	if err != nil {
    57  		return false, nil
    58  	}
    59  	res, err := apiClient.Client.Do(req) //nolint:bodyclose // golangcilint is dumb - this is closed
    60  	if err != nil {
    61  		return false, nil
    62  	}
    63  	defer closer.DrainAndCloseWithLogOnError(ctx, "apiClient response", res.Body)
    64  
    65  	return res.StatusCode == http.StatusOK, nil
    66  }
    67  
    68  func (apiClient *APIClient) Version(ctx context.Context) (*model.BuildVersionInfo, error) {
    69  	ctx, span := system.NewSpan(ctx, system.GetTracer(), "pkg/publicapi.Client.Version")
    70  	defer span.End()
    71  
    72  	req := VersionRequest{
    73  		ClientID: system.GetClientID(),
    74  	}
    75  
    76  	var res VersionResponse
    77  	if err := apiClient.Post(ctx, "version", req, &res); err != nil {
    78  		return nil, err
    79  	}
    80  
    81  	return res.VersionInfo, nil
    82  }
    83  
    84  func (apiClient *APIClient) Post(ctx context.Context, api string, reqData, resData interface{}) error {
    85  	ctx, span := system.NewSpan(ctx, system.GetTracer(), "pkg/publicapi.Client.Post")
    86  	defer span.End()
    87  
    88  	var body bytes.Buffer
    89  	var err error
    90  	if err = json.NewEncoder(&body).Encode(reqData); err != nil {
    91  		return bacerrors.NewResponseUnknownError(fmt.Errorf("publicapi: error encoding request body: %v", err))
    92  	}
    93  
    94  	addr := fmt.Sprintf("%s/%s", apiClient.BaseURI, api)
    95  	req, err := http.NewRequestWithContext(ctx, http.MethodPost, addr, &body)
    96  	if err != nil {
    97  		return bacerrors.NewResponseUnknownError(fmt.Errorf("publicapi: error creating Post request: %v", err))
    98  	}
    99  	req.Header.Set("Content-type", "application/json")
   100  	for header, value := range apiClient.DefaultHeaders {
   101  		req.Header.Set(header, value)
   102  	}
   103  	req.Close = true // don't keep connections lying around
   104  
   105  	var res *http.Response
   106  	res, err = apiClient.Client.Do(req)
   107  	if err != nil {
   108  		errString := err.Error()
   109  		if errorResponse, ok := err.(*bacerrors.ErrorResponse); ok {
   110  			return errorResponse
   111  		} else if errString == "context canceled" {
   112  			return bacerrors.NewContextCanceledError(err.Error())
   113  		} else {
   114  			return bacerrors.NewResponseUnknownError(fmt.Errorf("publicapi: after posting request: %v", err))
   115  		}
   116  	}
   117  
   118  	defer func() {
   119  		if err = res.Body.Close(); err != nil {
   120  			err = fmt.Errorf("error closing response body: %v", err)
   121  		}
   122  	}()
   123  
   124  	if res.StatusCode != http.StatusOK {
   125  		var responseBody []byte
   126  		responseBody, err = io.ReadAll(res.Body)
   127  		if err != nil {
   128  			return bacerrors.NewResponseUnknownError(fmt.Errorf("publicapi: error reading response body: %v", err))
   129  		}
   130  
   131  		var serverError *bacerrors.ErrorResponse
   132  		if err = model.JSONUnmarshalWithMax(responseBody, &serverError); err != nil {
   133  			return bacerrors.NewResponseUnknownError(fmt.Errorf("publicapi: after posting request: %v",
   134  				string(responseBody)))
   135  		}
   136  
   137  		if !reflect.DeepEqual(serverError, bacerrors.BacalhauErrorInterface(nil)) {
   138  			return serverError
   139  		}
   140  	}
   141  
   142  	err = json.NewDecoder(res.Body).Decode(resData)
   143  	if err != nil {
   144  		if err == io.EOF {
   145  			return nil // No error, just no data
   146  		} else {
   147  			return bacerrors.NewResponseUnknownError(fmt.Errorf("publicapi: error decoding response body: %v", err))
   148  		}
   149  	}
   150  
   151  	return nil
   152  }