Skip to content

Commit f3fefd3

Browse files
committed
feat: implement HTMLDebug for PGCoord with v2 API
1 parent e44ca5c commit f3fefd3

File tree

6 files changed

+427
-115
lines changed

6 files changed

+427
-115
lines changed

.prettierignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ helm/**/templates/*.yaml
8282

8383
# Testdata shouldn't be formatted.
8484
scripts/apitypings/testdata/**/*.ts
85+
enterprise/tailnet/testdata/*.golden.html
8586

8687
# Generated files shouldn't be formatted.
8788
site/e2e/provisionerGenerated.ts

Makefile

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -595,7 +595,15 @@ coderd/apidoc/swagger.json: $(shell find ./scripts/apidocgen $(FIND_EXCLUSIONS)
595595
./scripts/apidocgen/generate.sh
596596
pnpm run format:write:only ./docs/api ./docs/manifest.json ./coderd/apidoc/swagger.json
597597

598-
update-golden-files: cli/testdata/.gen-golden helm/coder/tests/testdata/.gen-golden helm/provisioner/tests/testdata/.gen-golden scripts/ci-report/testdata/.gen-golden enterprise/cli/testdata/.gen-golden coderd/.gen-golden provisioner/terraform/testdata/.gen-golden
598+
update-golden-files: \
599+
cli/testdata/.gen-golden \
600+
helm/coder/tests/testdata/.gen-golden \
601+
helm/provisioner/tests/testdata/.gen-golden \
602+
scripts/ci-report/testdata/.gen-golden \
603+
enterprise/cli/testdata/.gen-golden \
604+
enterprise/tailnet/testdata/.gen-golden \
605+
coderd/.gen-golden \
606+
provisioner/terraform/testdata/.gen-golden
599607
.PHONY: update-golden-files
600608

601609
cli/testdata/.gen-golden: $(wildcard cli/testdata/*.golden) $(wildcard cli/*.tpl) $(GO_SRC_FILES) $(wildcard cli/*_test.go)
@@ -606,6 +614,10 @@ enterprise/cli/testdata/.gen-golden: $(wildcard enterprise/cli/testdata/*.golden
606614
go test ./enterprise/cli -run="TestEnterpriseCommandHelp" -update
607615
touch "$@"
608616

617+
enterprise/tailnet/testdata/.gen-golden: $(wildcard enterprise/tailnet/testdata/*.golden.html) $(GO_SRC_FILES) $(wildcard enterprise/tailnet/*_test.go)
618+
go test ./enterprise/tailnet -run="TestDebugTemplate" -update
619+
touch "$@"
620+
609621
helm/coder/tests/testdata/.gen-golden: $(wildcard helm/coder/tests/testdata/*.yaml) $(wildcard helm/coder/tests/testdata/*.golden) $(GO_SRC_FILES) $(wildcard helm/coder/tests/*_test.go)
610622
go test ./helm/coder/tests -run=TestUpdateGoldenFiles -update
611623
touch "$@"

enterprise/tailnet/htmldebug.go

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
package tailnet
2+
3+
import (
4+
"context"
5+
"database/sql"
6+
"html/template"
7+
"net/http"
8+
"time"
9+
10+
"github.com/google/uuid"
11+
"golang.org/x/xerrors"
12+
gProto "google.golang.org/protobuf/proto"
13+
14+
"github.com/coder/coder/v2/coderd/database"
15+
"github.com/coder/coder/v2/tailnet/proto"
16+
)
17+
18+
type HTMLDebug struct {
19+
Coordinators []*HTMLCoordinator
20+
Peers []*HTMLPeer
21+
Tunnels []*HTMLTunnel
22+
}
23+
24+
type HTMLPeer struct {
25+
ID uuid.UUID
26+
CoordinatorID uuid.UUID
27+
LastWriteAge time.Duration
28+
Node *proto.Node
29+
Status database.TailnetStatus
30+
}
31+
32+
type HTMLCoordinator struct {
33+
ID uuid.UUID
34+
HeartbeatAge time.Duration
35+
}
36+
37+
type HTMLTunnel struct {
38+
CoordinatorID uuid.UUID
39+
SrcID uuid.UUID
40+
DstID uuid.UUID
41+
LastWriteAge time.Duration
42+
}
43+
44+
func (c *pgCoord) ServeHTTPDebug(w http.ResponseWriter, r *http.Request) {
45+
ctx := r.Context()
46+
debug, err := getDebug(ctx, c.store)
47+
if err != nil {
48+
w.WriteHeader(http.StatusInternalServerError)
49+
_, _ = w.Write([]byte(err.Error()))
50+
return
51+
}
52+
53+
w.Header().Set("Content-Type", "text/html; charset=utf-8")
54+
55+
err = debugTempl.Execute(w, debug)
56+
if err != nil {
57+
w.WriteHeader(http.StatusInternalServerError)
58+
_, _ = w.Write([]byte(err.Error()))
59+
return
60+
}
61+
}
62+
63+
func getDebug(ctx context.Context, store database.Store) (HTMLDebug, error) {
64+
out := HTMLDebug{}
65+
coords, err := store.GetAllTailnetCoordinators(ctx)
66+
if err != nil && !xerrors.Is(err, sql.ErrNoRows) {
67+
return HTMLDebug{}, xerrors.Errorf("failed to query coordinators: %w", err)
68+
}
69+
peers, err := store.GetAllTailnetPeers(ctx)
70+
if err != nil && !xerrors.Is(err, sql.ErrNoRows) {
71+
return HTMLDebug{}, xerrors.Errorf("failed to query peers: %w", err)
72+
}
73+
tunnels, err := store.GetAllTailnetTunnels(ctx)
74+
if err != nil && !xerrors.Is(err, sql.ErrNoRows) {
75+
return HTMLDebug{}, xerrors.Errorf("failed to query tunnels: %w", err)
76+
}
77+
now := time.Now() // call this once so all our ages are on the same timebase
78+
for _, coord := range coords {
79+
out.Coordinators = append(out.Coordinators, coordToHTML(coord, now))
80+
}
81+
for _, peer := range peers {
82+
ph, err := peerToHTML(peer, now)
83+
if err != nil {
84+
return HTMLDebug{}, err
85+
}
86+
out.Peers = append(out.Peers, ph)
87+
}
88+
for _, tunnel := range tunnels {
89+
out.Tunnels = append(out.Tunnels, tunnelToHTML(tunnel, now))
90+
}
91+
return out, nil
92+
}
93+
94+
func coordToHTML(d database.TailnetCoordinator, now time.Time) *HTMLCoordinator {
95+
return &HTMLCoordinator{
96+
ID: d.ID,
97+
HeartbeatAge: now.Sub(d.HeartbeatAt),
98+
}
99+
}
100+
101+
func peerToHTML(d database.TailnetPeer, now time.Time) (*HTMLPeer, error) {
102+
node := &proto.Node{}
103+
err := gProto.Unmarshal(d.Node, node)
104+
if err != nil {
105+
return nil, xerrors.Errorf("unmarshal node: %w", err)
106+
}
107+
return &HTMLPeer{
108+
ID: d.ID,
109+
CoordinatorID: d.CoordinatorID,
110+
LastWriteAge: now.Sub(d.UpdatedAt),
111+
Status: d.Status,
112+
Node: node,
113+
}, nil
114+
}
115+
116+
func tunnelToHTML(d database.TailnetTunnel, now time.Time) *HTMLTunnel {
117+
return &HTMLTunnel{
118+
CoordinatorID: d.CoordinatorID,
119+
SrcID: d.SrcID,
120+
DstID: d.DstID,
121+
LastWriteAge: now.Sub(d.UpdatedAt),
122+
}
123+
}
124+
125+
var coordinatorDebugTmpl = `
126+
<!DOCTYPE html>
127+
<html>
128+
<head>
129+
<meta charset="UTF-8">
130+
<style>
131+
th, td {
132+
padding-top: 6px;
133+
padding-bottom: 6px;
134+
padding-left: 10px;
135+
padding-right: 10px;
136+
text-align: left;
137+
}
138+
tr {
139+
border-bottom: 1px solid #ddd;
140+
}
141+
</style>
142+
</head>
143+
<body>
144+
<h2 id=coordinators><a href=#coordinators>#</a> coordinators: total {{ len .Coordinators }}</h2>
145+
<table>
146+
<tr style="margin-top:4px">
147+
<th>ID</th>
148+
<th>Heartbeat Age</th>
149+
</tr>
150+
{{- range .Coordinators}}
151+
<tr style="margin-top:4px">
152+
<td>{{ .ID }}</td>
153+
<td>{{ .HeartbeatAge }} ago</td>
154+
</tr>
155+
{{- end }}
156+
</table>
157+
158+
<h2 id=peers> <a href=#peers>#</a> peers: total {{ len .Peers }} </h2>
159+
<table>
160+
<tr style="margin-top:4px">
161+
<th>ID</th>
162+
<th>CoordinatorID</th>
163+
<th>Status</th>
164+
<th>Last Write Age</th>
165+
<th>Node</th>
166+
</tr>
167+
{{- range .Peers }}
168+
<tr style="margin-top:4px">
169+
<td>{{ .ID }}</td>
170+
<td>{{ .CoordinatorID }}</td>
171+
<td>{{ .Status }}</td>
172+
<td>{{ .LastWriteAge }} ago</td>
173+
<td style="white-space: pre;"><code>{{ .Node }}</code></td>
174+
</tr>
175+
{{- end }}
176+
</table>
177+
178+
<h2 id=tunnels><a href=#tunnels>#</a> tunnels: total {{ len .Tunnels }}</h2>
179+
<table>
180+
<tr style="margin-top:4px">
181+
<th>SrcID</th>
182+
<th>DstID</th>
183+
<th>CoordinatorID</th>
184+
<th>Last Write Age</th>
185+
</tr>
186+
{{- range .Tunnels }}
187+
<tr style="margin-top:4px">
188+
<td>{{ .SrcID }}</td>
189+
<td>{{ .DstID }}</td>
190+
<td>{{ .CoordinatorID }}</td>
191+
<td>{{ .LastWriteAge }} ago</td>
192+
</tr>
193+
{{- end }}
194+
</table>
195+
</body>
196+
</html>
197+
`
198+
199+
var debugTempl = template.Must(template.New("coordinator_debug").Parse(coordinatorDebugTmpl))

enterprise/tailnet/pgcoord.go

Lines changed: 0 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import (
66
"encoding/json"
77
"io"
88
"net"
9-
"net/http"
109
"net/netip"
1110
"strings"
1211
"sync"
@@ -19,7 +18,6 @@ import (
1918

2019
"github.com/cenkalti/backoff/v4"
2120
"github.com/google/uuid"
22-
"golang.org/x/exp/slices"
2321
"golang.org/x/xerrors"
2422
gProto "google.golang.org/protobuf/proto"
2523

@@ -28,7 +26,6 @@ import (
2826
"github.com/coder/coder/v2/coderd/database/dbauthz"
2927
"github.com/coder/coder/v2/coderd/database/pubsub"
3028
"github.com/coder/coder/v2/coderd/rbac"
31-
"github.com/coder/coder/v2/coderd/util/slice"
3229
agpl "github.com/coder/coder/v2/tailnet"
3330
)
3431

@@ -1296,29 +1293,6 @@ func (q *querier) setHealthy() {
12961293
q.healthy = true
12971294
}
12981295

1299-
func (q *querier) getAll(ctx context.Context) (map[uuid.UUID]database.TailnetAgent, map[uuid.UUID][]database.TailnetClient, error) {
1300-
agents, err := q.store.GetAllTailnetAgents(ctx)
1301-
if err != nil {
1302-
return nil, nil, xerrors.Errorf("get all tailnet agents: %w", err)
1303-
}
1304-
agentsMap := map[uuid.UUID]database.TailnetAgent{}
1305-
for _, agent := range agents {
1306-
agentsMap[agent.ID] = agent
1307-
}
1308-
clients, err := q.store.GetAllTailnetClients(ctx)
1309-
if err != nil {
1310-
return nil, nil, xerrors.Errorf("get all tailnet clients: %w", err)
1311-
}
1312-
clientsMap := map[uuid.UUID][]database.TailnetClient{}
1313-
for _, client := range clients {
1314-
for _, agentID := range client.AgentIds {
1315-
clientsMap[agentID] = append(clientsMap[agentID], client.TailnetClient)
1316-
}
1317-
}
1318-
1319-
return agentsMap, clientsMap, nil
1320-
}
1321-
13221296
func parseTunnelUpdate(msg string) ([]uuid.UUID, error) {
13231297
parts := strings.Split(msg, ",")
13241298
if len(parts) != 2 {
@@ -1721,91 +1695,3 @@ func (h *heartbeats) cleanup() {
17211695
}
17221696
h.logger.Debug(h.ctx, "cleaned up old coordinators")
17231697
}
1724-
1725-
func (c *pgCoord) ServeHTTPDebug(w http.ResponseWriter, r *http.Request) {
1726-
ctx := r.Context()
1727-
debug, err := c.htmlDebug(ctx)
1728-
if err != nil {
1729-
w.WriteHeader(http.StatusInternalServerError)
1730-
_, _ = w.Write([]byte(err.Error()))
1731-
return
1732-
}
1733-
1734-
agpl.CoordinatorHTTPDebug(debug)(w, r)
1735-
}
1736-
1737-
func (c *pgCoord) htmlDebug(ctx context.Context) (agpl.HTMLDebug, error) {
1738-
now := time.Now()
1739-
data := agpl.HTMLDebug{}
1740-
agents, clients, err := c.querier.getAll(ctx)
1741-
if err != nil {
1742-
return data, xerrors.Errorf("get all agents and clients: %w", err)
1743-
}
1744-
1745-
for _, agent := range agents {
1746-
htmlAgent := &agpl.HTMLAgent{
1747-
ID: agent.ID,
1748-
// Name: ??, TODO: get agent names
1749-
LastWriteAge: now.Sub(agent.UpdatedAt).Round(time.Second),
1750-
}
1751-
for _, conn := range clients[agent.ID] {
1752-
htmlAgent.Connections = append(htmlAgent.Connections, &agpl.HTMLClient{
1753-
ID: conn.ID,
1754-
Name: conn.ID.String(),
1755-
LastWriteAge: now.Sub(conn.UpdatedAt).Round(time.Second),
1756-
})
1757-
data.Nodes = append(data.Nodes, &agpl.HTMLNode{
1758-
ID: conn.ID,
1759-
Node: conn.Node,
1760-
})
1761-
}
1762-
slices.SortFunc(htmlAgent.Connections, func(a, b *agpl.HTMLClient) int {
1763-
return slice.Ascending(a.Name, b.Name)
1764-
})
1765-
1766-
data.Agents = append(data.Agents, htmlAgent)
1767-
data.Nodes = append(data.Nodes, &agpl.HTMLNode{
1768-
ID: agent.ID,
1769-
// Name: ??, TODO: get agent names
1770-
Node: agent.Node,
1771-
})
1772-
}
1773-
slices.SortFunc(data.Agents, func(a, b *agpl.HTMLAgent) int {
1774-
return slice.Ascending(a.Name, b.Name)
1775-
})
1776-
1777-
for agentID, conns := range clients {
1778-
if len(conns) == 0 {
1779-
continue
1780-
}
1781-
1782-
if _, ok := agents[agentID]; ok {
1783-
continue
1784-
}
1785-
agent := &agpl.HTMLAgent{
1786-
Name: "unknown",
1787-
ID: agentID,
1788-
}
1789-
for _, conn := range conns {
1790-
agent.Connections = append(agent.Connections, &agpl.HTMLClient{
1791-
Name: conn.ID.String(),
1792-
ID: conn.ID,
1793-
LastWriteAge: now.Sub(conn.UpdatedAt).Round(time.Second),
1794-
})
1795-
data.Nodes = append(data.Nodes, &agpl.HTMLNode{
1796-
ID: conn.ID,
1797-
Node: conn.Node,
1798-
})
1799-
}
1800-
slices.SortFunc(agent.Connections, func(a, b *agpl.HTMLClient) int {
1801-
return slice.Ascending(a.Name, b.Name)
1802-
})
1803-
1804-
data.MissingAgents = append(data.MissingAgents, agent)
1805-
}
1806-
slices.SortFunc(data.MissingAgents, func(a, b *agpl.HTMLAgent) int {
1807-
return slice.Ascending(a.Name, b.Name)
1808-
})
1809-
1810-
return data, nil
1811-
}

0 commit comments

Comments
 (0)