Skip to content
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
### Added

- Support for paths that begin with a tilde (`~`).
- Support for `coder ssh` flag configurations through the `coder.sshFlags` setting.

### Fixed

Expand Down
10 changes: 10 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,16 @@
"type": "boolean",
"default": false
},
"coder.sshFlags": {
"markdownDescription": "Additional flags to pass to the `coder ssh` command when establishing SSH connections. Enter each flag as a separate array item; values are passed verbatim and in order. See the [CLI ssh reference](https://coder.com/docs/reference/cli/ssh) for available flags.\n\nNote: `--network-info-dir` and `--ssh-host-prefix` are ignored (managed internally). Prefer `#coder.proxyLogDirectory#` over `--log-dir`/`-l` for full functionality.",
"type": "array",
"items": {
"type": "string"
},
"default": [
"--disable-autostart"
]
},
"coder.globalFlags": {
"markdownDescription": "Global flags to pass to every Coder CLI invocation. Enter each flag as a separate array item; values are passed verbatim and in order. Do **not** include the `coder` command itself. See the [CLI reference](https://coder.com/docs/reference/cli) for available global flags.\n\nNote that for `--header-command`, precedence is: `#coder.headerCommand#` setting, then `CODER_HEADER_COMMAND` environment variable, then the value specified here. The `--global-config` flag is explicitly ignored.",
"type": "array",
Expand Down
2 changes: 1 addition & 1 deletion src/api/workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import {
import { spawn } from "node:child_process";
import * as vscode from "vscode";

import { getGlobalFlags } from "../cliConfig";
import { type FeatureSet } from "../featureSet";
import { getGlobalFlags } from "../globalFlags";
import { escapeCommandArg } from "../util";
import { type UnidirectionalStream } from "../websocket/eventStreamConnection";

Expand Down
11 changes: 10 additions & 1 deletion src/globalFlags.ts → src/cliConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,16 @@ export function getGlobalFlags(
// Last takes precedence/overrides previous ones
return [
...(configs.get<string[]>("coder.globalFlags") || []),
...["--global-config", escapeCommandArg(configDir)],
"--global-config",
escapeCommandArg(configDir),
...getHeaderArgs(configs),
];
}

/**
* Returns SSH flags for the `coder ssh` command from user configuration.
*/
export function getSshFlags(configs: WorkspaceConfiguration): string[] {
// Make sure to match this default with the one in the package.json
return configs.get<string[]>("coder.sshFlags", ["--disable-autostart"]);
}
2 changes: 1 addition & 1 deletion src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@ import * as vscode from "vscode";
import { createWorkspaceIdentifier, extractAgents } from "./api/api-helper";
import { CoderApi } from "./api/coderApi";
import { needToken } from "./api/utils";
import { getGlobalFlags } from "./cliConfig";
import { type CliManager } from "./core/cliManager";
import { type ServiceContainer } from "./core/container";
import { type ContextManager } from "./core/contextManager";
import { type MementoManager } from "./core/mementoManager";
import { type PathResolver } from "./core/pathResolver";
import { type SecretsManager } from "./core/secretsManager";
import { CertificateError } from "./error";
import { getGlobalFlags } from "./globalFlags";
import { type Logger } from "./logging/logger";
import { maybeAskAgent, maybeAskUrl } from "./promptUtils";
import { escapeCommandArg, toRemoteAuthority, toSafeHost } from "./util";
Expand Down
153 changes: 107 additions & 46 deletions src/remote/remote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,14 @@ import {
import { extractAgents } from "../api/api-helper";
import { CoderApi } from "../api/coderApi";
import { needToken } from "../api/utils";
import { getGlobalFlags, getSshFlags } from "../cliConfig";
import { type Commands } from "../commands";
import { type CliManager } from "../core/cliManager";
import * as cliUtils from "../core/cliUtils";
import { type ServiceContainer } from "../core/container";
import { type ContextManager } from "../core/contextManager";
import { type PathResolver } from "../core/pathResolver";
import { featureSetForVersion, type FeatureSet } from "../featureSet";
import { getGlobalFlags } from "../globalFlags";
import { Inbox } from "../inbox";
import { type Logger } from "../logging/logger";
import {
Expand Down Expand Up @@ -501,8 +501,6 @@ export class Remote {
sshMonitor.onLogFilePathChange((newPath) => {
this.commands.workspaceLogPath = newPath;
}),
// Watch for logDir configuration changes
this.watchLogDirSetting(logDir, featureSet),
// Register the label formatter again because SSH overrides it!
vscode.extensions.onDidChange(() => {
// Dispose previous label formatter
Expand All @@ -516,6 +514,18 @@ export class Remote {
}),
...(await this.createAgentMetadataStatusBar(agent, workspaceClient)),
);

const settingsToWatch = [
{ setting: "coder.globalFlags", title: "Global flags" },
{ setting: "coder.sshFlags", title: "SSH flags" },
];
if (featureSet.proxyLogDirectory) {
settingsToWatch.push({
setting: "coder.proxyLogDirectory",
title: "Proxy log directory",
});
}
disposables.push(this.watchSettings(settingsToWatch));
} catch (ex) {
// Whatever error happens, make sure we clean up the disposables in case of failure
disposables.forEach((d) => d.dispose());
Expand Down Expand Up @@ -554,8 +564,10 @@ export class Remote {
}

/**
* Return the --log-dir argument value for the ProxyCommand. It may be an
* Return the --log-dir argument value for the ProxyCommand. It may be an
* empty string if the setting is not set or the cli does not support it.
*
* Value defined in the "coder.sshFlags" setting is not considered.
*/
private getLogDir(featureSet: FeatureSet): string {
if (!featureSet.proxyLogDirectory) {
Expand All @@ -571,16 +583,79 @@ export class Remote {
}

/**
* Formats the --log-dir argument for the ProxyCommand after making sure it
* Builds the ProxyCommand for SSH connections to Coder workspaces.
* Uses `coder ssh` for modern deployments with wildcard support,
* or falls back to `coder vscodessh` for older deployments.
*/
private async buildProxyCommand(
binaryPath: string,
label: string,
hostPrefix: string,
logDir: string,
useWildcardSSH: boolean,
): Promise<string> {
const vscodeConfig = vscode.workspace.getConfiguration();

const escapedBinaryPath = escapeCommandArg(binaryPath);
const globalConfig = getGlobalFlags(
vscodeConfig,
this.pathResolver.getGlobalConfigDir(label),
);
const logArgs = await this.getLogArgs(logDir);

if (useWildcardSSH) {
// User SSH flags are included first; internally-managed flags
// are appended last so they take precedence.
const userSshFlags = getSshFlags(vscodeConfig);
// Make sure to update the `coder.sshFlags` description if we add more internal flags here!
const internalFlags = [
"--stdio",
"--usage-app=vscode",
"--network-info-dir",
escapeCommandArg(this.pathResolver.getNetworkInfoPath()),
...logArgs,
"--ssh-host-prefix",
hostPrefix,
"%h",
];

const allFlags = [...userSshFlags, ...internalFlags];
return `${escapedBinaryPath} ${globalConfig.join(" ")} ssh ${allFlags.join(" ")}`;
} else {
const networkInfoDir = escapeCommandArg(
this.pathResolver.getNetworkInfoPath(),
);
const sessionTokenFile = escapeCommandArg(
this.pathResolver.getSessionTokenPath(label),
);
const urlFile = escapeCommandArg(this.pathResolver.getUrlPath(label));

const sshFlags = [
"--network-info-dir",
networkInfoDir,
...logArgs,
"--session-token-file",
sessionTokenFile,
"--url-file",
urlFile,
"%h",
];

return `${escapedBinaryPath} ${globalConfig.join(" ")} vscodessh ${sshFlags.join(" ")}`;
}
}

/**
* Returns the --log-dir argument for the ProxyCommand after making sure it
* has been created.
*/
private async formatLogArg(logDir: string): Promise<string> {
private async getLogArgs(logDir: string): Promise<string[]> {
if (!logDir) {
return "";
return [];
}
await fs.mkdir(logDir, { recursive: true });
this.logger.info("SSH proxy diagnostics are being written to", logDir);
return ` --log-dir ${escapeCommandArg(logDir)} -v`;
return ["--log-dir", escapeCommandArg(logDir), "-v"];
}

// updateSSHConfig updates the SSH configuration with a wildcard that handles
Expand Down Expand Up @@ -666,15 +741,13 @@ export class Remote {
? `${AuthorityPrefix}.${label}--`
: `${AuthorityPrefix}--`;

const globalConfigs = this.globalConfigs(label);

const proxyCommand = featureSet.wildcardSSH
? `${escapeCommandArg(binaryPath)}${globalConfigs} ssh --stdio --usage-app=vscode --disable-autostart --network-info-dir ${escapeCommandArg(this.pathResolver.getNetworkInfoPath())}${await this.formatLogArg(logDir)} --ssh-host-prefix ${hostPrefix} %h`
: `${escapeCommandArg(binaryPath)}${globalConfigs} vscodessh --network-info-dir ${escapeCommandArg(
this.pathResolver.getNetworkInfoPath(),
)}${await this.formatLogArg(logDir)} --session-token-file ${escapeCommandArg(this.pathResolver.getSessionTokenPath(label))} --url-file ${escapeCommandArg(
this.pathResolver.getUrlPath(label),
)} %h`;
const proxyCommand = await this.buildProxyCommand(
binaryPath,
label,
hostPrefix,
logDir,
featureSet.wildcardSSH,
);

const sshValues: SSHValues = {
Host: hostPrefix + `*`,
Expand Down Expand Up @@ -727,38 +800,26 @@ export class Remote {
return sshConfig.getRaw();
}

private globalConfigs(label: string): string {
const vscodeConfig = vscode.workspace.getConfiguration();
const args = getGlobalFlags(
vscodeConfig,
this.pathResolver.getGlobalConfigDir(label),
);
return ` ${args.join(" ")}`;
}

private watchLogDirSetting(
currentLogDir: string,
featureSet: FeatureSet,
private watchSettings(
settings: Array<{ setting: string; title: string }>,
): vscode.Disposable {
return vscode.workspace.onDidChangeConfiguration((e) => {
if (!e.affectsConfiguration("coder.proxyLogDirectory")) {
return;
}
const newLogDir = this.getLogDir(featureSet);
if (newLogDir === currentLogDir) {
return;
for (const { setting, title } of settings) {
if (!e.affectsConfiguration(setting)) {
continue;
}
vscode.window
.showInformationMessage(
`${title} setting changed. Reload window to apply.`,
"Reload",
)
.then((action) => {
if (action === "Reload") {
vscode.commands.executeCommand("workbench.action.reloadWindow");
}
});
break;
}

vscode.window
.showInformationMessage(
"Log directory configuration changed. Reload window to apply.",
"Reload",
)
.then((action) => {
if (action === "Reload") {
vscode.commands.executeCommand("workbench.action.reloadWindow");
}
});
});
}

Expand Down
Loading