From c7254ca1f5da89bc3139373761d6456aea775487 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Mon, 9 Aug 2021 03:55:56 +0000 Subject: [PATCH 01/15] feat: Add Ping command to monitor workspace latency --- internal/cmd/cmd.go | 27 +++++----- internal/cmd/ping.go | 123 +++++++++++++++++++++++++++++++++++++++++++ wsnet/dial.go | 5 ++ 3 files changed, 142 insertions(+), 13 deletions(-) create mode 100644 internal/cmd/ping.go diff --git a/internal/cmd/cmd.go b/internal/cmd/cmd.go index 4ad33209..e56a2fcc 100644 --- a/internal/cmd/cmd.go +++ b/internal/cmd/cmd.go @@ -22,25 +22,26 @@ func Make() *cobra.Command { } app.AddCommand( + agentCmd(), + completionCmd(), + configSSHCmd(), + envCmd(), // DEPRECATED. + genDocsCmd(app), + imgsCmd(), loginCmd(), logoutCmd(), + pingCmd(), + providersCmd(), + resourceCmd(), + satellitesCmd(), sshCmd(), - usersCmd(), - tagsCmd(), - configSSHCmd(), - envCmd(), // DEPRECATED. - workspacesCmd(), syncCmd(), - urlCmd(), + tagsCmd(), tokensCmd(), - resourceCmd(), - completionCmd(), - imgsCmd(), - providersCmd(), - genDocsCmd(app), - agentCmd(), tunnelCmd(), - satellitesCmd(), + urlCmd(), + usersCmd(), + workspacesCmd(), ) app.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "show verbose output") return app diff --git a/internal/cmd/ping.go b/internal/cmd/ping.go new file mode 100644 index 00000000..88f543dd --- /dev/null +++ b/internal/cmd/ping.go @@ -0,0 +1,123 @@ +package cmd + +import ( + "fmt" + "strings" + "time" + + "cdr.dev/coder-cli/coder-sdk" + "cdr.dev/coder-cli/pkg/clog" + "cdr.dev/coder-cli/wsnet" + "github.com/fatih/color" + "github.com/pion/webrtc/v3" + "github.com/spf13/cobra" + "nhooyr.io/websocket" +) + +func pingCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "ping [workspace_name]", + Short: "Ping a Coder workspace", + Example: `coder ping my-dev`, + RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + client, err := newClient(ctx, true) + if err != nil { + return err + } + workspace, err := findWorkspace(ctx, client, args[0], coder.Me) + if err != nil { + return err + } + if workspace.LatestStat.ContainerStatus != coder.WorkspaceOn { + return clog.Error("workspace not available", + fmt.Sprintf("current status: \"%s\"", workspace.LatestStat.ContainerStatus), + clog.BlankLine, + clog.Tipf("use \"coder workspaces rebuild %s\" to rebuild this workspace", workspace.Name), + ) + } + servers, err := client.ICEServers(ctx) + if err != nil { + return err + } + url := client.BaseURL() + connectionStart := time.Now() + dialer, err := wsnet.DialWebsocket(ctx, wsnet.ConnectEndpoint(&url, workspace.ID, client.Token()), &wsnet.DialOptions{ + ICEServers: servers, + TURNProxyAuthToken: client.Token(), + TURNRemoteProxyURL: &url, + TURNLocalProxyURL: &url, + }, &websocket.DialOptions{}) + if err != nil { + return err + } + connectionMS := float64(time.Since(connectionStart).Microseconds()) / 1000 + candidates, err := dialer.Candidates() + if err != nil { + return err + } + relay := candidates.Local.Typ == webrtc.ICECandidateTypeRelay + tunneled := false + properties := []string{} + candidateURLs := []string{} + + for _, server := range servers { + if server.Username == wsnet.TURNProxyICECandidate().Username { + candidateURLs = append(candidateURLs, fmt.Sprintf("turns:%s", url.Host)) + if !relay { + continue + } + tunneled = true + continue + } + + candidateURLs = append(candidateURLs, server.URLs...) + } + properties = append(properties, fmt.Sprintf("candidates=%s", strings.Join(candidateURLs, ","))) + + connectionText := "direct via STUN" + if relay { + connectionText = "proxied via TURN" + } + if tunneled { + connectionText = fmt.Sprintf("proxied via %s", url.Host) + } + + fmt.Printf("%s %s %s (%s) %s\n", + color.New(color.Bold, color.FgWhite).Sprint("PING"), + workspace.Name, + color.New(color.Bold, color.FgGreen).Sprintf("connected in %.2fms", connectionMS), + connectionText, + strings.Join(properties, " "), + ) + + ticker := time.NewTicker(time.Second) + seq := 1 + for { + select { + case <-ticker.C: + start := time.Now() + err := dialer.Ping(ctx) + if err != nil { + return err + } + pingMS := float64(time.Since(start).Microseconds()) / 1000 + connectionText = "you ↔ workspace" + if tunneled { + connectionText = fmt.Sprintf("you ↔ %s ↔ workspace", url.Host) + } + + fmt.Printf("%.2fms (%s) seq=%d\n", + pingMS, + connectionText, + seq) + seq++ + case <-ctx.Done(): + return nil + } + } + }, + } + + return cmd +} diff --git a/wsnet/dial.go b/wsnet/dial.go index 355d2c88..35e13870 100644 --- a/wsnet/dial.go +++ b/wsnet/dial.go @@ -301,6 +301,11 @@ func (d *Dialer) activeConnections() int { return int(stats.DataChannelsRequested-stats.DataChannelsClosed) - 1 } +// Candidates returns the candidate pair that was chosen for the connection. +func (d *Dialer) Candidates() (*webrtc.ICECandidatePair, error) { + return d.rtc.SCTP().Transport().ICETransport().GetSelectedCandidatePair() +} + // Close closes the RTC connection. // All data channels dialed will be closed. func (d *Dialer) Close() error { From e27f72879821de1599166edcc7e32659727f9380 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Mon, 9 Aug 2021 03:59:57 +0000 Subject: [PATCH 02/15] Handle shut off with nice error --- internal/cmd/ping.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/internal/cmd/ping.go b/internal/cmd/ping.go index 88f543dd..408043fc 100644 --- a/internal/cmd/ping.go +++ b/internal/cmd/ping.go @@ -1,7 +1,9 @@ package cmd import ( + "errors" "fmt" + "io" "strings" "time" @@ -99,6 +101,20 @@ func pingCmd() *cobra.Command { start := time.Now() err := dialer.Ping(ctx) if err != nil { + if errors.Is(err, io.EOF) { + workspace, err = findWorkspace(ctx, client, args[0], coder.Me) + if err != nil { + return err + } + if workspace.LatestStat.ContainerStatus != coder.WorkspaceOn { + return clog.Error("workspace changed state", + fmt.Sprintf("current status: \"%s\"", workspace.LatestStat.ContainerStatus), + clog.BlankLine, + clog.Tipf("use \"coder workspaces rebuild %s\" to rebuild this workspace", workspace.Name), + ) + } + return errors.New("connection was closed unexpectedly") + } return err } pingMS := float64(time.Since(start).Microseconds()) / 1000 From c1b49481511a26e5fab49d1c437a8767cad8dd38 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Mon, 9 Aug 2021 04:05:13 +0000 Subject: [PATCH 03/15] Organize funcs --- internal/cmd/ping.go | 52 ++++++++++++++++++++++++++------------------ 1 file changed, 31 insertions(+), 21 deletions(-) diff --git a/internal/cmd/ping.go b/internal/cmd/ping.go index 408043fc..91eea02f 100644 --- a/internal/cmd/ping.go +++ b/internal/cmd/ping.go @@ -1,6 +1,7 @@ package cmd import ( + "context" "errors" "fmt" "io" @@ -98,30 +99,10 @@ func pingCmd() *cobra.Command { for { select { case <-ticker.C: - start := time.Now() - err := dialer.Ping(ctx) + pingMS, connectionText, err := ping(ctx, client, dialer, tunneled, args[0]) if err != nil { - if errors.Is(err, io.EOF) { - workspace, err = findWorkspace(ctx, client, args[0], coder.Me) - if err != nil { - return err - } - if workspace.LatestStat.ContainerStatus != coder.WorkspaceOn { - return clog.Error("workspace changed state", - fmt.Sprintf("current status: \"%s\"", workspace.LatestStat.ContainerStatus), - clog.BlankLine, - clog.Tipf("use \"coder workspaces rebuild %s\" to rebuild this workspace", workspace.Name), - ) - } - return errors.New("connection was closed unexpectedly") - } return err } - pingMS := float64(time.Since(start).Microseconds()) / 1000 - connectionText = "you ↔ workspace" - if tunneled { - connectionText = fmt.Sprintf("you ↔ %s ↔ workspace", url.Host) - } fmt.Printf("%.2fms (%s) seq=%d\n", pingMS, @@ -137,3 +118,32 @@ func pingCmd() *cobra.Command { return cmd } + +func ping(ctx context.Context, client coder.Client, dialer *wsnet.Dialer, tunneled bool, workspaceName string) (float64, string, error) { + start := time.Now() + err := dialer.Ping(ctx) + if err != nil { + if errors.Is(err, io.EOF) { + workspace, err := findWorkspace(ctx, client, workspaceName, coder.Me) + if err != nil { + return -1, "", err + } + if workspace.LatestStat.ContainerStatus != coder.WorkspaceOn { + return -1, "", clog.Error("workspace changed state", + fmt.Sprintf("current status: \"%s\"", workspace.LatestStat.ContainerStatus), + clog.BlankLine, + clog.Tipf("use \"coder workspaces rebuild %s\" to rebuild this workspace", workspace.Name), + ) + } + return -1, "", errors.New("connection was closed unexpectedly") + } + return -1, "", err + } + pingMS := float64(time.Since(start).Microseconds()) / 1000 + url := client.BaseURL() + connectionText := "you ↔ workspace" + if tunneled { + connectionText = fmt.Sprintf("you ↔ %s ↔ workspace", url.Host) + } + return pingMS, connectionText, nil +} From 24f0de2eb5a639346eb582c1228436688f1f317c Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Mon, 9 Aug 2021 04:10:48 +0000 Subject: [PATCH 04/15] Add docs --- docs/coder.md | 1 + docs/coder_ping.md | 30 ++++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 docs/coder_ping.md diff --git a/docs/coder.md b/docs/coder.md index 17e7fa7f..d157a9f2 100644 --- a/docs/coder.md +++ b/docs/coder.md @@ -16,6 +16,7 @@ coder provides a CLI for working with an existing Coder installation * [coder images](coder_images.md) - Manage Coder images * [coder login](coder_login.md) - Authenticate this client for future operations * [coder logout](coder_logout.md) - Remove local authentication credentials if any exist +* [coder ping](coder_ping.md) - Ping a Coder workspace * [coder satellites](coder_satellites.md) - Interact with Coder satellite deployments * [coder ssh](coder_ssh.md) - Enter a shell of execute a command over SSH into a Coder workspace * [coder sync](coder_sync.md) - Establish a one way directory sync to a Coder workspace diff --git a/docs/coder_ping.md b/docs/coder_ping.md new file mode 100644 index 00000000..b454db63 --- /dev/null +++ b/docs/coder_ping.md @@ -0,0 +1,30 @@ +## coder ping + +Ping a Coder workspace + +``` +coder ping [workspace_name] [flags] +``` + +### Examples + +``` +coder ping my-dev +``` + +### Options + +``` + -h, --help help for ping +``` + +### Options inherited from parent commands + +``` + -v, --verbose show verbose output +``` + +### SEE ALSO + +* [coder](coder.md) - coder provides a CLI for working with an existing Coder installation + From 976eaef3fb3e0aa8fc12a1d357b637cc1d5639ae Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Mon, 9 Aug 2021 04:14:19 +0000 Subject: [PATCH 05/15] Organize imports --- internal/cmd/ping.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/internal/cmd/ping.go b/internal/cmd/ping.go index 91eea02f..a4da87bd 100644 --- a/internal/cmd/ping.go +++ b/internal/cmd/ping.go @@ -8,13 +8,14 @@ import ( "strings" "time" - "cdr.dev/coder-cli/coder-sdk" - "cdr.dev/coder-cli/pkg/clog" - "cdr.dev/coder-cli/wsnet" "github.com/fatih/color" "github.com/pion/webrtc/v3" "github.com/spf13/cobra" "nhooyr.io/websocket" + + "cdr.dev/coder-cli/coder-sdk" + "cdr.dev/coder-cli/pkg/clog" + "cdr.dev/coder-cli/wsnet" ) func pingCmd() *cobra.Command { From e0c271ca006f32387e01af3843b64f05ea942af9 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Mon, 9 Aug 2021 04:56:30 +0000 Subject: [PATCH 06/15] Move to subdommand of workspaces --- internal/cmd/cmd.go | 1 - internal/cmd/ping.go | 150 ------------------------------------ internal/cmd/workspaces.go | 153 +++++++++++++++++++++++++++++++++++-- 3 files changed, 147 insertions(+), 157 deletions(-) delete mode 100644 internal/cmd/ping.go diff --git a/internal/cmd/cmd.go b/internal/cmd/cmd.go index e56a2fcc..26df6bc4 100644 --- a/internal/cmd/cmd.go +++ b/internal/cmd/cmd.go @@ -30,7 +30,6 @@ func Make() *cobra.Command { imgsCmd(), loginCmd(), logoutCmd(), - pingCmd(), providersCmd(), resourceCmd(), satellitesCmd(), diff --git a/internal/cmd/ping.go b/internal/cmd/ping.go deleted file mode 100644 index a4da87bd..00000000 --- a/internal/cmd/ping.go +++ /dev/null @@ -1,150 +0,0 @@ -package cmd - -import ( - "context" - "errors" - "fmt" - "io" - "strings" - "time" - - "github.com/fatih/color" - "github.com/pion/webrtc/v3" - "github.com/spf13/cobra" - "nhooyr.io/websocket" - - "cdr.dev/coder-cli/coder-sdk" - "cdr.dev/coder-cli/pkg/clog" - "cdr.dev/coder-cli/wsnet" -) - -func pingCmd() *cobra.Command { - cmd := &cobra.Command{ - Use: "ping [workspace_name]", - Short: "Ping a Coder workspace", - Example: `coder ping my-dev`, - RunE: func(cmd *cobra.Command, args []string) error { - ctx := cmd.Context() - client, err := newClient(ctx, true) - if err != nil { - return err - } - workspace, err := findWorkspace(ctx, client, args[0], coder.Me) - if err != nil { - return err - } - if workspace.LatestStat.ContainerStatus != coder.WorkspaceOn { - return clog.Error("workspace not available", - fmt.Sprintf("current status: \"%s\"", workspace.LatestStat.ContainerStatus), - clog.BlankLine, - clog.Tipf("use \"coder workspaces rebuild %s\" to rebuild this workspace", workspace.Name), - ) - } - servers, err := client.ICEServers(ctx) - if err != nil { - return err - } - url := client.BaseURL() - connectionStart := time.Now() - dialer, err := wsnet.DialWebsocket(ctx, wsnet.ConnectEndpoint(&url, workspace.ID, client.Token()), &wsnet.DialOptions{ - ICEServers: servers, - TURNProxyAuthToken: client.Token(), - TURNRemoteProxyURL: &url, - TURNLocalProxyURL: &url, - }, &websocket.DialOptions{}) - if err != nil { - return err - } - connectionMS := float64(time.Since(connectionStart).Microseconds()) / 1000 - candidates, err := dialer.Candidates() - if err != nil { - return err - } - relay := candidates.Local.Typ == webrtc.ICECandidateTypeRelay - tunneled := false - properties := []string{} - candidateURLs := []string{} - - for _, server := range servers { - if server.Username == wsnet.TURNProxyICECandidate().Username { - candidateURLs = append(candidateURLs, fmt.Sprintf("turns:%s", url.Host)) - if !relay { - continue - } - tunneled = true - continue - } - - candidateURLs = append(candidateURLs, server.URLs...) - } - properties = append(properties, fmt.Sprintf("candidates=%s", strings.Join(candidateURLs, ","))) - - connectionText := "direct via STUN" - if relay { - connectionText = "proxied via TURN" - } - if tunneled { - connectionText = fmt.Sprintf("proxied via %s", url.Host) - } - - fmt.Printf("%s %s %s (%s) %s\n", - color.New(color.Bold, color.FgWhite).Sprint("PING"), - workspace.Name, - color.New(color.Bold, color.FgGreen).Sprintf("connected in %.2fms", connectionMS), - connectionText, - strings.Join(properties, " "), - ) - - ticker := time.NewTicker(time.Second) - seq := 1 - for { - select { - case <-ticker.C: - pingMS, connectionText, err := ping(ctx, client, dialer, tunneled, args[0]) - if err != nil { - return err - } - - fmt.Printf("%.2fms (%s) seq=%d\n", - pingMS, - connectionText, - seq) - seq++ - case <-ctx.Done(): - return nil - } - } - }, - } - - return cmd -} - -func ping(ctx context.Context, client coder.Client, dialer *wsnet.Dialer, tunneled bool, workspaceName string) (float64, string, error) { - start := time.Now() - err := dialer.Ping(ctx) - if err != nil { - if errors.Is(err, io.EOF) { - workspace, err := findWorkspace(ctx, client, workspaceName, coder.Me) - if err != nil { - return -1, "", err - } - if workspace.LatestStat.ContainerStatus != coder.WorkspaceOn { - return -1, "", clog.Error("workspace changed state", - fmt.Sprintf("current status: \"%s\"", workspace.LatestStat.ContainerStatus), - clog.BlankLine, - clog.Tipf("use \"coder workspaces rebuild %s\" to rebuild this workspace", workspace.Name), - ) - } - return -1, "", errors.New("connection was closed unexpectedly") - } - return -1, "", err - } - pingMS := float64(time.Since(start).Microseconds()) / 1000 - url := client.BaseURL() - connectionText := "you ↔ workspace" - if tunneled { - connectionText = fmt.Sprintf("you ↔ %s ↔ workspace", url.Host) - } - return pingMS, connectionText, nil -} diff --git a/internal/cmd/workspaces.go b/internal/cmd/workspaces.go index d1135cf4..0041bc0c 100644 --- a/internal/cmd/workspaces.go +++ b/internal/cmd/workspaces.go @@ -4,17 +4,24 @@ import ( "bytes" "context" "encoding/json" + "errors" "fmt" "io" "io/ioutil" + "strings" + "time" "cdr.dev/coder-cli/coder-sdk" "cdr.dev/coder-cli/internal/coderutil" "cdr.dev/coder-cli/internal/x/xcobra" "cdr.dev/coder-cli/pkg/clog" "cdr.dev/coder-cli/pkg/tablewriter" + "cdr.dev/coder-cli/wsnet" + "nhooyr.io/websocket" + "github.com/fatih/color" "github.com/manifoldco/promptui" + "github.com/pion/webrtc/v3" "github.com/spf13/cobra" "golang.org/x/xerrors" ) @@ -38,16 +45,17 @@ func workspacesCmd() *cobra.Command { } cmd.AddCommand( + createWorkspaceCmd(), + editWorkspaceCmd(), lsWorkspacesCommand(), - stopWorkspacesCmd(), + pingWorkspaceCommand(), + rebuildWorkspaceCommand(), rmWorkspacesCmd(), + setPolicyTemplate(), + stopWorkspacesCmd(), watchBuildLogCommand(), - rebuildWorkspaceCommand(), - createWorkspaceCmd(), - workspaceFromConfigCmd(true), workspaceFromConfigCmd(false), - editWorkspaceCmd(), - setPolicyTemplate(), + workspaceFromConfigCmd(true), ) return cmd } @@ -120,6 +128,139 @@ func lsWorkspacesCommand() *cobra.Command { return cmd } +func pingWorkspaceCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "ping [workspace_name]", + Short: "ping Coder workspaces by name", + Long: "ping Coder workspaces by name", + Example: `coder workspaces ping front-end-workspace`, + Args: xcobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + client, err := newClient(ctx, true) + if err != nil { + return err + } + workspace, err := findWorkspace(ctx, client, args[0], coder.Me) + if err != nil { + return err + } + if workspace.LatestStat.ContainerStatus != coder.WorkspaceOn { + return clog.Error("workspace not available", + fmt.Sprintf("current status: \"%s\"", workspace.LatestStat.ContainerStatus), + clog.BlankLine, + clog.Tipf("use \"coder workspaces rebuild %s\" to rebuild this workspace", workspace.Name), + ) + } + servers, err := client.ICEServers(ctx) + if err != nil { + return err + } + url := client.BaseURL() + connectionStart := time.Now() + dialer, err := wsnet.DialWebsocket(ctx, wsnet.ConnectEndpoint(&url, workspace.ID, client.Token()), &wsnet.DialOptions{ + ICEServers: servers, + TURNProxyAuthToken: client.Token(), + TURNRemoteProxyURL: &url, + TURNLocalProxyURL: &url, + }, &websocket.DialOptions{}) + if err != nil { + return err + } + connectionMS := float64(time.Since(connectionStart).Microseconds()) / 1000 + candidates, err := dialer.Candidates() + if err != nil { + return err + } + relay := candidates.Local.Typ == webrtc.ICECandidateTypeRelay + tunneled := false + properties := []string{} + candidateURLs := []string{} + + for _, server := range servers { + if server.Username == wsnet.TURNProxyICECandidate().Username { + candidateURLs = append(candidateURLs, fmt.Sprintf("turns:%s", url.Host)) + if !relay { + continue + } + tunneled = true + continue + } + + candidateURLs = append(candidateURLs, server.URLs...) + } + properties = append(properties, fmt.Sprintf("candidates=%s", strings.Join(candidateURLs, ","))) + + connectionText := "direct via STUN" + if relay { + connectionText = "proxied via TURN" + } + if tunneled { + connectionText = fmt.Sprintf("proxied via %s", url.Host) + } + + fmt.Printf("%s %s %s (%s) %s\n", + color.New(color.Bold, color.FgWhite).Sprint("PING"), + workspace.Name, + color.New(color.Bold, color.FgGreen).Sprintf("connected in %.2fms", connectionMS), + connectionText, + strings.Join(properties, " "), + ) + + ticker := time.NewTicker(time.Second) + seq := 1 + for { + select { + case <-ticker.C: + pingMS, connectionText, err := ping(ctx, client, dialer, tunneled, args[0]) + if err != nil { + return err + } + + fmt.Printf("%.2fms (%s) seq=%d\n", + pingMS, + connectionText, + seq) + seq++ + case <-ctx.Done(): + return nil + } + } + }, + } + + return cmd +} + +func ping(ctx context.Context, client coder.Client, dialer *wsnet.Dialer, tunneled bool, workspaceName string) (float64, string, error) { + start := time.Now() + err := dialer.Ping(ctx) + if err != nil { + if errors.Is(err, io.EOF) { + workspace, err := findWorkspace(ctx, client, workspaceName, coder.Me) + if err != nil { + return -1, "", err + } + if workspace.LatestStat.ContainerStatus != coder.WorkspaceOn { + return -1, "", clog.Error("workspace changed state", + fmt.Sprintf("current status: \"%s\"", workspace.LatestStat.ContainerStatus), + clog.BlankLine, + clog.Tipf("use \"coder workspaces rebuild %s\" to rebuild this workspace", workspace.Name), + ) + } + return -1, "", errors.New("connection was closed unexpectedly") + } + return -1, "", err + } + pingMS := float64(time.Since(start).Microseconds()) / 1000 + url := client.BaseURL() + connectionText := "you ↔ workspace" + if tunneled { + connectionText = fmt.Sprintf("you ↔ %s ↔ workspace", url.Host) + } + return pingMS, connectionText, nil +} + func stopWorkspacesCmd() *cobra.Command { var user string cmd := &cobra.Command{ From 4d9cd9ca40f7f1ef4cc6a82858fbca80216f7c3f Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Mon, 9 Aug 2021 05:48:32 +0000 Subject: [PATCH 07/15] Refactor to be smarter --- internal/cmd/workspaces.go | 189 ++++++++++++++++++++----------------- 1 file changed, 102 insertions(+), 87 deletions(-) diff --git a/internal/cmd/workspaces.go b/internal/cmd/workspaces.go index 0041bc0c..a50a8bff 100644 --- a/internal/cmd/workspaces.go +++ b/internal/cmd/workspaces.go @@ -145,83 +145,20 @@ func pingWorkspaceCommand() *cobra.Command { if err != nil { return err } - if workspace.LatestStat.ContainerStatus != coder.WorkspaceOn { - return clog.Error("workspace not available", - fmt.Sprintf("current status: \"%s\"", workspace.LatestStat.ContainerStatus), - clog.BlankLine, - clog.Tipf("use \"coder workspaces rebuild %s\" to rebuild this workspace", workspace.Name), - ) - } - servers, err := client.ICEServers(ctx) - if err != nil { - return err - } - url := client.BaseURL() - connectionStart := time.Now() - dialer, err := wsnet.DialWebsocket(ctx, wsnet.ConnectEndpoint(&url, workspace.ID, client.Token()), &wsnet.DialOptions{ - ICEServers: servers, - TURNProxyAuthToken: client.Token(), - TURNRemoteProxyURL: &url, - TURNLocalProxyURL: &url, - }, &websocket.DialOptions{}) - if err != nil { - return err - } - connectionMS := float64(time.Since(connectionStart).Microseconds()) / 1000 - candidates, err := dialer.Candidates() - if err != nil { - return err - } - relay := candidates.Local.Typ == webrtc.ICECandidateTypeRelay - tunneled := false - properties := []string{} - candidateURLs := []string{} - - for _, server := range servers { - if server.Username == wsnet.TURNProxyICECandidate().Username { - candidateURLs = append(candidateURLs, fmt.Sprintf("turns:%s", url.Host)) - if !relay { - continue - } - tunneled = true - continue - } - - candidateURLs = append(candidateURLs, server.URLs...) - } - properties = append(properties, fmt.Sprintf("candidates=%s", strings.Join(candidateURLs, ","))) - connectionText := "direct via STUN" - if relay { - connectionText = "proxied via TURN" - } - if tunneled { - connectionText = fmt.Sprintf("proxied via %s", url.Host) + pinger := &wsPinger{ + client: client, + workspace: workspace, } - fmt.Printf("%s %s %s (%s) %s\n", - color.New(color.Bold, color.FgWhite).Sprint("PING"), - workspace.Name, - color.New(color.Bold, color.FgGreen).Sprintf("connected in %.2fms", connectionMS), - connectionText, - strings.Join(properties, " "), - ) - ticker := time.NewTicker(time.Second) - seq := 1 for { select { case <-ticker.C: - pingMS, connectionText, err := ping(ctx, client, dialer, tunneled, args[0]) + err := pinger.ping(ctx) if err != nil { return err } - - fmt.Printf("%.2fms (%s) seq=%d\n", - pingMS, - connectionText, - seq) - seq++ case <-ctx.Done(): return nil } @@ -232,33 +169,111 @@ func pingWorkspaceCommand() *cobra.Command { return cmd } -func ping(ctx context.Context, client coder.Client, dialer *wsnet.Dialer, tunneled bool, workspaceName string) (float64, string, error) { - start := time.Now() - err := dialer.Ping(ctx) +type wsPinger struct { + client coder.Client + workspace *coder.Workspace + dialer *wsnet.Dialer + tunneled bool +} + +// Only return fatal errors +func (w *wsPinger) ping(ctx context.Context) error { + ctx, cancelFunc := context.WithTimeout(ctx, time.Second*15) + defer cancelFunc() + url := w.client.BaseURL() + logFail := func(msg string) { + fmt.Printf("%s: %s\n", color.New(color.Bold, color.FgRed).Sprint("——"), msg) + } + logSuccess := func(timeStr, msg string) { + fmt.Printf("%s: %s\n", color.New(color.Bold, color.FgGreen).Sprint(timeStr), msg) + } + + // If the dialer is nil we create a new! + if w.dialer == nil { + servers, err := w.client.ICEServers(ctx) + if err != nil { + logFail(fmt.Sprintf("list ice servers: %s", err.Error())) + return nil + } + workspace, err := w.client.WorkspaceByID(ctx, w.workspace.ID) + if err != nil { + return err + } + if workspace.LatestStat.ContainerStatus != coder.WorkspaceOn { + logFail(fmt.Sprintf("workspace is unreachable (status=%s)", workspace.LatestStat.ContainerStatus)) + return nil + } + connectStart := time.Now() + w.dialer, err = wsnet.DialWebsocket(ctx, wsnet.ConnectEndpoint(&url, w.workspace.ID, w.client.Token()), &wsnet.DialOptions{ + ICEServers: servers, + TURNProxyAuthToken: w.client.Token(), + TURNRemoteProxyURL: &url, + TURNLocalProxyURL: &url, + }, &websocket.DialOptions{}) + if err != nil { + logFail(fmt.Sprintf("dial workspace: %s", err.Error())) + return nil + } + connectMS := float64(time.Since(connectStart).Microseconds()) / 1000 + + candidates, err := w.dialer.Candidates() + if err != nil { + return err + } + isRelaying := candidates.Local.Typ == webrtc.ICECandidateTypeRelay + w.tunneled = false + candidateURLs := []string{} + + for _, server := range servers { + if server.Username == wsnet.TURNProxyICECandidate().Username { + candidateURLs = append(candidateURLs, fmt.Sprintf("turns:%s", url.Host)) + if !isRelaying { + continue + } + w.tunneled = true + continue + } + + candidateURLs = append(candidateURLs, server.URLs...) + } + + connectionText := "direct via STUN" + if isRelaying { + connectionText = "proxied via TURN" + } + if w.tunneled { + connectionText = fmt.Sprintf("proxied via %s", url.Host) + } + logSuccess("——", fmt.Sprintf( + "connected in %.2fms (%s) candidates=%s", + connectMS, + connectionText, + strings.Join(candidateURLs, ","), + )) + } + + pingStart := time.Now() + err := w.dialer.Ping(ctx) if err != nil { if errors.Is(err, io.EOF) { - workspace, err := findWorkspace(ctx, client, workspaceName, coder.Me) - if err != nil { - return -1, "", err - } - if workspace.LatestStat.ContainerStatus != coder.WorkspaceOn { - return -1, "", clog.Error("workspace changed state", - fmt.Sprintf("current status: \"%s\"", workspace.LatestStat.ContainerStatus), - clog.BlankLine, - clog.Tipf("use \"coder workspaces rebuild %s\" to rebuild this workspace", workspace.Name), - ) - } - return -1, "", errors.New("connection was closed unexpectedly") + w.dialer = nil + logFail("connection timed out") + return nil + } + if errors.Is(err, webrtc.ErrConnectionClosed) { + w.dialer = nil + logFail("webrtc connection is closed") + return nil } - return -1, "", err + return fmt.Errorf("ping workspace: %w", err) } - pingMS := float64(time.Since(start).Microseconds()) / 1000 - url := client.BaseURL() + pingMS := float64(time.Since(pingStart).Microseconds()) / 1000 connectionText := "you ↔ workspace" - if tunneled { + if w.tunneled { connectionText = fmt.Sprintf("you ↔ %s ↔ workspace", url.Host) } - return pingMS, connectionText, nil + logSuccess(fmt.Sprintf("%.2fms", pingMS), connectionText) + return nil } func stopWorkspacesCmd() *cobra.Command { From 75894f9c5332811e3ebe220312216a4c48f09ea6 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Mon, 9 Aug 2021 06:00:30 +0000 Subject: [PATCH 08/15] Enable scheme filtering --- internal/cmd/workspaces.go | 58 ++++++++++++++++++++++++++++++++------ 1 file changed, 49 insertions(+), 9 deletions(-) diff --git a/internal/cmd/workspaces.go b/internal/cmd/workspaces.go index a50a8bff..b9c8c65d 100644 --- a/internal/cmd/workspaces.go +++ b/internal/cmd/workspaces.go @@ -21,6 +21,7 @@ import ( "github.com/fatih/color" "github.com/manifoldco/promptui" + "github.com/pion/ice/v2" "github.com/pion/webrtc/v3" "github.com/spf13/cobra" "golang.org/x/xerrors" @@ -129,6 +130,10 @@ func lsWorkspacesCommand() *cobra.Command { } func pingWorkspaceCommand() *cobra.Command { + var ( + schemes []string + ) + cmd := &cobra.Command{ Use: "ping [workspace_name]", Short: "ping Coder workspaces by name", @@ -146,9 +151,19 @@ func pingWorkspaceCommand() *cobra.Command { return err } + iceSchemes := map[ice.SchemeType]interface{}{} + for _, rawScheme := range schemes { + scheme := ice.NewSchemeType(rawScheme) + if scheme == ice.Unknown { + return fmt.Errorf("scheme type %q not recognized", rawScheme) + } + iceSchemes[scheme] = nil + } + pinger := &wsPinger{ - client: client, - workspace: workspace, + client: client, + workspace: workspace, + iceSchemes: iceSchemes, } ticker := time.NewTicker(time.Second) @@ -166,14 +181,16 @@ func pingWorkspaceCommand() *cobra.Command { }, } + cmd.Flags().StringSliceVarP(&schemes, "scheme", "s", []string{"stun", "stuns", "turn", "turns"}, "customize schemes to filter ice servers") return cmd } type wsPinger struct { - client coder.Client - workspace *coder.Workspace - dialer *wsnet.Dialer - tunneled bool + client coder.Client + workspace *coder.Workspace + dialer *wsnet.Dialer + iceSchemes map[ice.SchemeType]interface{} + tunneled bool } // Only return fatal errors @@ -195,6 +212,29 @@ func (w *wsPinger) ping(ctx context.Context) error { logFail(fmt.Sprintf("list ice servers: %s", err.Error())) return nil } + filteredServers := make([]webrtc.ICEServer, 0) + for _, server := range servers { + good := true + for _, rawUrl := range server.URLs { + url, err := ice.ParseURL(rawUrl) + if err != nil { + return fmt.Errorf("parse url %q: %w", rawUrl, err) + } + if _, ok := w.iceSchemes[url.Scheme]; !ok { + good = false + } + } + if good { + filteredServers = append(filteredServers, server) + } + } + if len(filteredServers) == 0 { + schemes := make([]string, 0) + for scheme := range w.iceSchemes { + schemes = append(schemes, scheme.String()) + } + return fmt.Errorf("no ice servers match the schemes provided: %s", strings.Join(schemes, ",")) + } workspace, err := w.client.WorkspaceByID(ctx, w.workspace.ID) if err != nil { return err @@ -205,7 +245,7 @@ func (w *wsPinger) ping(ctx context.Context) error { } connectStart := time.Now() w.dialer, err = wsnet.DialWebsocket(ctx, wsnet.ConnectEndpoint(&url, w.workspace.ID, w.client.Token()), &wsnet.DialOptions{ - ICEServers: servers, + ICEServers: filteredServers, TURNProxyAuthToken: w.client.Token(), TURNRemoteProxyURL: &url, TURNLocalProxyURL: &url, @@ -224,9 +264,9 @@ func (w *wsPinger) ping(ctx context.Context) error { w.tunneled = false candidateURLs := []string{} - for _, server := range servers { + for _, server := range filteredServers { if server.Username == wsnet.TURNProxyICECandidate().Username { - candidateURLs = append(candidateURLs, fmt.Sprintf("turns:%s", url.Host)) + candidateURLs = append(candidateURLs, fmt.Sprintf("turn:%s", url.Host)) if !isRelaying { continue } From 2294b0ffe53d404dbf6b94d979fc169f808e43f6 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Mon, 9 Aug 2021 06:04:09 +0000 Subject: [PATCH 09/15] Add count flag --- internal/cmd/workspaces.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/internal/cmd/workspaces.go b/internal/cmd/workspaces.go index b9c8c65d..c348d8ef 100644 --- a/internal/cmd/workspaces.go +++ b/internal/cmd/workspaces.go @@ -8,6 +8,7 @@ import ( "fmt" "io" "io/ioutil" + "os" "strings" "time" @@ -132,6 +133,7 @@ func lsWorkspacesCommand() *cobra.Command { func pingWorkspaceCommand() *cobra.Command { var ( schemes []string + count int ) cmd := &cobra.Command{ @@ -166,6 +168,7 @@ func pingWorkspaceCommand() *cobra.Command { iceSchemes: iceSchemes, } + seq := 0 ticker := time.NewTicker(time.Second) for { select { @@ -174,6 +177,10 @@ func pingWorkspaceCommand() *cobra.Command { if err != nil { return err } + seq++ + if count != 0 && seq >= count { + os.Exit(0) + } case <-ctx.Done(): return nil } @@ -182,6 +189,7 @@ func pingWorkspaceCommand() *cobra.Command { } cmd.Flags().StringSliceVarP(&schemes, "scheme", "s", []string{"stun", "stuns", "turn", "turns"}, "customize schemes to filter ice servers") + cmd.Flags().IntVarP(&count, "count", "c", 0, "stop after replies") return cmd } From 1c6d9d6bc41651530ef191cb913f40a7a98d947b Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Mon, 9 Aug 2021 06:06:40 +0000 Subject: [PATCH 10/15] Fix import order --- internal/cmd/workspaces.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/internal/cmd/workspaces.go b/internal/cmd/workspaces.go index c348d8ef..6b702de8 100644 --- a/internal/cmd/workspaces.go +++ b/internal/cmd/workspaces.go @@ -12,13 +12,14 @@ import ( "strings" "time" + "nhooyr.io/websocket" + "cdr.dev/coder-cli/coder-sdk" "cdr.dev/coder-cli/internal/coderutil" "cdr.dev/coder-cli/internal/x/xcobra" "cdr.dev/coder-cli/pkg/clog" "cdr.dev/coder-cli/pkg/tablewriter" "cdr.dev/coder-cli/wsnet" - "nhooyr.io/websocket" "github.com/fatih/color" "github.com/manifoldco/promptui" @@ -223,10 +224,10 @@ func (w *wsPinger) ping(ctx context.Context) error { filteredServers := make([]webrtc.ICEServer, 0) for _, server := range servers { good := true - for _, rawUrl := range server.URLs { - url, err := ice.ParseURL(rawUrl) + for _, rawURL := range server.URLs { + url, err := ice.ParseURL(rawURL) if err != nil { - return fmt.Errorf("parse url %q: %w", rawUrl, err) + return fmt.Errorf("parse url %q: %w", rawURL, err) } if _, ok := w.iceSchemes[url.Scheme]; !ok { good = false From b8010ed83bf48c52bf5f3ce14ff63d47c2882f6d Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Mon, 9 Aug 2021 06:08:18 +0000 Subject: [PATCH 11/15] Disable linting for nested if --- internal/cmd/workspaces.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/cmd/workspaces.go b/internal/cmd/workspaces.go index 6b702de8..5294f5b5 100644 --- a/internal/cmd/workspaces.go +++ b/internal/cmd/workspaces.go @@ -215,6 +215,7 @@ func (w *wsPinger) ping(ctx context.Context) error { } // If the dialer is nil we create a new! + // nolint:nestif if w.dialer == nil { servers, err := w.client.ICEServers(ctx) if err != nil { From bc1096e87fbb36f2e3a460595794a7e2723441f7 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Mon, 9 Aug 2021 06:08:39 +0000 Subject: [PATCH 12/15] Generate docs --- docs/coder.md | 1 - docs/coder_ping.md | 30 ----------------------------- docs/coder_workspaces.md | 1 + docs/coder_workspaces_ping.md | 36 +++++++++++++++++++++++++++++++++++ 4 files changed, 37 insertions(+), 31 deletions(-) delete mode 100644 docs/coder_ping.md create mode 100644 docs/coder_workspaces_ping.md diff --git a/docs/coder.md b/docs/coder.md index d157a9f2..17e7fa7f 100644 --- a/docs/coder.md +++ b/docs/coder.md @@ -16,7 +16,6 @@ coder provides a CLI for working with an existing Coder installation * [coder images](coder_images.md) - Manage Coder images * [coder login](coder_login.md) - Authenticate this client for future operations * [coder logout](coder_logout.md) - Remove local authentication credentials if any exist -* [coder ping](coder_ping.md) - Ping a Coder workspace * [coder satellites](coder_satellites.md) - Interact with Coder satellite deployments * [coder ssh](coder_ssh.md) - Enter a shell of execute a command over SSH into a Coder workspace * [coder sync](coder_sync.md) - Establish a one way directory sync to a Coder workspace diff --git a/docs/coder_ping.md b/docs/coder_ping.md deleted file mode 100644 index b454db63..00000000 --- a/docs/coder_ping.md +++ /dev/null @@ -1,30 +0,0 @@ -## coder ping - -Ping a Coder workspace - -``` -coder ping [workspace_name] [flags] -``` - -### Examples - -``` -coder ping my-dev -``` - -### Options - -``` - -h, --help help for ping -``` - -### Options inherited from parent commands - -``` - -v, --verbose show verbose output -``` - -### SEE ALSO - -* [coder](coder.md) - coder provides a CLI for working with an existing Coder installation - diff --git a/docs/coder_workspaces.md b/docs/coder_workspaces.md index a7ec4615..936db713 100644 --- a/docs/coder_workspaces.md +++ b/docs/coder_workspaces.md @@ -26,6 +26,7 @@ Perform operations on the Coder workspaces owned by the active user. * [coder workspaces edit](coder_workspaces_edit.md) - edit an existing workspace and initiate a rebuild. * [coder workspaces edit-from-config](coder_workspaces_edit-from-config.md) - change the template a workspace is tracking * [coder workspaces ls](coder_workspaces_ls.md) - list all workspaces owned by the active user +* [coder workspaces ping](coder_workspaces_ping.md) - ping Coder workspaces by name * [coder workspaces policy-template](coder_workspaces_policy-template.md) - Set workspace policy template * [coder workspaces rebuild](coder_workspaces_rebuild.md) - rebuild a Coder workspace * [coder workspaces rm](coder_workspaces_rm.md) - remove Coder workspaces by name diff --git a/docs/coder_workspaces_ping.md b/docs/coder_workspaces_ping.md new file mode 100644 index 00000000..1dbcce4d --- /dev/null +++ b/docs/coder_workspaces_ping.md @@ -0,0 +1,36 @@ +## coder workspaces ping + +ping Coder workspaces by name + +### Synopsis + +ping Coder workspaces by name + +``` +coder workspaces ping [workspace_name] [flags] +``` + +### Examples + +``` +coder workspaces ping front-end-workspace +``` + +### Options + +``` + -c, --count int stop after replies + -h, --help help for ping + -s, --scheme strings customize schemes to filter ice servers (default [stun,stuns,turn,turns]) +``` + +### Options inherited from parent commands + +``` + -v, --verbose show verbose output +``` + +### SEE ALSO + +* [coder workspaces](coder_workspaces.md) - Interact with Coder workspaces + From 8eb3b84ec22d417d46253afc678168044b02d571 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Mon, 9 Aug 2021 17:18:17 +0000 Subject: [PATCH 13/15] Extract funcs --- internal/cmd/workspaces.go | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/internal/cmd/workspaces.go b/internal/cmd/workspaces.go index 5294f5b5..6cf64922 100644 --- a/internal/cmd/workspaces.go +++ b/internal/cmd/workspaces.go @@ -138,7 +138,7 @@ func pingWorkspaceCommand() *cobra.Command { ) cmd := &cobra.Command{ - Use: "ping [workspace_name]", + Use: "ping ", Short: "ping Coder workspaces by name", Long: "ping Coder workspaces by name", Example: `coder workspaces ping front-end-workspace`, @@ -179,7 +179,7 @@ func pingWorkspaceCommand() *cobra.Command { return err } seq++ - if count != 0 && seq >= count { + if count > 0 && seq >= count { os.Exit(0) } case <-ctx.Done(): @@ -202,27 +202,29 @@ type wsPinger struct { tunneled bool } +func (w *wsPinger) logFail(msg string) { + fmt.Printf("%s: %s\n", color.New(color.Bold, color.FgRed).Sprint("——"), msg) +} + +func (w *wsPinger) logSuccess(timeStr, msg string) { + fmt.Printf("%s: %s\n", color.New(color.Bold, color.FgGreen).Sprint(timeStr), msg) +} + // Only return fatal errors func (w *wsPinger) ping(ctx context.Context) error { ctx, cancelFunc := context.WithTimeout(ctx, time.Second*15) defer cancelFunc() url := w.client.BaseURL() - logFail := func(msg string) { - fmt.Printf("%s: %s\n", color.New(color.Bold, color.FgRed).Sprint("——"), msg) - } - logSuccess := func(timeStr, msg string) { - fmt.Printf("%s: %s\n", color.New(color.Bold, color.FgGreen).Sprint(timeStr), msg) - } // If the dialer is nil we create a new! // nolint:nestif if w.dialer == nil { servers, err := w.client.ICEServers(ctx) if err != nil { - logFail(fmt.Sprintf("list ice servers: %s", err.Error())) + w.logFail(fmt.Sprintf("list ice servers: %s", err.Error())) return nil } - filteredServers := make([]webrtc.ICEServer, 0) + filteredServers := make([]webrtc.ICEServer, 0, len(servers)) for _, server := range servers { good := true for _, rawURL := range server.URLs { @@ -250,7 +252,7 @@ func (w *wsPinger) ping(ctx context.Context) error { return err } if workspace.LatestStat.ContainerStatus != coder.WorkspaceOn { - logFail(fmt.Sprintf("workspace is unreachable (status=%s)", workspace.LatestStat.ContainerStatus)) + w.logFail(fmt.Sprintf("workspace is unreachable (status=%s)", workspace.LatestStat.ContainerStatus)) return nil } connectStart := time.Now() @@ -261,7 +263,7 @@ func (w *wsPinger) ping(ctx context.Context) error { TURNLocalProxyURL: &url, }, &websocket.DialOptions{}) if err != nil { - logFail(fmt.Sprintf("dial workspace: %s", err.Error())) + w.logFail(fmt.Sprintf("dial workspace: %s", err.Error())) return nil } connectMS := float64(time.Since(connectStart).Microseconds()) / 1000 @@ -294,7 +296,7 @@ func (w *wsPinger) ping(ctx context.Context) error { if w.tunneled { connectionText = fmt.Sprintf("proxied via %s", url.Host) } - logSuccess("——", fmt.Sprintf( + w.logSuccess("——", fmt.Sprintf( "connected in %.2fms (%s) candidates=%s", connectMS, connectionText, @@ -307,12 +309,12 @@ func (w *wsPinger) ping(ctx context.Context) error { if err != nil { if errors.Is(err, io.EOF) { w.dialer = nil - logFail("connection timed out") + w.logFail("connection timed out") return nil } if errors.Is(err, webrtc.ErrConnectionClosed) { w.dialer = nil - logFail("webrtc connection is closed") + w.logFail("webrtc connection is closed") return nil } return fmt.Errorf("ping workspace: %w", err) @@ -322,7 +324,7 @@ func (w *wsPinger) ping(ctx context.Context) error { if w.tunneled { connectionText = fmt.Sprintf("you ↔ %s ↔ workspace", url.Host) } - logSuccess(fmt.Sprintf("%.2fms", pingMS), connectionText) + w.logSuccess(fmt.Sprintf("%.2fms", pingMS), connectionText) return nil } From fcf1dc0100125545973378645a3cabcc31cc279a Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Mon, 9 Aug 2021 17:22:41 +0000 Subject: [PATCH 14/15] Update docs --- docs/coder_workspaces_ping.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/coder_workspaces_ping.md b/docs/coder_workspaces_ping.md index 1dbcce4d..cf55fff1 100644 --- a/docs/coder_workspaces_ping.md +++ b/docs/coder_workspaces_ping.md @@ -7,7 +7,7 @@ ping Coder workspaces by name ping Coder workspaces by name ``` -coder workspaces ping [workspace_name] [flags] +coder workspaces ping [flags] ``` ### Examples From b1a808352856a2b5a459dfc00d943336782688f3 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Mon, 9 Aug 2021 18:23:08 +0000 Subject: [PATCH 15/15] Remove receiver --- internal/cmd/workspaces.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/cmd/workspaces.go b/internal/cmd/workspaces.go index 6cf64922..ff70b64d 100644 --- a/internal/cmd/workspaces.go +++ b/internal/cmd/workspaces.go @@ -202,11 +202,11 @@ type wsPinger struct { tunneled bool } -func (w *wsPinger) logFail(msg string) { +func (*wsPinger) logFail(msg string) { fmt.Printf("%s: %s\n", color.New(color.Bold, color.FgRed).Sprint("——"), msg) } -func (w *wsPinger) logSuccess(timeStr, msg string) { +func (*wsPinger) logSuccess(timeStr, msg string) { fmt.Printf("%s: %s\n", color.New(color.Bold, color.FgGreen).Sprint(timeStr), msg) }