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 {