Skip to content

Commit c092833

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 c092833

File tree

7 files changed

+239
-126
lines changed

7 files changed

+239
-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: 81 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,73 @@ 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 result = await withCancellableProgress(
243+
async ({ signal, progress }) => {
244+
progress.report({ message: "Resolving CLI..." });
245+
const env = await this.resolveCliEnv(client);
246+
if (!env.featureSet.supportBundle) {
247+
throw new Error(
248+
"Support bundles require Coder CLI v2.10.0 or later. Please update your Coder deployment.",
249+
);
250+
}
251+
252+
const outputUri = await this.promptSupportBundlePath();
253+
if (!outputUri) {
254+
return undefined;
255+
}
256+
257+
progress.report({ message: "Collecting diagnostics..." });
258+
await cliExec.supportBundle(env, workspaceId, outputUri.fsPath, signal);
259+
return outputUri;
260+
},
261+
{
262+
location: vscode.ProgressLocation.Notification,
263+
title: `Creating support bundle for ${workspaceId}`,
264+
cancellable: true,
265+
},
266+
);
267+
268+
if (result.ok) {
269+
if (!result.value) {
270+
return;
271+
}
272+
const action = await vscode.window.showInformationMessage(
273+
`Support bundle saved to ${result.value.fsPath}`,
274+
"Reveal in File Explorer",
275+
);
276+
if (action === "Reveal in File Explorer") {
277+
await vscode.commands.executeCommand("revealFileInOS", result.value);
278+
}
279+
return;
280+
}
281+
282+
if (result.cancelled) {
283+
return;
284+
}
285+
286+
this.logger.error("Support bundle failed", result.error);
287+
vscode.window.showErrorMessage(
288+
`Support bundle failed: ${toError(result.error).message}`,
289+
);
290+
}
291+
292+
private promptSupportBundlePath(): Thenable<vscode.Uri | undefined> {
293+
const defaultName = `coder-support-${Math.floor(Date.now() / 1000)}.zip`;
294+
return vscode.window.showSaveDialog({
295+
defaultUri: vscode.Uri.file(path.join(os.homedir(), defaultName)),
296+
filters: { "Zip files": ["zip"] },
297+
title: "Save Support Bundle",
298+
});
299+
}
300+
231301
/**
232302
* View the logs for the currently connected workspace.
233303
*/
@@ -720,8 +790,10 @@ export class Commands {
720790
location: vscode.ProgressLocation.Notification,
721791
title: `Starting ping for ${workspaceId}...`,
722792
},
723-
async () => {
793+
async (progress) => {
794+
progress.report({ message: "Resolving CLI..." });
724795
const env = await this.resolveCliEnv(client);
796+
progress.report({ message: "Starting..." });
725797
cliExec.ping(env, workspaceId);
726798
},
727799
);
@@ -763,7 +835,9 @@ export class Commands {
763835
}
764836

765837
/** Resolve a CliEnv, preferring a locally cached binary over a network fetch. */
766-
private async resolveCliEnv(client: CoderApi): Promise<cliExec.CliEnv> {
838+
private async resolveCliEnv(
839+
client: CoderApi,
840+
): Promise<cliExec.CliEnv & { featureSet: FeatureSet }> {
767841
const baseUrl = client.getAxiosInstance().defaults.baseURL;
768842
if (!baseUrl) {
769843
throw new Error("You are not logged in");
@@ -780,7 +854,7 @@ export class Commands {
780854
const configDir = this.pathResolver.getGlobalConfigDir(safeHost);
781855
const configs = vscode.workspace.getConfiguration();
782856
const auth = resolveCliAuth(configs, featureSet, baseUrl, configDir);
783-
return { binary, configs, auth };
857+
return { binary, configs, auth, featureSet };
784858
}
785859

786860
/**

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)