fix: Fixes to running on kubernetes
Chart Release / chart (push) Failing after 13s
Server Deploy / deploy (push) Successful in 6m35s

This commit is contained in:
2026-07-31 10:34:10 +01:00
parent de78688093
commit 165114471f
31 changed files with 1880 additions and 278 deletions
+3
View File
@@ -20,6 +20,9 @@ func actorFromCtx(c *gin.Context) string {
}
func RegisterRoutes(r *gin.Engine) {
r.GET("/healthz", handleHealthz)
r.GET("/readyz", handleReadyz)
r.GET("/install", handleInstallScript)
r.GET("/install.ps1", handleInstallScriptWindows)
r.GET("/update", handleUpdateScript)
+61
View File
@@ -0,0 +1,61 @@
package api
import (
"context"
"net/http"
"time"
"gitea.hostxtra.co.uk/mrhid6/vantage/server/internal/auth"
"gitea.hostxtra.co.uk/mrhid6/vantage/server/internal/db"
"github.com/gin-gonic/gin"
)
// The two probes answer different questions on purpose.
//
// /healthz is liveness: the process is up and serving. It touches nothing
// external, because a Mongo outage must not make Kubernetes restart every
// server pod — a restart loop cannot fix someone else's database, and it
// destroys every open command stream and console session on the way.
//
// /readyz is readiness: this pod can serve a request end to end, which needs
// both Mongo and Redis. A failing readiness probe pulls the pod out of the
// Service and leaves it running, which is the behaviour that matters during a
// dependency blip.
//
// Both sit outside /api, so neither the session middleware nor the licence
// gate applies. Neither reveals anything beyond up or down.
const probeTimeout = 2 * time.Second
func handleHealthz(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"status": "ok"})
}
func handleReadyz(c *gin.Context) {
ctx, cancel := context.WithTimeout(c.Request.Context(), probeTimeout)
defer cancel()
checks := gin.H{"mongo": "ok", "redis": "ok"}
ready := true
if db.Client == nil {
checks["mongo"] = "not initialised"
ready = false
} else if err := db.Client.Ping(ctx, nil); err != nil {
checks["mongo"] = "unreachable"
ready = false
}
if err := auth.PingRedis(ctx); err != nil {
checks["redis"] = "unreachable"
ready = false
}
status := http.StatusOK
state := "ok"
if !ready {
status = http.StatusServiceUnavailable
state = "unready"
}
c.JSON(status, gin.H{"status": state, "checks": checks})
}
+41 -25
View File
@@ -5,7 +5,6 @@ import (
"fmt"
"io"
"net/http"
"os"
"regexp"
"strconv"
"strings"
@@ -50,23 +49,42 @@ func getServerRunLog(c *gin.Context) {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
return
}
path := services.ServerRunLogPath(runID, serverID)
b, err := os.ReadFile(path)
if err != nil {
if !services.HasServerRunLog(runID, serverID) {
c.JSON(http.StatusNotFound, gin.H{"error": "no logs"})
return
}
c.Data(http.StatusOK, "text/plain; charset=utf-8", b)
c.Header("Content-Type", "text/plain; charset=utf-8")
c.Status(http.StatusOK)
// Streamed in pages rather than read whole. A log capped at 200k lines is
// tens of megabytes, and holding that in memory per concurrent download is
// how one curious user takes a pod down.
var after int64
for {
lines, last, err := services.ReadServerRunLog(runID, serverID, after, logPageSize)
if err != nil || len(lines) == 0 {
return
}
for _, l := range lines {
_, _ = c.Writer.WriteString(l)
_, _ = c.Writer.WriteString("\n")
}
c.Writer.Flush()
after = last
}
}
// logPageSize bounds one read of the log store. Large enough that a normal log
// is one or two queries, small enough that no single response buffers much.
const logPageSize = 2000
func streamServerRunLog(c *gin.Context) {
runID, serverID := c.Param("runId"), c.Param("serverId")
if !uuidLike.MatchString(runID) || !uuidLike.MatchString(serverID) {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
return
}
path := services.ServerRunLogPath(runID, serverID)
c.Writer.Header().Set("Content-Type", "text/event-stream")
c.Writer.Header().Set("Cache-Control", "no-cache")
c.Writer.Header().Set("Connection", "keep-alive")
@@ -78,29 +96,27 @@ func streamServerRunLog(c *gin.Context) {
return
}
var offset int64
// The cursor is a sequence number now, not a byte offset: lines come from
// the log store, which any pod can read, rather than from a file only this
// one has.
var after int64
sendNew := func() {
f, err := os.Open(path)
if err != nil {
return
}
defer f.Close()
if _, err := f.Seek(offset, 0); err != nil {
return
}
buf := make([]byte, 32*1024)
for {
n, _ := f.Read(buf)
if n <= 0 {
break
lines, last, err := services.ReadServerRunLog(runID, serverID, after, logPageSize)
if err != nil || len(lines) == 0 {
return
}
offset += int64(n)
for _, line := range splitSSE(buf[:n]) {
_, _ = c.Writer.WriteString("data: " + line + "\n")
after = last
for _, line := range lines {
for _, part := range splitSSE([]byte(line)) {
_, _ = c.Writer.WriteString("data: " + part + "\n")
}
_, _ = c.Writer.WriteString("\n")
}
_, _ = c.Writer.WriteString("\n")
flusher.Flush()
if len(lines) < logPageSize {
return
}
}
}