hhctl CLI and Terraform provider for Hammerhead
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
bf340cdab4
19 files changed
+2442
No files matched your search
@@ -0,0 +1,109 @@
|
||||
// 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
|
||||
}
|
||||
Reference in new issue
Block a user