2022-10-16 20:17:24 +00:00
|
|
|
package garage
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"context"
|
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"net/http"
|
|
|
|
)
|
|
|
|
|
|
|
|
// AdminClient is a helper type for performing actions against the garage admin
|
|
|
|
// interface.
|
|
|
|
type AdminClient struct {
|
|
|
|
c *http.Client
|
|
|
|
addr string
|
|
|
|
adminToken string
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewAdminClient initializes and returns an AdminClient which will use the
|
|
|
|
// given address and adminToken for all requests made.
|
|
|
|
func NewAdminClient(addr, adminToken string) *AdminClient {
|
|
|
|
return &AdminClient{
|
|
|
|
c: &http.Client{
|
|
|
|
Transport: http.DefaultTransport.(*http.Transport).Clone(),
|
|
|
|
},
|
|
|
|
addr: addr,
|
|
|
|
adminToken: adminToken,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Do performs an HTTP request with the given method (GET, POST) and path, and
|
|
|
|
// using the json marshaling of the given body as the request body (unless body
|
|
|
|
// is nil). It will JSON unmarshal the response into rcv, unless rcv is nil.
|
|
|
|
func (c *AdminClient) Do(
|
|
|
|
ctx context.Context, rcv interface{}, method, path string, body interface{},
|
|
|
|
) error {
|
|
|
|
|
|
|
|
var bodyR io.Reader
|
|
|
|
|
|
|
|
if body != nil {
|
|
|
|
bodyBuf := new(bytes.Buffer)
|
|
|
|
bodyR = bodyBuf
|
|
|
|
|
|
|
|
if err := json.NewEncoder(bodyBuf).Encode(body); err != nil {
|
|
|
|
return fmt.Errorf("json marshaling body: %w", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
urlStr := fmt.Sprintf("http://%s%s", c.addr, path)
|
|
|
|
|
|
|
|
req, err := http.NewRequestWithContext(ctx, method, urlStr, bodyR)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("initializing request: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
req.Header.Set("Authorization", "Bearer "+c.adminToken)
|
|
|
|
|
|
|
|
res, err := c.c.Do(req)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("performing http request: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
defer res.Body.Close()
|
|
|
|
|
|
|
|
if res.StatusCode != 200 {
|
|
|
|
return fmt.Errorf("unexpected %s response returned", res.Status)
|
|
|
|
}
|
|
|
|
|
|
|
|
if rcv == nil {
|
|
|
|
|
|
|
|
if _, err := io.Copy(io.Discard, res.Body); err != nil {
|
|
|
|
return fmt.Errorf("discarding response body: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := json.NewDecoder(res.Body).Decode(rcv); err != nil {
|
|
|
|
return fmt.Errorf("decoding json response body: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
2022-10-16 20:17:26 +00:00
|
|
|
|
|
|
|
// Wait will block until the instance connected to can see at least
|
|
|
|
// ReplicationFactor-1 other garage instances. If the context is canceled it
|
|
|
|
// will return the context error.
|
|
|
|
func (c *AdminClient) Wait(ctx context.Context) error {
|
|
|
|
for {
|
|
|
|
|
|
|
|
var clusterStatus struct {
|
|
|
|
KnownNodes map[string]struct {
|
|
|
|
IsUp bool `json:"is_up"`
|
|
|
|
} `json:"knownNodes"`
|
|
|
|
}
|
|
|
|
|
|
|
|
err := c.Do(ctx, &clusterStatus, "GET", "/v0/status", nil)
|
|
|
|
|
|
|
|
if ctxErr := ctx.Err(); ctxErr != nil {
|
|
|
|
return ctxErr
|
|
|
|
|
|
|
|
} else if err != nil {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
var numUp int
|
|
|
|
|
|
|
|
for _, knownNode := range clusterStatus.KnownNodes {
|
|
|
|
if knownNode.IsUp {
|
|
|
|
numUp++
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if numUp >= ReplicationFactor-1 {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|