Skip to content

Commit 803e89e

Browse files
committed
feat: AGPL coordinator supports v2 Tailnet API
1 parent dbadae5 commit 803e89e

17 files changed

+1494
-1012
lines changed

.prettierignore.include

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ helm/**/templates/*.yaml
99
# Testdata shouldn't be formatted.
1010
scripts/apitypings/testdata/**/*.ts
1111
enterprise/tailnet/testdata/*.golden.html
12+
tailnet/testdata/*.golden.html
1213

1314
# Generated files shouldn't be formatted.
1415
site/e2e/provisionerGenerated.ts

Makefile

+5
Original file line numberDiff line numberDiff line change
@@ -602,6 +602,7 @@ update-golden-files: \
602602
scripts/ci-report/testdata/.gen-golden \
603603
enterprise/cli/testdata/.gen-golden \
604604
enterprise/tailnet/testdata/.gen-golden \
605+
tailnet/testdata/.gen-golden \
605606
coderd/.gen-golden \
606607
provisioner/terraform/testdata/.gen-golden
607608
.PHONY: update-golden-files
@@ -614,6 +615,10 @@ enterprise/cli/testdata/.gen-golden: $(wildcard enterprise/cli/testdata/*.golden
614615
go test ./enterprise/cli -run="TestEnterpriseCommandHelp" -update
615616
touch "$@"
616617

618+
tailnet/testdata/.gen-golden: $(wildcard tailnet/testdata/*.golden.html) $(GO_SRC_FILES) $(wildcard tailnet/*_test.go)
619+
go test ./tailnet -run="TestDebugTemplate" -update
620+
touch "$@"
621+
617622
enterprise/tailnet/testdata/.gen-golden: $(wildcard enterprise/tailnet/testdata/*.golden.html) $(GO_SRC_FILES) $(wildcard enterprise/tailnet/*_test.go)
618623
go test ./enterprise/tailnet -run="TestDebugTemplate" -update
619624
touch "$@"

enterprise/tailnet/connio.go

+6-6
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ func (c *connIO) recvLoop() {
8686
if c.disconnected {
8787
b.kind = proto.CoordinateResponse_PeerUpdate_DISCONNECTED
8888
}
89-
if err := sendCtx(c.coordCtx, c.bindings, b); err != nil {
89+
if err := agpl.SendCtx(c.coordCtx, c.bindings, b); err != nil {
9090
c.logger.Debug(c.coordCtx, "parent context expired while withdrawing bindings", slog.Error(err))
9191
}
9292
// only remove tunnels on graceful disconnect. If we remove tunnels for lost peers, then
@@ -97,14 +97,14 @@ func (c *connIO) recvLoop() {
9797
tKey: tKey{src: c.UniqueID()},
9898
active: false,
9999
}
100-
if err := sendCtx(c.coordCtx, c.tunnels, t); err != nil {
100+
if err := agpl.SendCtx(c.coordCtx, c.tunnels, t); err != nil {
101101
c.logger.Debug(c.coordCtx, "parent context expired while withdrawing tunnels", slog.Error(err))
102102
}
103103
}
104104
}()
105105
defer c.Close()
106106
for {
107-
req, err := recvCtx(c.peerCtx, c.requests)
107+
req, err := agpl.RecvCtx(c.peerCtx, c.requests)
108108
if err != nil {
109109
if xerrors.Is(err, context.Canceled) ||
110110
xerrors.Is(err, context.DeadlineExceeded) ||
@@ -132,7 +132,7 @@ func (c *connIO) handleRequest(req *proto.CoordinateRequest) error {
132132
node: req.UpdateSelf.Node,
133133
kind: proto.CoordinateResponse_PeerUpdate_NODE,
134134
}
135-
if err := sendCtx(c.coordCtx, c.bindings, b); err != nil {
135+
if err := agpl.SendCtx(c.coordCtx, c.bindings, b); err != nil {
136136
c.logger.Debug(c.peerCtx, "failed to send binding", slog.Error(err))
137137
return err
138138
}
@@ -156,7 +156,7 @@ func (c *connIO) handleRequest(req *proto.CoordinateRequest) error {
156156
},
157157
active: true,
158158
}
159-
if err := sendCtx(c.coordCtx, c.tunnels, t); err != nil {
159+
if err := agpl.SendCtx(c.coordCtx, c.tunnels, t); err != nil {
160160
c.logger.Debug(c.peerCtx, "failed to send add tunnel", slog.Error(err))
161161
return err
162162
}
@@ -177,7 +177,7 @@ func (c *connIO) handleRequest(req *proto.CoordinateRequest) error {
177177
},
178178
active: false,
179179
}
180-
if err := sendCtx(c.coordCtx, c.tunnels, t); err != nil {
180+
if err := agpl.SendCtx(c.coordCtx, c.tunnels, t); err != nil {
181181
c.logger.Debug(c.peerCtx, "failed to send remove tunnel", slog.Error(err))
182182
return err
183183
}

enterprise/tailnet/coordinator.go

+210-2
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,16 @@ import (
55
"context"
66
"encoding/json"
77
"errors"
8+
"fmt"
9+
"html/template"
810
"io"
911
"net"
1012
"net/http"
1113
"sync"
14+
"time"
15+
16+
"github.com/coder/coder/v2/coderd/util/slice"
17+
"golang.org/x/exp/slices"
1218

1319
"github.com/google/uuid"
1420
lru "github.com/hashicorp/golang-lru/v2"
@@ -719,7 +725,209 @@ func (c *haCoordinator) ServeHTTPDebug(w http.ResponseWriter, r *http.Request) {
719725
c.mutex.RLock()
720726
defer c.mutex.RUnlock()
721727

722-
agpl.CoordinatorHTTPDebug(
723-
agpl.HTTPDebugFromLocal(true, c.agentSockets, c.agentToConnectionSockets, c.nodes, c.agentNameCache),
728+
CoordinatorHTTPDebug(
729+
HTTPDebugFromLocal(true, c.agentSockets, c.agentToConnectionSockets, c.nodes, c.agentNameCache),
724730
)(w, r)
725731
}
732+
733+
func HTTPDebugFromLocal(
734+
ha bool,
735+
agentSocketsMap map[uuid.UUID]agpl.Queue,
736+
agentToConnectionSocketsMap map[uuid.UUID]map[uuid.UUID]agpl.Queue,
737+
nodesMap map[uuid.UUID]*agpl.Node,
738+
agentNameCache *lru.Cache[uuid.UUID, string],
739+
) HTMLDebugHA {
740+
now := time.Now()
741+
data := HTMLDebugHA{HA: ha}
742+
for id, conn := range agentSocketsMap {
743+
start, lastWrite := conn.Stats()
744+
agent := &HTMLAgent{
745+
Name: conn.Name(),
746+
ID: id,
747+
CreatedAge: now.Sub(time.Unix(start, 0)).Round(time.Second),
748+
LastWriteAge: now.Sub(time.Unix(lastWrite, 0)).Round(time.Second),
749+
Overwrites: int(conn.Overwrites()),
750+
}
751+
752+
for id, conn := range agentToConnectionSocketsMap[id] {
753+
start, lastWrite := conn.Stats()
754+
agent.Connections = append(agent.Connections, &HTMLClient{
755+
Name: conn.Name(),
756+
ID: id,
757+
CreatedAge: now.Sub(time.Unix(start, 0)).Round(time.Second),
758+
LastWriteAge: now.Sub(time.Unix(lastWrite, 0)).Round(time.Second),
759+
})
760+
}
761+
slices.SortFunc(agent.Connections, func(a, b *HTMLClient) int {
762+
return slice.Ascending(a.Name, b.Name)
763+
})
764+
765+
data.Agents = append(data.Agents, agent)
766+
}
767+
slices.SortFunc(data.Agents, func(a, b *HTMLAgent) int {
768+
return slice.Ascending(a.Name, b.Name)
769+
})
770+
771+
for agentID, conns := range agentToConnectionSocketsMap {
772+
if len(conns) == 0 {
773+
continue
774+
}
775+
776+
if _, ok := agentSocketsMap[agentID]; ok {
777+
continue
778+
}
779+
780+
agentName, ok := agentNameCache.Get(agentID)
781+
if !ok {
782+
agentName = "unknown"
783+
}
784+
agent := &HTMLAgent{
785+
Name: agentName,
786+
ID: agentID,
787+
}
788+
for id, conn := range conns {
789+
start, lastWrite := conn.Stats()
790+
agent.Connections = append(agent.Connections, &HTMLClient{
791+
Name: conn.Name(),
792+
ID: id,
793+
CreatedAge: now.Sub(time.Unix(start, 0)).Round(time.Second),
794+
LastWriteAge: now.Sub(time.Unix(lastWrite, 0)).Round(time.Second),
795+
})
796+
}
797+
slices.SortFunc(agent.Connections, func(a, b *HTMLClient) int {
798+
return slice.Ascending(a.Name, b.Name)
799+
})
800+
801+
data.MissingAgents = append(data.MissingAgents, agent)
802+
}
803+
slices.SortFunc(data.MissingAgents, func(a, b *HTMLAgent) int {
804+
return slice.Ascending(a.Name, b.Name)
805+
})
806+
807+
for id, node := range nodesMap {
808+
name, _ := agentNameCache.Get(id)
809+
data.Nodes = append(data.Nodes, &HTMLNode{
810+
ID: id,
811+
Name: name,
812+
Node: node,
813+
})
814+
}
815+
slices.SortFunc(data.Nodes, func(a, b *HTMLNode) int {
816+
return slice.Ascending(a.Name+a.ID.String(), b.Name+b.ID.String())
817+
})
818+
819+
return data
820+
}
821+
822+
func CoordinatorHTTPDebug(data HTMLDebugHA) func(w http.ResponseWriter, _ *http.Request) {
823+
return func(w http.ResponseWriter, _ *http.Request) {
824+
w.Header().Set("Content-Type", "text/html; charset=utf-8")
825+
826+
tmpl, err := template.New("coordinator_debug").Funcs(template.FuncMap{
827+
"marshal": func(v any) template.JS {
828+
a, err := json.MarshalIndent(v, "", " ")
829+
if err != nil {
830+
//nolint:gosec
831+
return template.JS(fmt.Sprintf(`{"err": %q}`, err))
832+
}
833+
//nolint:gosec
834+
return template.JS(a)
835+
},
836+
}).Parse(haCoordinatorDebugTmpl)
837+
if err != nil {
838+
w.WriteHeader(http.StatusInternalServerError)
839+
_, _ = w.Write([]byte(err.Error()))
840+
return
841+
}
842+
843+
err = tmpl.Execute(w, data)
844+
if err != nil {
845+
w.WriteHeader(http.StatusInternalServerError)
846+
_, _ = w.Write([]byte(err.Error()))
847+
return
848+
}
849+
}
850+
}
851+
852+
type HTMLDebugHA struct {
853+
HA bool
854+
Agents []*HTMLAgent
855+
MissingAgents []*HTMLAgent
856+
Nodes []*HTMLNode
857+
}
858+
859+
type HTMLAgent struct {
860+
Name string
861+
ID uuid.UUID
862+
CreatedAge time.Duration
863+
LastWriteAge time.Duration
864+
Overwrites int
865+
Connections []*HTMLClient
866+
}
867+
868+
type HTMLClient struct {
869+
Name string
870+
ID uuid.UUID
871+
CreatedAge time.Duration
872+
LastWriteAge time.Duration
873+
}
874+
875+
type HTMLNode struct {
876+
ID uuid.UUID
877+
Name string
878+
Node any
879+
}
880+
881+
var haCoordinatorDebugTmpl = `
882+
<!DOCTYPE html>
883+
<html>
884+
<head>
885+
<meta charset="UTF-8">
886+
</head>
887+
<body>
888+
{{- if .HA }}
889+
<h1>high-availability wireguard coordinator debug</h1>
890+
<h4 style="margin-top:-25px">warning: this only provides info from the node that served the request, if there are multiple replicas this data may be incomplete</h4>
891+
{{- else }}
892+
<h1>in-memory wireguard coordinator debug</h1>
893+
{{- end }}
894+
895+
<h2 id=agents> <a href=#agents>#</a> agents: total {{ len .Agents }} </h2>
896+
<ul>
897+
{{- range .Agents }}
898+
<li style="margin-top:4px">
899+
<b>{{ .Name }}</b> (<code>{{ .ID }}</code>): created {{ .CreatedAge }} ago, write {{ .LastWriteAge }} ago, overwrites {{ .Overwrites }}
900+
<h3 style="margin:0px;font-size:16px;font-weight:400"> connections: total {{ len .Connections}} </h3>
901+
<ul>
902+
{{- range .Connections }}
903+
<li><b>{{ .Name }}</b> (<code>{{ .ID }}</code>): created {{ .CreatedAge }} ago, write {{ .LastWriteAge }} ago </li>
904+
{{- end }}
905+
</ul>
906+
</li>
907+
{{- end }}
908+
</ul>
909+
910+
<h2 id=missing-agents><a href=#missing-agents>#</a> missing agents: total {{ len .MissingAgents }}</h2>
911+
<ul>
912+
{{- range .MissingAgents}}
913+
<li style="margin-top:4px"><b>{{ .Name }}</b> (<code>{{ .ID }}</code>): created ? ago, write ? ago, overwrites ? </li>
914+
<h3 style="margin:0px;font-size:16px;font-weight:400"> connections: total {{ len .Connections }} </h3>
915+
<ul>
916+
{{- range .Connections }}
917+
<li><b>{{ .Name }}</b> (<code>{{ .ID }}</code>): created {{ .CreatedAge }} ago, write {{ .LastWriteAge }} ago </li>
918+
{{- end }}
919+
</ul>
920+
{{- end }}
921+
</ul>
922+
923+
<h2 id=nodes><a href=#nodes>#</a> nodes: total {{ len .Nodes }}</h2>
924+
<ul>
925+
{{- range .Nodes }}
926+
<li style="margin-top:4px"><b>{{ .Name }}</b> (<code>{{ .ID }}</code>):
927+
<span style="white-space: pre;"><code>{{ marshal .Node }}</code></span>
928+
</li>
929+
{{- end }}
930+
</ul>
931+
</body>
932+
</html>
933+
`

0 commit comments

Comments
 (0)