Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 24 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,11 @@
"title": "Speed Test Workspace",
"category": "Coder"
},
{
"command": "coder.supportBundle",
"title": "Create Support Bundle",
"category": "Coder"
},
{
"command": "coder.viewLogs",
"title": "Coder: View Logs",
Expand Down Expand Up @@ -381,6 +386,10 @@
{
"command": "coder.pingWorkspace:views",
"title": "Ping"
},
{
"command": "coder.supportBundle:views",
"title": "Support Bundle"
}
],
"menus": {
Expand All @@ -405,6 +414,10 @@
"command": "coder.speedTest",
"when": "coder.authenticated"
},
{
"command": "coder.supportBundle",
"when": "coder.authenticated"
},
{
"command": "coder.navigateToWorkspace",
"when": "coder.workspace.connected"
Expand All @@ -425,6 +438,10 @@
"command": "coder.pingWorkspace:views",
"when": "false"
},
{
"command": "coder.supportBundle:views",
"when": "false"
},
{
"command": "coder.workspace.update",
"when": "coder.workspace.updatable"
Expand Down Expand Up @@ -535,12 +552,17 @@
{
"command": "coder.pingWorkspace:views",
"when": "coder.authenticated && viewItem =~ /\\+running/",
"group": "navigation"
"group": "navigation@1"
},
{
"command": "coder.speedTest:views",
"when": "coder.authenticated && viewItem =~ /\\+running/",
"group": "navigation"
"group": "navigation@2"
},
{
"command": "coder.supportBundle:views",
"when": "coder.authenticated && viewItem =~ /\\+running/",
"group": "navigation@3"
}
],
"statusBar/remoteIndicator": [
Expand Down
88 changes: 81 additions & 7 deletions src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
type WorkspaceAgent,
} from "coder/site/src/api/typesGenerated";
import * as fs from "node:fs/promises";
import * as os from "node:os";
import * as path from "node:path";
import * as semver from "semver";
import * as vscode from "vscode";
Expand All @@ -22,7 +23,7 @@ import { type SecretsManager } from "./core/secretsManager";
import { type DeploymentManager } from "./deployment/deploymentManager";
import { CertificateError } from "./error/certificateError";
import { toError } from "./error/errorUtils";
import { featureSetForVersion } from "./featureSet";
import { type FeatureSet, featureSetForVersion } from "./featureSet";
import { type Logger } from "./logging/logger";
import { type LoginCoordinator } from "./login/loginCoordinator";
import { withCancellableProgress, withProgress } from "./progress";
Expand Down Expand Up @@ -196,15 +197,17 @@ export class Commands {
const trimmedDuration = duration.trim();

const result = await withCancellableProgress(
async ({ signal }) => {
async ({ signal, progress }) => {
progress.report({ message: "Resolving CLI..." });
const env = await this.resolveCliEnv(client);
progress.report({ message: "Running..." });
return cliExec.speedtest(env, workspaceId, trimmedDuration, signal);
},
{
location: vscode.ProgressLocation.Notification,
title: trimmedDuration
? `Running speed test (${trimmedDuration})...`
: "Running speed test...",
? `Speed test for ${workspaceId} (${trimmedDuration})`
: `Speed test for ${workspaceId}`,
cancellable: true,
},
);
Expand All @@ -228,6 +231,73 @@ export class Commands {
);
}

public async supportBundle(item?: OpenableTreeItem): Promise<void> {
const resolved = await this.resolveClientAndWorkspace(item);
if (!resolved) {
return;
}

const { client, workspaceId } = resolved;

const result = await withCancellableProgress(
async ({ signal, progress }) => {
progress.report({ message: "Resolving CLI..." });
const env = await this.resolveCliEnv(client);
if (!env.featureSet.supportBundle) {
throw new Error(
"Support bundles require Coder CLI v2.10.0 or later. Please update your Coder deployment.",
);
}

const outputUri = await this.promptSupportBundlePath();
if (!outputUri) {
return undefined;
}

progress.report({ message: "Collecting diagnostics..." });
await cliExec.supportBundle(env, workspaceId, outputUri.fsPath, signal);
return outputUri;
},
{
location: vscode.ProgressLocation.Notification,
title: `Creating support bundle for ${workspaceId}`,
cancellable: true,
},
);

if (result.ok) {
if (!result.value) {
return;
}
const action = await vscode.window.showInformationMessage(
`Support bundle saved to ${result.value.fsPath}`,
"Reveal in File Explorer",
);
if (action === "Reveal in File Explorer") {
await vscode.commands.executeCommand("revealFileInOS", result.value);
}
return;
}

if (result.cancelled) {
return;
}

this.logger.error("Support bundle failed", result.error);
vscode.window.showErrorMessage(
`Support bundle failed: ${toError(result.error).message}`,
);
}

private promptSupportBundlePath(): Thenable<vscode.Uri | undefined> {
const defaultName = `coder-support-${Math.floor(Date.now() / 1000)}.zip`;
return vscode.window.showSaveDialog({
defaultUri: vscode.Uri.file(path.join(os.homedir(), defaultName)),
filters: { "Zip files": ["zip"] },
title: "Save Support Bundle",
});
}

/**
* View the logs for the currently connected workspace.
*/
Expand Down Expand Up @@ -720,8 +790,10 @@ export class Commands {
location: vscode.ProgressLocation.Notification,
title: `Starting ping for ${workspaceId}...`,
},
async () => {
async (progress) => {
progress.report({ message: "Resolving CLI..." });
const env = await this.resolveCliEnv(client);
progress.report({ message: "Starting..." });
cliExec.ping(env, workspaceId);
},
);
Expand Down Expand Up @@ -763,7 +835,9 @@ export class Commands {
}

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

/**
Expand Down
27 changes: 27 additions & 0 deletions src/core/cliExec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,33 @@ export async function speedtest(
}
}

/**
* Run `coder support bundle` and save the output zip to the given path.
*/
export async function supportBundle(
env: CliEnv,
workspaceName: string,
outputPath: string,
signal?: AbortSignal,
): Promise<string> {
const globalFlags = getGlobalFlags(env.configs, env.auth);
const args = [
...globalFlags,
"support",
"bundle",
workspaceName,
"--output-file",
outputPath,
"--yes",
];
try {
const result = await execFileAsync(env.binary, args, { signal });
return result.stdout;
} catch (error) {
throw cliError(error);
}
}

/**
* Run `coder ping` in a PTY terminal with Ctrl+C support.
*/
Expand Down
8 changes: 8 additions & 0 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,14 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
"coder.speedTest:views",
commands.speedTest.bind(commands),
),
vscode.commands.registerCommand(
"coder.supportBundle",
commands.supportBundle.bind(commands),
),
vscode.commands.registerCommand(
"coder.supportBundle:views",
commands.supportBundle.bind(commands),
),
);

const remote = new Remote(serviceContainer, commands, ctx);
Expand Down
3 changes: 3 additions & 0 deletions src/featureSet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export interface FeatureSet {
buildReason: boolean;
keyringAuth: boolean;
keyringTokenRead: boolean;
supportBundle: boolean;
}

/**
Expand Down Expand Up @@ -47,5 +48,7 @@ export function featureSetForVersion(
keyringAuth: versionAtLeast(version, "2.29.0"),
// `coder login token` for reading tokens from the keyring
keyringTokenRead: versionAtLeast(version, "2.31.0"),
// `coder support bundle` (officially released/unhidden in 2.10.0)
supportBundle: versionAtLeast(version, "2.10.0"),
};
}
Loading