repositories / bugabinga.net
bugabinga.net
personal infrastructure for bugabinga!
owned by admin
services/toad/internal/api/web.go
Rawpackage api
import (
"embed"
"fmt"
"html/template"
"net/http"
"sort"
"strings"
"time"
"bugabinga.net/toad/internal/enroll"
"bugabinga.net/toad/internal/rollout"
"bugabinga.net/toad/internal/store"
)
//go:embed web/*
var webAssets embed.FS
var dashboardTemplate = template.Must(template.New("dashboard.html").Funcs(template.FuncMap{
"shortDigest": shortDigest,
"shorten": shorten,
"elapsed": elapsed,
"probation": probation,
}).ParseFS(webAssets, "web/dashboard.html"))
type dashboard struct {
Services []serviceView
Selected *serviceView
Healthy int
}
type serviceView struct {
Service enroll.Service
State store.State
Container rollout.ContainerState
Unit rollout.UnitState
Health string
Operations []operationView
}
type operationView struct {
Operation store.Operation
Log string
}
func (m *Manager) serveDashboard(w http.ResponseWriter, r *http.Request) {
secureWebResponse(w)
views := make([]serviceView, 0, len(m.Enrollment.Names()))
for _, name := range m.Enrollment.Names() {
service, _ := m.Enrollment.Lookup(name)
view := serviceView{Service: service, Health: "unknown"}
view.State, _ = m.Store.ReadState(name)
view.Container, _ = m.Engine.Host.Container(r.Context(), service.ContainerName)
view.Unit, _ = m.Engine.Host.Unit(r.Context(), service.UnitName())
if view.Container.Running && view.Unit.ActiveState == "active" && m.Engine.Host.Probe(r.Context(), service.ReadinessURL) == nil {
view.Health = "healthy"
} else if view.State.Current != nil {
view.Health = "unhealthy"
}
views = append(views, view)
}
sort.Slice(views, func(i, j int) bool { return views[i].Service.Name < views[j].Service.Name })
selected := r.URL.Query().Get("service")
if selected == "" && len(views) > 0 {
selected = views[0].Service.Name
}
healthy := 0
for _, view := range views {
if view.Health == "healthy" {
healthy++
}
}
var detail *serviceView
for index := range views {
if views[index].Service.Name == selected {
detail = &views[index]
break
}
}
if selected != "" && detail == nil {
http.NotFound(w, r)
return
}
if detail != nil {
detail.Operations = m.operationViews(detail.Service.Name)
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Header().Set("Cache-Control", "no-store")
if err := dashboardTemplate.Execute(w, dashboard{Services: views, Selected: detail, Healthy: healthy}); err != nil {
http.Error(w, "render dashboard", http.StatusInternalServerError)
}
}
func (m *Manager) operationViews(service string) []operationView {
ids, err := m.Store.OperationIDs(service)
if err != nil {
return nil
}
sort.Sort(sort.Reverse(sort.StringSlice(ids)))
if len(ids) > 20 {
ids = ids[:20]
}
views := make([]operationView, 0, len(ids))
for _, id := range ids {
operation, err := m.Store.ReadOperation(service, id)
if err != nil {
continue
}
log, _ := m.Store.ReadLog(service, id)
views = append(views, operationView{Operation: operation, Log: string(log)})
}
return views
}
func serveWebAsset(w http.ResponseWriter, r *http.Request) {
secureWebResponse(w)
name := strings.TrimPrefix(r.URL.Path, "/assets/")
if name != "dashboard.css" && name != "dashboard.js" && name != "symbols-nerd-font-mono.woff2" {
http.NotFound(w, r)
return
}
data, err := webAssets.ReadFile("web/" + name)
if err != nil {
http.NotFound(w, r)
return
}
contentType := map[string]string{
"dashboard.css": "text/css; charset=utf-8",
"dashboard.js": "text/javascript; charset=utf-8",
"symbols-nerd-font-mono.woff2": "font/woff2",
}[name]
w.Header().Set("Content-Type", contentType)
w.Header().Set("Cache-Control", "public, max-age=86400")
_, _ = w.Write(data)
}
func secureWebResponse(w http.ResponseWriter) {
w.Header().Set("Content-Security-Policy", "default-src 'none'; style-src 'self'; script-src 'self'; font-src 'self'; img-src 'self'; base-uri 'none'; frame-ancestors 'none'")
w.Header().Set("Referrer-Policy", "no-referrer")
w.Header().Set("X-Content-Type-Options", "nosniff")
}
func shortDigest(revision *store.Revision) string {
if revision == nil {
return "none"
}
return shorten(revision.Digest)
}
func shorten(value string) string {
if len(value) <= 24 {
return value
}
return value[:15] + "…" + value[len(value)-7:]
}
func elapsed(value time.Time) string {
if value.IsZero() {
return "never"
}
duration := time.Since(value)
if duration < time.Minute {
return "now"
}
if duration < time.Hour {
return fmt.Sprintf("%dm", int(duration.Minutes()))
}
if duration < 24*time.Hour {
return fmt.Sprintf("%dh", int(duration.Hours()))
}
return fmt.Sprintf("%dd", int(duration.Hours()/24))
}
func probation(service enroll.Service) string {
return fmt.Sprintf("deadline %s · stable %s · restarts %d", service.Probation.Deadline, service.Probation.StableFor, service.Probation.RestartBudget)
}