Initial Beach: homelab control panel
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
6938da4d7a
18 files changed
+2071
No files matched your search
@@ -0,0 +1,111 @@
|
||||
package beach
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type TunnelSite struct {
|
||||
Name string `json:"name"`
|
||||
Online bool `json:"online"`
|
||||
Local bool `json:"local"`
|
||||
Version string `json:"version,omitempty"`
|
||||
LastSeen time.Time `json:"lastSeen"`
|
||||
}
|
||||
|
||||
type TunnelPort struct {
|
||||
Name string `json:"name"`
|
||||
Proto string `json:"proto"`
|
||||
Hostname string `json:"hostname,omitempty"`
|
||||
Port int `json:"port"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Up bool `json:"up"` // the edge reports it listening without error
|
||||
Known bool `json:"known"`
|
||||
Healthy int `json:"healthy"`
|
||||
Targets int `json:"targets"`
|
||||
}
|
||||
|
||||
type Tunnels struct {
|
||||
Sites []TunnelSite `json:"sites"`
|
||||
Ports []TunnelPort `json:"ports"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// FetchTunnels reads GET /api/orgs/{org}/state from Hammerhead with an API key.
|
||||
func FetchTunnels(ctx context.Context, base, key, org string) Tunnels {
|
||||
u := strings.TrimRight(base, "/") + "/api/orgs/" + url.PathEscape(org) + "/state"
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", u, nil)
|
||||
if err != nil {
|
||||
return Tunnels{Error: err.Error()}
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+key)
|
||||
resp, err := (&http.Client{Timeout: 10 * time.Second}).Do(req)
|
||||
if err != nil {
|
||||
return Tunnels{Error: err.Error()}
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
b, _ := io.ReadAll(io.LimitReader(resp.Body, 8<<20))
|
||||
if resp.StatusCode != 200 {
|
||||
return Tunnels{Error: fmt.Sprintf("hammerhead: %s", resp.Status)}
|
||||
}
|
||||
t, err := parseTunnels(b)
|
||||
if err != nil {
|
||||
return Tunnels{Error: err.Error()}
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
func parseTunnels(b []byte) (Tunnels, error) {
|
||||
var st struct {
|
||||
Sites []struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Local bool `json:"local"`
|
||||
Version string `json:"version"`
|
||||
LastSeen time.Time `json:"lastSeen"`
|
||||
} `json:"sites"`
|
||||
Online map[string]bool `json:"online"`
|
||||
Mappings []struct {
|
||||
Name string `json:"name"`
|
||||
Proto string `json:"proto"`
|
||||
Hostname string `json:"hostname"`
|
||||
Port int `json:"port"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Targets []any `json:"targets"`
|
||||
Status struct {
|
||||
Error string `json:"error"`
|
||||
Targets []struct {
|
||||
Healthy bool `json:"healthy"`
|
||||
} `json:"targets"`
|
||||
} `json:"status"`
|
||||
Known bool `json:"statusKnown"`
|
||||
} `json:"mappings"`
|
||||
}
|
||||
if err := json.Unmarshal(b, &st); err != nil {
|
||||
return Tunnels{}, fmt.Errorf("hammerhead: unexpected response: %w", err)
|
||||
}
|
||||
t := Tunnels{Sites: []TunnelSite{}, Ports: []TunnelPort{}}
|
||||
for _, s := range st.Sites {
|
||||
t.Sites = append(t.Sites, TunnelSite{Name: s.Name, Online: st.Online[s.ID], Local: s.Local, Version: s.Version, LastSeen: s.LastSeen})
|
||||
}
|
||||
for _, m := range st.Mappings {
|
||||
p := TunnelPort{Name: m.Name, Proto: m.Proto, Hostname: m.Hostname, Port: m.Port, Enabled: m.Enabled, Known: m.Known, Targets: len(m.Targets)}
|
||||
p.Up = m.Enabled && m.Known && m.Status.Error == ""
|
||||
for _, th := range m.Status.Targets {
|
||||
if th.Healthy {
|
||||
p.Healthy++
|
||||
}
|
||||
}
|
||||
t.Ports = append(t.Ports, p)
|
||||
}
|
||||
sort.Slice(t.Sites, func(i, j int) bool { return t.Sites[i].Name < t.Sites[j].Name })
|
||||
sort.Slice(t.Ports, func(i, j int) bool { return t.Ports[i].Name < t.Ports[j].Name })
|
||||
return t, nil
|
||||
}
|
||||
Reference in new issue
Block a user