Skip to content

Commit 1cd5f38

Browse files
authored
feat: add debug server for tailnet coordinators (#5861)
Implements a Tailscale-like debug server for our in-memory coordinator. This should provide some visibility into why connections could be failing. Resolves: #5845 ![image](https://user-images.githubusercontent.com/6332295/214680832-2724d633-2d54-44d6-a7ce-5841e5824ee5.png)
1 parent 8830ddf commit 1cd5f38

16 files changed

+261
-34
lines changed

agent/agent_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -1193,7 +1193,7 @@ func (c *client) ListenWorkspaceAgent(_ context.Context) (net.Conn, error) {
11931193
}
11941194
c.t.Cleanup(c.lastWorkspaceAgent)
11951195
go func() {
1196-
_ = c.coordinator.ServeAgent(serverConn, c.agentID)
1196+
_ = c.coordinator.ServeAgent(serverConn, c.agentID, "")
11971197
close(closed)
11981198
}()
11991199
return clientConn, nil

coderd/apidoc/docs.go

+22
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/apidoc/swagger.json

+18
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/coderd.go

+19
Original file line numberDiff line numberDiff line change
@@ -613,6 +613,25 @@ func New(options *Options) *API {
613613
r.Get("/", api.workspaceApplicationAuth)
614614
})
615615
})
616+
617+
r.Route("/debug", func(r chi.Router) {
618+
r.Use(
619+
apiKeyMiddleware,
620+
// Ensure only owners can access debug endpoints.
621+
func(next http.Handler) http.Handler {
622+
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
623+
if !api.Authorize(r, rbac.ActionRead, rbac.ResourceDebugInfo) {
624+
httpapi.ResourceNotFound(rw)
625+
return
626+
}
627+
628+
next.ServeHTTP(rw, r)
629+
})
630+
},
631+
)
632+
633+
r.Get("/coordinator", api.debugCoordinator)
634+
})
616635
})
617636

618637
if options.SwaggerEndpoint {

coderd/coderdtest/authorize.go

+5
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,11 @@ func AGPLRoutes(a *AuthTester) (map[string]string, map[string]RouteCheck) {
272272
AssertAction: rbac.ActionRead,
273273
AssertObject: rbac.ResourceTemplate,
274274
},
275+
276+
"GET:/api/v2/debug/coordinator": {
277+
AssertAction: rbac.ActionRead,
278+
AssertObject: rbac.ResourceDebugInfo,
279+
},
275280
}
276281

277282
// Routes like proxy routes support all HTTP methods. A helper func to expand

coderd/coderdtest/swaggerparser.go

+3-2
Original file line numberDiff line numberDiff line change
@@ -327,7 +327,7 @@ func assertAccept(t *testing.T, comment SwaggerComment) {
327327
}
328328
}
329329

330-
var allowedProduceTypes = []string{"json", "text/event-stream"}
330+
var allowedProduceTypes = []string{"json", "text/event-stream", "text/html"}
331331

332332
func assertProduce(t *testing.T, comment SwaggerComment) {
333333
var hasResponseModel bool
@@ -344,7 +344,8 @@ func assertProduce(t *testing.T, comment SwaggerComment) {
344344
} else {
345345
if (comment.router == "/workspaceagents/me/app-health" && comment.method == "post") ||
346346
(comment.router == "/workspaceagents/me/version" && comment.method == "post") ||
347-
(comment.router == "/licenses/{id}" && comment.method == "delete") {
347+
(comment.router == "/licenses/{id}" && comment.method == "delete") ||
348+
(comment.router == "/debug/coordinator" && comment.method == "get") {
348349
return // Exception: HTTP 200 is returned without response entity
349350
}
350351

coderd/debug.go

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package coderd
2+
3+
import "net/http"
4+
5+
// @Summary Debug Info Wireguard Coordinator
6+
// @ID debug-info-wireguard-coordinator
7+
// @Security CoderSessionToken
8+
// @Produce text/html
9+
// @Tags Debug
10+
// @Success 200
11+
// @Router /debug/coordinator [get]
12+
func (api *API) debugCoordinator(rw http.ResponseWriter, r *http.Request) {
13+
(*api.TailnetCoordinator.Load()).ServeHTTPDebug(rw, r)
14+
}

coderd/rbac/object.go

+5
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,11 @@ var (
150150
ResourceReplicas = Object{
151151
Type: "replicas",
152152
}
153+
154+
// ResourceDebugInfo controls access to the debug routes `/api/v2/debug/*`.
155+
ResourceDebugInfo = Object{
156+
Type: "debug_info",
157+
}
153158
)
154159

155160
// Object is used to create objects for authz checks when you have none in

coderd/workspaceagents.go

+11-1
Original file line numberDiff line numberDiff line change
@@ -521,6 +521,16 @@ func (api *API) workspaceAgentCoordinate(rw http.ResponseWriter, r *http.Request
521521
})
522522
return
523523
}
524+
525+
workspace, err := api.Database.GetWorkspaceByID(ctx, build.WorkspaceID)
526+
if err != nil {
527+
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
528+
Message: "Internal error fetching workspace.",
529+
Detail: err.Error(),
530+
})
531+
return
532+
}
533+
524534
// Ensure the resource is still valid!
525535
// We only accept agents for resources on the latest build.
526536
ensureLatestBuild := func() error {
@@ -618,7 +628,7 @@ func (api *API) workspaceAgentCoordinate(rw http.ResponseWriter, r *http.Request
618628
closeChan := make(chan struct{})
619629
go func() {
620630
defer close(closeChan)
621-
err := (*api.TailnetCoordinator.Load()).ServeAgent(wsNetConn, workspaceAgent.ID)
631+
err := (*api.TailnetCoordinator.Load()).ServeAgent(wsNetConn, workspaceAgent.ID, fmt.Sprintf("%s-%s", workspace.Name, workspaceAgent.Name))
622632
if err != nil {
623633
api.Logger.Warn(ctx, "tailnet coordinator agent error", slog.Error(err))
624634
_ = conn.Close(websocket.StatusInternalError, err.Error())

coderd/wsconncache/wsconncache_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ func (c *client) ListenWorkspaceAgent(_ context.Context) (net.Conn, error) {
207207
<-closed
208208
})
209209
go func() {
210-
_ = c.coordinator.ServeAgent(serverConn, c.agentID)
210+
_ = c.coordinator.ServeAgent(serverConn, c.agentID, "")
211211
close(closed)
212212
}()
213213
return clientConn, nil

docs/api/debug.md

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Debug
2+
3+
## Debug Info Wireguard Coordinator
4+
5+
### Code samples
6+
7+
```shell
8+
# Example request using curl
9+
curl -X GET http://coder-server:8080/api/v2/debug/coordinator \
10+
-H 'Coder-Session-Token: API_KEY'
11+
```
12+
13+
`GET /debug/coordinator`
14+
15+
### Responses
16+
17+
| Status | Meaning | Description | Schema |
18+
| ------ | ------------------------------------------------------- | ----------- | ------ |
19+
| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | |
20+
21+
To perform this operation, you must be authenticated. [Learn more](authentication.md).

docs/manifest.json

+4
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,10 @@
364364
"title": "Builds",
365365
"path": "./api/builds.md"
366366
},
367+
{
368+
"title": "Debug",
369+
"path": "./api/debug.md"
370+
},
367371
{
368372
"title": "Enterprise",
369373
"path": "./api/enterprise.md"

enterprise/tailnet/coordinator.go

+9-1
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@ import (
55
"context"
66
"encoding/json"
77
"errors"
8+
"fmt"
89
"io"
910
"net"
11+
"net/http"
1012
"sync"
1113
"time"
1214

@@ -174,7 +176,7 @@ func (c *haCoordinator) handleNextClientMessage(id, agent uuid.UUID, decoder *js
174176

175177
// ServeAgent accepts a WebSocket connection to an agent that listens to
176178
// incoming connections and publishes node updates.
177-
func (c *haCoordinator) ServeAgent(conn net.Conn, id uuid.UUID) error {
179+
func (c *haCoordinator) ServeAgent(conn net.Conn, id uuid.UUID, _ string) error {
178180
// Tell clients on other instances to send a callmemaybe to us.
179181
err := c.publishAgentHello(id)
180182
if err != nil {
@@ -573,3 +575,9 @@ func (c *haCoordinator) formatAgentUpdate(id uuid.UUID, node *agpl.Node) ([]byte
573575

574576
return buf.Bytes(), nil
575577
}
578+
579+
func (*haCoordinator) ServeHTTPDebug(w http.ResponseWriter, _ *http.Request) {
580+
w.Header().Set("Content-Type", "text/html; charset=utf-8")
581+
fmt.Fprintf(w, "<h1>coordinator</h1>")
582+
fmt.Fprintf(w, "<h2>ha debug coming soon</h2>")
583+
}

enterprise/tailnet/coordinator_test.go

+5-5
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ func TestCoordinatorSingle(t *testing.T) {
6060
id := uuid.New()
6161
closeChan := make(chan struct{})
6262
go func() {
63-
err := coordinator.ServeAgent(server, id)
63+
err := coordinator.ServeAgent(server, id, "")
6464
assert.NoError(t, err)
6565
close(closeChan)
6666
}()
@@ -91,7 +91,7 @@ func TestCoordinatorSingle(t *testing.T) {
9191
agentID := uuid.New()
9292
closeAgentChan := make(chan struct{})
9393
go func() {
94-
err := coordinator.ServeAgent(agentServerWS, agentID)
94+
err := coordinator.ServeAgent(agentServerWS, agentID, "")
9595
assert.NoError(t, err)
9696
close(closeAgentChan)
9797
}()
@@ -142,7 +142,7 @@ func TestCoordinatorSingle(t *testing.T) {
142142
})
143143
closeAgentChan = make(chan struct{})
144144
go func() {
145-
err := coordinator.ServeAgent(agentServerWS, agentID)
145+
err := coordinator.ServeAgent(agentServerWS, agentID, "")
146146
assert.NoError(t, err)
147147
close(closeAgentChan)
148148
}()
@@ -184,7 +184,7 @@ func TestCoordinatorHA(t *testing.T) {
184184
agentID := uuid.New()
185185
closeAgentChan := make(chan struct{})
186186
go func() {
187-
err := coordinator1.ServeAgent(agentServerWS, agentID)
187+
err := coordinator1.ServeAgent(agentServerWS, agentID, "")
188188
assert.NoError(t, err)
189189
close(closeAgentChan)
190190
}()
@@ -240,7 +240,7 @@ func TestCoordinatorHA(t *testing.T) {
240240
})
241241
closeAgentChan = make(chan struct{})
242242
go func() {
243-
err := coordinator1.ServeAgent(agentServerWS, agentID)
243+
err := coordinator1.ServeAgent(agentServerWS, agentID, "")
244244
assert.NoError(t, err)
245245
close(closeAgentChan)
246246
}()

0 commit comments

Comments
 (0)