pkg/skoop/ui/web.go (211 lines of code) (raw):
package ui
import (
"embed"
"errors"
"fmt"
"html/template"
"io/fs"
"net/http"
"strings"
"github.com/alibaba/kubeskoop/pkg/skoop/context"
"github.com/alibaba/kubeskoop/pkg/skoop/model"
"github.com/gorilla/mux"
"k8s.io/apimachinery/pkg/util/json"
"k8s.io/klog/v2"
)
//go:embed html
var embeddedWebFiles embed.FS
type uiArgs struct {
DiagnoseInfo string
GraphSvg template.HTML
Cluster template.JSStr
Nodes template.JSStr
Edges template.JSStr
}
type suspicion struct {
Level string `json:"level"`
Message string `json:"message"`
}
type nodeInfo struct {
ID string `json:"id"`
Type string `json:"type"`
SuspicionLevel string `json:"suspicion_level"`
Suspicions []suspicion `json:"suspicions"`
}
type packet struct {
Src string `json:"src"`
Dst string `json:"dst"`
Dport uint16 `json:"dport"`
Protocol string `json:"protocol"`
}
type edgeInfo struct {
ID string `json:"id"`
Type string `json:"type"`
Action string `json:"action"`
Oif string `json:"oif"`
Iif string `json:"iif"`
Packet packet `json:"packet"`
}
type clusterInfo struct {
SuspicionLevel string `json:"suspicion_level"`
Suspicions []suspicion `json:"suspicions"`
}
type WebUI struct {
ctx *context.Context
g *D2
p *model.PacketPath
globalSus []model.Suspicion
template *template.Template
address string
}
func NewWebUI(ctx *context.Context, globalSuspicions []model.Suspicion, p *model.PacketPath, address string) (*WebUI, error) {
g, err := NewD2(p)
if err != nil {
return nil, err
}
ui := &WebUI{
ctx: ctx,
p: p,
g: g,
globalSus: globalSuspicions,
template: template.New("kubeskoop"),
address: address,
}
err = ui.loadTemplates()
if err != nil {
return nil, err
}
return ui, nil
}
func (u *WebUI) loadTemplates() error {
_, err := u.template.ParseFS(embeddedWebFiles, "html/index.html", "html/default.svg")
return err
}
func (u *WebUI) Serve() error {
rtr := mux.NewRouter()
rtr.HandleFunc("/", u.handleUI)
rtr.HandleFunc("/svg/{filename}", u.handleSVG)
http.Handle("/", rtr)
klog.V(0).Infof("HTTP server listening on http://%s", u.address)
return http.ListenAndServe(u.address, nil)
}
func (u *WebUI) handleUI(w http.ResponseWriter, _ *http.Request) {
args := uiArgs{
DiagnoseInfo: fmt.Sprintf("%s -> %s", u.ctx.TaskConfig().SourceEndpoint, u.ctx.TaskConfig().DstEndpoint),
}
svg, err := u.g.ToSvg()
if err != nil {
http.Error(w, fmt.Sprintf("error generate svg: %s", err), http.StatusInternalServerError)
return
}
svgString := string(svg)
args.GraphSvg = template.HTML(svgString)
suspicionLevel := model.SuspicionLevel(model.SuspicionLevelInfo)
var globalSuspicions []suspicion
for _, sus := range u.globalSus {
globalSuspicions = append(globalSuspicions, suspicion{
Level: sus.Level.String(),
Message: sus.Message,
})
if suspicionLevel < sus.Level {
suspicionLevel = sus.Level
}
}
cluster := clusterInfo{
SuspicionLevel: suspicionLevel.String(),
Suspicions: globalSuspicions,
}
jsonBytes, err := json.Marshal(cluster)
if err != nil {
http.Error(w, fmt.Sprintf("error marshal node info: %s", err), http.StatusInternalServerError)
return
}
args.Cluster = template.JSStr(jsonBytes)
var nodes []nodeInfo
for _, node := range u.p.Nodes() {
n := nodeInfo{
ID: node.GetID(),
Type: string(node.GetType()),
SuspicionLevel: node.MaxSuspicionLevel().String(),
Suspicions: nil,
}
for _, sus := range node.GetSuspicions() {
n.Suspicions = append(n.Suspicions, suspicion{
Level: sus.Level.String(),
Message: strings.Replace(sus.Message, "\n", "\\n", -1),
})
}
nodes = append(nodes, n)
}
jsonBytes, err = json.Marshal(nodes)
if err != nil {
http.Error(w, fmt.Sprintf("error marshal node info: %s", err), http.StatusInternalServerError)
return
}
args.Nodes = template.JSStr(jsonBytes)
var edges []edgeInfo
for _, link := range u.p.Links() {
srcAttrs := map[string]string{}
if link.SourceAttribute != nil {
srcAttrs = link.SourceAttribute.GetAttrs()
}
dstAttrs := map[string]string{}
if link.DestinationAttribute != nil {
dstAttrs = link.DestinationAttribute.GetAttrs()
}
e := edgeInfo{
ID: link.GetID(),
Type: string(link.Type),
Packet: packet{
Src: link.Packet.Src.String(),
Dst: link.Packet.Dst.String(),
Dport: link.Packet.Dport,
Protocol: string(link.Packet.Protocol),
},
Oif: "-",
Iif: "-",
}
action := link.Destination.ActionOf(link)
if action != nil {
e.Action = string(action.Type)
}
if oif, ok := srcAttrs["if"]; ok {
e.Oif = oif
}
if iif, ok := dstAttrs["if"]; ok {
e.Iif = iif
}
edges = append(edges, e)
}
jsonBytes, err = json.Marshal(edges)
if err != nil {
http.Error(w, fmt.Sprintf("error marshal edge info: %s", err), http.StatusInternalServerError)
return
}
args.Edges = template.JSStr(jsonBytes)
w.Header().Add("Content-Type", "text/html; charset=utf-8")
err = u.template.ExecuteTemplate(w, "index.html", &args)
if err != nil {
http.Error(w, fmt.Sprintf("error render template: %s", err), http.StatusInternalServerError)
return
}
}
func (u *WebUI) handleSVG(w http.ResponseWriter, r *http.Request) {
filename := mux.Vars(r)["filename"]
f, err := embeddedWebFiles.ReadFile(fmt.Sprintf("html/svg/%s", filename))
if err != nil && !errors.Is(err, fs.ErrNotExist) {
http.Error(w, fmt.Sprintf("error get file: %s", err), http.StatusInternalServerError)
return
}
w.Header().Add("Content-Type", "image/svg+xml")
if errors.Is(err, fs.ErrNotExist) {
name := strings.TrimRight(filename, ".svg")
err = u.template.ExecuteTemplate(w, "default.svg", map[string]string{
"label": name,
})
if err != nil {
http.Error(w, fmt.Sprintf("error render template: %s", err), http.StatusInternalServerError)
}
return
}
_, _ = w.Write(f)
}