mirror of
https://github.com/coder/coder.git
synced 2025-07-06 15:41:45 +00:00
feat: add coder ping
(#6161)
This commit is contained in:
138
cli/ping.go
Normal file
138
cli/ping.go
Normal file
@ -0,0 +1,138 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"cdr.dev/slog"
|
||||
"cdr.dev/slog/sloggers/sloghuman"
|
||||
|
||||
"github.com/coder/coder/cli/cliui"
|
||||
"github.com/coder/coder/codersdk"
|
||||
)
|
||||
|
||||
func ping() *cobra.Command {
|
||||
var (
|
||||
pingNum int
|
||||
pingTimeout time.Duration
|
||||
pingWait time.Duration
|
||||
verbose bool
|
||||
)
|
||||
cmd := &cobra.Command{
|
||||
Annotations: workspaceCommand,
|
||||
Use: "ping <workspace>",
|
||||
Short: "Ping a workspace",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx, cancel := context.WithCancel(cmd.Context())
|
||||
defer cancel()
|
||||
|
||||
client, err := CreateClient(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
workspaceName := args[0]
|
||||
_, workspaceAgent, err := getWorkspaceAndAgent(ctx, cmd, client, codersdk.Me, workspaceName, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var logger slog.Logger
|
||||
if verbose {
|
||||
logger = slog.Make(sloghuman.Sink(cmd.OutOrStdout())).Leveled(slog.LevelDebug)
|
||||
}
|
||||
|
||||
conn, err := client.DialWorkspaceAgent(ctx, workspaceAgent.ID, &codersdk.DialWorkspaceAgentOptions{Logger: logger})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
derpMap := conn.DERPMap()
|
||||
_ = derpMap
|
||||
|
||||
n := 0
|
||||
didP2p := false
|
||||
start := time.Now()
|
||||
for {
|
||||
if n > 0 {
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
n++
|
||||
|
||||
ctx, cancel := context.WithTimeout(ctx, pingTimeout)
|
||||
dur, p2p, pong, err := conn.Ping(ctx)
|
||||
cancel()
|
||||
if err != nil {
|
||||
if xerrors.Is(err, context.DeadlineExceeded) {
|
||||
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "ping to %q timed out \n", workspaceName)
|
||||
if n == pingNum {
|
||||
return nil
|
||||
}
|
||||
continue
|
||||
}
|
||||
if xerrors.Is(err, context.Canceled) {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err.Error() == "no matching peer" {
|
||||
continue
|
||||
}
|
||||
|
||||
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "ping to %q failed %s\n", workspaceName, err.Error())
|
||||
if n == pingNum {
|
||||
return nil
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
dur = dur.Round(time.Millisecond)
|
||||
var via string
|
||||
if p2p {
|
||||
if !didP2p {
|
||||
_, _ = fmt.Fprintln(cmd.OutOrStdout(), "p2p connection established in",
|
||||
cliui.Styles.DateTimeStamp.Render(time.Since(start).Round(time.Millisecond).String()),
|
||||
)
|
||||
}
|
||||
didP2p = true
|
||||
|
||||
via = fmt.Sprintf("%s via %s",
|
||||
cliui.Styles.Fuchsia.Render("p2p"),
|
||||
cliui.Styles.Code.Render(pong.Endpoint),
|
||||
)
|
||||
} else {
|
||||
derpName := "unknown"
|
||||
derpRegion, ok := derpMap.Regions[pong.DERPRegionID]
|
||||
if ok {
|
||||
derpName = derpRegion.RegionName
|
||||
}
|
||||
via = fmt.Sprintf("%s via %s",
|
||||
cliui.Styles.Fuchsia.Render("proxied"),
|
||||
cliui.Styles.Code.Render(fmt.Sprintf("DERP(%s)", derpName)),
|
||||
)
|
||||
}
|
||||
|
||||
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "pong from %s %s in %s\n",
|
||||
cliui.Styles.Keyword.Render(workspaceName),
|
||||
via,
|
||||
cliui.Styles.DateTimeStamp.Render(dur.String()),
|
||||
)
|
||||
|
||||
if n == pingNum {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "Enables verbose logging.")
|
||||
cmd.Flags().DurationVarP(&pingWait, "wait", "", time.Second, "Specifies how long to wait between pings.")
|
||||
cmd.Flags().DurationVarP(&pingTimeout, "timeout", "t", 5*time.Second, "Specifies how long to wait for a ping to complete.")
|
||||
cmd.Flags().IntVarP(&pingNum, "num", "n", 10, "Specifies the number of pings to perform.")
|
||||
return cmd
|
||||
}
|
54
cli/ping_test.go
Normal file
54
cli/ping_test.go
Normal file
@ -0,0 +1,54 @@
|
||||
package cli_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"cdr.dev/slog/sloggers/slogtest"
|
||||
|
||||
"github.com/coder/coder/agent"
|
||||
"github.com/coder/coder/cli/clitest"
|
||||
"github.com/coder/coder/codersdk/agentsdk"
|
||||
"github.com/coder/coder/pty/ptytest"
|
||||
"github.com/coder/coder/testutil"
|
||||
)
|
||||
|
||||
func TestPing(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("OK", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client, workspace, agentToken := setupWorkspaceForAgent(t, nil)
|
||||
cmd, root := clitest.New(t, "ping", workspace.Name)
|
||||
clitest.SetupConfig(t, client, root)
|
||||
pty := ptytest.New(t)
|
||||
cmd.SetIn(pty.Input())
|
||||
cmd.SetErr(pty.Output())
|
||||
cmd.SetOut(pty.Output())
|
||||
|
||||
agentClient := agentsdk.New(client.URL)
|
||||
agentClient.SetSessionToken(agentToken)
|
||||
agentCloser := agent.New(agent.Options{
|
||||
Client: agentClient,
|
||||
Logger: slogtest.Make(t, nil).Named("agent"),
|
||||
})
|
||||
defer func() {
|
||||
_ = agentCloser.Close()
|
||||
}()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
defer cancel()
|
||||
|
||||
cmdDone := tGo(t, func() {
|
||||
err := cmd.ExecuteContext(ctx)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
pty.ExpectMatch("pong from " + workspace.Name)
|
||||
cancel()
|
||||
<-cmdDone
|
||||
})
|
||||
}
|
@ -85,10 +85,12 @@ func Core() []*cobra.Command {
|
||||
login(),
|
||||
logout(),
|
||||
parameters(),
|
||||
ping(),
|
||||
portForward(),
|
||||
publickey(),
|
||||
rename(),
|
||||
resetPassword(),
|
||||
restart(),
|
||||
scaletest(),
|
||||
schedules(),
|
||||
show(),
|
||||
@ -97,7 +99,6 @@ func Core() []*cobra.Command {
|
||||
start(),
|
||||
state(),
|
||||
stop(),
|
||||
restart(),
|
||||
templates(),
|
||||
tokens(),
|
||||
update(),
|
||||
|
@ -71,7 +71,7 @@ func speedtest() *cobra.Command {
|
||||
return ctx.Err()
|
||||
case <-ticker.C:
|
||||
}
|
||||
dur, p2p, err := conn.Ping(ctx)
|
||||
dur, p2p, _, err := conn.Ping(ctx)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
1
cli/testdata/coder_--help.golden
vendored
1
cli/testdata/coder_--help.golden
vendored
@ -36,6 +36,7 @@ Workspace Commands:
|
||||
create Create a workspace
|
||||
delete Delete a workspace
|
||||
list List workspaces
|
||||
ping Ping a workspace
|
||||
rename Rename a workspace
|
||||
restart Restart a workspace
|
||||
schedule Schedule automated start and stop times for workspaces
|
||||
|
26
cli/testdata/coder_ping_--help.golden
vendored
Normal file
26
cli/testdata/coder_ping_--help.golden
vendored
Normal file
@ -0,0 +1,26 @@
|
||||
Ping a workspace
|
||||
|
||||
Usage:
|
||||
coder ping <workspace> [flags]
|
||||
|
||||
Flags:
|
||||
-h, --help help for ping
|
||||
-n, --num int Specifies the number of pings to perform. (default 10)
|
||||
-t, --timeout duration Specifies how long to wait for a ping to complete. (default 5s)
|
||||
-v, --verbose Enables verbose logging.
|
||||
--wait duration Specifies how long to wait between pings. (default 1s)
|
||||
|
||||
Global Flags:
|
||||
--global-config coder Path to the global coder config directory.
|
||||
Consumes $CODER_CONFIG_DIR (default "~/.config/coderv2")
|
||||
--header stringArray HTTP headers added to all requests. Provide as "Key=Value".
|
||||
Consumes $CODER_HEADER
|
||||
--no-feature-warning Suppress warnings about unlicensed features.
|
||||
Consumes $CODER_NO_FEATURE_WARNING
|
||||
--no-version-warning Suppress warning when client and server versions do not match.
|
||||
Consumes $CODER_NO_VERSION_WARNING
|
||||
--token string Specify an authentication token. For security reasons setting
|
||||
CODER_SESSION_TOKEN is preferred.
|
||||
Consumes $CODER_SESSION_TOKEN
|
||||
--url string URL to a deployment.
|
||||
Consumes $CODER_URL
|
@ -204,7 +204,7 @@ type sshNetworkStats struct {
|
||||
}
|
||||
|
||||
func collectNetworkStats(ctx context.Context, agentConn *codersdk.WorkspaceAgentConn, start, end time.Time, counts map[netlogtype.Connection]netlogtype.Counts) (*sshNetworkStats, error) {
|
||||
latency, p2p, err := agentConn.Ping(ctx)
|
||||
latency, p2p, _, err := agentConn.Ping(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -17,6 +17,7 @@ import (
|
||||
"github.com/google/uuid"
|
||||
"golang.org/x/crypto/ssh"
|
||||
"golang.org/x/xerrors"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/net/speedtest"
|
||||
|
||||
"github.com/coder/coder/coderd/tracing"
|
||||
@ -136,7 +137,7 @@ func (c *WorkspaceAgentConn) AwaitReachable(ctx context.Context) bool {
|
||||
|
||||
// Ping pings the agent and returns the round-trip time.
|
||||
// The bool returns true if the ping was made P2P.
|
||||
func (c *WorkspaceAgentConn) Ping(ctx context.Context) (time.Duration, bool, error) {
|
||||
func (c *WorkspaceAgentConn) Ping(ctx context.Context) (time.Duration, bool, *ipnstate.PingResult, error) {
|
||||
ctx, span := tracing.StartSpan(ctx)
|
||||
defer span.End()
|
||||
|
||||
|
@ -49,6 +49,7 @@ coder [flags]
|
||||
- [coder list](coder_list.md) - List workspaces
|
||||
- [coder login](coder_login.md) - Authenticate with Coder deployment
|
||||
- [coder logout](coder_logout.md) - Unauthenticate your local session
|
||||
- [coder ping](coder_ping.md) - Ping a workspace
|
||||
- [coder port-forward](coder_port-forward.md) - Forward ports from machine to a workspace
|
||||
- [coder publickey](coder_publickey.md) - Output your Coder public key used for Git operations
|
||||
- [coder rename](coder_rename.md) - Rename a workspace
|
||||
|
38
docs/cli/coder_ping.md
Normal file
38
docs/cli/coder_ping.md
Normal file
@ -0,0 +1,38 @@
|
||||
## coder ping
|
||||
|
||||
Ping a workspace
|
||||
|
||||
```
|
||||
coder ping <workspace> [flags]
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
-h, --help help for ping
|
||||
-n, --num int Specifies the number of pings to perform. (default 10)
|
||||
-t, --timeout duration Specifies how long to wait for a ping to complete. (default 5s)
|
||||
-v, --verbose Enables verbose logging.
|
||||
--wait duration Specifies how long to wait between pings. (default 1s)
|
||||
```
|
||||
|
||||
### Options inherited from parent commands
|
||||
|
||||
```
|
||||
--global-config coder Path to the global coder config directory.
|
||||
Consumes $CODER_CONFIG_DIR (default "~/.config/coderv2")
|
||||
--header stringArray HTTP headers added to all requests. Provide as "Key=Value".
|
||||
Consumes $CODER_HEADER
|
||||
--no-feature-warning Suppress warnings about unlicensed features.
|
||||
Consumes $CODER_NO_FEATURE_WARNING
|
||||
--no-version-warning Suppress warning when client and server versions do not match.
|
||||
Consumes $CODER_NO_VERSION_WARNING
|
||||
--token string Specify an authentication token. For security reasons setting CODER_SESSION_TOKEN is preferred.
|
||||
Consumes $CODER_SESSION_TOKEN
|
||||
--url string URL to a deployment.
|
||||
Consumes $CODER_URL
|
||||
```
|
||||
|
||||
### SEE ALSO
|
||||
|
||||
- [coder](coder.md) -
|
@ -453,6 +453,10 @@
|
||||
"title": "logout",
|
||||
"path": "./cli/coder_logout.md"
|
||||
},
|
||||
{
|
||||
"title": "ping",
|
||||
"path": "./cli/coder_ping.md"
|
||||
},
|
||||
{
|
||||
"title": "port-forward",
|
||||
"path": "./cli/coder_port-forward.md"
|
||||
|
@ -84,7 +84,7 @@ func TestReplicas(t *testing.T) {
|
||||
require.Eventually(t, func() bool {
|
||||
ctx, cancelFunc := context.WithTimeout(context.Background(), testutil.WaitShort)
|
||||
defer cancelFunc()
|
||||
_, _, err = conn.Ping(ctx)
|
||||
_, _, _, err = conn.Ping(ctx)
|
||||
return err == nil
|
||||
}, testutil.WaitLong, testutil.IntervalFast)
|
||||
_ = conn.Close()
|
||||
@ -129,7 +129,7 @@ func TestReplicas(t *testing.T) {
|
||||
require.Eventually(t, func() bool {
|
||||
ctx, cancelFunc := context.WithTimeout(context.Background(), testutil.IntervalSlow)
|
||||
defer cancelFunc()
|
||||
_, _, err = conn.Ping(ctx)
|
||||
_, _, _, err = conn.Ping(ctx)
|
||||
return err == nil
|
||||
}, testutil.WaitLong, testutil.IntervalFast)
|
||||
_ = conn.Close()
|
||||
|
@ -141,7 +141,7 @@ func waitForDisco(ctx context.Context, logs io.Writer, conn *codersdk.WorkspaceA
|
||||
for i := 0; i < pingAttempts; i++ {
|
||||
_, _ = fmt.Fprintf(logs, "\tDisco ping attempt %d/%d...\n", i+1, pingAttempts)
|
||||
pingCtx, cancel := context.WithTimeout(ctx, defaultRequestTimeout)
|
||||
_, p2p, err := conn.Ping(pingCtx)
|
||||
_, p2p, _, err := conn.Ping(pingCtx)
|
||||
cancel()
|
||||
if err == nil {
|
||||
_, _ = fmt.Fprintf(logs, "\tDisco ping succeeded after %d attempts, p2p = %v\n", i+1, p2p)
|
||||
|
@ -426,7 +426,7 @@ func (c *Conn) Status() *ipnstate.Status {
|
||||
|
||||
// Ping sends a Disco ping to the Wireguard engine.
|
||||
// The bool returned is true if the ping was performed P2P.
|
||||
func (c *Conn) Ping(ctx context.Context, ip netip.Addr) (time.Duration, bool, error) {
|
||||
func (c *Conn) Ping(ctx context.Context, ip netip.Addr) (time.Duration, bool, *ipnstate.PingResult, error) {
|
||||
errCh := make(chan error, 1)
|
||||
prChan := make(chan *ipnstate.PingResult, 1)
|
||||
go c.wireguardEngine.Ping(ip, tailcfg.PingDisco, func(pr *ipnstate.PingResult) {
|
||||
@ -438,11 +438,11 @@ func (c *Conn) Ping(ctx context.Context, ip netip.Addr) (time.Duration, bool, er
|
||||
})
|
||||
select {
|
||||
case err := <-errCh:
|
||||
return 0, false, err
|
||||
return 0, false, nil, err
|
||||
case <-ctx.Done():
|
||||
return 0, false, ctx.Err()
|
||||
return 0, false, nil, ctx.Err()
|
||||
case pr := <-prChan:
|
||||
return time.Duration(pr.LatencySeconds * float64(time.Second)), pr.Endpoint != "", nil
|
||||
return time.Duration(pr.LatencySeconds * float64(time.Second)), pr.Endpoint != "", pr, nil
|
||||
}
|
||||
}
|
||||
|
||||
@ -471,7 +471,7 @@ func (c *Conn) AwaitReachable(ctx context.Context, ip netip.Addr) bool {
|
||||
ctx, cancel := context.WithTimeout(ctx, 5*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
_, _, err := c.Ping(ctx, ip)
|
||||
_, _, _, err := c.Ping(ctx, ip)
|
||||
if err == nil {
|
||||
completed()
|
||||
}
|
||||
|
Reference in New Issue
Block a user