package tailnet import ( "context" "database/sql" "html/template" "net/http" "time" "github.com/google/uuid" "golang.org/x/xerrors" gProto "google.golang.org/protobuf/proto" "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/tailnet/proto" ) type HTMLDebug struct { Coordinators []*HTMLCoordinator Peers []*HTMLPeer Tunnels []*HTMLTunnel } type HTMLPeer struct { ID uuid.UUID CoordinatorID uuid.UUID LastWriteAge time.Duration Node string Status database.TailnetStatus } type HTMLCoordinator struct { ID uuid.UUID HeartbeatAge time.Duration } type HTMLTunnel struct { CoordinatorID uuid.UUID SrcID uuid.UUID DstID uuid.UUID LastWriteAge time.Duration } func (c *pgCoord) ServeHTTPDebug(w http.ResponseWriter, r *http.Request) { ctx := r.Context() debug, err := getDebug(ctx, c.store) if err != nil { w.WriteHeader(http.StatusInternalServerError) _, _ = w.Write([]byte(err.Error())) return } w.Header().Set("Content-Type", "text/html; charset=utf-8") err = debugTempl.Execute(w, debug) if err != nil { w.WriteHeader(http.StatusInternalServerError) _, _ = w.Write([]byte(err.Error())) return } } func getDebug(ctx context.Context, store database.Store) (HTMLDebug, error) { out := HTMLDebug{} coords, err := store.GetAllTailnetCoordinators(ctx) if err != nil && !xerrors.Is(err, sql.ErrNoRows) { return HTMLDebug{}, xerrors.Errorf("failed to query coordinators: %w", err) } peers, err := store.GetAllTailnetPeers(ctx) if err != nil && !xerrors.Is(err, sql.ErrNoRows) { return HTMLDebug{}, xerrors.Errorf("failed to query peers: %w", err) } tunnels, err := store.GetAllTailnetTunnels(ctx) if err != nil && !xerrors.Is(err, sql.ErrNoRows) { return HTMLDebug{}, xerrors.Errorf("failed to query tunnels: %w", err) } now := time.Now() // call this once so all our ages are on the same timebase for _, coord := range coords { out.Coordinators = append(out.Coordinators, coordToHTML(coord, now)) } for _, peer := range peers { ph, err := peerToHTML(peer, now) if err != nil { return HTMLDebug{}, err } out.Peers = append(out.Peers, ph) } for _, tunnel := range tunnels { out.Tunnels = append(out.Tunnels, tunnelToHTML(tunnel, now)) } return out, nil } func coordToHTML(d database.TailnetCoordinator, now time.Time) *HTMLCoordinator { return &HTMLCoordinator{ ID: d.ID, HeartbeatAge: now.Sub(d.HeartbeatAt), } } func peerToHTML(d database.TailnetPeer, now time.Time) (*HTMLPeer, error) { node := &proto.Node{} err := gProto.Unmarshal(d.Node, node) if err != nil { return nil, xerrors.Errorf("unmarshal node: %w", err) } return &HTMLPeer{ ID: d.ID, CoordinatorID: d.CoordinatorID, LastWriteAge: now.Sub(d.UpdatedAt), Status: d.Status, Node: node.String(), }, nil } func tunnelToHTML(d database.TailnetTunnel, now time.Time) *HTMLTunnel { return &HTMLTunnel{ CoordinatorID: d.CoordinatorID, SrcID: d.SrcID, DstID: d.DstID, LastWriteAge: now.Sub(d.UpdatedAt), } } var coordinatorDebugTmpl = `

# coordinators: total {{ len .Coordinators }}

{{- range .Coordinators}} {{- end }}
ID Heartbeat Age
{{ .ID }} {{ .HeartbeatAge }} ago

# peers: total {{ len .Peers }}

{{- range .Peers }} {{- end }}
ID CoordinatorID Status Last Write Age Node
{{ .ID }} {{ .CoordinatorID }} {{ .Status }} {{ .LastWriteAge }} ago {{ .Node }}

# tunnels: total {{ len .Tunnels }}

{{- range .Tunnels }} {{- end }}
SrcID DstID CoordinatorID Last Write Age
{{ .SrcID }} {{ .DstID }} {{ .CoordinatorID }} {{ .LastWriteAge }} ago
` var debugTempl = template.Must(template.New("coordinator_debug").Parse(coordinatorDebugTmpl))