hhctl CLI and Terraform provider for Hammerhead

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
VeillaxandClaude Sonnet 5.5 committed 2026-10-27 17:02:15 -06:00
commit bf340cdab4
19 files changed
+2442

No files matched your search

+109
View File
@@ -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
}