110 lines
2.6 KiB
Go
110 lines
2.6 KiB
Go
// Package client talks to Hammerhead's public HTTP API with an API key.
|
|
package client
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// Client is a Hammerhead API client. Org is the default organization for org-scoped calls.
|
|
type Client struct {
|
|
BaseURL string
|
|
Token string
|
|
Org string
|
|
HTTP *http.Client
|
|
}
|
|
|
|
// New returns a client for the server at baseURL authenticating with an hhk_ API key.
|
|
func New(baseURL, token, org string) *Client {
|
|
return &Client{BaseURL: strings.TrimRight(baseURL, "/"), Token: token, Org: org, HTTP: &http.Client{Timeout: 30 * time.Second}}
|
|
}
|
|
|
|
// APIError is an error response from the server.
|
|
type APIError struct {
|
|
Status int
|
|
Message string
|
|
}
|
|
|
|
func (e *APIError) Error() string {
|
|
return fmt.Sprintf("hammerhead: %s (HTTP %d)", e.Message, e.Status)
|
|
}
|
|
|
|
// IsNotFound reports whether err is a 404 from the server.
|
|
func IsNotFound(err error) bool {
|
|
e, ok := err.(*APIError)
|
|
return ok && e.Status == http.StatusNotFound
|
|
}
|
|
|
|
func (c *Client) do(method, path string, in, out any) error {
|
|
var body io.Reader
|
|
if in != nil {
|
|
b, err := json.Marshal(in)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
body = bytes.NewReader(b)
|
|
}
|
|
req, err := http.NewRequest(method, c.BaseURL+path, body)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
req.Header.Set("Authorization", "Bearer "+c.Token)
|
|
if in != nil {
|
|
req.Header.Set("Content-Type", "application/json")
|
|
}
|
|
resp, err := c.HTTP.Do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer resp.Body.Close()
|
|
raw, err := io.ReadAll(io.LimitReader(resp.Body, 32<<20))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if resp.StatusCode >= 300 {
|
|
var e struct {
|
|
Error string `json:"error"`
|
|
}
|
|
msg := strings.TrimSpace(string(raw))
|
|
if json.Unmarshal(raw, &e) == nil && e.Error != "" {
|
|
msg = e.Error
|
|
}
|
|
return &APIError{resp.StatusCode, msg}
|
|
}
|
|
if out != nil && len(raw) > 0 {
|
|
return json.Unmarshal(raw, out)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *Client) orgPath(format string, args ...any) (string, error) {
|
|
if c.Org == "" {
|
|
return "", fmt.Errorf("no organization set (use --org, HAMMERHEAD_ORG, or an organization key)")
|
|
}
|
|
return "/api/orgs/" + url.PathEscape(c.Org) + fmt.Sprintf(format, args...), nil
|
|
}
|
|
|
|
func esc(id string) string { return url.PathEscape(id) }
|
|
|
|
// ResolveOrg fills in Org when the key can see exactly one organization.
|
|
func (c *Client) ResolveOrg() error {
|
|
if c.Org != "" {
|
|
return nil
|
|
}
|
|
orgs, err := c.Orgs()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(orgs) != 1 {
|
|
return fmt.Errorf("the key can reach %d organizations; choose one with --org", len(orgs))
|
|
}
|
|
c.Org = orgs[0].ID
|
|
return nil
|
|
}
|