mirror of
https://github.com/coder/coder.git
synced 2026-06-03 04:58:23 +00:00
bddb808b25
Fixes all our Go file imports to match the preferred spec that we've _mostly_ been using. For example: ``` import ( "context" "time" "github.com/prometheus/client_golang/prometheus" "golang.org/x/xerrors" "gopkg.in/natefinch/lumberjack.v2" "cdr.dev/slog/v3" "github.com/coder/coder/v2/codersdk/agentsdk" "github.com/coder/serpent" ) ``` 3 groups: standard library, 3rd partly libs, Coder libs. This PR makes the change across the codebase. The PR in the stack above modifies our formatting to maintain this state of affairs, and is a separate PR so it's possible to review that one in detail.
373 lines
9.9 KiB
Go
373 lines
9.9 KiB
Go
package cli
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/netip"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/briandowns/spinner"
|
|
"golang.org/x/xerrors"
|
|
"tailscale.com/ipn/ipnstate"
|
|
"tailscale.com/tailcfg"
|
|
|
|
"cdr.dev/slog/v3"
|
|
"cdr.dev/slog/v3/sloggers/sloghuman"
|
|
"github.com/coder/coder/v2/cli/cliui"
|
|
"github.com/coder/coder/v2/cli/cliutil"
|
|
"github.com/coder/coder/v2/coderd/util/ptr"
|
|
"github.com/coder/coder/v2/codersdk"
|
|
"github.com/coder/coder/v2/codersdk/healthsdk"
|
|
"github.com/coder/coder/v2/codersdk/workspacesdk"
|
|
"github.com/coder/pretty"
|
|
"github.com/coder/serpent"
|
|
)
|
|
|
|
type pingSummary struct {
|
|
Workspace string `table:"workspace,nosort"`
|
|
Total int `table:"total"`
|
|
Successful int `table:"successful"`
|
|
Min *time.Duration `table:"min"`
|
|
Avg *time.Duration `table:"avg"`
|
|
Max *time.Duration `table:"max"`
|
|
Variance *time.Duration `table:"variance"`
|
|
latencySum float64
|
|
runningAvg float64
|
|
m2 float64
|
|
}
|
|
|
|
func (s *pingSummary) addResult(r *ipnstate.PingResult) {
|
|
s.Total++
|
|
if r == nil || r.Err != "" {
|
|
return
|
|
}
|
|
s.Successful++
|
|
if s.Min == nil || r.LatencySeconds < s.Min.Seconds() {
|
|
s.Min = ptr.Ref(time.Duration(r.LatencySeconds * float64(time.Second)))
|
|
}
|
|
if s.Max == nil || r.LatencySeconds > s.Max.Seconds() {
|
|
s.Max = ptr.Ref(time.Duration(r.LatencySeconds * float64(time.Second)))
|
|
}
|
|
s.latencySum += r.LatencySeconds
|
|
|
|
d := r.LatencySeconds - s.runningAvg
|
|
s.runningAvg += d / float64(s.Successful)
|
|
d2 := r.LatencySeconds - s.runningAvg
|
|
s.m2 += d * d2
|
|
}
|
|
|
|
// Write finalizes the summary and writes it
|
|
func (s *pingSummary) Write(w io.Writer) {
|
|
if s.Successful > 0 {
|
|
s.Avg = ptr.Ref(time.Duration(s.latencySum / float64(s.Successful) * float64(time.Second)))
|
|
}
|
|
if s.Successful > 1 {
|
|
s.Variance = ptr.Ref(time.Duration((s.m2 / float64(s.Successful-1)) * float64(time.Second)))
|
|
}
|
|
out, err := cliui.DisplayTable([]*pingSummary{s}, "", nil)
|
|
if err != nil {
|
|
_, _ = fmt.Fprintf(w, "Failed to display ping summary: %v\n", err)
|
|
return
|
|
}
|
|
width := len(strings.Split(out, "\n")[0])
|
|
_, _ = fmt.Println(strings.Repeat("-", width))
|
|
_, _ = fmt.Fprint(w, out)
|
|
}
|
|
|
|
func (r *RootCmd) ping() *serpent.Command {
|
|
var (
|
|
pingNum int64
|
|
pingTimeout time.Duration
|
|
pingWait time.Duration
|
|
pingTimeLocal bool
|
|
pingTimeUTC bool
|
|
)
|
|
|
|
cmd := &serpent.Command{
|
|
Annotations: workspaceCommand,
|
|
Use: "ping <workspace>",
|
|
Short: "Ping a workspace",
|
|
Middleware: serpent.Chain(
|
|
serpent.RequireNArgs(1),
|
|
),
|
|
Handler: func(inv *serpent.Invocation) error {
|
|
client, err := r.InitClient(inv)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
ctx, cancel := context.WithCancel(inv.Context())
|
|
defer cancel()
|
|
appearanceConfig := initAppearance(ctx, client)
|
|
notifyCtx, notifyCancel := inv.SignalNotifyContext(ctx, StopSignals...)
|
|
defer notifyCancel()
|
|
|
|
workspaceName := inv.Args[0]
|
|
_, workspaceAgent, _, err := GetWorkspaceAndAgent(
|
|
ctx, inv, client,
|
|
false, // Do not autostart for a ping.
|
|
workspaceName,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Start spinner after any build logs have finished streaming
|
|
spin := spinner.New(spinner.CharSets[5], 100*time.Millisecond)
|
|
spin.Writer = inv.Stderr
|
|
spin.Suffix = pretty.Sprint(cliui.DefaultStyles.Keyword, " Collecting diagnostics...")
|
|
if !r.verbose {
|
|
spin.Start()
|
|
}
|
|
|
|
opts := &workspacesdk.DialAgentOptions{}
|
|
|
|
if r.verbose {
|
|
opts.Logger = inv.Logger.AppendSinks(sloghuman.Sink(inv.Stdout)).Leveled(slog.LevelDebug)
|
|
}
|
|
|
|
if r.disableDirect {
|
|
opts.BlockEndpoints = true
|
|
}
|
|
if !r.disableNetworkTelemetry {
|
|
opts.EnableTelemetry = true
|
|
}
|
|
wsClient := workspacesdk.New(client)
|
|
conn, err := wsClient.DialAgent(ctx, workspaceAgent.ID, opts)
|
|
if err != nil {
|
|
spin.Stop()
|
|
return err
|
|
}
|
|
defer conn.Close()
|
|
|
|
derpMap := conn.TailnetConn().DERPMap()
|
|
|
|
diagCtx, diagCancel := context.WithTimeout(inv.Context(), 30*time.Second)
|
|
defer diagCancel()
|
|
diags := conn.GetPeerDiagnostics()
|
|
|
|
// Silent ping to determine whether we should show diags
|
|
_, didP2p, _, _ := conn.Ping(ctx)
|
|
|
|
ni := conn.TailnetConn().GetNetInfo()
|
|
connDiags := cliui.ConnDiags{
|
|
DisableDirect: r.disableDirect,
|
|
LocalNetInfo: ni,
|
|
Verbose: r.verbose,
|
|
PingP2P: didP2p,
|
|
TroubleshootingURL: appearanceConfig.DocsURL + "/admin/networking/troubleshooting",
|
|
}
|
|
|
|
awsRanges, err := cliutil.FetchAWSIPRanges(diagCtx, cliutil.AWSIPRangesURL)
|
|
if err != nil {
|
|
opts.Logger.Debug(inv.Context(), "failed to retrieve AWS IP ranges", slog.Error(err))
|
|
}
|
|
|
|
connDiags.ClientIPIsAWS = isAWSIP(awsRanges, ni)
|
|
|
|
connInfo, err := wsClient.AgentConnectionInfoGeneric(diagCtx)
|
|
if err != nil || connInfo.DERPMap == nil {
|
|
spin.Stop()
|
|
return xerrors.Errorf("Failed to retrieve connection info from server: %w\n", err)
|
|
}
|
|
connDiags.ConnInfo = connInfo
|
|
ifReport, err := healthsdk.RunInterfacesReport()
|
|
if err == nil {
|
|
connDiags.LocalInterfaces = &ifReport
|
|
} else {
|
|
_, _ = fmt.Fprintf(inv.Stdout, "Failed to retrieve local interfaces report: %v\n", err)
|
|
}
|
|
|
|
agentNetcheck, err := conn.Netcheck(diagCtx)
|
|
if err == nil {
|
|
connDiags.AgentNetcheck = &agentNetcheck
|
|
connDiags.AgentIPIsAWS = isAWSIP(awsRanges, agentNetcheck.NetInfo)
|
|
} else {
|
|
var sdkErr *codersdk.Error
|
|
if errors.As(err, &sdkErr) && sdkErr.StatusCode() == http.StatusNotFound {
|
|
_, _ = fmt.Fprint(inv.Stdout, "Could not generate full connection report as the workspace agent is outdated\n")
|
|
} else {
|
|
_, _ = fmt.Fprintf(inv.Stdout, "Failed to retrieve connection report from agent: %v\n", err)
|
|
}
|
|
}
|
|
|
|
spin.Stop()
|
|
cliui.PeerDiagnostics(inv.Stderr, diags)
|
|
connDiags.Write(inv.Stderr)
|
|
results := &pingSummary{
|
|
Workspace: workspaceName,
|
|
}
|
|
var (
|
|
pong *ipnstate.PingResult
|
|
dur time.Duration
|
|
p2p bool
|
|
)
|
|
n := 0
|
|
start := time.Now()
|
|
pingLoop:
|
|
for {
|
|
if n > 0 {
|
|
time.Sleep(pingWait)
|
|
}
|
|
n++
|
|
|
|
ctx, cancel := context.WithTimeout(ctx, pingTimeout)
|
|
dur, p2p, pong, err = conn.Ping(ctx)
|
|
pongTime := time.Now()
|
|
if pingTimeUTC {
|
|
pongTime = pongTime.UTC()
|
|
}
|
|
cancel()
|
|
results.addResult(pong)
|
|
if err != nil {
|
|
if xerrors.Is(err, context.DeadlineExceeded) {
|
|
_, _ = fmt.Fprintf(inv.Stdout, "ping to %q timed out \n", workspaceName)
|
|
if n == int(pingNum) {
|
|
return nil
|
|
}
|
|
continue
|
|
}
|
|
if xerrors.Is(err, context.Canceled) {
|
|
return nil
|
|
}
|
|
|
|
if err.Error() == "no matching peer" {
|
|
continue
|
|
}
|
|
|
|
_, _ = fmt.Fprintf(inv.Stdout, "ping to %q failed %s\n", workspaceName, err.Error())
|
|
if n == int(pingNum) {
|
|
return nil
|
|
}
|
|
continue
|
|
}
|
|
|
|
dur = dur.Round(time.Millisecond)
|
|
var via string
|
|
if p2p {
|
|
if !didP2p {
|
|
_, _ = fmt.Fprintln(inv.Stdout, "p2p connection established in",
|
|
pretty.Sprint(cliui.DefaultStyles.DateTimeStamp, time.Since(start).Round(time.Millisecond).String()),
|
|
)
|
|
}
|
|
didP2p = true
|
|
|
|
via = fmt.Sprintf("%s via %s",
|
|
pretty.Sprint(cliui.DefaultStyles.Fuchsia, "p2p"),
|
|
pretty.Sprint(cliui.DefaultStyles.Code, pong.Endpoint),
|
|
)
|
|
} else {
|
|
derpName := "unknown"
|
|
derpRegion, ok := derpMap.Regions[pong.DERPRegionID]
|
|
if ok {
|
|
derpName = derpRegion.RegionName
|
|
}
|
|
via = fmt.Sprintf("%s via %s",
|
|
pretty.Sprint(cliui.DefaultStyles.Fuchsia, "proxied"),
|
|
pretty.Sprint(cliui.DefaultStyles.Code, fmt.Sprintf("DERP(%s)", derpName)),
|
|
)
|
|
}
|
|
|
|
var displayTime string
|
|
if pingTimeLocal || pingTimeUTC {
|
|
displayTime = pretty.Sprintf(cliui.DefaultStyles.DateTimeStamp, "[%s] ", pongTime.Format(time.RFC3339))
|
|
}
|
|
|
|
_, _ = fmt.Fprintf(inv.Stdout, "%spong from %s %s in %s\n",
|
|
displayTime,
|
|
pretty.Sprint(cliui.DefaultStyles.Keyword, workspaceName),
|
|
via,
|
|
pretty.Sprint(cliui.DefaultStyles.DateTimeStamp, dur.String()),
|
|
)
|
|
|
|
select {
|
|
case <-notifyCtx.Done():
|
|
break pingLoop
|
|
default:
|
|
if n == int(pingNum) {
|
|
break pingLoop
|
|
}
|
|
}
|
|
}
|
|
|
|
if p2p {
|
|
msg := "✔ You are connected directly (p2p)"
|
|
if pong != nil && isPrivateEndpoint(pong.Endpoint) {
|
|
msg += ", over a private network"
|
|
}
|
|
_, _ = fmt.Fprintln(inv.Stderr, msg)
|
|
} else {
|
|
_, _ = fmt.Fprintf(inv.Stderr, "❗ You are connected via a DERP relay, not directly (p2p)\n"+
|
|
" %s#common-problems-with-direct-connections\n", connDiags.TroubleshootingURL)
|
|
}
|
|
|
|
results.Write(inv.Stdout)
|
|
|
|
return nil
|
|
},
|
|
}
|
|
|
|
cmd.Options = serpent.OptionSet{
|
|
{
|
|
Flag: "wait",
|
|
Description: "Specifies how long to wait between pings.",
|
|
Default: "1s",
|
|
Value: serpent.DurationOf(&pingWait),
|
|
},
|
|
{
|
|
Flag: "timeout",
|
|
FlagShorthand: "t",
|
|
Default: "5s",
|
|
Description: "Specifies how long to wait for a ping to complete.",
|
|
Value: serpent.DurationOf(&pingTimeout),
|
|
},
|
|
{
|
|
Flag: "num",
|
|
FlagShorthand: "n",
|
|
Description: "Specifies the number of pings to perform. By default, pings will continue until interrupted.",
|
|
Value: serpent.Int64Of(&pingNum),
|
|
},
|
|
{
|
|
Flag: "time",
|
|
Description: "Show the response time of each pong in local time.",
|
|
Value: serpent.BoolOf(&pingTimeLocal),
|
|
},
|
|
{
|
|
Flag: "utc",
|
|
Description: "Show the response time of each pong in UTC (implies --time).",
|
|
Value: serpent.BoolOf(&pingTimeUTC),
|
|
},
|
|
}
|
|
return cmd
|
|
}
|
|
|
|
func isAWSIP(awsRanges *cliutil.AWSIPRanges, ni *tailcfg.NetInfo) bool {
|
|
if awsRanges == nil {
|
|
return false
|
|
}
|
|
if ni.GlobalV4 != "" {
|
|
ip, err := netip.ParseAddr(ni.GlobalV4)
|
|
if err == nil && awsRanges.CheckIP(ip) {
|
|
return true
|
|
}
|
|
}
|
|
if ni.GlobalV6 != "" {
|
|
ip, err := netip.ParseAddr(ni.GlobalV6)
|
|
if err == nil && awsRanges.CheckIP(ip) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func isPrivateEndpoint(endpoint string) bool {
|
|
ip, err := netip.ParseAddrPort(endpoint)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
return ip.Addr().IsPrivate()
|
|
}
|