Skip to content

Commit

Permalink
feat: add coder ping (coder#6161)
Browse files Browse the repository at this point in the history
  • Loading branch information
coadler authored Feb 13, 2023
1 parent 2157bff commit a54de60
Show file tree
Hide file tree
Showing 14 changed files with 276 additions and 12 deletions.
138 changes: 138 additions & 0 deletions cli/ping.go
Original file line number Diff line number Diff line change
@@ -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 changes: 54 additions & 0 deletions cli/ping_test.go
Original file line number Diff line number Diff line change
@@ -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
})
}
3 changes: 2 additions & 1 deletion cli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,12 @@ func Core() []*cobra.Command {
login(),
logout(),
parameters(),
ping(),
portForward(),
publickey(),
rename(),
resetPassword(),
restart(),
scaletest(),
schedules(),
show(),
Expand All @@ -97,7 +99,6 @@ func Core() []*cobra.Command {
start(),
state(),
stop(),
restart(),
templates(),
tokens(),
update(),
Expand Down
2 changes: 1 addition & 1 deletion cli/speedtest.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
1 change: 1 addition & 0 deletions cli/testdata/coder_--help.golden
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
26 changes: 26 additions & 0 deletions cli/testdata/coder_ping_--help.golden
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion cli/vscodessh.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
3 changes: 2 additions & 1 deletion codersdk/workspaceagentconn.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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()

Expand Down
1 change: 1 addition & 0 deletions docs/cli/coder.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
38 changes: 38 additions & 0 deletions docs/cli/coder_ping.md
Original file line number Diff line number Diff line change
@@ -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) -
4 changes: 4 additions & 0 deletions docs/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
4 changes: 2 additions & 2 deletions enterprise/coderd/replicas_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Expand Down
2 changes: 1 addition & 1 deletion scaletest/agentconn/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading

0 comments on commit a54de60

Please sign in to comment.