Files
VeillaxandClaude Sonnet 5.5 227c890409
CI / test (push) Successful in 4m52s
Release / release (push) Successful in 5m24s
Add Sandbar alert channel, tags for resources and sites, use Reef's secretbox
Sandbar channels raise and resolve alerts through its API, with the API key
sealed at rest and never returned to the browser. Resources and sites can be
tagged and filtered by tag; sites can be renamed. The secretbox package now
comes from Reef v0.1.9 instead of a private copy.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-27 03:32:43 -06:00

95 lines
3.3 KiB
Go

// Hammerhead is the control plane: web UI + API that tells blacktip and bamboo what to do.
package main
import (
"context"
"flag"
"fmt"
"log"
"net"
"net/http"
"os"
"strconv"
"time"
"git.veillax.com/Veillax/hammerhead"
"git.veillax.com/Veillax/reef/dotenv"
"git.veillax.com/Veillax/reef/secretbox"
"git.veillax.com/Veillax/reef/selfupdate"
)
// version is set at build time (deploy/build.sh).
var version = "dev"
// localAddr turns a listen address into one this machine can dial: ":8080" and "0.0.0.0:8080"
// become "127.0.0.1:8080".
func localAddr(listen string) string {
host, port, err := net.SplitHostPort(listen)
if err != nil {
return listen
}
if host == "" || host == "0.0.0.0" || host == "::" {
host = "127.0.0.1"
}
return net.JoinHostPort(host, port)
}
func env(k, def string) string {
if v := os.Getenv(k); v != "" {
return v
}
return def
}
func main() {
// secrets come from a .env file (or the real environment), read before flags so flags can default to them
envFile := dotenv.ArgPath(os.Args[1:], ".env")
if used, err := dotenv.Load(envFile); err != nil {
log.Fatalf("read %s: %v", envFile, err)
} else if used != "" {
log.Printf("loaded settings from %s", used)
}
listen := flag.String("listen", env("HAMMERHEAD_LISTEN", ":8080"), "address for the web UI and API")
data := flag.String("data", env("HAMMERHEAD_DATA", "./hammerhead-data"), "directory for the database")
url := flag.String("url", env("HAMMERHEAD_URL", ""), "public URL people reach hammerhead at (used for login redirects and SSO); can also be set in Settings")
flag.String("env", ".env", "file to read secrets and settings from")
genkey := flag.Bool("genkey", false, "print a new random HAMMERHEAD_MASTER_KEY and exit")
autoUpdate := flag.Bool("auto-update", os.Getenv("HAMMERHEAD_AUTO_UPDATE") == "1", "check for new releases every few hours and update and restart by itself (Linux and macOS)")
showVersion := flag.Bool("version", false, "print the version and exit")
flag.Parse()
if *showVersion {
os.Stdout.WriteString("hammerhead " + version + "\n")
return
}
if *genkey {
fmt.Println(secretbox.GenerateKey())
return
}
days, _ := strconv.Atoi(os.Getenv("HAMMERHEAD_LOG_DAYS"))
srv, err := hammerhead.New(*data, hammerhead.Options{
AdminEmail: os.Getenv("HAMMERHEAD_ADMIN_EMAIL"),
AdminPassword: os.Getenv("HAMMERHEAD_ADMIN_PASSWORD"),
DashboardURL: *url,
MasterKey: os.Getenv("HAMMERHEAD_MASTER_KEY"),
MetricsToken: os.Getenv("HAMMERHEAD_METRICS_TOKEN"),
LogRetentionDays: days,
SelfDomain: os.Getenv("HAMMERHEAD_SELF_DOMAIN"),
SelfTarget: localAddr(*listen),
SelfTunnel: os.Getenv("HAMMERHEAD_SELF_TUNNEL"),
Version: version,
AutoUpdate: *autoUpdate,
})
if err != nil {
log.Fatal(err)
}
defer srv.Close()
if *autoUpdate {
go selfupdate.Run(context.Background(), selfupdate.Config{Server: "https://git.veillax.com", Repo: "Veillax/hammerhead", App: "hammerhead", Current: version, Logf: log.Printf, Restart: func(exe string) error { srv.Close(); return selfupdate.Restart(exe) }})
}
log.Printf("hammerhead %s listening on %s (open it in a browser to finish setup)", version, *listen)
hs := &http.Server{Addr: *listen, Handler: srv.Handler(), ReadHeaderTimeout: 10 * time.Second}
log.Fatal(hs.ListenAndServe())
}