Skip to content

Commit b1c2fea

Browse files
authored
feat(cli): add support cmd (#12328)
Part of #12163 - Adds a command coder support bundle <workspace> that generates a support bundle and writes it to coder-support-$(date +%s).zip. - Note: this is hidden currently until the rest of the functionality is fleshed out.
1 parent e5d9114 commit b1c2fea

File tree

4 files changed

+400
-1
lines changed

4 files changed

+400
-1
lines changed

cli/root.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ func (r *RootCmd) Core() []*clibase.Cmd {
123123
r.vscodeSSH(),
124124
r.workspaceAgent(),
125125
r.expCmd(),
126+
r.support(),
126127
}
127128
}
128129

cli/support.go

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
package cli
2+
3+
import (
4+
"archive/zip"
5+
"bytes"
6+
"encoding/json"
7+
"fmt"
8+
"os"
9+
"path/filepath"
10+
"strings"
11+
"text/tabwriter"
12+
"time"
13+
14+
"golang.org/x/xerrors"
15+
16+
"cdr.dev/slog"
17+
"cdr.dev/slog/sloggers/sloghuman"
18+
"github.com/coder/coder/v2/cli/clibase"
19+
"github.com/coder/coder/v2/codersdk"
20+
"github.com/coder/coder/v2/support"
21+
)
22+
23+
func (r *RootCmd) support() *clibase.Cmd {
24+
supportCmd := &clibase.Cmd{
25+
Use: "support",
26+
Short: "Commands for troubleshooting issues with a Coder deployment.",
27+
Handler: func(inv *clibase.Invocation) error {
28+
return inv.Command.HelpHandler(inv)
29+
},
30+
Hidden: true, // TODO: un-hide once the must-haves from #12160 are completed.
31+
Children: []*clibase.Cmd{
32+
r.supportBundle(),
33+
},
34+
}
35+
return supportCmd
36+
}
37+
38+
func (r *RootCmd) supportBundle() *clibase.Cmd {
39+
var outputPath string
40+
client := new(codersdk.Client)
41+
cmd := &clibase.Cmd{
42+
Use: "bundle <workspace> [<agent>]",
43+
Short: "Generate a support bundle to troubleshoot issues connecting to a workspace.",
44+
Long: `This command generates a file containing detailed troubleshooting information about the Coder deployment and workspace connections. You must specify a single workspace (and optionally an agent name).`,
45+
Middleware: clibase.Chain(
46+
clibase.RequireRangeArgs(0, 2),
47+
r.InitClient(client),
48+
),
49+
Handler: func(inv *clibase.Invocation) error {
50+
var (
51+
log = slog.Make(sloghuman.Sink(inv.Stderr)).
52+
Leveled(slog.LevelDebug)
53+
deps = support.Deps{
54+
Client: client,
55+
Log: log,
56+
}
57+
)
58+
59+
if len(inv.Args) == 0 {
60+
return xerrors.Errorf("must specify workspace name")
61+
}
62+
ws, err := namedWorkspace(inv.Context(), client, inv.Args[0])
63+
if err != nil {
64+
return xerrors.Errorf("invalid workspace: %w", err)
65+
}
66+
67+
deps.WorkspaceID = ws.ID
68+
69+
agentName := ""
70+
if len(inv.Args) > 1 {
71+
agentName = inv.Args[1]
72+
}
73+
74+
agt, found := findAgent(agentName, ws.LatestBuild.Resources)
75+
if !found {
76+
return xerrors.Errorf("could not find agent named %q for workspace", agentName)
77+
}
78+
79+
deps.AgentID = agt.ID
80+
81+
if outputPath == "" {
82+
cwd, err := filepath.Abs(".")
83+
if err != nil {
84+
return xerrors.Errorf("could not determine current working directory: %w", err)
85+
}
86+
fname := fmt.Sprintf("coder-support-%d.zip", time.Now().Unix())
87+
outputPath = filepath.Join(cwd, fname)
88+
}
89+
90+
w, err := os.Create(outputPath)
91+
if err != nil {
92+
return xerrors.Errorf("create output file: %w", err)
93+
}
94+
zwr := zip.NewWriter(w)
95+
defer zwr.Close()
96+
97+
bun, err := support.Run(inv.Context(), &deps)
98+
if err != nil {
99+
_ = os.Remove(outputPath) // best effort
100+
return xerrors.Errorf("create support bundle: %w", err)
101+
}
102+
103+
if err := writeBundle(bun, zwr); err != nil {
104+
_ = os.Remove(outputPath) // best effort
105+
return xerrors.Errorf("write support bundle to %s: %w", outputPath, err)
106+
}
107+
return nil
108+
},
109+
}
110+
cmd.Options = clibase.OptionSet{
111+
{
112+
Flag: "output",
113+
FlagShorthand: "o",
114+
Env: "CODER_SUPPORT_BUNDLE_OUTPUT",
115+
Description: "File path for writing the generated support bundle. Defaults to coder-support-$(date +%s).zip.",
116+
Value: clibase.StringOf(&outputPath),
117+
},
118+
}
119+
120+
return cmd
121+
}
122+
123+
func findAgent(agentName string, haystack []codersdk.WorkspaceResource) (*codersdk.WorkspaceAgent, bool) {
124+
for _, res := range haystack {
125+
for _, agt := range res.Agents {
126+
if agentName == "" {
127+
// just return the first
128+
return &agt, true
129+
}
130+
if agt.Name == agentName {
131+
return &agt, true
132+
}
133+
}
134+
}
135+
return nil, false
136+
}
137+
138+
func writeBundle(src *support.Bundle, dest *zip.Writer) error {
139+
for k, v := range map[string]any{
140+
"deployment/buildinfo.json": src.Deployment.BuildInfo,
141+
"deployment/config.json": src.Deployment.Config,
142+
"deployment/experiments.json": src.Deployment.Experiments,
143+
"deployment/health.json": src.Deployment.HealthReport,
144+
"network/netcheck_local.json": src.Network.NetcheckLocal,
145+
"network/netcheck_remote.json": src.Network.NetcheckRemote,
146+
"workspace/workspace.json": src.Workspace.Workspace,
147+
"workspace/agent.json": src.Workspace.Agent,
148+
} {
149+
f, err := dest.Create(k)
150+
if err != nil {
151+
return xerrors.Errorf("create file %q in archive: %w", k, err)
152+
}
153+
enc := json.NewEncoder(f)
154+
enc.SetIndent("", " ")
155+
if err := enc.Encode(v); err != nil {
156+
return xerrors.Errorf("write json to %q: %w", k, err)
157+
}
158+
}
159+
160+
for k, v := range map[string]string{
161+
"network/coordinator_debug.html": src.Network.CoordinatorDebug,
162+
"network/tailnet_debug.html": src.Network.TailnetDebug,
163+
"workspace/build_logs.txt": humanizeBuildLogs(src.Workspace.BuildLogs),
164+
"workspace/agent_startup_logs.txt": humanizeAgentLogs(src.Workspace.AgentStartupLogs),
165+
"logs.txt": strings.Join(src.Logs, "\n"),
166+
} {
167+
f, err := dest.Create(k)
168+
if err != nil {
169+
return xerrors.Errorf("create file %q in archive: %w", k, err)
170+
}
171+
if _, err := f.Write([]byte(v)); err != nil {
172+
return xerrors.Errorf("write file %q in archive: %w", k, err)
173+
}
174+
}
175+
if err := dest.Close(); err != nil {
176+
return xerrors.Errorf("close zip file: %w", err)
177+
}
178+
return nil
179+
}
180+
181+
func humanizeAgentLogs(ls []codersdk.WorkspaceAgentLog) string {
182+
var buf bytes.Buffer
183+
tw := tabwriter.NewWriter(&buf, 0, 2, 1, ' ', 0)
184+
for _, l := range ls {
185+
_, _ = fmt.Fprintf(tw, "%s\t[%s]\t%s\n",
186+
l.CreatedAt.Format("2006-01-02 15:04:05.000"), // for consistency with slog
187+
string(l.Level),
188+
l.Output,
189+
)
190+
}
191+
_ = tw.Flush()
192+
return buf.String()
193+
}
194+
195+
func humanizeBuildLogs(ls []codersdk.ProvisionerJobLog) string {
196+
var buf bytes.Buffer
197+
tw := tabwriter.NewWriter(&buf, 0, 2, 1, ' ', 0)
198+
for _, l := range ls {
199+
_, _ = fmt.Fprintf(tw, "%s\t[%s]\t%s\t%s\t%s\n",
200+
l.CreatedAt.Format("2006-01-02 15:04:05.000"), // for consistency with slog
201+
string(l.Level),
202+
string(l.Source),
203+
l.Stage,
204+
l.Output,
205+
)
206+
}
207+
_ = tw.Flush()
208+
return buf.String()
209+
}

0 commit comments

Comments
 (0)