diff --git a/cli/support.go b/cli/support.go index 5dfe7a45a151b..fa7c58261bd41 100644 --- a/cli/support.go +++ b/cli/support.go @@ -184,16 +184,8 @@ func (r *RootCmd) supportBundle() *serpent.Command { _ = os.Remove(outputPath) // best effort return xerrors.Errorf("create support bundle: %w", err) } - docsURL := bun.Deployment.Config.Values.DocsURL.String() - deployHealthSummary := bun.Deployment.HealthReport.Summarize(docsURL) - if len(deployHealthSummary) > 0 { - cliui.Warn(inv.Stdout, "Deployment health issues detected:", deployHealthSummary...) - } - clientNetcheckSummary := bun.Network.Netcheck.Summarize("Client netcheck:", docsURL) - if len(clientNetcheckSummary) > 0 { - cliui.Warn(inv.Stdout, "Networking issues detected:", deployHealthSummary...) - } + summarizeBundle(inv, bun) bun.CLILogs = cliLogBuf.Bytes() if err := writeBundle(bun, zwr); err != nil { @@ -225,6 +217,40 @@ func (r *RootCmd) supportBundle() *serpent.Command { return cmd } +// summarizeBundle makes a best-effort attempt to write a short summary +// of the support bundle to the user's terminal. +func summarizeBundle(inv *serpent.Invocation, bun *support.Bundle) { + if bun == nil { + cliui.Error(inv.Stdout, "No support bundle generated!") + return + } + + if bun.Deployment.Config == nil { + cliui.Error(inv.Stdout, "No deployment configuration available!") + return + } + + docsURL := bun.Deployment.Config.Values.DocsURL.String() + if bun.Deployment.HealthReport == nil { + cliui.Error(inv.Stdout, "No deployment health report available!") + return + } + deployHealthSummary := bun.Deployment.HealthReport.Summarize(docsURL) + if len(deployHealthSummary) > 0 { + cliui.Warn(inv.Stdout, "Deployment health issues detected:", deployHealthSummary...) + } + + if bun.Network.Netcheck == nil { + cliui.Error(inv.Stdout, "No network troubleshooting information available!") + return + } + + clientNetcheckSummary := bun.Network.Netcheck.Summarize("Client netcheck:", docsURL) + if len(clientNetcheckSummary) > 0 { + cliui.Warn(inv.Stdout, "Networking issues detected:", deployHealthSummary...) + } +} + func findAgent(agentName string, haystack []codersdk.WorkspaceResource) (*codersdk.WorkspaceAgent, bool) { for _, res := range haystack { for _, agt := range res.Agents { diff --git a/cli/support_test.go b/cli/support_test.go index d53aac66c820c..6fe8f015c3f2b 100644 --- a/cli/support_test.go +++ b/cli/support_test.go @@ -5,6 +5,9 @@ import ( "bytes" "encoding/json" "io" + "net/http" + "net/http/httptest" + "net/url" "os" "path/filepath" "runtime" @@ -14,6 +17,7 @@ import ( "tailscale.com/ipn/ipnstate" "github.com/google/uuid" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/coder/coder/v2/agent" @@ -156,6 +160,53 @@ func TestSupportBundle(t *testing.T) { err := inv.Run() require.ErrorContains(t, err, "failed authorization check") }) + + // This ensures that the CLI does not panic when trying to generate a support bundle + // against a fake server that returns an empty response for all requests. This essentially + // ensures that (almost) all of the support bundle generating code paths get a zero value. + t.Run("DontPanic", func(t *testing.T) { + t.Parallel() + + for _, code := range []int{ + http.StatusOK, + http.StatusUnauthorized, + http.StatusForbidden, + http.StatusNotFound, + http.StatusInternalServerError, + } { + t.Run(http.StatusText(code), func(t *testing.T) { + t.Parallel() + // Start up a fake server + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Logf("received request: %s %s", r.Method, r.URL) + switch r.URL.Path { + case "/api/v2/authcheck": + // Fake auth check + resp := codersdk.AuthorizationResponse{ + "Read DeploymentValues": true, + } + w.WriteHeader(http.StatusOK) + assert.NoError(t, json.NewEncoder(w).Encode(resp)) + default: + // Simply return a blank response for everything else. + w.WriteHeader(code) + } + })) + defer srv.Close() + u, err := url.Parse(srv.URL) + require.NoError(t, err) + client := codersdk.New(u) + + d := t.TempDir() + path := filepath.Join(d, "bundle.zip") + + inv, root := clitest.New(t, "support", "bundle", "--url-override", srv.URL, "--output-file", path, "--yes") + clitest.SetupConfig(t, client, root) + err = inv.Run() + require.NoError(t, err) + }) + } + }) } // nolint:revive // It's a control flag, but this is just a test.