mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: show tailnet peer diagnostics after coder ping (#12314)
Beginnings of a solution to #12297 Doesn't cover disco or definitively display whether we successfully connected to DERP, but shows some checklist diagnostics for connecting to an agent. For this first PR, I just added it to `coder ping` to see how we like it, but could be incorporated into `coder ssh` _et al._ after a timeout. ``` $ coder ping dogfood2 p2p connection established in 147ms pong from dogfood2 p2p via 95.217.xxx.yyy:42631 in 147ms pong from dogfood2 p2p via 95.217.xxx.yyy:42631 in 140ms pong from dogfood2 p2p via 95.217.xxx.yyy:42631 in 140ms ✔ preferred DERP region 999 (Council Bluffs, Iowa) ✔ sent local data to Coder networking coodinator ✔ received remote agent data from Coder networking coordinator preferred DERP 10013 (Europe Fly.io (Paris)) endpoints: 95.217.xxx.yyy:42631, 95.217.xxx.yyy:37576, 172.17.0.1:37576, 172.20.0.10:37576 ✔ Wireguard handshake 11s ago ```
This commit is contained in:
@@ -2,13 +2,17 @@ package cliui
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
"github.com/coder/coder/v2/tailnet"
|
||||
)
|
||||
|
||||
var errAgentShuttingDown = xerrors.New("agent is shutting down")
|
||||
@@ -281,3 +285,55 @@ type closeFunc func() error
|
||||
func (c closeFunc) Close() error {
|
||||
return c()
|
||||
}
|
||||
|
||||
func PeerDiagnostics(w io.Writer, d tailnet.PeerDiagnostics) {
|
||||
if d.PreferredDERP > 0 {
|
||||
rn, ok := d.DERPRegionNames[d.PreferredDERP]
|
||||
if !ok {
|
||||
rn = "unknown"
|
||||
}
|
||||
_, _ = fmt.Fprintf(w, "✔ preferred DERP region: %d (%s)\n", d.PreferredDERP, rn)
|
||||
} else {
|
||||
_, _ = fmt.Fprint(w, "✘ not connected to DERP\n")
|
||||
}
|
||||
if d.SentNode {
|
||||
_, _ = fmt.Fprint(w, "✔ sent local data to Coder networking coodinator\n")
|
||||
} else {
|
||||
_, _ = fmt.Fprint(w, "✘ have not sent local data to Coder networking coordinator\n")
|
||||
}
|
||||
if d.ReceivedNode != nil {
|
||||
dp := d.ReceivedNode.DERP
|
||||
dn := ""
|
||||
// should be 127.3.3.40:N where N is the DERP region
|
||||
ap := strings.Split(dp, ":")
|
||||
if len(ap) == 2 {
|
||||
dp = ap[1]
|
||||
di, err := strconv.Atoi(dp)
|
||||
if err == nil {
|
||||
var ok bool
|
||||
dn, ok = d.DERPRegionNames[di]
|
||||
if ok {
|
||||
dn = fmt.Sprintf("(%s)", dn)
|
||||
} else {
|
||||
dn = "(unknown)"
|
||||
}
|
||||
}
|
||||
}
|
||||
_, _ = fmt.Fprintf(w,
|
||||
"✔ received remote agent data from Coder networking coordinator\n preferred DERP region: %s %s\n endpoints: %s\n",
|
||||
dp, dn, strings.Join(d.ReceivedNode.Endpoints, ", "))
|
||||
} else {
|
||||
_, _ = fmt.Fprint(w, "✘ have not received remote agent data from Coder networking coordinator\n")
|
||||
}
|
||||
if !d.LastWireguardHandshake.IsZero() {
|
||||
ago := time.Since(d.LastWireguardHandshake)
|
||||
symbol := "✔"
|
||||
// wireguard is supposed to refresh handshake on 5 minute intervals
|
||||
if ago > 5*time.Minute {
|
||||
symbol = "⚠"
|
||||
}
|
||||
_, _ = fmt.Fprintf(w, "%s Wireguard handshake %s ago\n", symbol, ago.Round(time.Second))
|
||||
} else {
|
||||
_, _ = fmt.Fprint(w, "✘ Wireguard is not connected\n")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"context"
|
||||
"io"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
@@ -15,12 +16,14 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/xerrors"
|
||||
"tailscale.com/tailcfg"
|
||||
|
||||
"github.com/coder/coder/v2/cli/clibase"
|
||||
"github.com/coder/coder/v2/cli/clitest"
|
||||
"github.com/coder/coder/v2/cli/cliui"
|
||||
"github.com/coder/coder/v2/coderd/util/ptr"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
"github.com/coder/coder/v2/tailnet"
|
||||
"github.com/coder/coder/v2/testutil"
|
||||
)
|
||||
|
||||
@@ -476,3 +479,191 @@ func TestAgent(t *testing.T) {
|
||||
require.NoError(t, cmd.Invoke().Run())
|
||||
})
|
||||
}
|
||||
|
||||
func TestPeerDiagnostics(t *testing.T) {
|
||||
t.Parallel()
|
||||
testCases := []struct {
|
||||
name string
|
||||
diags tailnet.PeerDiagnostics
|
||||
want []*regexp.Regexp // must be ordered, can omit lines
|
||||
}{
|
||||
{
|
||||
name: "noPreferredDERP",
|
||||
diags: tailnet.PeerDiagnostics{
|
||||
PreferredDERP: 0,
|
||||
DERPRegionNames: make(map[int]string),
|
||||
SentNode: true,
|
||||
ReceivedNode: &tailcfg.Node{DERP: "127.3.3.40:999"},
|
||||
LastWireguardHandshake: time.Now(),
|
||||
},
|
||||
want: []*regexp.Regexp{
|
||||
regexp.MustCompile("^✘ not connected to DERP$"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "preferredDERP",
|
||||
diags: tailnet.PeerDiagnostics{
|
||||
PreferredDERP: 23,
|
||||
DERPRegionNames: map[int]string{
|
||||
23: "testo",
|
||||
},
|
||||
SentNode: true,
|
||||
ReceivedNode: &tailcfg.Node{DERP: "127.3.3.40:999"},
|
||||
LastWireguardHandshake: time.Now(),
|
||||
},
|
||||
want: []*regexp.Regexp{
|
||||
regexp.MustCompile(`^✔ preferred DERP region: 23 \(testo\)$`),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "sentNode",
|
||||
diags: tailnet.PeerDiagnostics{
|
||||
PreferredDERP: 0,
|
||||
DERPRegionNames: map[int]string{},
|
||||
SentNode: true,
|
||||
ReceivedNode: &tailcfg.Node{DERP: "127.3.3.40:999"},
|
||||
LastWireguardHandshake: time.Time{},
|
||||
},
|
||||
want: []*regexp.Regexp{
|
||||
regexp.MustCompile(`^✔ sent local data to Coder networking coodinator$`),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "didntSendNode",
|
||||
diags: tailnet.PeerDiagnostics{
|
||||
PreferredDERP: 0,
|
||||
DERPRegionNames: map[int]string{},
|
||||
SentNode: false,
|
||||
ReceivedNode: &tailcfg.Node{DERP: "127.3.3.40:999"},
|
||||
LastWireguardHandshake: time.Time{},
|
||||
},
|
||||
want: []*regexp.Regexp{
|
||||
regexp.MustCompile(`^✘ have not sent local data to Coder networking coordinator$`),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "receivedNodeDERPOKNoEndpoints",
|
||||
diags: tailnet.PeerDiagnostics{
|
||||
PreferredDERP: 0,
|
||||
DERPRegionNames: map[int]string{999: "Embedded"},
|
||||
SentNode: true,
|
||||
ReceivedNode: &tailcfg.Node{DERP: "127.3.3.40:999"},
|
||||
LastWireguardHandshake: time.Time{},
|
||||
},
|
||||
want: []*regexp.Regexp{
|
||||
regexp.MustCompile(`^✔ received remote agent data from Coder networking coordinator$`),
|
||||
regexp.MustCompile(`preferred DERP region: 999 \(Embedded\)$`),
|
||||
regexp.MustCompile(`endpoints: $`),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "receivedNodeDERPUnknownNoEndpoints",
|
||||
diags: tailnet.PeerDiagnostics{
|
||||
PreferredDERP: 0,
|
||||
DERPRegionNames: map[int]string{},
|
||||
SentNode: true,
|
||||
ReceivedNode: &tailcfg.Node{DERP: "127.3.3.40:999"},
|
||||
LastWireguardHandshake: time.Time{},
|
||||
},
|
||||
want: []*regexp.Regexp{
|
||||
regexp.MustCompile(`^✔ received remote agent data from Coder networking coordinator$`),
|
||||
regexp.MustCompile(`preferred DERP region: 999 \(unknown\)$`),
|
||||
regexp.MustCompile(`endpoints: $`),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "receivedNodeEndpointsNoDERP",
|
||||
diags: tailnet.PeerDiagnostics{
|
||||
PreferredDERP: 0,
|
||||
DERPRegionNames: map[int]string{999: "Embedded"},
|
||||
SentNode: true,
|
||||
ReceivedNode: &tailcfg.Node{Endpoints: []string{"99.88.77.66:4555", "33.22.11.0:3444"}},
|
||||
LastWireguardHandshake: time.Time{},
|
||||
},
|
||||
want: []*regexp.Regexp{
|
||||
regexp.MustCompile(`^✔ received remote agent data from Coder networking coordinator$`),
|
||||
regexp.MustCompile(`preferred DERP region:\s*$`),
|
||||
regexp.MustCompile(`endpoints: 99\.88\.77\.66:4555, 33\.22\.11\.0:3444$`),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "didntReceiveNode",
|
||||
diags: tailnet.PeerDiagnostics{
|
||||
PreferredDERP: 0,
|
||||
DERPRegionNames: map[int]string{},
|
||||
SentNode: false,
|
||||
ReceivedNode: nil,
|
||||
LastWireguardHandshake: time.Time{},
|
||||
},
|
||||
want: []*regexp.Regexp{
|
||||
regexp.MustCompile(`^✘ have not received remote agent data from Coder networking coordinator$`),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "noWireguardHandshake",
|
||||
diags: tailnet.PeerDiagnostics{
|
||||
PreferredDERP: 0,
|
||||
DERPRegionNames: map[int]string{},
|
||||
SentNode: false,
|
||||
ReceivedNode: nil,
|
||||
LastWireguardHandshake: time.Time{},
|
||||
},
|
||||
want: []*regexp.Regexp{
|
||||
regexp.MustCompile(`^✘ Wireguard is not connected$`),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "wireguardHandshakeRecent",
|
||||
diags: tailnet.PeerDiagnostics{
|
||||
PreferredDERP: 0,
|
||||
DERPRegionNames: map[int]string{},
|
||||
SentNode: false,
|
||||
ReceivedNode: nil,
|
||||
LastWireguardHandshake: time.Now().Add(-5 * time.Second),
|
||||
},
|
||||
want: []*regexp.Regexp{
|
||||
regexp.MustCompile(`^✔ Wireguard handshake \d+s ago$`),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "wireguardHandshakeOld",
|
||||
diags: tailnet.PeerDiagnostics{
|
||||
PreferredDERP: 0,
|
||||
DERPRegionNames: map[int]string{},
|
||||
SentNode: false,
|
||||
ReceivedNode: nil,
|
||||
LastWireguardHandshake: time.Now().Add(-450 * time.Second), // 7m30s
|
||||
},
|
||||
want: []*regexp.Regexp{
|
||||
regexp.MustCompile(`^⚠ Wireguard handshake 7m\d+s ago$`),
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
r, w := io.Pipe()
|
||||
go func() {
|
||||
defer w.Close()
|
||||
cliui.PeerDiagnostics(w, tc.diags)
|
||||
}()
|
||||
s := bufio.NewScanner(r)
|
||||
i := 0
|
||||
got := make([]string, 0)
|
||||
for s.Scan() {
|
||||
got = append(got, s.Text())
|
||||
if i < len(tc.want) {
|
||||
reg := tc.want[i]
|
||||
if reg.Match(s.Bytes()) {
|
||||
i++
|
||||
}
|
||||
}
|
||||
}
|
||||
if i < len(tc.want) {
|
||||
t.Logf("failed to match regexp: %s\ngot:\n%s", tc.want[i].String(), strings.Join(got, "\n"))
|
||||
t.FailNow()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,6 +135,8 @@ func (r *RootCmd) ping() *clibase.Cmd {
|
||||
)
|
||||
|
||||
if n == int(pingNum) {
|
||||
diags := conn.GetPeerDiagnostics()
|
||||
cliui.PeerDiagnostics(inv.Stdout, diags)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,4 +42,32 @@ func TestPing(t *testing.T) {
|
||||
cancel()
|
||||
<-cmdDone
|
||||
})
|
||||
|
||||
t.Run("1Ping", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client, workspace, agentToken := setupWorkspaceForAgent(t)
|
||||
inv, root := clitest.New(t, "ping", "-n", "1", workspace.Name)
|
||||
clitest.SetupConfig(t, client, root)
|
||||
pty := ptytest.New(t)
|
||||
inv.Stdin = pty.Input()
|
||||
inv.Stderr = pty.Output()
|
||||
inv.Stdout = pty.Output()
|
||||
|
||||
_ = agenttest.New(t, client.URL, agentToken)
|
||||
_ = coderdtest.AwaitWorkspaceAgents(t, client, workspace.ID)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
defer cancel()
|
||||
|
||||
cmdDone := tGo(t, func() {
|
||||
err := inv.WithContext(ctx).Run()
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
pty.ExpectMatch("pong from " + workspace.Name)
|
||||
pty.ExpectMatch("✔ received remote agent data from Coder networking coordinator")
|
||||
cancel()
|
||||
<-cmdDone
|
||||
})
|
||||
}
|
||||
|
||||
+2
-2
@@ -563,7 +563,7 @@ func getWorkspaceAndAgent(ctx context.Context, inv *clibase.Invocation, client *
|
||||
|
||||
if workspace.LatestBuild.Transition != codersdk.WorkspaceTransitionStart {
|
||||
if !autostart {
|
||||
return codersdk.Workspace{}, codersdk.WorkspaceAgent{}, xerrors.New("workspace must be in start transition to ssh")
|
||||
return codersdk.Workspace{}, codersdk.WorkspaceAgent{}, xerrors.New("workspace must be started")
|
||||
}
|
||||
// Autostart the workspace for the user.
|
||||
// For some failure modes, return a better message.
|
||||
@@ -579,7 +579,7 @@ func getWorkspaceAndAgent(ctx context.Context, inv *clibase.Invocation, client *
|
||||
// It cannot be in any pending or failed state.
|
||||
if workspace.LatestBuild.Status != codersdk.WorkspaceStatusStopped {
|
||||
return codersdk.Workspace{}, codersdk.WorkspaceAgent{},
|
||||
xerrors.Errorf("workspace must be in start transition to ssh, was unable to autostart as the last build job is %q, expected %q",
|
||||
xerrors.Errorf("workspace must be started; was unable to autostart as the last build job is %q, expected %q",
|
||||
workspace.LatestBuild.Status,
|
||||
codersdk.WorkspaceStatusStopped,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user