Skip to content

Commit f2a9e51

Browse files
authored
feat(cli/support): confirm before creating bundle (coder#12684)
Forces user to confirm before creating a support bundle. Also adds contextual information to the bundle under cli_logs.txt.
1 parent 8ea5fb7 commit f2a9e51

File tree

3 files changed

+107
-18
lines changed

3 files changed

+107
-18
lines changed

cli/support.go

Lines changed: 99 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"encoding/base64"
77
"encoding/json"
88
"fmt"
9+
"net/url"
910
"os"
1011
"path/filepath"
1112
"strings"
@@ -16,6 +17,7 @@ import (
1617

1718
"cdr.dev/slog"
1819
"cdr.dev/slog/sloggers/sloghuman"
20+
"github.com/coder/coder/v2/cli/cliui"
1921
"github.com/coder/coder/v2/codersdk"
2022
"github.com/coder/coder/v2/support"
2123
"github.com/coder/serpent"
@@ -36,8 +38,26 @@ func (r *RootCmd) support() *serpent.Command {
3638
return supportCmd
3739
}
3840

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+
3958
func (r *RootCmd) supportBundle() *serpent.Command {
4059
var outputPath string
60+
var coderURLOverride string
4161
client := new(codersdk.Client)
4262
cmd := &serpent.Command{
4363
Use: "bundle <workspace> [<agent>]",
@@ -48,14 +68,52 @@ func (r *RootCmd) supportBundle() *serpent.Command {
4868
r.InitClient(client),
4969
),
5070
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),
5899
)
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+
}
59117

60118
if len(inv.Args) == 0 {
61119
return xerrors.Errorf("must specify workspace name")
@@ -64,8 +122,10 @@ func (r *RootCmd) supportBundle() *serpent.Command {
64122
if err != nil {
65123
return xerrors.Errorf("invalid workspace: %w", err)
66124
}
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+
)
69129

70130
agentName := ""
71131
if len(inv.Args) > 1 {
@@ -76,8 +136,10 @@ func (r *RootCmd) supportBundle() *serpent.Command {
76136
if !found {
77137
return xerrors.Errorf("could not find agent named %q for workspace", agentName)
78138
}
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+
)
81143

82144
if outputPath == "" {
83145
cwd, err := filepath.Abs(".")
@@ -87,6 +149,7 @@ func (r *RootCmd) supportBundle() *serpent.Command {
87149
fname := fmt.Sprintf("coder-support-%d.zip", time.Now().Unix())
88150
outputPath = filepath.Join(cwd, fname)
89151
}
152+
cliLog.Debug(inv.Context(), "output path", slog.F("path", outputPath))
90153

91154
w, err := os.Create(outputPath)
92155
if err != nil {
@@ -95,27 +158,48 @@ func (r *RootCmd) supportBundle() *serpent.Command {
95158
zwr := zip.NewWriter(w)
96159
defer zwr.Close()
97160

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+
98173
bun, err := support.Run(inv.Context(), &deps)
99174
if err != nil {
100175
_ = os.Remove(outputPath) // best effort
101176
return xerrors.Errorf("create support bundle: %w", err)
102177
}
178+
bun.CLILogs = cliLogBuf.Bytes()
103179

104180
if err := writeBundle(bun, zwr); err != nil {
105181
_ = os.Remove(outputPath) // best effort
106182
return xerrors.Errorf("write support bundle to %s: %w", outputPath, err)
107183
}
184+
_, _ = fmt.Fprintln(inv.Stderr, "Wrote support bundle to "+outputPath)
108185
return nil
109186
},
110187
}
111188
cmd.Options = serpent.OptionSet{
189+
cliui.SkipPromptOption(),
112190
{
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",
116194
Description: "File path for writing the generated support bundle. Defaults to coder-support-$(date +%s).zip.",
117195
Value: serpent.StringOf(&outputPath),
118196
},
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+
},
119203
}
120204

121205
return cmd
@@ -182,6 +266,7 @@ func writeBundle(src *support.Bundle, dest *zip.Writer) error {
182266
"agent/prometheus.txt": string(src.Agent.Prometheus),
183267
"workspace/template_file.zip": string(templateVersionBytes),
184268
"logs.txt": strings.Join(src.Logs, "\n"),
269+
"cli_logs.txt": string(src.CLILogs),
185270
} {
186271
f, err := dest.Create(k)
187272
if err != nil {

cli/support_test.go

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ func TestSupportBundle(t *testing.T) {
7676

7777
d := t.TempDir()
7878
path := filepath.Join(d, "bundle.zip")
79-
inv, root := clitest.New(t, "support", "bundle", r.Workspace.Name, "--output", path)
79+
inv, root := clitest.New(t, "support", "bundle", r.Workspace.Name, "--output-file", path, "--yes")
8080
//nolint: gocritic // requires owner privilege
8181
clitest.SetupConfig(t, client, root)
8282
err = inv.Run()
@@ -88,7 +88,7 @@ func TestSupportBundle(t *testing.T) {
8888
t.Parallel()
8989
client := coderdtest.New(t, nil)
9090
_ = coderdtest.CreateFirstUser(t, client)
91-
inv, root := clitest.New(t, "support", "bundle")
91+
inv, root := clitest.New(t, "support", "bundle", "--yes")
9292
//nolint: gocritic // requires owner privilege
9393
clitest.SetupConfig(t, client, root)
9494
err := inv.Run()
@@ -103,7 +103,7 @@ func TestSupportBundle(t *testing.T) {
103103
OrganizationID: admin.OrganizationID,
104104
OwnerID: admin.UserID,
105105
}).Do() // without agent!
106-
inv, root := clitest.New(t, "support", "bundle", r.Workspace.Name)
106+
inv, root := clitest.New(t, "support", "bundle", r.Workspace.Name, "--yes")
107107
//nolint: gocritic // requires owner privilege
108108
clitest.SetupConfig(t, client, root)
109109
err := inv.Run()
@@ -119,7 +119,7 @@ func TestSupportBundle(t *testing.T) {
119119
OrganizationID: user.OrganizationID,
120120
OwnerID: member.ID,
121121
}).WithAgent().Do()
122-
inv, root := clitest.New(t, "support", "bundle", r.Workspace.Name)
122+
inv, root := clitest.New(t, "support", "bundle", r.Workspace.Name, "--yes")
123123
clitest.SetupConfig(t, memberClient, root)
124124
err := inv.Run()
125125
require.ErrorContains(t, err, "failed authorization check")
@@ -219,6 +219,9 @@ func assertBundleContents(t *testing.T, path string) {
219219
case "logs.txt":
220220
bs := readBytesFromZip(t, f)
221221
require.NotEmpty(t, bs, "logs should not be empty")
222+
case "cli_logs.txt":
223+
bs := readBytesFromZip(t, f)
224+
require.NotEmpty(t, bs, "CLI logs should not be empty")
222225
default:
223226
require.Failf(t, "unexpected file in bundle", f.Name)
224227
}

support/support.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ type Bundle struct {
3333
Workspace Workspace `json:"workspace"`
3434
Agent Agent `json:"agent"`
3535
Logs []string `json:"logs"`
36+
CLILogs []byte `json:"cli_logs"`
3637
}
3738

3839
type Deployment struct {

0 commit comments

Comments
 (0)