Teststand
Some checks failed
Deploy KubeViz / deploy (push) Has been cancelled

This commit is contained in:
2026-03-01 07:40:49 +01:00
commit 1a0bbe9dfd
58 changed files with 7756 additions and 0 deletions

View File

@@ -0,0 +1,217 @@
package httpserver
import (
"fmt"
"image"
"image/color"
"image/draw"
"image/png"
"math"
"net/http"
"sort"
"strings"
"kubeviz/internal/analyze"
"kubeviz/internal/graph"
"kubeviz/internal/model"
)
type point struct {
X float64
Y float64
}
func (s *Server) handleExportSVG(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
resp, ok := s.exportGraph(w, r)
if !ok {
return
}
svg := renderSVG(resp)
w.Header().Set("Content-Type", "image/svg+xml")
w.Header().Set("Content-Disposition", "inline; filename=graph.svg")
_, _ = w.Write([]byte(svg))
}
func (s *Server) handleExportPNG(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
resp, ok := s.exportGraph(w, r)
if !ok {
return
}
img := renderPNG(resp)
w.Header().Set("Content-Type", "image/png")
w.Header().Set("Content-Disposition", "inline; filename=graph.png")
_ = png.Encode(w, img)
}
func (s *Server) exportGraph(w http.ResponseWriter, r *http.Request) (model.GraphResponse, bool) {
sid, err := s.sessionID(w, r)
if err != nil {
http.Error(w, "session error", http.StatusInternalServerError)
return model.GraphResponse{}, false
}
dataset := s.store.GetDataset(sid)
if dataset == nil {
http.Error(w, "no manifests uploaded", http.StatusNotFound)
return model.GraphResponse{}, false
}
filters := graph.Filters{
Namespace: r.URL.Query().Get("namespace"),
Kind: r.URL.Query().Get("kind"),
Query: r.URL.Query().Get("q"),
FocusID: r.URL.Query().Get("focus"),
Relations: parseRelations(r.URL.Query().Get("relations")),
GroupBy: r.URL.Query().Get("groupBy"),
CollapsedGroups: parseCSVSet(r.URL.Query().Get("collapsed")),
}
checkCfg := analyze.DefaultConfig()
if raw := r.URL.Query().Get("checkRules"); raw != "" {
checkCfg = analyze.ConfigFromEnabled(parseCSVSet(raw))
}
_, healthHints := analyze.AnalyzeDataset(dataset, checkCfg)
return graph.BuildGraph(dataset, filters, healthHints), true
}
func layout(nodes []model.GraphNode) map[string]point {
out := map[string]point{}
sorted := make([]model.GraphNode, len(nodes))
copy(sorted, nodes)
sort.Slice(sorted, func(i, j int) bool { return sorted[i].ID < sorted[j].ID })
n := len(sorted)
if n == 0 {
return out
}
centerX, centerY := 640.0, 360.0
radius := 220.0
if n > 40 {
radius = 280
}
for i, node := range sorted {
angle := 2 * math.Pi * float64(i) / float64(n)
out[node.ID] = point{
X: centerX + radius*math.Cos(angle),
Y: centerY + radius*math.Sin(angle),
}
}
return out
}
func renderSVG(resp model.GraphResponse) string {
coords := layout(resp.Nodes)
var b strings.Builder
b.WriteString(`<svg xmlns="http://www.w3.org/2000/svg" width="1280" height="720" viewBox="0 0 1280 720">`)
b.WriteString(`<rect width="100%" height="100%" fill="#f5f7fb"/>`)
for _, edge := range resp.Edges {
s, sok := coords[edge.Source]
t, tok := coords[edge.Target]
if !sok || !tok {
continue
}
b.WriteString(fmt.Sprintf(`<line x1="%.1f" y1="%.1f" x2="%.1f" y2="%.1f" stroke="#9ca3af" stroke-width="1.5"/>`, s.X, s.Y, t.X, t.Y))
}
for _, node := range resp.Nodes {
p := coords[node.ID]
fill := "#2563eb"
if node.IsSensitive {
fill = "#dc2626"
}
label := escapeXML(fmt.Sprintf("%s/%s", node.Kind, node.Name))
b.WriteString(fmt.Sprintf(`<circle cx="%.1f" cy="%.1f" r="20" fill="%s" opacity="0.9"/>`, p.X, p.Y, fill))
b.WriteString(fmt.Sprintf(`<text x="%.1f" y="%.1f" font-size="11" fill="#111827" text-anchor="middle">%s</text>`, p.X, p.Y+34, label))
}
b.WriteString(`</svg>`)
return b.String()
}
func renderPNG(resp model.GraphResponse) image.Image {
img := image.NewRGBA(image.Rect(0, 0, 1280, 720))
draw.Draw(img, img.Bounds(), &image.Uniform{C: color.RGBA{245, 247, 251, 255}}, image.Point{}, draw.Src)
coords := layout(resp.Nodes)
edgeColor := color.RGBA{156, 163, 175, 255}
for _, edge := range resp.Edges {
s, sok := coords[edge.Source]
t, tok := coords[edge.Target]
if !sok || !tok {
continue
}
drawLine(img, int(s.X), int(s.Y), int(t.X), int(t.Y), edgeColor)
}
for _, node := range resp.Nodes {
p := coords[node.ID]
fill := color.RGBA{37, 99, 235, 255}
if node.IsSensitive {
fill = color.RGBA{220, 38, 38, 255}
}
drawCircle(img, int(p.X), int(p.Y), 20, fill)
}
return img
}
func drawLine(img *image.RGBA, x0, y0, x1, y1 int, c color.Color) {
dx := abs(x1 - x0)
sx := -1
if x0 < x1 {
sx = 1
}
dy := -abs(y1 - y0)
sy := -1
if y0 < y1 {
sy = 1
}
err := dx + dy
for {
if image.Pt(x0, y0).In(img.Bounds()) {
img.Set(x0, y0, c)
}
if x0 == x1 && y0 == y1 {
break
}
e2 := 2 * err
if e2 >= dy {
err += dy
x0 += sx
}
if e2 <= dx {
err += dx
y0 += sy
}
}
}
func drawCircle(img *image.RGBA, cx, cy, r int, c color.Color) {
for y := -r; y <= r; y++ {
for x := -r; x <= r; x++ {
if x*x+y*y <= r*r {
px := cx + x
py := cy + y
if image.Pt(px, py).In(img.Bounds()) {
img.Set(px, py, c)
}
}
}
}
}
func abs(v int) int {
if v < 0 {
return -v
}
return v
}
func escapeXML(s string) string {
replacer := strings.NewReplacer("&", "&amp;", "<", "&lt;", ">", "&gt;", `"`, "&quot;")
return replacer.Replace(s)
}