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(),
|
login(),
|
||||||
logout(),
|
logout(),
|
||||||
parameters(),
|
parameters(),
|
||||||
|
ping(),
|
||||||
portForward(),
|
portForward(),
|
||||||
publickey(),
|
publickey(),
|
||||||
rename(),
|
rename(),
|
||||||
resetPassword(),
|
resetPassword(),
|
||||||
|
restart(),
|
||||||
scaletest(),
|
scaletest(),
|
||||||
schedules(),
|
schedules(),
|
||||||
show(),
|
show(),
|
||||||
@ -97,7 +99,6 @@ func Core() []*cobra.Command {
|
|||||||
start(),
|
start(),
|
||||||
state(),
|
state(),
|
||||||
stop(),
|
stop(),
|
||||||
restart(),
|
|
||||||
templates(),
|
templates(),
|
||||||
tokens(),
|
tokens(),
|
||||||
update(),
|
update(),
|
||||||
|
@ -71,7 +71,7 @@ func speedtest() *cobra.Command {
|
|||||||
return ctx.Err()
|
return ctx.Err()
|
||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
}
|
}
|
||||||
dur, p2p, err := conn.Ping(ctx)
|
dur, p2p, _, err := conn.Ping(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
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
|
create Create a workspace
|
||||||
delete Delete a workspace
|
delete Delete a workspace
|
||||||
list List workspaces
|
list List workspaces
|
||||||
|
ping Ping a workspace
|
||||||
rename Rename a workspace
|
rename Rename a workspace
|
||||||
restart Restart a workspace
|
restart Restart a workspace
|
||||||
schedule Schedule automated start and stop times for workspaces
|
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) {
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,7 @@ import (
|
|||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"golang.org/x/crypto/ssh"
|
"golang.org/x/crypto/ssh"
|
||||||
"golang.org/x/xerrors"
|
"golang.org/x/xerrors"
|
||||||
|
"tailscale.com/ipn/ipnstate"
|
||||||
"tailscale.com/net/speedtest"
|
"tailscale.com/net/speedtest"
|
||||||
|
|
||||||
"github.com/coder/coder/coderd/tracing"
|
"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.
|
// Ping pings the agent and returns the round-trip time.
|
||||||
// The bool returns true if the ping was made P2P.
|
// 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)
|
ctx, span := tracing.StartSpan(ctx)
|
||||||
defer span.End()
|
defer span.End()
|
||||||
|
|
||||||
|
@ -49,6 +49,7 @@ coder [flags]
|
|||||||
- [coder list](coder_list.md) - List workspaces
|
- [coder list](coder_list.md) - List workspaces
|
||||||
- [coder login](coder_login.md) - Authenticate with Coder deployment
|
- [coder login](coder_login.md) - Authenticate with Coder deployment
|
||||||
- [coder logout](coder_logout.md) - Unauthenticate your local session
|
- [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 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 publickey](coder_publickey.md) - Output your Coder public key used for Git operations
|
||||||
- [coder rename](coder_rename.md) - Rename a workspace
|
- [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",
|
"title": "logout",
|
||||||
"path": "./cli/coder_logout.md"
|
"path": "./cli/coder_logout.md"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"title": "ping",
|
||||||
|
"path": "./cli/coder_ping.md"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"title": "port-forward",
|
"title": "port-forward",
|
||||||
"path": "./cli/coder_port-forward.md"
|
"path": "./cli/coder_port-forward.md"
|
||||||
|
@ -84,7 +84,7 @@ func TestReplicas(t *testing.T) {
|
|||||||
require.Eventually(t, func() bool {
|
require.Eventually(t, func() bool {
|
||||||
ctx, cancelFunc := context.WithTimeout(context.Background(), testutil.WaitShort)
|
ctx, cancelFunc := context.WithTimeout(context.Background(), testutil.WaitShort)
|
||||||
defer cancelFunc()
|
defer cancelFunc()
|
||||||
_, _, err = conn.Ping(ctx)
|
_, _, _, err = conn.Ping(ctx)
|
||||||
return err == nil
|
return err == nil
|
||||||
}, testutil.WaitLong, testutil.IntervalFast)
|
}, testutil.WaitLong, testutil.IntervalFast)
|
||||||
_ = conn.Close()
|
_ = conn.Close()
|
||||||
@ -129,7 +129,7 @@ func TestReplicas(t *testing.T) {
|
|||||||
require.Eventually(t, func() bool {
|
require.Eventually(t, func() bool {
|
||||||
ctx, cancelFunc := context.WithTimeout(context.Background(), testutil.IntervalSlow)
|
ctx, cancelFunc := context.WithTimeout(context.Background(), testutil.IntervalSlow)
|
||||||
defer cancelFunc()
|
defer cancelFunc()
|
||||||
_, _, err = conn.Ping(ctx)
|
_, _, _, err = conn.Ping(ctx)
|
||||||
return err == nil
|
return err == nil
|
||||||
}, testutil.WaitLong, testutil.IntervalFast)
|
}, testutil.WaitLong, testutil.IntervalFast)
|
||||||
_ = conn.Close()
|
_ = conn.Close()
|
||||||
|
@ -141,7 +141,7 @@ func waitForDisco(ctx context.Context, logs io.Writer, conn *codersdk.WorkspaceA
|
|||||||
for i := 0; i < pingAttempts; i++ {
|
for i := 0; i < pingAttempts; i++ {
|
||||||
_, _ = fmt.Fprintf(logs, "\tDisco ping attempt %d/%d...\n", i+1, pingAttempts)
|
_, _ = fmt.Fprintf(logs, "\tDisco ping attempt %d/%d...\n", i+1, pingAttempts)
|
||||||
pingCtx, cancel := context.WithTimeout(ctx, defaultRequestTimeout)
|
pingCtx, cancel := context.WithTimeout(ctx, defaultRequestTimeout)
|
||||||
_, p2p, err := conn.Ping(pingCtx)
|
_, p2p, _, err := conn.Ping(pingCtx)
|
||||||
cancel()
|
cancel()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
_, _ = fmt.Fprintf(logs, "\tDisco ping succeeded after %d attempts, p2p = %v\n", i+1, p2p)
|
_, _ = 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.
|
// Ping sends a Disco ping to the Wireguard engine.
|
||||||
// The bool returned is true if the ping was performed P2P.
|
// 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)
|
errCh := make(chan error, 1)
|
||||||
prChan := make(chan *ipnstate.PingResult, 1)
|
prChan := make(chan *ipnstate.PingResult, 1)
|
||||||
go c.wireguardEngine.Ping(ip, tailcfg.PingDisco, func(pr *ipnstate.PingResult) {
|
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 {
|
select {
|
||||||
case err := <-errCh:
|
case err := <-errCh:
|
||||||
return 0, false, err
|
return 0, false, nil, err
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return 0, false, ctx.Err()
|
return 0, false, nil, ctx.Err()
|
||||||
case pr := <-prChan:
|
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)
|
ctx, cancel := context.WithTimeout(ctx, 5*time.Minute)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
_, _, err := c.Ping(ctx, ip)
|
_, _, _, err := c.Ping(ctx, ip)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
completed()
|
completed()
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user