mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: implement HTMLDebug for PGCoord with v2 API (#10914)
Implements HTMLDebug for the PGCoordinator with the new v2 API and related DB tables.
This commit is contained in:
@@ -82,6 +82,7 @@ helm/**/templates/*.yaml
|
|||||||
|
|
||||||
# Testdata shouldn't be formatted.
|
# Testdata shouldn't be formatted.
|
||||||
scripts/apitypings/testdata/**/*.ts
|
scripts/apitypings/testdata/**/*.ts
|
||||||
|
enterprise/tailnet/testdata/*.golden.html
|
||||||
|
|
||||||
# Generated files shouldn't be formatted.
|
# Generated files shouldn't be formatted.
|
||||||
site/e2e/provisionerGenerated.ts
|
site/e2e/provisionerGenerated.ts
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ helm/**/templates/*.yaml
|
|||||||
|
|
||||||
# Testdata shouldn't be formatted.
|
# Testdata shouldn't be formatted.
|
||||||
scripts/apitypings/testdata/**/*.ts
|
scripts/apitypings/testdata/**/*.ts
|
||||||
|
enterprise/tailnet/testdata/*.golden.html
|
||||||
|
|
||||||
# Generated files shouldn't be formatted.
|
# Generated files shouldn't be formatted.
|
||||||
site/e2e/provisionerGenerated.ts
|
site/e2e/provisionerGenerated.ts
|
||||||
|
|||||||
@@ -595,7 +595,15 @@ coderd/apidoc/swagger.json: $(shell find ./scripts/apidocgen $(FIND_EXCLUSIONS)
|
|||||||
./scripts/apidocgen/generate.sh
|
./scripts/apidocgen/generate.sh
|
||||||
pnpm run format:write:only ./docs/api ./docs/manifest.json ./coderd/apidoc/swagger.json
|
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
|
.PHONY: update-golden-files
|
||||||
|
|
||||||
cli/testdata/.gen-golden: $(wildcard cli/testdata/*.golden) $(wildcard cli/*.tpl) $(GO_SRC_FILES) $(wildcard cli/*_test.go)
|
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
|
|||||||
go test ./enterprise/cli -run="TestEnterpriseCommandHelp" -update
|
go test ./enterprise/cli -run="TestEnterpriseCommandHelp" -update
|
||||||
touch "$@"
|
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)
|
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
|
go test ./helm/coder/tests -run=TestUpdateGoldenFiles -update
|
||||||
touch "$@"
|
touch "$@"
|
||||||
|
|||||||
@@ -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))
|
||||||
@@ -6,7 +6,6 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -19,7 +18,6 @@ import (
|
|||||||
|
|
||||||
"github.com/cenkalti/backoff/v4"
|
"github.com/cenkalti/backoff/v4"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"golang.org/x/exp/slices"
|
|
||||||
"golang.org/x/xerrors"
|
"golang.org/x/xerrors"
|
||||||
gProto "google.golang.org/protobuf/proto"
|
gProto "google.golang.org/protobuf/proto"
|
||||||
|
|
||||||
@@ -28,7 +26,6 @@ import (
|
|||||||
"github.com/coder/coder/v2/coderd/database/dbauthz"
|
"github.com/coder/coder/v2/coderd/database/dbauthz"
|
||||||
"github.com/coder/coder/v2/coderd/database/pubsub"
|
"github.com/coder/coder/v2/coderd/database/pubsub"
|
||||||
"github.com/coder/coder/v2/coderd/rbac"
|
"github.com/coder/coder/v2/coderd/rbac"
|
||||||
"github.com/coder/coder/v2/coderd/util/slice"
|
|
||||||
agpl "github.com/coder/coder/v2/tailnet"
|
agpl "github.com/coder/coder/v2/tailnet"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1296,29 +1293,6 @@ func (q *querier) setHealthy() {
|
|||||||
q.healthy = true
|
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) {
|
func parseTunnelUpdate(msg string) ([]uuid.UUID, error) {
|
||||||
parts := strings.Split(msg, ",")
|
parts := strings.Split(msg, ",")
|
||||||
if len(parts) != 2 {
|
if len(parts) != 2 {
|
||||||
@@ -1721,91 +1695,3 @@ func (h *heartbeats) cleanup() {
|
|||||||
}
|
}
|
||||||
h.logger.Debug(h.ctx, "cleaned up old coordinators")
|
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
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,19 +1,35 @@
|
|||||||
package tailnet
|
package tailnet
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"flag"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/golang/mock/gomock"
|
"github.com/golang/mock/gomock"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
gProto "google.golang.org/protobuf/proto"
|
||||||
|
|
||||||
"cdr.dev/slog"
|
"cdr.dev/slog"
|
||||||
"cdr.dev/slog/sloggers/slogtest"
|
"cdr.dev/slog/sloggers/slogtest"
|
||||||
|
|
||||||
|
"github.com/coder/coder/v2/coderd/database"
|
||||||
"github.com/coder/coder/v2/coderd/database/dbmock"
|
"github.com/coder/coder/v2/coderd/database/dbmock"
|
||||||
|
"github.com/coder/coder/v2/coderd/database/dbtestutil"
|
||||||
|
"github.com/coder/coder/v2/tailnet/proto"
|
||||||
"github.com/coder/coder/v2/testutil"
|
"github.com/coder/coder/v2/testutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// UpdateGoldenFiles indicates golden files should be updated.
|
||||||
|
// To update the golden files:
|
||||||
|
// make update-golden-files
|
||||||
|
var UpdateGoldenFiles = flag.Bool("update", false, "update .golden files")
|
||||||
|
|
||||||
// TestHeartbeat_Cleanup is internal so that we can overwrite the cleanup period and not wait an hour for the timed
|
// TestHeartbeat_Cleanup is internal so that we can overwrite the cleanup period and not wait an hour for the timed
|
||||||
// cleanup.
|
// cleanup.
|
||||||
func TestHeartbeat_Cleanup(t *testing.T) {
|
func TestHeartbeat_Cleanup(t *testing.T) {
|
||||||
@@ -50,3 +66,122 @@ func TestHeartbeat_Cleanup(t *testing.T) {
|
|||||||
}
|
}
|
||||||
close(waitForCleanup)
|
close(waitForCleanup)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDebugTemplate(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
t.Skip("newlines screw up golden files on windows")
|
||||||
|
}
|
||||||
|
c1 := uuid.MustParse("01000000-1111-1111-1111-111111111111")
|
||||||
|
c2 := uuid.MustParse("02000000-1111-1111-1111-111111111111")
|
||||||
|
p1 := uuid.MustParse("01000000-2222-2222-2222-222222222222")
|
||||||
|
p2 := uuid.MustParse("02000000-2222-2222-2222-222222222222")
|
||||||
|
in := HTMLDebug{
|
||||||
|
Coordinators: []*HTMLCoordinator{
|
||||||
|
{
|
||||||
|
ID: c1,
|
||||||
|
HeartbeatAge: 2 * time.Second,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: c2,
|
||||||
|
HeartbeatAge: time.Second,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Peers: []*HTMLPeer{
|
||||||
|
{
|
||||||
|
ID: p1,
|
||||||
|
CoordinatorID: c1,
|
||||||
|
LastWriteAge: 5 * time.Second,
|
||||||
|
Status: database.TailnetStatusOk,
|
||||||
|
Node: `id:1 preferred_derp:999 endpoints:"192.168.0.49:4449"`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: p2,
|
||||||
|
CoordinatorID: c2,
|
||||||
|
LastWriteAge: 7 * time.Second,
|
||||||
|
Status: database.TailnetStatusLost,
|
||||||
|
Node: `id:2 preferred_derp:999 endpoints:"192.168.0.33:4449"`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Tunnels: []*HTMLTunnel{
|
||||||
|
{
|
||||||
|
CoordinatorID: c1,
|
||||||
|
SrcID: p1,
|
||||||
|
DstID: p2,
|
||||||
|
LastWriteAge: 3 * time.Second,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
err := debugTempl.Execute(buf, in)
|
||||||
|
require.NoError(t, err)
|
||||||
|
actual := buf.Bytes()
|
||||||
|
|
||||||
|
goldenPath := filepath.Join("testdata", "debug.golden.html")
|
||||||
|
if *UpdateGoldenFiles {
|
||||||
|
t.Logf("update golden file %s", goldenPath)
|
||||||
|
err := os.WriteFile(goldenPath, actual, 0o600)
|
||||||
|
require.NoError(t, err, "update golden file")
|
||||||
|
}
|
||||||
|
|
||||||
|
expected, err := os.ReadFile(goldenPath)
|
||||||
|
require.NoError(t, err, "read golden file, run \"make update-golden-files\" and commit the changes")
|
||||||
|
|
||||||
|
require.Equal(
|
||||||
|
t, string(expected), string(actual),
|
||||||
|
"golden file mismatch: %s, run \"make update-golden-files\", verify and commit the changes",
|
||||||
|
goldenPath,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetDebug(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
if !dbtestutil.WillUsePostgres() {
|
||||||
|
t.Skip("test only with postgres")
|
||||||
|
}
|
||||||
|
store, _ := dbtestutil.NewDB(t)
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
coordID := uuid.New()
|
||||||
|
_, err := store.UpsertTailnetCoordinator(ctx, coordID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
peerID := uuid.New()
|
||||||
|
node := &proto.Node{PreferredDerp: 44}
|
||||||
|
nodeb, err := gProto.Marshal(node)
|
||||||
|
require.NoError(t, err)
|
||||||
|
_, err = store.UpsertTailnetPeer(ctx, database.UpsertTailnetPeerParams{
|
||||||
|
ID: peerID,
|
||||||
|
CoordinatorID: coordID,
|
||||||
|
Node: nodeb,
|
||||||
|
Status: database.TailnetStatusLost,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
dstID := uuid.New()
|
||||||
|
_, err = store.UpsertTailnetTunnel(ctx, database.UpsertTailnetTunnelParams{
|
||||||
|
CoordinatorID: coordID,
|
||||||
|
SrcID: peerID,
|
||||||
|
DstID: dstID,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
debug, err := getDebug(ctx, store)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Len(t, debug.Coordinators, 1)
|
||||||
|
require.Len(t, debug.Peers, 1)
|
||||||
|
require.Len(t, debug.Tunnels, 1)
|
||||||
|
|
||||||
|
require.Equal(t, coordID, debug.Coordinators[0].ID)
|
||||||
|
|
||||||
|
require.Equal(t, peerID, debug.Peers[0].ID)
|
||||||
|
require.Equal(t, coordID, debug.Peers[0].CoordinatorID)
|
||||||
|
require.Equal(t, database.TailnetStatusLost, debug.Peers[0].Status)
|
||||||
|
require.Equal(t, node.String(), debug.Peers[0].Node)
|
||||||
|
|
||||||
|
require.Equal(t, coordID, debug.Tunnels[0].CoordinatorID)
|
||||||
|
require.Equal(t, peerID, debug.Tunnels[0].SrcID)
|
||||||
|
require.Equal(t, dstID, debug.Tunnels[0].DstID)
|
||||||
|
}
|
||||||
|
|||||||
+77
@@ -0,0 +1,77 @@
|
|||||||
|
|
||||||
|
<!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 2</h2>
|
||||||
|
<table>
|
||||||
|
<tr style="margin-top:4px">
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Heartbeat Age</th>
|
||||||
|
</tr>
|
||||||
|
<tr style="margin-top:4px">
|
||||||
|
<td>01000000-1111-1111-1111-111111111111</td>
|
||||||
|
<td>2s ago</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="margin-top:4px">
|
||||||
|
<td>02000000-1111-1111-1111-111111111111</td>
|
||||||
|
<td>1s ago</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h2 id=peers> <a href=#peers>#</a> peers: total 2 </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>
|
||||||
|
<tr style="margin-top:4px">
|
||||||
|
<td>01000000-2222-2222-2222-222222222222</td>
|
||||||
|
<td>01000000-1111-1111-1111-111111111111</td>
|
||||||
|
<td>ok</td>
|
||||||
|
<td>5s ago</td>
|
||||||
|
<td style="white-space: pre;"><code>id:1 preferred_derp:999 endpoints:"192.168.0.49:4449"</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr style="margin-top:4px">
|
||||||
|
<td>02000000-2222-2222-2222-222222222222</td>
|
||||||
|
<td>02000000-1111-1111-1111-111111111111</td>
|
||||||
|
<td>lost</td>
|
||||||
|
<td>7s ago</td>
|
||||||
|
<td style="white-space: pre;"><code>id:2 preferred_derp:999 endpoints:"192.168.0.33:4449"</code></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h2 id=tunnels><a href=#tunnels>#</a> tunnels: total 1</h2>
|
||||||
|
<table>
|
||||||
|
<tr style="margin-top:4px">
|
||||||
|
<th>SrcID</th>
|
||||||
|
<th>DstID</th>
|
||||||
|
<th>CoordinatorID</th>
|
||||||
|
<th>Last Write Age</th>
|
||||||
|
</tr>
|
||||||
|
<tr style="margin-top:4px">
|
||||||
|
<td>01000000-2222-2222-2222-222222222222</td>
|
||||||
|
<td>02000000-2222-2222-2222-222222222222</td>
|
||||||
|
<td>01000000-1111-1111-1111-111111111111</td>
|
||||||
|
<td>3s ago</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -82,6 +82,7 @@ result
|
|||||||
|
|
||||||
# Testdata shouldn't be formatted.
|
# Testdata shouldn't be formatted.
|
||||||
../scripts/apitypings/testdata/**/*.ts
|
../scripts/apitypings/testdata/**/*.ts
|
||||||
|
../enterprise/tailnet/testdata/*.golden.html
|
||||||
|
|
||||||
# Generated files shouldn't be formatted.
|
# Generated files shouldn't be formatted.
|
||||||
e2e/provisionerGenerated.ts
|
e2e/provisionerGenerated.ts
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ result
|
|||||||
|
|
||||||
# Testdata shouldn't be formatted.
|
# Testdata shouldn't be formatted.
|
||||||
../scripts/apitypings/testdata/**/*.ts
|
../scripts/apitypings/testdata/**/*.ts
|
||||||
|
../enterprise/tailnet/testdata/*.golden.html
|
||||||
|
|
||||||
# Generated files shouldn't be formatted.
|
# Generated files shouldn't be formatted.
|
||||||
e2e/provisionerGenerated.ts
|
e2e/provisionerGenerated.ts
|
||||||
|
|||||||
Reference in New Issue
Block a user