2022-10-16 20:17:24 +00:00
|
|
|
package garage
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"context"
|
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"net/http"
|
2022-11-13 15:45:42 +00:00
|
|
|
"net/http/httputil"
|
2022-11-05 14:23:29 +00:00
|
|
|
"time"
|
2022-11-13 15:45:42 +00:00
|
|
|
|
2022-11-16 16:25:55 +00:00
|
|
|
"github.com/mediocregopher/mediocre-go-lib/v2/mctx"
|
2022-11-13 15:45:42 +00:00
|
|
|
"github.com/mediocregopher/mediocre-go-lib/v2/mlog"
|
2022-10-16 20:17:24 +00:00
|
|
|
)
|
|
|
|
|
2022-10-25 19:15:09 +00:00
|
|
|
// AdminClientError gets returned from AdminClient's Do method for non-200
|
|
|
|
// errors.
|
|
|
|
type AdminClientError struct {
|
|
|
|
StatusCode int
|
|
|
|
Body []byte
|
|
|
|
}
|
|
|
|
|
|
|
|
func (e AdminClientError) Error() string {
|
|
|
|
return fmt.Sprintf("%d response from admin: %q", e.StatusCode, e.Body)
|
|
|
|
}
|
|
|
|
|
2022-10-16 20:17:24 +00:00
|
|
|
// AdminClient is a helper type for performing actions against the garage admin
|
|
|
|
// interface.
|
|
|
|
type AdminClient struct {
|
|
|
|
c *http.Client
|
|
|
|
addr string
|
|
|
|
adminToken string
|
2022-11-13 15:45:42 +00:00
|
|
|
logger *mlog.Logger
|
2022-10-16 20:17:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// NewAdminClient initializes and returns an AdminClient which will use the
|
|
|
|
// given address and adminToken for all requests made.
|
2022-11-13 15:45:42 +00:00
|
|
|
//
|
|
|
|
// If Logger is nil then logs will be suppressed.
|
|
|
|
func NewAdminClient(addr, adminToken string, logger *mlog.Logger) *AdminClient {
|
2022-10-16 20:17:24 +00:00
|
|
|
return &AdminClient{
|
|
|
|
c: &http.Client{
|
|
|
|
Transport: http.DefaultTransport.(*http.Transport).Clone(),
|
|
|
|
},
|
|
|
|
addr: addr,
|
|
|
|
adminToken: adminToken,
|
2022-11-13 15:45:42 +00:00
|
|
|
logger: logger,
|
2022-10-16 20:17:24 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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)
|
|
|
|
|
2022-11-13 15:45:42 +00:00
|
|
|
if c.logger.MaxLevel() >= mlog.LevelDebug.Int() {
|
|
|
|
|
|
|
|
reqB, err := httputil.DumpRequestOut(req, true)
|
|
|
|
if err != nil {
|
|
|
|
c.logger.Error(ctx, "failed to dump http request", err)
|
|
|
|
} else {
|
|
|
|
c.logger.Debug(ctx, "------ request ------\n"+string(reqB)+"\n")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-10-16 20:17:24 +00:00
|
|
|
res, err := c.c.Do(req)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("performing http request: %w", err)
|
|
|
|
}
|
|
|
|
|
2022-11-13 15:45:42 +00:00
|
|
|
if c.logger.MaxLevel() >= mlog.LevelDebug.Int() {
|
|
|
|
|
|
|
|
resB, err := httputil.DumpResponse(res, true)
|
|
|
|
if err != nil {
|
|
|
|
c.logger.Error(ctx, "failed to dump http response", err)
|
|
|
|
} else {
|
|
|
|
c.logger.Debug(ctx, "------ response ------\n"+string(resB)+"\n")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-10-16 20:17:24 +00:00
|
|
|
defer res.Body.Close()
|
|
|
|
|
2022-11-22 11:51:21 +00:00
|
|
|
if res.StatusCode >= 300 {
|
2022-10-25 19:15:09 +00:00
|
|
|
body, _ := io.ReadAll(res.Body)
|
|
|
|
return AdminClientError{
|
|
|
|
StatusCode: res.StatusCode,
|
|
|
|
Body: body,
|
|
|
|
}
|
2022-10-16 20:17:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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 {
|
2022-11-13 15:49:23 +00:00
|
|
|
|
|
|
|
for first := true; ; first = false {
|
|
|
|
|
|
|
|
if !first {
|
|
|
|
time.Sleep(250 * time.Millisecond)
|
|
|
|
}
|
2022-10-16 20:17:26 +00:00
|
|
|
|
|
|
|
var clusterStatus struct {
|
2024-06-11 12:54:26 +00:00
|
|
|
Nodes []struct {
|
|
|
|
IsUp bool `json:"isUp"`
|
|
|
|
} `json:"nodes"`
|
2022-10-16 20:17:26 +00:00
|
|
|
}
|
|
|
|
|
2024-06-11 12:54:26 +00:00
|
|
|
err := c.Do(ctx, &clusterStatus, "GET", "/v1/status", nil)
|
2022-10-16 20:17:26 +00:00
|
|
|
|
|
|
|
if ctxErr := ctx.Err(); ctxErr != nil {
|
|
|
|
return ctxErr
|
|
|
|
|
|
|
|
} else if err != nil {
|
2022-11-16 16:25:55 +00:00
|
|
|
c.logger.Warn(ctx, "waiting for instance to become ready", err)
|
2022-10-16 20:17:26 +00:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
var numUp int
|
|
|
|
|
2024-06-11 12:54:26 +00:00
|
|
|
for _, node := range clusterStatus.Nodes {
|
|
|
|
if node.IsUp {
|
2022-10-16 20:17:26 +00:00
|
|
|
numUp++
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-11-16 16:25:55 +00:00
|
|
|
ctx := mctx.Annotate(ctx,
|
2024-06-11 12:54:26 +00:00
|
|
|
"numNodes", len(clusterStatus.Nodes),
|
2022-11-16 16:27:42 +00:00
|
|
|
"numUp", numUp,
|
2022-11-16 16:25:55 +00:00
|
|
|
)
|
|
|
|
|
2022-10-16 20:17:26 +00:00
|
|
|
if numUp >= ReplicationFactor-1 {
|
2022-11-16 16:25:55 +00:00
|
|
|
c.logger.Debug(ctx, "instance appears to be online")
|
2022-10-16 20:17:26 +00:00
|
|
|
return nil
|
|
|
|
}
|
2022-11-05 14:23:29 +00:00
|
|
|
|
2022-11-16 16:25:55 +00:00
|
|
|
c.logger.Debug(ctx, "instance not online yet, will continue waiting")
|
2022-10-16 20:17:26 +00:00
|
|
|
}
|
|
|
|
}
|