Skip to content

Commit c394308

Browse files
authored
Add configurable SSH flags via coder.sshFlags setting (coder#670)
- Add `coder.sshFlags` setting for passing custom flags to `coder ssh` - Watch SSH-related settings and prompt user to reload when changed - Renames `globalFlags.ts` → `cliConfig.ts` to consolidate CLI configuration logic Closes coder#666
1 parent cd59d8f commit c394308

File tree

8 files changed

+246
-137
lines changed

8 files changed

+246
-137
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
### Added
66

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

910
### Fixed
1011

package.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,16 @@
120120
"type": "boolean",
121121
"default": false
122122
},
123+
"coder.sshFlags": {
124+
"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.",
125+
"type": "array",
126+
"items": {
127+
"type": "string"
128+
},
129+
"default": [
130+
"--disable-autostart"
131+
]
132+
},
123133
"coder.globalFlags": {
124134
"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.",
125135
"type": "array",

src/api/workspace.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ import {
88
import { spawn } from "node:child_process";
99
import * as vscode from "vscode";
1010

11+
import { getGlobalFlags } from "../cliConfig";
1112
import { type FeatureSet } from "../featureSet";
12-
import { getGlobalFlags } from "../globalFlags";
1313
import { escapeCommandArg } from "../util";
1414
import { type UnidirectionalStream } from "../websocket/eventStreamConnection";
1515

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,16 @@ export function getGlobalFlags(
1414
// Last takes precedence/overrides previous ones
1515
return [
1616
...(configs.get<string[]>("coder.globalFlags") || []),
17-
...["--global-config", escapeCommandArg(configDir)],
17+
"--global-config",
18+
escapeCommandArg(configDir),
1819
...getHeaderArgs(configs),
1920
];
2021
}
22+
23+
/**
24+
* Returns SSH flags for the `coder ssh` command from user configuration.
25+
*/
26+
export function getSshFlags(configs: WorkspaceConfiguration): string[] {
27+
// Make sure to match this default with the one in the package.json
28+
return configs.get<string[]>("coder.sshFlags", ["--disable-autostart"]);
29+
}

src/commands.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,14 @@ import * as vscode from "vscode";
1010
import { createWorkspaceIdentifier, extractAgents } from "./api/api-helper";
1111
import { CoderApi } from "./api/coderApi";
1212
import { needToken } from "./api/utils";
13+
import { getGlobalFlags } from "./cliConfig";
1314
import { type CliManager } from "./core/cliManager";
1415
import { type ServiceContainer } from "./core/container";
1516
import { type ContextManager } from "./core/contextManager";
1617
import { type MementoManager } from "./core/mementoManager";
1718
import { type PathResolver } from "./core/pathResolver";
1819
import { type SecretsManager } from "./core/secretsManager";
1920
import { CertificateError } from "./error";
20-
import { getGlobalFlags } from "./globalFlags";
2121
import { type Logger } from "./logging/logger";
2222
import { maybeAskAgent, maybeAskUrl } from "./promptUtils";
2323
import { escapeCommandArg, toRemoteAuthority, toSafeHost } from "./util";

src/remote/remote.ts

Lines changed: 107 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,14 @@ import {
2020
import { extractAgents } from "../api/api-helper";
2121
import { CoderApi } from "../api/coderApi";
2222
import { needToken } from "../api/utils";
23+
import { getGlobalFlags, getSshFlags } from "../cliConfig";
2324
import { type Commands } from "../commands";
2425
import { type CliManager } from "../core/cliManager";
2526
import * as cliUtils from "../core/cliUtils";
2627
import { type ServiceContainer } from "../core/container";
2728
import { type ContextManager } from "../core/contextManager";
2829
import { type PathResolver } from "../core/pathResolver";
2930
import { featureSetForVersion, type FeatureSet } from "../featureSet";
30-
import { getGlobalFlags } from "../globalFlags";
3131
import { Inbox } from "../inbox";
3232
import { type Logger } from "../logging/logger";
3333
import {
@@ -501,8 +501,6 @@ export class Remote {
501501
sshMonitor.onLogFilePathChange((newPath) => {
502502
this.commands.workspaceLogPath = newPath;
503503
}),
504-
// Watch for logDir configuration changes
505-
this.watchLogDirSetting(logDir, featureSet),
506504
// Register the label formatter again because SSH overrides it!
507505
vscode.extensions.onDidChange(() => {
508506
// Dispose previous label formatter
@@ -516,6 +514,18 @@ export class Remote {
516514
}),
517515
...(await this.createAgentMetadataStatusBar(agent, workspaceClient)),
518516
);
517+
518+
const settingsToWatch = [
519+
{ setting: "coder.globalFlags", title: "Global flags" },
520+
{ setting: "coder.sshFlags", title: "SSH flags" },
521+
];
522+
if (featureSet.proxyLogDirectory) {
523+
settingsToWatch.push({
524+
setting: "coder.proxyLogDirectory",
525+
title: "Proxy log directory",
526+
});
527+
}
528+
disposables.push(this.watchSettings(settingsToWatch));
519529
} catch (ex) {
520530
// Whatever error happens, make sure we clean up the disposables in case of failure
521531
disposables.forEach((d) => d.dispose());
@@ -554,8 +564,10 @@ export class Remote {
554564
}
555565

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

573585
/**
574-
* Formats the --log-dir argument for the ProxyCommand after making sure it
586+
* Builds the ProxyCommand for SSH connections to Coder workspaces.
587+
* Uses `coder ssh` for modern deployments with wildcard support,
588+
* or falls back to `coder vscodessh` for older deployments.
589+
*/
590+
private async buildProxyCommand(
591+
binaryPath: string,
592+
label: string,
593+
hostPrefix: string,
594+
logDir: string,
595+
useWildcardSSH: boolean,
596+
): Promise<string> {
597+
const vscodeConfig = vscode.workspace.getConfiguration();
598+
599+
const escapedBinaryPath = escapeCommandArg(binaryPath);
600+
const globalConfig = getGlobalFlags(
601+
vscodeConfig,
602+
this.pathResolver.getGlobalConfigDir(label),
603+
);
604+
const logArgs = await this.getLogArgs(logDir);
605+
606+
if (useWildcardSSH) {
607+
// User SSH flags are included first; internally-managed flags
608+
// are appended last so they take precedence.
609+
const userSshFlags = getSshFlags(vscodeConfig);
610+
// Make sure to update the `coder.sshFlags` description if we add more internal flags here!
611+
const internalFlags = [
612+
"--stdio",
613+
"--usage-app=vscode",
614+
"--network-info-dir",
615+
escapeCommandArg(this.pathResolver.getNetworkInfoPath()),
616+
...logArgs,
617+
"--ssh-host-prefix",
618+
hostPrefix,
619+
"%h",
620+
];
621+
622+
const allFlags = [...userSshFlags, ...internalFlags];
623+
return `${escapedBinaryPath} ${globalConfig.join(" ")} ssh ${allFlags.join(" ")}`;
624+
} else {
625+
const networkInfoDir = escapeCommandArg(
626+
this.pathResolver.getNetworkInfoPath(),
627+
);
628+
const sessionTokenFile = escapeCommandArg(
629+
this.pathResolver.getSessionTokenPath(label),
630+
);
631+
const urlFile = escapeCommandArg(this.pathResolver.getUrlPath(label));
632+
633+
const sshFlags = [
634+
"--network-info-dir",
635+
networkInfoDir,
636+
...logArgs,
637+
"--session-token-file",
638+
sessionTokenFile,
639+
"--url-file",
640+
urlFile,
641+
"%h",
642+
];
643+
644+
return `${escapedBinaryPath} ${globalConfig.join(" ")} vscodessh ${sshFlags.join(" ")}`;
645+
}
646+
}
647+
648+
/**
649+
* Returns the --log-dir argument for the ProxyCommand after making sure it
575650
* has been created.
576651
*/
577-
private async formatLogArg(logDir: string): Promise<string> {
652+
private async getLogArgs(logDir: string): Promise<string[]> {
578653
if (!logDir) {
579-
return "";
654+
return [];
580655
}
581656
await fs.mkdir(logDir, { recursive: true });
582657
this.logger.info("SSH proxy diagnostics are being written to", logDir);
583-
return ` --log-dir ${escapeCommandArg(logDir)} -v`;
658+
return ["--log-dir", escapeCommandArg(logDir), "-v"];
584659
}
585660

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

669-
const globalConfigs = this.globalConfigs(label);
670-
671-
const proxyCommand = featureSet.wildcardSSH
672-
? `${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`
673-
: `${escapeCommandArg(binaryPath)}${globalConfigs} vscodessh --network-info-dir ${escapeCommandArg(
674-
this.pathResolver.getNetworkInfoPath(),
675-
)}${await this.formatLogArg(logDir)} --session-token-file ${escapeCommandArg(this.pathResolver.getSessionTokenPath(label))} --url-file ${escapeCommandArg(
676-
this.pathResolver.getUrlPath(label),
677-
)} %h`;
744+
const proxyCommand = await this.buildProxyCommand(
745+
binaryPath,
746+
label,
747+
hostPrefix,
748+
logDir,
749+
featureSet.wildcardSSH,
750+
);
678751

679752
const sshValues: SSHValues = {
680753
Host: hostPrefix + `*`,
@@ -727,38 +800,26 @@ export class Remote {
727800
return sshConfig.getRaw();
728801
}
729802

730-
private globalConfigs(label: string): string {
731-
const vscodeConfig = vscode.workspace.getConfiguration();
732-
const args = getGlobalFlags(
733-
vscodeConfig,
734-
this.pathResolver.getGlobalConfigDir(label),
735-
);
736-
return ` ${args.join(" ")}`;
737-
}
738-
739-
private watchLogDirSetting(
740-
currentLogDir: string,
741-
featureSet: FeatureSet,
803+
private watchSettings(
804+
settings: Array<{ setting: string; title: string }>,
742805
): vscode.Disposable {
743806
return vscode.workspace.onDidChangeConfiguration((e) => {
744-
if (!e.affectsConfiguration("coder.proxyLogDirectory")) {
745-
return;
746-
}
747-
const newLogDir = this.getLogDir(featureSet);
748-
if (newLogDir === currentLogDir) {
749-
return;
807+
for (const { setting, title } of settings) {
808+
if (!e.affectsConfiguration(setting)) {
809+
continue;
810+
}
811+
vscode.window
812+
.showInformationMessage(
813+
`${title} setting changed. Reload window to apply.`,
814+
"Reload",
815+
)
816+
.then((action) => {
817+
if (action === "Reload") {
818+
vscode.commands.executeCommand("workbench.action.reloadWindow");
819+
}
820+
});
821+
break;
750822
}
751-
752-
vscode.window
753-
.showInformationMessage(
754-
"Log directory configuration changed. Reload window to apply.",
755-
"Reload",
756-
)
757-
.then((action) => {
758-
if (action === "Reload") {
759-
vscode.commands.executeCommand("workbench.action.reloadWindow");
760-
}
761-
});
762823
});
763824
}
764825

0 commit comments

Comments
 (0)