Skip to content

Commit c16c2b8

Browse files
committed
feat: AGPL coordinator supports v2 Tailnet API
1 parent 53453c0 commit c16c2b8

19 files changed

+1478
-1034
lines changed

.prettierignore

+1
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ helm/**/templates/*.yaml
8383
# Testdata shouldn't be formatted.
8484
scripts/apitypings/testdata/**/*.ts
8585
enterprise/tailnet/testdata/*.golden.html
86+
tailnet/testdata/*.golden.html
8687

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

.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

+209-2
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,22 @@ import (
55
"context"
66
"encoding/json"
77
"errors"
8+
"fmt"
9+
"html/template"
810
"io"
911
"net"
1012
"net/http"
1113
"sync"
14+
"time"
1215

1316
"github.com/google/uuid"
1417
lru "github.com/hashicorp/golang-lru/v2"
18+
"golang.org/x/exp/slices"
1519
"golang.org/x/xerrors"
1620

1721
"cdr.dev/slog"
1822
"github.com/coder/coder/v2/coderd/database/pubsub"
23+
"github.com/coder/coder/v2/coderd/util/slice"
1924
"github.com/coder/coder/v2/codersdk"
2025
agpl "github.com/coder/coder/v2/tailnet"
2126
)
@@ -719,7 +724,209 @@ func (c *haCoordinator) ServeHTTPDebug(w http.ResponseWriter, r *http.Request) {
719724
c.mutex.RLock()
720725
defer c.mutex.RUnlock()
721726

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

0 commit comments

Comments
 (0)