feat(cli): add hidden netcheck command (#8136)

This commit is contained in:
Colin Adler
2023-06-21 14:33:19 -05:00
committed by GitHub
parent 7703bb77a7
commit bc739bdfce
8 changed files with 189 additions and 0 deletions
+62
View File
@@ -0,0 +1,62 @@
package cli
import (
"context"
"encoding/json"
"fmt"
"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{
Use: "netcheck",
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(), 30*time.Second)
defer cancel()
connInfo, err := client.WorkspaceAgentConnectionInfo(ctx)
if err != nil {
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,
})
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))
}
_, _ = inv.Stdout.Write([]byte("\n"))
return nil
},
}
cmd.Options = clibase.OptionSet{}
return cmd
}
+34
View File
@@ -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)
}
+1
View File
@@ -107,6 +107,7 @@ func (r *RootCmd) Core() []*clibase.Cmd {
// Hidden
r.gitssh(),
r.netcheck(),
r.vscodeSSH(),
r.workspaceAgent(),
}
+28
View File
@@ -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": [
+24
View File
@@ -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": [
+1
View File
@@ -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.workspaceAgentConnectionGeneric)
r.Route("/me", func(r chi.Router) {
r.Use(httpmw.ExtractWorkspaceAgent(options.Database))
r.Get("/manifest", api.workspaceAgentManifest)
+19
View File
@@ -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.
+20
View File
@@ -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