6
6
"encoding/base64"
7
7
"encoding/json"
8
8
"fmt"
9
+ "net/url"
9
10
"os"
10
11
"path/filepath"
11
12
"strings"
@@ -16,6 +17,7 @@ import (
16
17
17
18
"cdr.dev/slog"
18
19
"cdr.dev/slog/sloggers/sloghuman"
20
+ "github.com/coder/coder/v2/cli/cliui"
19
21
"github.com/coder/coder/v2/codersdk"
20
22
"github.com/coder/coder/v2/support"
21
23
"github.com/coder/serpent"
@@ -36,8 +38,26 @@ func (r *RootCmd) support() *serpent.Command {
36
38
return supportCmd
37
39
}
38
40
41
+ var supportBundleBlurb = cliui .Bold ("This will collect the following information:\n " ) +
42
+ ` - Coder deployment version
43
+ - Coder deployment Configuration (sanitized), including enabled experiments
44
+ - Coder deployment health snapshot
45
+ - Coder deployment Network troubleshooting information
46
+ - Workspace configuration, parameters, and build logs
47
+ - Template version and source code for the given workspace
48
+ - Agent details (with environment variable sanitized)
49
+ - Agent network diagnostics
50
+ - Agent logs
51
+ ` + cliui .Bold ("Note: " ) +
52
+ cliui .Wrap (`While we try to sanitize sensitive data from support bundles, we cannot guarantee that they do not contain information that you or your organization may consider sensitive.\n` ) +
53
+ cliui .Bold ("Please confirm that you will:\n " ) +
54
+ " - Review the support bundle before distribution\n " +
55
+ " - Only distribute it via trusted channels\n " +
56
+ cliui .Bold ("Continue? " )
57
+
39
58
func (r * RootCmd ) supportBundle () * serpent.Command {
40
59
var outputPath string
60
+ var coderURLOverride string
41
61
client := new (codersdk.Client )
42
62
cmd := & serpent.Command {
43
63
Use : "bundle <workspace> [<agent>]" ,
@@ -48,14 +68,52 @@ func (r *RootCmd) supportBundle() *serpent.Command {
48
68
r .InitClient (client ),
49
69
),
50
70
Handler : func (inv * serpent.Invocation ) error {
51
- var (
52
- log = slog .Make (sloghuman .Sink (inv .Stderr )).
53
- Leveled (slog .LevelDebug )
54
- deps = support.Deps {
55
- Client : client ,
56
- Log : log ,
57
- }
71
+ var cliLogBuf bytes.Buffer
72
+ cliLogW := sloghuman .Sink (& cliLogBuf )
73
+ cliLog := slog .Make (cliLogW ).Leveled (slog .LevelDebug )
74
+ if r .verbose {
75
+ cliLog = cliLog .AppendSinks (sloghuman .Sink (inv .Stderr ))
76
+ }
77
+ ans , err := cliui .Prompt (inv , cliui.PromptOptions {
78
+ Text : supportBundleBlurb ,
79
+ Secret : false ,
80
+ IsConfirm : true ,
81
+ })
82
+ if err != nil || ans != cliui .ConfirmYes {
83
+ return err
84
+ }
85
+ if skip , _ := inv .ParsedFlags ().GetBool ("yes" ); skip {
86
+ cliLog .Debug (inv .Context (), "user auto-confirmed" )
87
+ } else {
88
+ cliLog .Debug (inv .Context (), "user confirmed manually" , slog .F ("answer" , ans ))
89
+ }
90
+
91
+ vi := defaultVersionInfo ()
92
+ cliLog .Debug (inv .Context (), "version info" ,
93
+ slog .F ("version" , vi .Version ),
94
+ slog .F ("build_time" , vi .BuildTime ),
95
+ slog .F ("external_url" , vi .ExternalURL ),
96
+ slog .F ("slim" , vi .Slim ),
97
+ slog .F ("agpl" , vi .AGPL ),
98
+ slog .F ("boring_crypto" , vi .BoringCrypto ),
58
99
)
100
+ cliLog .Debug (inv .Context (), "invocation" , slog .F ("args" , strings .Join (os .Args , " " )))
101
+
102
+ // Check if we're running inside a workspace
103
+ if val , found := os .LookupEnv ("CODER" ); found && val == "true" {
104
+ _ , _ = fmt .Fprintln (inv .Stderr , "Running inside Coder workspace; this can affect results!" )
105
+ cliLog .Debug (inv .Context (), "running inside coder workspace" )
106
+ }
107
+
108
+ if coderURLOverride != "" && coderURLOverride != client .URL .String () {
109
+ u , err := url .Parse (coderURLOverride )
110
+ if err != nil {
111
+ return xerrors .Errorf ("invalid value for Coder URL override: %w" , err )
112
+ }
113
+ _ , _ = fmt .Fprintf (inv .Stderr , "Overrode Coder URL to %q; this can affect results!\n " , coderURLOverride )
114
+ cliLog .Debug (inv .Context (), "coder url overridden" , slog .F ("url" , coderURLOverride ))
115
+ client .URL = u
116
+ }
59
117
60
118
if len (inv .Args ) == 0 {
61
119
return xerrors .Errorf ("must specify workspace name" )
@@ -64,8 +122,10 @@ func (r *RootCmd) supportBundle() *serpent.Command {
64
122
if err != nil {
65
123
return xerrors .Errorf ("invalid workspace: %w" , err )
66
124
}
67
-
68
- deps .WorkspaceID = ws .ID
125
+ cliLog .Debug (inv .Context (), "found workspace" ,
126
+ slog .F ("workspace_name" , ws .Name ),
127
+ slog .F ("workspace_id" , ws .ID ),
128
+ )
69
129
70
130
agentName := ""
71
131
if len (inv .Args ) > 1 {
@@ -76,8 +136,10 @@ func (r *RootCmd) supportBundle() *serpent.Command {
76
136
if ! found {
77
137
return xerrors .Errorf ("could not find agent named %q for workspace" , agentName )
78
138
}
79
-
80
- deps .AgentID = agt .ID
139
+ cliLog .Debug (inv .Context (), "found workspace agent" ,
140
+ slog .F ("agent_name" , agt .Name ),
141
+ slog .F ("agent_id" , agt .ID ),
142
+ )
81
143
82
144
if outputPath == "" {
83
145
cwd , err := filepath .Abs ("." )
@@ -87,6 +149,7 @@ func (r *RootCmd) supportBundle() *serpent.Command {
87
149
fname := fmt .Sprintf ("coder-support-%d.zip" , time .Now ().Unix ())
88
150
outputPath = filepath .Join (cwd , fname )
89
151
}
152
+ cliLog .Debug (inv .Context (), "output path" , slog .F ("path" , outputPath ))
90
153
91
154
w , err := os .Create (outputPath )
92
155
if err != nil {
@@ -95,27 +158,48 @@ func (r *RootCmd) supportBundle() *serpent.Command {
95
158
zwr := zip .NewWriter (w )
96
159
defer zwr .Close ()
97
160
161
+ clientLog := slog .Make ().Leveled (slog .LevelDebug )
162
+ if r .verbose {
163
+ clientLog .AppendSinks (sloghuman .Sink (inv .Stderr ))
164
+ }
165
+ deps := support.Deps {
166
+ Client : client ,
167
+ // Support adds a sink so we don't need to supply one ourselves.
168
+ Log : clientLog ,
169
+ WorkspaceID : ws .ID ,
170
+ AgentID : agt .ID ,
171
+ }
172
+
98
173
bun , err := support .Run (inv .Context (), & deps )
99
174
if err != nil {
100
175
_ = os .Remove (outputPath ) // best effort
101
176
return xerrors .Errorf ("create support bundle: %w" , err )
102
177
}
178
+ bun .CLILogs = cliLogBuf .Bytes ()
103
179
104
180
if err := writeBundle (bun , zwr ); err != nil {
105
181
_ = os .Remove (outputPath ) // best effort
106
182
return xerrors .Errorf ("write support bundle to %s: %w" , outputPath , err )
107
183
}
184
+ _ , _ = fmt .Fprintln (inv .Stderr , "Wrote support bundle to " + outputPath )
108
185
return nil
109
186
},
110
187
}
111
188
cmd .Options = serpent.OptionSet {
189
+ cliui .SkipPromptOption (),
112
190
{
113
- Flag : "output" ,
114
- FlagShorthand : "o " ,
115
- Env : "CODER_SUPPORT_BUNDLE_OUTPUT " ,
191
+ Flag : "output-file " ,
192
+ FlagShorthand : "O " ,
193
+ Env : "CODER_SUPPORT_BUNDLE_OUTPUT_FILE " ,
116
194
Description : "File path for writing the generated support bundle. Defaults to coder-support-$(date +%s).zip." ,
117
195
Value : serpent .StringOf (& outputPath ),
118
196
},
197
+ {
198
+ Flag : "url-override" ,
199
+ Env : "CODER_SUPPORT_BUNDLE_URL_OVERRIDE" ,
200
+ Description : "Override the URL to your Coder deployment. This may be useful, for example, if you need to troubleshoot a specific Coder replica." ,
201
+ Value : serpent .StringOf (& coderURLOverride ),
202
+ },
119
203
}
120
204
121
205
return cmd
@@ -182,6 +266,7 @@ func writeBundle(src *support.Bundle, dest *zip.Writer) error {
182
266
"agent/prometheus.txt" : string (src .Agent .Prometheus ),
183
267
"workspace/template_file.zip" : string (templateVersionBytes ),
184
268
"logs.txt" : strings .Join (src .Logs , "\n " ),
269
+ "cli_logs.txt" : string (src .CLILogs ),
185
270
} {
186
271
f , err := dest .Create (k )
187
272
if err != nil {
0 commit comments