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 }