Luigit
repositories / bugabinga.net

bugabinga.net

personal infrastructure for bugabinga!

owned by admin

services/toad/internal/api/web.go

Raw
package 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)
}