Skip to content

feat: implement HTMLDebug for PGCoord with v2 API #10914

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Nov 28, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
feat: implement HTMLDebug for PGCoord with v2 API
  • Loading branch information
spikecurtis committed Nov 28, 2023
commit adc5917536689a6b563c125606ab6adc9d997007
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ helm/**/templates/*.yaml

# Testdata shouldn't be formatted.
scripts/apitypings/testdata/**/*.ts
enterprise/tailnet/testdata/*.golden.html

# Generated files shouldn't be formatted.
site/e2e/provisionerGenerated.ts
Expand Down
1 change: 1 addition & 0 deletions .prettierignore.include
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ helm/**/templates/*.yaml

# Testdata shouldn't be formatted.
scripts/apitypings/testdata/**/*.ts
enterprise/tailnet/testdata/*.golden.html

# Generated files shouldn't be formatted.
site/e2e/provisionerGenerated.ts
Expand Down
14 changes: 13 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -595,7 +595,15 @@ coderd/apidoc/swagger.json: $(shell find ./scripts/apidocgen $(FIND_EXCLUSIONS)
./scripts/apidocgen/generate.sh
pnpm run format:write:only ./docs/api ./docs/manifest.json ./coderd/apidoc/swagger.json

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
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 \
enterprise/tailnet/testdata/.gen-golden \
coderd/.gen-golden \
provisioner/terraform/testdata/.gen-golden
.PHONY: update-golden-files

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

enterprise/tailnet/testdata/.gen-golden: $(wildcard enterprise/tailnet/testdata/*.golden.html) $(GO_SRC_FILES) $(wildcard enterprise/tailnet/*_test.go)
go test ./enterprise/tailnet -run="TestDebugTemplate" -update
touch "$@"

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)
go test ./helm/coder/tests -run=TestUpdateGoldenFiles -update
touch "$@"
Expand Down
199 changes: 199 additions & 0 deletions enterprise/tailnet/htmldebug.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
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 = `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
th, td {
padding-top: 6px;
padding-bottom: 6px;
padding-left: 10px;
padding-right: 10px;
text-align: left;
}
tr {
border-bottom: 1px solid #ddd;
}
</style>
</head>
<body>
<h2 id=coordinators><a href=#coordinators>#</a> coordinators: total {{ len .Coordinators }}</h2>
<table>
<tr style="margin-top:4px">
<th>ID</th>
<th>Heartbeat Age</th>
</tr>
{{- range .Coordinators}}
<tr style="margin-top:4px">
<td>{{ .ID }}</td>
<td>{{ .HeartbeatAge }} ago</td>
</tr>
{{- end }}
</table>

<h2 id=peers> <a href=#peers>#</a> peers: total {{ len .Peers }} </h2>
<table>
<tr style="margin-top:4px">
<th>ID</th>
<th>CoordinatorID</th>
<th>Status</th>
<th>Last Write Age</th>
<th>Node</th>
</tr>
{{- range .Peers }}
<tr style="margin-top:4px">
<td>{{ .ID }}</td>
<td>{{ .CoordinatorID }}</td>
<td>{{ .Status }}</td>
<td>{{ .LastWriteAge }} ago</td>
<td style="white-space: pre;"><code>{{ .Node }}</code></td>
</tr>
{{- end }}
</table>

<h2 id=tunnels><a href=#tunnels>#</a> tunnels: total {{ len .Tunnels }}</h2>
<table>
<tr style="margin-top:4px">
<th>SrcID</th>
<th>DstID</th>
<th>CoordinatorID</th>
<th>Last Write Age</th>
</tr>
{{- range .Tunnels }}
<tr style="margin-top:4px">
<td>{{ .SrcID }}</td>
<td>{{ .DstID }}</td>
<td>{{ .CoordinatorID }}</td>
<td>{{ .LastWriteAge }} ago</td>
</tr>
{{- end }}
</table>
</body>
</html>
`

var debugTempl = template.Must(template.New("coordinator_debug").Parse(coordinatorDebugTmpl))
114 changes: 0 additions & 114 deletions enterprise/tailnet/pgcoord.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
"encoding/json"
"io"
"net"
"net/http"
"net/netip"
"strings"
"sync"
Expand All @@ -19,7 +18,6 @@ import (

"github.com/cenkalti/backoff/v4"
"github.com/google/uuid"
"golang.org/x/exp/slices"
"golang.org/x/xerrors"
gProto "google.golang.org/protobuf/proto"

Expand All @@ -28,7 +26,6 @@ import (
"github.com/coder/coder/v2/coderd/database/dbauthz"
"github.com/coder/coder/v2/coderd/database/pubsub"
"github.com/coder/coder/v2/coderd/rbac"
"github.com/coder/coder/v2/coderd/util/slice"
agpl "github.com/coder/coder/v2/tailnet"
)

Expand Down Expand Up @@ -1296,29 +1293,6 @@ func (q *querier) setHealthy() {
q.healthy = true
}

func (q *querier) getAll(ctx context.Context) (map[uuid.UUID]database.TailnetAgent, map[uuid.UUID][]database.TailnetClient, error) {
agents, err := q.store.GetAllTailnetAgents(ctx)
if err != nil {
return nil, nil, xerrors.Errorf("get all tailnet agents: %w", err)
}
agentsMap := map[uuid.UUID]database.TailnetAgent{}
for _, agent := range agents {
agentsMap[agent.ID] = agent
}
clients, err := q.store.GetAllTailnetClients(ctx)
if err != nil {
return nil, nil, xerrors.Errorf("get all tailnet clients: %w", err)
}
clientsMap := map[uuid.UUID][]database.TailnetClient{}
for _, client := range clients {
for _, agentID := range client.AgentIds {
clientsMap[agentID] = append(clientsMap[agentID], client.TailnetClient)
}
}

return agentsMap, clientsMap, nil
}

func parseTunnelUpdate(msg string) ([]uuid.UUID, error) {
parts := strings.Split(msg, ",")
if len(parts) != 2 {
Expand Down Expand Up @@ -1721,91 +1695,3 @@ func (h *heartbeats) cleanup() {
}
h.logger.Debug(h.ctx, "cleaned up old coordinators")
}

func (c *pgCoord) ServeHTTPDebug(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
debug, err := c.htmlDebug(ctx)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
_, _ = w.Write([]byte(err.Error()))
return
}

agpl.CoordinatorHTTPDebug(debug)(w, r)
}

func (c *pgCoord) htmlDebug(ctx context.Context) (agpl.HTMLDebug, error) {
now := time.Now()
data := agpl.HTMLDebug{}
agents, clients, err := c.querier.getAll(ctx)
if err != nil {
return data, xerrors.Errorf("get all agents and clients: %w", err)
}

for _, agent := range agents {
htmlAgent := &agpl.HTMLAgent{
ID: agent.ID,
// Name: ??, TODO: get agent names
LastWriteAge: now.Sub(agent.UpdatedAt).Round(time.Second),
}
for _, conn := range clients[agent.ID] {
htmlAgent.Connections = append(htmlAgent.Connections, &agpl.HTMLClient{
ID: conn.ID,
Name: conn.ID.String(),
LastWriteAge: now.Sub(conn.UpdatedAt).Round(time.Second),
})
data.Nodes = append(data.Nodes, &agpl.HTMLNode{
ID: conn.ID,
Node: conn.Node,
})
}
slices.SortFunc(htmlAgent.Connections, func(a, b *agpl.HTMLClient) int {
return slice.Ascending(a.Name, b.Name)
})

data.Agents = append(data.Agents, htmlAgent)
data.Nodes = append(data.Nodes, &agpl.HTMLNode{
ID: agent.ID,
// Name: ??, TODO: get agent names
Node: agent.Node,
})
}
slices.SortFunc(data.Agents, func(a, b *agpl.HTMLAgent) int {
return slice.Ascending(a.Name, b.Name)
})

for agentID, conns := range clients {
if len(conns) == 0 {
continue
}

if _, ok := agents[agentID]; ok {
continue
}
agent := &agpl.HTMLAgent{
Name: "unknown",
ID: agentID,
}
for _, conn := range conns {
agent.Connections = append(agent.Connections, &agpl.HTMLClient{
Name: conn.ID.String(),
ID: conn.ID,
LastWriteAge: now.Sub(conn.UpdatedAt).Round(time.Second),
})
data.Nodes = append(data.Nodes, &agpl.HTMLNode{
ID: conn.ID,
Node: conn.Node,
})
}
slices.SortFunc(agent.Connections, func(a, b *agpl.HTMLClient) int {
return slice.Ascending(a.Name, b.Name)
})

data.MissingAgents = append(data.MissingAgents, agent)
}
slices.SortFunc(data.MissingAgents, func(a, b *agpl.HTMLAgent) int {
return slice.Ascending(a.Name, b.Name)
})

return data, nil
}
Loading