From 15dd253fefe5611dec46c10c4c56978734387abd Mon Sep 17 00:00:00 2001 From: Spike Curtis Date: Mon, 27 Nov 2023 16:44:10 +0400 Subject: [PATCH 1/2] feat: add queries for PGCoord HTMLDebug --- coderd/database/dbauthz/dbauthz.go | 21 ++++++ coderd/database/dbmem/dbmem.go | 12 ++++ coderd/database/dbmetrics/dbmetrics.go | 21 ++++++ coderd/database/dbmock/dbmock.go | 45 ++++++++++++ coderd/database/querier.go | 4 ++ coderd/database/queries.sql.go | 94 ++++++++++++++++++++++++++ coderd/database/queries/tailnet.sql | 11 +++ 7 files changed, 208 insertions(+) diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index 83ea22b628779..9fb52099e9f00 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -881,6 +881,27 @@ func (q *querier) GetAllTailnetClients(ctx context.Context) ([]database.GetAllTa return q.db.GetAllTailnetClients(ctx) } +func (q *querier) GetAllTailnetCoordinators(ctx context.Context) ([]database.TailnetCoordinator, error) { + if err := q.authorizeContext(ctx, rbac.ActionRead, rbac.ResourceTailnetCoordinator); err != nil { + return nil, err + } + return q.db.GetAllTailnetCoordinators(ctx) +} + +func (q *querier) GetAllTailnetPeers(ctx context.Context) ([]database.TailnetPeer, error) { + if err := q.authorizeContext(ctx, rbac.ActionRead, rbac.ResourceTailnetCoordinator); err != nil { + return nil, err + } + return q.db.GetAllTailnetPeers(ctx) +} + +func (q *querier) GetAllTailnetTunnels(ctx context.Context) ([]database.TailnetTunnel, error) { + if err := q.authorizeContext(ctx, rbac.ActionRead, rbac.ResourceTailnetCoordinator); err != nil { + return nil, err + } + return q.db.GetAllTailnetTunnels(ctx) +} + func (q *querier) GetAppSecurityKey(ctx context.Context) (string, error) { // No authz checks return q.db.GetAppSecurityKey(ctx) diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index b62ef08927bfd..3b78d67ebeba4 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -1276,6 +1276,18 @@ func (*FakeQuerier) GetAllTailnetClients(_ context.Context) ([]database.GetAllTa return nil, ErrUnimplemented } +func (*FakeQuerier) GetAllTailnetCoordinators(context.Context) ([]database.TailnetCoordinator, error) { + return nil, ErrUnimplemented +} + +func (*FakeQuerier) GetAllTailnetPeers(context.Context) ([]database.TailnetPeer, error) { + return nil, ErrUnimplemented +} + +func (*FakeQuerier) GetAllTailnetTunnels(context.Context) ([]database.TailnetTunnel, error) { + return nil, ErrUnimplemented +} + func (q *FakeQuerier) GetAppSecurityKey(_ context.Context) (string, error) { q.mutex.RLock() defer q.mutex.RUnlock() diff --git a/coderd/database/dbmetrics/dbmetrics.go b/coderd/database/dbmetrics/dbmetrics.go index 5d8d0f1030088..e37153463a24a 100644 --- a/coderd/database/dbmetrics/dbmetrics.go +++ b/coderd/database/dbmetrics/dbmetrics.go @@ -314,6 +314,27 @@ func (m metricsStore) GetAllTailnetClients(ctx context.Context) ([]database.GetA return r0, r1 } +func (m metricsStore) GetAllTailnetCoordinators(ctx context.Context) ([]database.TailnetCoordinator, error) { + start := time.Now() + r0, r1 := m.s.GetAllTailnetCoordinators(ctx) + m.queryLatencies.WithLabelValues("GetAllTailnetCoordinators").Observe(time.Since(start).Seconds()) + return r0, r1 +} + +func (m metricsStore) GetAllTailnetPeers(ctx context.Context) ([]database.TailnetPeer, error) { + start := time.Now() + r0, r1 := m.s.GetAllTailnetPeers(ctx) + m.queryLatencies.WithLabelValues("GetAllTailnetPeers").Observe(time.Since(start).Seconds()) + return r0, r1 +} + +func (m metricsStore) GetAllTailnetTunnels(ctx context.Context) ([]database.TailnetTunnel, error) { + start := time.Now() + r0, r1 := m.s.GetAllTailnetTunnels(ctx) + m.queryLatencies.WithLabelValues("GetAllTailnetTunnels").Observe(time.Since(start).Seconds()) + return r0, r1 +} + func (m metricsStore) GetAppSecurityKey(ctx context.Context) (string, error) { start := time.Now() key, err := m.s.GetAppSecurityKey(ctx) diff --git a/coderd/database/dbmock/dbmock.go b/coderd/database/dbmock/dbmock.go index 2bf37350efbaa..bcc03a77ddd1a 100644 --- a/coderd/database/dbmock/dbmock.go +++ b/coderd/database/dbmock/dbmock.go @@ -532,6 +532,51 @@ func (mr *MockStoreMockRecorder) GetAllTailnetClients(arg0 interface{}) *gomock. return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAllTailnetClients", reflect.TypeOf((*MockStore)(nil).GetAllTailnetClients), arg0) } +// GetAllTailnetCoordinators mocks base method. +func (m *MockStore) GetAllTailnetCoordinators(arg0 context.Context) ([]database.TailnetCoordinator, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetAllTailnetCoordinators", arg0) + ret0, _ := ret[0].([]database.TailnetCoordinator) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetAllTailnetCoordinators indicates an expected call of GetAllTailnetCoordinators. +func (mr *MockStoreMockRecorder) GetAllTailnetCoordinators(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAllTailnetCoordinators", reflect.TypeOf((*MockStore)(nil).GetAllTailnetCoordinators), arg0) +} + +// GetAllTailnetPeers mocks base method. +func (m *MockStore) GetAllTailnetPeers(arg0 context.Context) ([]database.TailnetPeer, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetAllTailnetPeers", arg0) + ret0, _ := ret[0].([]database.TailnetPeer) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetAllTailnetPeers indicates an expected call of GetAllTailnetPeers. +func (mr *MockStoreMockRecorder) GetAllTailnetPeers(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAllTailnetPeers", reflect.TypeOf((*MockStore)(nil).GetAllTailnetPeers), arg0) +} + +// GetAllTailnetTunnels mocks base method. +func (m *MockStore) GetAllTailnetTunnels(arg0 context.Context) ([]database.TailnetTunnel, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetAllTailnetTunnels", arg0) + ret0, _ := ret[0].([]database.TailnetTunnel) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetAllTailnetTunnels indicates an expected call of GetAllTailnetTunnels. +func (mr *MockStoreMockRecorder) GetAllTailnetTunnels(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAllTailnetTunnels", reflect.TypeOf((*MockStore)(nil).GetAllTailnetTunnels), arg0) +} + // GetAppSecurityKey mocks base method. func (m *MockStore) GetAppSecurityKey(arg0 context.Context) (string, error) { m.ctrl.T.Helper() diff --git a/coderd/database/querier.go b/coderd/database/querier.go index 10c6be43a8179..d1b3e070d8b13 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -74,6 +74,10 @@ type sqlcQuerier interface { GetActiveWorkspaceBuildsByTemplateID(ctx context.Context, templateID uuid.UUID) ([]WorkspaceBuild, error) GetAllTailnetAgents(ctx context.Context) ([]TailnetAgent, error) GetAllTailnetClients(ctx context.Context) ([]GetAllTailnetClientsRow, error) + // For PG Coordinator HTMLDebug + GetAllTailnetCoordinators(ctx context.Context) ([]TailnetCoordinator, error) + GetAllTailnetPeers(ctx context.Context) ([]TailnetPeer, error) + GetAllTailnetTunnels(ctx context.Context) ([]TailnetTunnel, error) GetAppSecurityKey(ctx context.Context) (string, error) GetApplicationName(ctx context.Context) (string, error) // GetAuditLogsBefore retrieves `row_limit` number of audit logs before the provided diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index c5aac3013fb11..254515c00811a 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -4754,6 +4754,100 @@ func (q *sqlQuerier) GetAllTailnetClients(ctx context.Context) ([]GetAllTailnetC return items, nil } +const getAllTailnetCoordinators = `-- name: GetAllTailnetCoordinators :many + +SELECT id, heartbeat_at FROM tailnet_coordinators +` + +// For PG Coordinator HTMLDebug +func (q *sqlQuerier) GetAllTailnetCoordinators(ctx context.Context) ([]TailnetCoordinator, error) { + rows, err := q.db.QueryContext(ctx, getAllTailnetCoordinators) + if err != nil { + return nil, err + } + defer rows.Close() + var items []TailnetCoordinator + for rows.Next() { + var i TailnetCoordinator + if err := rows.Scan(&i.ID, &i.HeartbeatAt); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getAllTailnetPeers = `-- name: GetAllTailnetPeers :many +SELECT id, coordinator_id, updated_at, node, status FROM tailnet_peers +` + +func (q *sqlQuerier) GetAllTailnetPeers(ctx context.Context) ([]TailnetPeer, error) { + rows, err := q.db.QueryContext(ctx, getAllTailnetPeers) + if err != nil { + return nil, err + } + defer rows.Close() + var items []TailnetPeer + for rows.Next() { + var i TailnetPeer + if err := rows.Scan( + &i.ID, + &i.CoordinatorID, + &i.UpdatedAt, + &i.Node, + &i.Status, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getAllTailnetTunnels = `-- name: GetAllTailnetTunnels :many +SELECT coordinator_id, src_id, dst_id, updated_at FROM tailnet_tunnels +` + +func (q *sqlQuerier) GetAllTailnetTunnels(ctx context.Context) ([]TailnetTunnel, error) { + rows, err := q.db.QueryContext(ctx, getAllTailnetTunnels) + if err != nil { + return nil, err + } + defer rows.Close() + var items []TailnetTunnel + for rows.Next() { + var i TailnetTunnel + if err := rows.Scan( + &i.CoordinatorID, + &i.SrcID, + &i.DstID, + &i.UpdatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const getTailnetAgents = `-- name: GetTailnetAgents :many SELECT id, coordinator_id, updated_at, node FROM tailnet_agents diff --git a/coderd/database/queries/tailnet.sql b/coderd/database/queries/tailnet.sql index 011bc6900f2e7..6128c944f9619 100644 --- a/coderd/database/queries/tailnet.sql +++ b/coderd/database/queries/tailnet.sql @@ -199,3 +199,14 @@ SELECT tailnet_tunnels.src_id as peer_id, tailnet_peers.coordinator_id, tailnet_ FROM tailnet_tunnels INNER JOIN tailnet_peers ON tailnet_tunnels.src_id = tailnet_peers.id WHERE tailnet_tunnels.dst_id = $1; + +-- For PG Coordinator HTMLDebug + +-- name: GetAllTailnetCoordinators :many +SELECT * FROM tailnet_coordinators; + +-- name: GetAllTailnetPeers :many +SELECT * FROM tailnet_peers; + +-- name: GetAllTailnetTunnels :many +SELECT * FROM tailnet_tunnels; From adc5917536689a6b563c125606ab6adc9d997007 Mon Sep 17 00:00:00 2001 From: Spike Curtis Date: Tue, 28 Nov 2023 11:36:08 +0400 Subject: [PATCH 2/2] feat: implement HTMLDebug for PGCoord with v2 API --- .prettierignore | 1 + .prettierignore.include | 1 + Makefile | 14 +- enterprise/tailnet/htmldebug.go | 199 ++++++++++++++++++ enterprise/tailnet/pgcoord.go | 114 ---------- enterprise/tailnet/pgcoord_internal_test.go | 135 ++++++++++++ enterprise/tailnet/testdata/debug.golden.html | 77 +++++++ site/.eslintignore | 1 + site/.prettierignore | 1 + 9 files changed, 428 insertions(+), 115 deletions(-) create mode 100644 enterprise/tailnet/htmldebug.go create mode 100644 enterprise/tailnet/testdata/debug.golden.html diff --git a/.prettierignore b/.prettierignore index 011d66b70991e..37cbd3fef3973 100644 --- a/.prettierignore +++ b/.prettierignore @@ -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 diff --git a/.prettierignore.include b/.prettierignore.include index 3a42bc75ecf9f..fd7f94f13d136 100644 --- a/.prettierignore.include +++ b/.prettierignore.include @@ -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 diff --git a/Makefile b/Makefile index 72e44308c6f03..1240bb57b1eab 100644 --- a/Makefile +++ b/Makefile @@ -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) @@ -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 "$@" diff --git a/enterprise/tailnet/htmldebug.go b/enterprise/tailnet/htmldebug.go new file mode 100644 index 0000000000000..282c1bc9e543f --- /dev/null +++ b/enterprise/tailnet/htmldebug.go @@ -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 = ` + + + + + + + +

# coordinators: total {{ len .Coordinators }}

+ + + + + + {{- range .Coordinators}} + + + + + {{- end }} +
IDHeartbeat Age
{{ .ID }}{{ .HeartbeatAge }} ago
+ +

# peers: total {{ len .Peers }}

+ + + + + + + + + {{- range .Peers }} + + + + + + + + {{- end }} +
IDCoordinatorIDStatusLast Write AgeNode
{{ .ID }}{{ .CoordinatorID }}{{ .Status }}{{ .LastWriteAge }} ago{{ .Node }}
+ +

# tunnels: total {{ len .Tunnels }}

+ + + + + + + + {{- range .Tunnels }} + + + + + + + {{- end }} +
SrcIDDstIDCoordinatorIDLast Write Age
{{ .SrcID }}{{ .DstID }}{{ .CoordinatorID }}{{ .LastWriteAge }} ago
+ + +` + +var debugTempl = template.Must(template.New("coordinator_debug").Parse(coordinatorDebugTmpl)) diff --git a/enterprise/tailnet/pgcoord.go b/enterprise/tailnet/pgcoord.go index 3803b8cb20b6c..a999e5586b2dd 100644 --- a/enterprise/tailnet/pgcoord.go +++ b/enterprise/tailnet/pgcoord.go @@ -6,7 +6,6 @@ import ( "encoding/json" "io" "net" - "net/http" "net/netip" "strings" "sync" @@ -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" @@ -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" ) @@ -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 { @@ -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 -} diff --git a/enterprise/tailnet/pgcoord_internal_test.go b/enterprise/tailnet/pgcoord_internal_test.go index 95481e6af3cc4..a580a5fedda75 100644 --- a/enterprise/tailnet/pgcoord_internal_test.go +++ b/enterprise/tailnet/pgcoord_internal_test.go @@ -1,19 +1,35 @@ package tailnet import ( + "bytes" "context" + "flag" + "os" + "path/filepath" + "runtime" "testing" "time" "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/sloggers/slogtest" + "github.com/coder/coder/v2/coderd/database" "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" ) +// 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 // cleanup. func TestHeartbeat_Cleanup(t *testing.T) { @@ -50,3 +66,122 @@ func TestHeartbeat_Cleanup(t *testing.T) { } 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) +} diff --git a/enterprise/tailnet/testdata/debug.golden.html b/enterprise/tailnet/testdata/debug.golden.html new file mode 100644 index 0000000000000..8f6648c620bdf --- /dev/null +++ b/enterprise/tailnet/testdata/debug.golden.html @@ -0,0 +1,77 @@ + + + + + + + + +

# coordinators: total 2

+ + + + + + + + + + + + + +
IDHeartbeat Age
01000000-1111-1111-1111-1111111111112s ago
02000000-1111-1111-1111-1111111111111s ago
+ +

# peers: total 2

+ + + + + + + + + + + + + + + + + + + + + + +
IDCoordinatorIDStatusLast Write AgeNode
01000000-2222-2222-2222-22222222222201000000-1111-1111-1111-111111111111ok5s agoid:1 preferred_derp:999 endpoints:"192.168.0.49:4449"
02000000-2222-2222-2222-22222222222202000000-1111-1111-1111-111111111111lost7s agoid:2 preferred_derp:999 endpoints:"192.168.0.33:4449"
+ +

# tunnels: total 1

+ + + + + + + + + + + + + +
SrcIDDstIDCoordinatorIDLast Write Age
01000000-2222-2222-2222-22222222222202000000-2222-2222-2222-22222222222201000000-1111-1111-1111-1111111111113s ago
+ + diff --git a/site/.eslintignore b/site/.eslintignore index 20570ccb94cfd..033d259091b67 100644 --- a/site/.eslintignore +++ b/site/.eslintignore @@ -82,6 +82,7 @@ result # Testdata shouldn't be formatted. ../scripts/apitypings/testdata/**/*.ts +../enterprise/tailnet/testdata/*.golden.html # Generated files shouldn't be formatted. e2e/provisionerGenerated.ts diff --git a/site/.prettierignore b/site/.prettierignore index 20570ccb94cfd..033d259091b67 100644 --- a/site/.prettierignore +++ b/site/.prettierignore @@ -82,6 +82,7 @@ result # Testdata shouldn't be formatted. ../scripts/apitypings/testdata/**/*.ts +../enterprise/tailnet/testdata/*.golden.html # Generated files shouldn't be formatted. e2e/provisionerGenerated.ts