Hub runtime: allocate port 8788, add Go skeleton and Docker deployment
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
3
src/go.mod
Normal file
3
src/go.mod
Normal file
@@ -0,0 +1,3 @@
|
||||
module hub
|
||||
|
||||
go 1.25
|
||||
132
src/main.go
Normal file
132
src/main.go
Normal file
@@ -0,0 +1,132 @@
|
||||
// Command hub is the Central AI Hub service.
|
||||
//
|
||||
// Skeleton (AUD-48): it serves GET /health only. POST /ask and POST /approve
|
||||
// arrive in AUD-49. The wire format is frozen in docs/hub-api.md.
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
serviceName = "hub"
|
||||
serviceVersion = "1"
|
||||
defaultPort = "8788"
|
||||
)
|
||||
|
||||
var started = time.Now()
|
||||
|
||||
type healthResponse struct {
|
||||
OK bool `json:"ok"`
|
||||
Service string `json:"service"`
|
||||
Version string `json:"version"`
|
||||
UptimeS int64 `json:"uptime_s"`
|
||||
Checks map[string]string `json:"checks"`
|
||||
}
|
||||
|
||||
type errorBody struct {
|
||||
Status string `json:"status"`
|
||||
Error errorInfo `json:"error"`
|
||||
}
|
||||
|
||||
type errorInfo struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// port reads HUB_PORT. It falls back to 8788.
|
||||
func port() string {
|
||||
if p := os.Getenv("HUB_PORT"); p != "" {
|
||||
return p
|
||||
}
|
||||
return defaultPort
|
||||
}
|
||||
|
||||
func writeJSON(w http.ResponseWriter, code int, v any) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.WriteHeader(code)
|
||||
_ = json.NewEncoder(w).Encode(v)
|
||||
}
|
||||
|
||||
func writeError(w http.ResponseWriter, code int, errCode, message string) {
|
||||
writeJSON(w, code, errorBody{
|
||||
Status: "error",
|
||||
Error: errorInfo{Code: errCode, Message: message},
|
||||
})
|
||||
}
|
||||
|
||||
// handleHealth is GET /health. Liveness only. No authentication.
|
||||
func handleHealth(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
writeError(w, http.StatusMethodNotAllowed, "bad_request", "Use GET for this route.")
|
||||
return
|
||||
}
|
||||
// OmniRoute and MQTT are wired in AUD-49, so they read "unknown" here.
|
||||
writeJSON(w, http.StatusOK, healthResponse{
|
||||
OK: true,
|
||||
Service: serviceName,
|
||||
Version: serviceVersion,
|
||||
UptimeS: int64(time.Since(started).Seconds()),
|
||||
Checks: map[string]string{
|
||||
"omniroute": "unknown",
|
||||
"mqtt": "unknown",
|
||||
"log": "ok",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// handleNotFound answers an unknown path with the frozen error envelope.
|
||||
func handleNotFound(w http.ResponseWriter, r *http.Request) {
|
||||
writeError(w, http.StatusNotFound, "not_found", "Unknown path.")
|
||||
}
|
||||
|
||||
// healthcheck calls this process's own /health. It is the container health probe,
|
||||
// because the runtime image has no shell and no curl.
|
||||
func healthcheck() {
|
||||
url := fmt.Sprintf("http://127.0.0.1:%s/health", port())
|
||||
client := &http.Client{Timeout: 1 * time.Second}
|
||||
resp, err := client.Get(url)
|
||||
if err != nil {
|
||||
log.Printf("healthcheck failed: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
log.Printf("healthcheck status: %d", resp.StatusCode)
|
||||
os.Exit(1)
|
||||
}
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
func main() {
|
||||
hc := flag.Bool("healthcheck", false, "Call this process's /health and exit.")
|
||||
flag.Parse()
|
||||
if *hc {
|
||||
healthcheck()
|
||||
return
|
||||
}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/health", handleHealth)
|
||||
mux.HandleFunc("/", handleNotFound)
|
||||
|
||||
srv := &http.Server{
|
||||
Addr: ":" + port(),
|
||||
Handler: mux,
|
||||
ReadHeaderTimeout: 1 * time.Second,
|
||||
ReadTimeout: 1 * time.Second,
|
||||
WriteTimeout: 1 * time.Second,
|
||||
IdleTimeout: 60 * time.Second,
|
||||
}
|
||||
|
||||
log.Printf("%s %s listening on :%s", serviceName, serviceVersion, port())
|
||||
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
log.Fatalf("%s stopped: %v", serviceName, err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user