From 95a6797c306b32abd4824df2d006302c5dbdca3e Mon Sep 17 00:00:00 2001 From: Colin Adler Date: Wed, 21 Jun 2023 18:44:56 +0000 Subject: [PATCH 1/4] feat(cli): add hidden netcheck command Allows printing network debug info from the CLI, for examining the local network of a user. --- cli/netcheck.go | 59 +++++++++++++++++++++++++++++++++++++ cli/netcheck_test.go | 34 +++++++++++++++++++++ cli/root.go | 1 + coderd/coderd.go | 1 + codersdk/workspaceagents.go | 20 +++++++++++++ 5 files changed, 115 insertions(+) create mode 100644 cli/netcheck.go create mode 100644 cli/netcheck_test.go diff --git a/cli/netcheck.go b/cli/netcheck.go new file mode 100644 index 0000000000000..156b0b585dbf0 --- /dev/null +++ b/cli/netcheck.go @@ -0,0 +1,59 @@ +package cli + +import ( + "context" + "encoding/json" + "time" + + "golang.org/x/xerrors" + + "github.com/coder/coder/cli/clibase" + "github.com/coder/coder/coderd/healthcheck" + "github.com/coder/coder/codersdk" +) + +func (r *RootCmd) netcheck() *clibase.Cmd { + client := new(codersdk.Client) + + cmd := &clibase.Cmd{ + Annotations: workspaceCommand, + Use: "netcheck", + Short: "Print network debug information", + Hidden: true, + Middleware: clibase.Chain( + r.InitClient(client), + ), + Handler: func(inv *clibase.Invocation) error { + ctx, cancel := context.WithTimeout(inv.Context(), 10*time.Second) + defer cancel() + + connInfo, err := client.WorkspaceAgentConnectionInfo(ctx) + if err != nil { + return err + } + + var report healthcheck.DERPReport + report.Run(ctx, &healthcheck.DERPReportOptions{ + DERPMap: connInfo.DERPMap, + }) + + raw, err := json.MarshalIndent(report, "", " ") + if err != nil { + return err + } + + n, err := inv.Stdout.Write(raw) + if err != nil { + return err + } + if n != len(raw) { + return xerrors.Errorf("failed to write all bytes to stdout; wrote %d, len %d", n, len(raw)) + } + + return nil + }, + } + + cmd.Options = clibase.OptionSet{} + return cmd +} diff --git a/cli/netcheck_test.go b/cli/netcheck_test.go new file mode 100644 index 0000000000000..a9b346554facf --- /dev/null +++ b/cli/netcheck_test.go @@ -0,0 +1,34 @@ +package cli_test + +import ( + "bytes" + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/coder/coder/cli/clitest" + "github.com/coder/coder/coderd/healthcheck" + "github.com/coder/coder/pty/ptytest" +) + +func TestNetcheck(t *testing.T) { + t.Parallel() + + pty := ptytest.New(t) + config := login(t, pty) + + var out bytes.Buffer + inv, _ := clitest.New(t, "netcheck", "--global-config", string(config)) + inv.Stdout = &out + + clitest.StartWithWaiter(t, inv).RequireSuccess() + + var report healthcheck.DERPReport + require.NoError(t, json.Unmarshal(out.Bytes(), &report)) + + assert.True(t, report.Healthy) + require.Len(t, report.Regions, 1) + require.Len(t, report.Regions[1].NodeReports, 1) +} diff --git a/cli/root.go b/cli/root.go index b2c33d0f1b547..618f8827e1f7a 100644 --- a/cli/root.go +++ b/cli/root.go @@ -107,6 +107,7 @@ func (r *RootCmd) Core() []*clibase.Cmd { // Hidden r.gitssh(), + r.netcheck(), r.vscodeSSH(), r.workspaceAgent(), } diff --git a/coderd/coderd.go b/coderd/coderd.go index 4497e747d0c49..ba64275feccdf 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -672,6 +672,7 @@ func New(options *Options) *API { r.Post("/azure-instance-identity", api.postWorkspaceAuthAzureInstanceIdentity) r.Post("/aws-instance-identity", api.postWorkspaceAuthAWSInstanceIdentity) r.Post("/google-instance-identity", api.postWorkspaceAuthGoogleInstanceIdentity) + r.Get("/connection", api.workspaceAgentConnection) r.Route("/me", func(r chi.Router) { r.Use(httpmw.ExtractWorkspaceAgent(options.Database)) r.Get("/manifest", api.workspaceAgentManifest) diff --git a/codersdk/workspaceagents.go b/codersdk/workspaceagents.go index d9df5530b6762..24fac161da292 100644 --- a/codersdk/workspaceagents.go +++ b/codersdk/workspaceagents.go @@ -170,6 +170,26 @@ type WorkspaceAgentConnectionInfo struct { DERPMap *tailcfg.DERPMap `json:"derp_map"` } +func (c *Client) WorkspaceAgentConnectionInfo(ctx context.Context) (*WorkspaceAgentConnectionInfo, error) { + res, err := c.Request(ctx, http.MethodGet, "/api/v2/workspaceagents/connection", nil) + if err != nil { + return nil, err + } + defer res.Body.Close() + + if res.StatusCode != http.StatusOK { + return nil, ReadBodyAsError(res) + } + + var info WorkspaceAgentConnectionInfo + err = json.NewDecoder(res.Body).Decode(&info) + if err != nil { + return nil, xerrors.Errorf("decode connection info: %w", err) + } + + return &info, nil +} + // @typescript-ignore DialWorkspaceAgentOptions type DialWorkspaceAgentOptions struct { Logger slog.Logger From 08367db5e676780761325c8c36b88a143f2fd319 Mon Sep 17 00:00:00 2001 From: Colin Adler Date: Wed, 21 Jun 2023 18:47:22 +0000 Subject: [PATCH 2/4] fixup! feat(cli): add hidden netcheck command --- cli/netcheck.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/cli/netcheck.go b/cli/netcheck.go index 156b0b585dbf0..168fc34ecaba6 100644 --- a/cli/netcheck.go +++ b/cli/netcheck.go @@ -16,10 +16,9 @@ func (r *RootCmd) netcheck() *clibase.Cmd { client := new(codersdk.Client) cmd := &clibase.Cmd{ - Annotations: workspaceCommand, - Use: "netcheck", - Short: "Print network debug information", - Hidden: true, + Use: "netcheck", + Short: "Print network debug information", + Hidden: true, Middleware: clibase.Chain( r.InitClient(client), ), From dc3f54d8f646cdfbd14dda8e499c07167c8e2d72 Mon Sep 17 00:00:00 2001 From: Colin Adler Date: Wed, 21 Jun 2023 19:02:21 +0000 Subject: [PATCH 3/4] fixup! feat(cli): add hidden netcheck command --- coderd/apidoc/docs.go | 28 ++++++++++++++++++++++++++++ coderd/apidoc/swagger.json | 24 ++++++++++++++++++++++++ coderd/coderd.go | 2 +- coderd/workspaceagents.go | 19 +++++++++++++++++++ 4 files changed, 72 insertions(+), 1 deletion(-) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 8fc32811518c7..2f4fd2fbeffa0 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -4006,6 +4006,34 @@ const docTemplate = `{ } } }, + "/workspaceagents/connection": { + "get": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Agents" + ], + "summary": "Get connection info for workspace agent generic", + "operationId": "get-connection-info-for-workspace-agent-generic", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/codersdk.WorkspaceAgentConnectionInfo" + } + } + }, + "x-apidocgen": { + "skip": true + } + } + }, "/workspaceagents/google-instance-identity": { "post": { "security": [ diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index df0524bad7e78..9444a2b803661 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -3528,6 +3528,30 @@ } } }, + "/workspaceagents/connection": { + "get": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "produces": ["application/json"], + "tags": ["Agents"], + "summary": "Get connection info for workspace agent generic", + "operationId": "get-connection-info-for-workspace-agent-generic", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/codersdk.WorkspaceAgentConnectionInfo" + } + } + }, + "x-apidocgen": { + "skip": true + } + } + }, "/workspaceagents/google-instance-identity": { "post": { "security": [ diff --git a/coderd/coderd.go b/coderd/coderd.go index ba64275feccdf..41ddcf4bbda58 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -672,7 +672,7 @@ func New(options *Options) *API { r.Post("/azure-instance-identity", api.postWorkspaceAuthAzureInstanceIdentity) r.Post("/aws-instance-identity", api.postWorkspaceAuthAWSInstanceIdentity) r.Post("/google-instance-identity", api.postWorkspaceAuthGoogleInstanceIdentity) - r.Get("/connection", api.workspaceAgentConnection) + r.Get("/connection", api.workspaceAgentConnectionGeneric) r.Route("/me", func(r chi.Router) { r.Use(httpmw.ExtractWorkspaceAgent(options.Database)) r.Get("/manifest", api.workspaceAgentManifest) diff --git a/coderd/workspaceagents.go b/coderd/workspaceagents.go index e6ec7b46741b2..a0ccbb966f610 100644 --- a/coderd/workspaceagents.go +++ b/coderd/workspaceagents.go @@ -805,6 +805,25 @@ func (api *API) workspaceAgentConnection(rw http.ResponseWriter, r *http.Request }) } +// workspaceAgentConnectionGeneric is the same as workspaceAgentConnection but +// without the workspaceagent path parameter. +// +// @Summary Get connection info for workspace agent generic +// @ID get-connection-info-for-workspace-agent-generic +// @Security CoderSessionToken +// @Produce json +// @Tags Agents +// @Success 200 {object} codersdk.WorkspaceAgentConnectionInfo +// @Router /workspaceagents/connection [get] +// @x-apidocgen {"skip": true} +func (api *API) workspaceAgentConnectionGeneric(rw http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + httpapi.Write(ctx, rw, http.StatusOK, codersdk.WorkspaceAgentConnectionInfo{ + DERPMap: api.DERPMap, + }) +} + // @Summary Coordinate workspace agent via Tailnet // @Description It accepts a WebSocket connection to an agent that listens to // @Description incoming connections and publishes node updates. From 83ca8c89563b5c74c30b6249580701b963b2c7f0 Mon Sep 17 00:00:00 2001 From: Colin Adler Date: Wed, 21 Jun 2023 19:14:33 +0000 Subject: [PATCH 4/4] fixup! feat(cli): add hidden netcheck command --- cli/netcheck.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/cli/netcheck.go b/cli/netcheck.go index 168fc34ecaba6..4215c4dbf09b1 100644 --- a/cli/netcheck.go +++ b/cli/netcheck.go @@ -3,6 +3,7 @@ package cli import ( "context" "encoding/json" + "fmt" "time" "golang.org/x/xerrors" @@ -17,13 +18,13 @@ func (r *RootCmd) netcheck() *clibase.Cmd { cmd := &clibase.Cmd{ Use: "netcheck", - Short: "Print network debug information", + Short: "Print network debug information for DERP and STUN", Hidden: true, Middleware: clibase.Chain( r.InitClient(client), ), Handler: func(inv *clibase.Invocation) error { - ctx, cancel := context.WithTimeout(inv.Context(), 10*time.Second) + ctx, cancel := context.WithTimeout(inv.Context(), 30*time.Second) defer cancel() connInfo, err := client.WorkspaceAgentConnectionInfo(ctx) @@ -31,6 +32,8 @@ func (r *RootCmd) netcheck() *clibase.Cmd { return err } + _, _ = fmt.Fprint(inv.Stderr, "Gathering a network report. This may take a few seconds...\n\n") + var report healthcheck.DERPReport report.Run(ctx, &healthcheck.DERPReportOptions{ DERPMap: connInfo.DERPMap, @@ -49,6 +52,7 @@ func (r *RootCmd) netcheck() *clibase.Cmd { return xerrors.Errorf("failed to write all bytes to stdout; wrote %d, len %d", n, len(raw)) } + _, _ = inv.Stdout.Write([]byte("\n")) return nil }, }