Skip to content

Commit 7f8ecfd

Browse files
committed
feat: add Coder Support Bundle command (#751)
Add a `Coder: Create Support Bundle` command that runs `coder support bundle` against a workspace and saves the resulting zip via a save dialog. The command is available in the command palette and sidebar context menu for running workspaces. Changes: - Add `supportBundle` feature flag gated at CLI v2.10.0 - Add `cliExec.supportBundle()` with --output-file and --yes flags - Add command handler with save dialog, progress reporting, and file reveal - Register command palette and sidebar context menu entries with ordering - Return featureSet from resolveCliEnv for version gating - Add progress reporting to speedTest and pingWorkspace commands - Refactor cliExec and featureSet tests for conciseness
1 parent 0d52e7d commit 7f8ecfd

File tree

7 files changed

+231
-126
lines changed

7 files changed

+231
-126
lines changed

package.json

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,11 @@
324324
"title": "Speed Test Workspace",
325325
"category": "Coder"
326326
},
327+
{
328+
"command": "coder.supportBundle",
329+
"title": "Create Support Bundle",
330+
"category": "Coder"
331+
},
327332
{
328333
"command": "coder.viewLogs",
329334
"title": "Coder: View Logs",
@@ -381,6 +386,10 @@
381386
{
382387
"command": "coder.pingWorkspace:views",
383388
"title": "Ping"
389+
},
390+
{
391+
"command": "coder.supportBundle:views",
392+
"title": "Support Bundle"
384393
}
385394
],
386395
"menus": {
@@ -405,6 +414,10 @@
405414
"command": "coder.speedTest",
406415
"when": "coder.authenticated"
407416
},
417+
{
418+
"command": "coder.supportBundle",
419+
"when": "coder.authenticated"
420+
},
408421
{
409422
"command": "coder.navigateToWorkspace",
410423
"when": "coder.workspace.connected"
@@ -425,6 +438,10 @@
425438
"command": "coder.pingWorkspace:views",
426439
"when": "false"
427440
},
441+
{
442+
"command": "coder.supportBundle:views",
443+
"when": "false"
444+
},
428445
{
429446
"command": "coder.workspace.update",
430447
"when": "coder.workspace.updatable"
@@ -535,12 +552,17 @@
535552
{
536553
"command": "coder.pingWorkspace:views",
537554
"when": "coder.authenticated && viewItem =~ /\\+running/",
538-
"group": "navigation"
555+
"group": "navigation@1"
539556
},
540557
{
541558
"command": "coder.speedTest:views",
542559
"when": "coder.authenticated && viewItem =~ /\\+running/",
543-
"group": "navigation"
560+
"group": "navigation@2"
561+
},
562+
{
563+
"command": "coder.supportBundle:views",
564+
"when": "coder.authenticated && viewItem =~ /\\+running/",
565+
"group": "navigation@3"
544566
}
545567
],
546568
"statusBar/remoteIndicator": [

src/commands.ts

Lines changed: 73 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
type WorkspaceAgent,
44
} from "coder/site/src/api/typesGenerated";
55
import * as fs from "node:fs/promises";
6+
import * as os from "node:os";
67
import * as path from "node:path";
78
import * as semver from "semver";
89
import * as vscode from "vscode";
@@ -22,7 +23,7 @@ import { type SecretsManager } from "./core/secretsManager";
2223
import { type DeploymentManager } from "./deployment/deploymentManager";
2324
import { CertificateError } from "./error/certificateError";
2425
import { toError } from "./error/errorUtils";
25-
import { featureSetForVersion } from "./featureSet";
26+
import { type FeatureSet, featureSetForVersion } from "./featureSet";
2627
import { type Logger } from "./logging/logger";
2728
import { type LoginCoordinator } from "./login/loginCoordinator";
2829
import { withCancellableProgress, withProgress } from "./progress";
@@ -196,15 +197,17 @@ export class Commands {
196197
const trimmedDuration = duration.trim();
197198

198199
const result = await withCancellableProgress(
199-
async ({ signal }) => {
200+
async ({ signal, progress }) => {
201+
progress.report({ message: "Resolving CLI..." });
200202
const env = await this.resolveCliEnv(client);
203+
progress.report({ message: "Running..." });
201204
return cliExec.speedtest(env, workspaceId, trimmedDuration, signal);
202205
},
203206
{
204207
location: vscode.ProgressLocation.Notification,
205208
title: trimmedDuration
206-
? `Running speed test (${trimmedDuration})...`
207-
: "Running speed test...",
209+
? `Speed test for ${workspaceId} (${trimmedDuration})`
210+
: `Speed test for ${workspaceId}`,
208211
cancellable: true,
209212
},
210213
);
@@ -228,6 +231,65 @@ export class Commands {
228231
);
229232
}
230233

234+
public async supportBundle(item?: OpenableTreeItem): Promise<void> {
235+
const resolved = await this.resolveClientAndWorkspace(item);
236+
if (!resolved) {
237+
return;
238+
}
239+
240+
const { client, workspaceId } = resolved;
241+
242+
const defaultName = `coder-support-${Math.floor(Date.now() / 1000)}.zip`;
243+
const outputUri = await vscode.window.showSaveDialog({
244+
defaultUri: vscode.Uri.file(path.join(os.homedir(), defaultName)),
245+
filters: { "Zip files": ["zip"] },
246+
title: "Save Support Bundle",
247+
});
248+
if (!outputUri) {
249+
return;
250+
}
251+
const outputPath = outputUri.fsPath;
252+
253+
const result = await withCancellableProgress(
254+
async ({ signal, progress }) => {
255+
progress.report({ message: "Resolving CLI..." });
256+
const env = await this.resolveCliEnv(client);
257+
if (!env.featureSet.supportBundle) {
258+
throw new Error(
259+
"Support bundles require Coder CLI v2.10.0 or later. Please update your Coder deployment.",
260+
);
261+
}
262+
progress.report({ message: "Collecting diagnostics..." });
263+
return cliExec.supportBundle(env, workspaceId, outputPath, signal);
264+
},
265+
{
266+
location: vscode.ProgressLocation.Notification,
267+
title: `Creating support bundle for ${workspaceId}`,
268+
cancellable: true,
269+
},
270+
);
271+
272+
if (result.ok) {
273+
const action = await vscode.window.showInformationMessage(
274+
`Support bundle saved to ${outputPath}`,
275+
"Reveal in File Explorer",
276+
);
277+
if (action === "Reveal in File Explorer") {
278+
await vscode.commands.executeCommand("revealFileInOS", outputUri);
279+
}
280+
return;
281+
}
282+
283+
if (result.cancelled) {
284+
return;
285+
}
286+
287+
this.logger.error("Support bundle failed", result.error);
288+
vscode.window.showErrorMessage(
289+
`Support bundle failed: ${toError(result.error).message}`,
290+
);
291+
}
292+
231293
/**
232294
* View the logs for the currently connected workspace.
233295
*/
@@ -720,8 +782,10 @@ export class Commands {
720782
location: vscode.ProgressLocation.Notification,
721783
title: `Starting ping for ${workspaceId}...`,
722784
},
723-
async () => {
785+
async (progress) => {
786+
progress.report({ message: "Resolving CLI..." });
724787
const env = await this.resolveCliEnv(client);
788+
progress.report({ message: "Starting..." });
725789
cliExec.ping(env, workspaceId);
726790
},
727791
);
@@ -763,7 +827,9 @@ export class Commands {
763827
}
764828

765829
/** Resolve a CliEnv, preferring a locally cached binary over a network fetch. */
766-
private async resolveCliEnv(client: CoderApi): Promise<cliExec.CliEnv> {
830+
private async resolveCliEnv(
831+
client: CoderApi,
832+
): Promise<cliExec.CliEnv & { featureSet: FeatureSet }> {
767833
const baseUrl = client.getAxiosInstance().defaults.baseURL;
768834
if (!baseUrl) {
769835
throw new Error("You are not logged in");
@@ -780,7 +846,7 @@ export class Commands {
780846
const configDir = this.pathResolver.getGlobalConfigDir(safeHost);
781847
const configs = vscode.workspace.getConfiguration();
782848
const auth = resolveCliAuth(configs, featureSet, baseUrl, configDir);
783-
return { binary, configs, auth };
849+
return { binary, configs, auth, featureSet };
784850
}
785851

786852
/**

src/core/cliExec.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,33 @@ export async function speedtest(
104104
}
105105
}
106106

107+
/**
108+
* Run `coder support bundle` and save the output zip to the given path.
109+
*/
110+
export async function supportBundle(
111+
env: CliEnv,
112+
workspaceName: string,
113+
outputPath: string,
114+
signal?: AbortSignal,
115+
): Promise<string> {
116+
const globalFlags = getGlobalFlags(env.configs, env.auth);
117+
const args = [
118+
...globalFlags,
119+
"support",
120+
"bundle",
121+
workspaceName,
122+
"--output-file",
123+
outputPath,
124+
"--yes",
125+
];
126+
try {
127+
const result = await execFileAsync(env.binary, args, { signal });
128+
return result.stdout;
129+
} catch (error) {
130+
throw cliError(error);
131+
}
132+
}
133+
107134
/**
108135
* Run `coder ping` in a PTY terminal with Ctrl+C support.
109136
*/

src/extension.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,14 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
323323
"coder.speedTest:views",
324324
commands.speedTest.bind(commands),
325325
),
326+
vscode.commands.registerCommand(
327+
"coder.supportBundle",
328+
commands.supportBundle.bind(commands),
329+
),
330+
vscode.commands.registerCommand(
331+
"coder.supportBundle:views",
332+
commands.supportBundle.bind(commands),
333+
),
326334
);
327335

328336
const remote = new Remote(serviceContainer, commands, ctx);

src/featureSet.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export interface FeatureSet {
77
buildReason: boolean;
88
keyringAuth: boolean;
99
keyringTokenRead: boolean;
10+
supportBundle: boolean;
1011
}
1112

1213
/**
@@ -47,5 +48,7 @@ export function featureSetForVersion(
4748
keyringAuth: versionAtLeast(version, "2.29.0"),
4849
// `coder login token` for reading tokens from the keyring
4950
keyringTokenRead: versionAtLeast(version, "2.31.0"),
51+
// `coder support bundle` (officially released/unhidden in 2.10.0)
52+
supportBundle: versionAtLeast(version, "2.10.0"),
5053
};
5154
}

0 commit comments

Comments
 (0)