Skip to content

Commit ccbb835

Browse files
authored
fix: hostname and proxy command generation (#95)
- latest Coder versions won't accept the SSH connections if proxy command does not include the workspace owner name - for wildcard configuration the ssh config stays the same but the actual hostname provided to the Toolbox will include the workspace owner name For wildcard config: - the ssh config hostname follows the `coder-jetbrains-toolbox-dev.coder.com--*` pattern - the proxy command will have a similar host prefix - the hostname provided to Toolbox follows the `coder-jetbrains-toolbox-dev.coder.com--${ws.ownerName}--${ws.name}.${agent.name}` pattern For non wildcard config: - the ssh config hostname follows the `coder-jetbrains-toolbox--${ws.ownerName}--${ws.name}.${agent.name}--dev.coder.com` pattern - the proxy command will have the username and hostname in the ${ws.ownerName}/${ws.name}.${agent.name} format - the hostname provided to Toolbox follows the `coder-jetbrains-toolbox-dev.coder.com--${ws.ownerName}--${ws.name}.${agent.name}` pattern - resolves #94
1 parent d5fddf4 commit ccbb835

31 files changed

+318
-152
lines changed

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@
1010

1111
- connections to the workspace are no longer established automatically after agent started with error.
1212

13+
### Fixed
14+
15+
- SSH connection will no longer fail with newer Coder deployments due to misconfiguration of hostname and proxy command.
16+
1317
## 0.1.5 - 2025-04-14
1418

1519
### Fixed

src/main/kotlin/com/coder/toolbox/CoderRemoteEnvironment.kt

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.coder.toolbox
22

33
import com.coder.toolbox.browser.BrowserUtil
4+
import com.coder.toolbox.cli.CoderCLIManager
45
import com.coder.toolbox.models.WorkspaceAndAgentStatus
56
import com.coder.toolbox.sdk.CoderRestClient
67
import com.coder.toolbox.sdk.ex.APIResponseException
@@ -35,6 +36,7 @@ import kotlin.time.Duration.Companion.seconds
3536
class CoderRemoteEnvironment(
3637
private val context: CoderToolboxContext,
3738
private val client: CoderRestClient,
39+
private val cli: CoderCLIManager,
3840
private var workspace: Workspace,
3941
private var agent: WorkspaceAgent,
4042
) : RemoteProviderEnvironment("${workspace.name}.${agent.name}"), BeforeConnectionHook, AfterDisconnectHook {
@@ -48,6 +50,8 @@ class CoderRemoteEnvironment(
4850

4951
override val actionsList: MutableStateFlow<List<ActionDescription>> = MutableStateFlow(getAvailableActions())
5052

53+
fun asPairOfWorkspaceAndAgent(): Pair<Workspace, WorkspaceAgent> = Pair(workspace, agent)
54+
5155
private fun getAvailableActions(): List<ActionDescription> {
5256
val actions = mutableListOf(
5357
Action(context.i18n.ptrl("Open web terminal")) {
@@ -151,8 +155,8 @@ class CoderRemoteEnvironment(
151155
*/
152156
override suspend
153157
fun getContentsView(): EnvironmentContentsView = EnvironmentView(
154-
context.settingsStore.readOnly(),
155158
client.url,
159+
cli,
156160
workspace,
157161
agent
158162
)

src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt

+3-3
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ class CoderRemoteProvider(
9191
it.name
9292
}?.map { agent ->
9393
// If we have an environment already, update that.
94-
val env = CoderRemoteEnvironment(context, client, ws, agent)
94+
val env = CoderRemoteEnvironment(context, client, cli, ws, agent)
9595
lastEnvironments.firstOrNull { it == env }?.let {
9696
it.update(ws, agent)
9797
it
@@ -109,7 +109,7 @@ class CoderRemoteProvider(
109109
// Reconfigure if environments changed.
110110
if (lastEnvironments.size != resolvedEnvironments.size || lastEnvironments != resolvedEnvironments) {
111111
context.logger.info("Workspaces have changed, reconfiguring CLI: $resolvedEnvironments")
112-
cli.configSsh(resolvedEnvironments.map { it.name }.toSet())
112+
cli.configSsh(resolvedEnvironments.map { it.asPairOfWorkspaceAndAgent() }.toSet())
113113
}
114114

115115
environments.update {
@@ -149,7 +149,7 @@ class CoderRemoteProvider(
149149
triggerSshConfig.onReceive { shouldTrigger ->
150150
if (shouldTrigger) {
151151
context.logger.trace("workspace poller waked up because it should reconfigure the ssh configurations")
152-
cli.configSsh(lastEnvironments.map { it.name }.toSet())
152+
cli.configSsh(lastEnvironments.map { it.asPairOfWorkspaceAndAgent() }.toSet())
153153
}
154154
}
155155
}

src/main/kotlin/com/coder/toolbox/cli/CoderCLIManager.kt

+35-25
Original file line numberDiff line numberDiff line change
@@ -223,11 +223,11 @@ class CoderCLIManager(
223223
* This can take supported features for testing purposes only.
224224
*/
225225
fun configSsh(
226-
workspaceNames: Set<String>,
226+
wsWithAgents: Set<Pair<Workspace, WorkspaceAgent>>,
227227
feats: Features = features,
228228
) {
229229
logger.info("Configuring SSH config at ${settings.sshConfigPath}")
230-
writeSSHConfig(modifySSHConfig(readSSHConfig(), workspaceNames, feats))
230+
writeSSHConfig(modifySSHConfig(readSSHConfig(), wsWithAgents, feats))
231231
}
232232

233233
/**
@@ -249,13 +249,13 @@ class CoderCLIManager(
249249
*/
250250
private fun modifySSHConfig(
251251
contents: String?,
252-
workspaceNames: Set<String>,
252+
wsWithAgents: Set<Pair<Workspace, WorkspaceAgent>>,
253253
feats: Features,
254254
): String? {
255255
val host = deploymentURL.safeHost()
256256
val startBlock = "# --- START CODER JETBRAINS TOOLBOX $host"
257257
val endBlock = "# --- END CODER JETBRAINS TOOLBOX $host"
258-
val isRemoving = workspaceNames.isEmpty()
258+
val isRemoving = wsWithAgents.isEmpty()
259259
val baseArgs =
260260
listOfNotNull(
261261
escape(localBinaryPath.toString()),
@@ -304,34 +304,39 @@ class CoderCLIManager(
304304
.plus("\n\n")
305305
.plus(
306306
"""
307-
Host ${getHostnamePrefix(deploymentURL)}-bg--*
307+
Host ${getBackgroundHostnamePrefix(deploymentURL)}--*
308308
ProxyCommand ${backgroundProxyArgs.joinToString(" ")} --ssh-host-prefix ${
309-
getHostnamePrefix(
309+
getBackgroundHostnamePrefix(
310310
deploymentURL
311311
)
312-
}-bg-- %h
312+
}-- %h
313313
""".trimIndent()
314314
.plus("\n" + options.prependIndent(" "))
315315
.plus(extraConfig),
316316
).replace("\n", System.lineSeparator()) +
317317
System.lineSeparator() + endBlock
318318
} else {
319-
workspaceNames.joinToString(
319+
wsWithAgents.joinToString(
320320
System.lineSeparator(),
321321
startBlock + System.lineSeparator(),
322322
System.lineSeparator() + endBlock,
323323
transform = {
324324
"""
325-
Host ${getHostName(deploymentURL, it)}
326-
ProxyCommand ${proxyArgs.joinToString(" ")} $it
325+
Host ${getHostname(deploymentURL, it.workspace(), it.agent())}
326+
ProxyCommand ${proxyArgs.joinToString(" ")} ${getWsByOwner(it.workspace(), it.agent())}
327327
""".trimIndent()
328328
.plus("\n" + options.prependIndent(" "))
329329
.plus(extraConfig)
330330
.plus("\n")
331331
.plus(
332332
"""
333-
Host ${getBackgroundHostName(deploymentURL, it)}
334-
ProxyCommand ${backgroundProxyArgs.joinToString(" ")} $it
333+
Host ${getBackgroundHostname(deploymentURL, it.workspace(), it.agent())}
334+
ProxyCommand ${backgroundProxyArgs.joinToString(" ")} ${
335+
getWsByOwner(
336+
it.workspace(),
337+
it.agent()
338+
)
339+
}
335340
""".trimIndent()
336341
.plus("\n" + options.prependIndent(" "))
337342
.plus(extraConfig),
@@ -506,25 +511,30 @@ class CoderCLIManager(
506511
}
507512
}
508513

514+
fun getHostname(url: URL, ws: Workspace, agent: WorkspaceAgent): String {
515+
return if (settings.isSshWildcardConfigEnabled && features.wildcardSsh) {
516+
"${getHostnamePrefix(url)}--${ws.ownerName}--${ws.name}.${agent.name}"
517+
} else {
518+
"coder-jetbrains-toolbox--${ws.ownerName}--${ws.name}.${agent.name}--${url.safeHost()}"
519+
}
520+
}
521+
522+
fun getBackgroundHostname(url: URL, ws: Workspace, agent: WorkspaceAgent): String {
523+
return "${getHostname(url, ws, agent)}--bg"
524+
}
525+
509526
companion object {
510527
private val tokenRegex = "--token [^ ]+".toRegex()
511528

512-
fun getHostnamePrefix(url: URL): String = "coder-jetbrains-toolbox-${url.safeHost()}"
529+
private fun getHostnamePrefix(url: URL): String = "coder-jetbrains-toolbox-${url.safeHost()}"
513530

514-
fun getWildcardHostname(url: URL, workspace: Workspace, agent: WorkspaceAgent): String =
515-
"${getHostnamePrefix(url)}-bg--${workspace.name}.${agent.name}"
531+
private fun getBackgroundHostnamePrefix(url: URL): String = "coder-jetbrains-toolbox-${url.safeHost()}-bg"
516532

517-
fun getHostname(url: URL, workspace: Workspace, agent: WorkspaceAgent) =
518-
getHostName(url, "${workspace.name}.${agent.name}")
533+
private fun getWsByOwner(ws: Workspace, agent: WorkspaceAgent): String =
534+
"${ws.ownerName}/${ws.name}.${agent.name}"
519535

520-
fun getHostName(
521-
url: URL,
522-
workspaceName: String,
523-
): String = "coder-jetbrains-toolbox-$workspaceName--${url.safeHost()}"
536+
private fun Pair<Workspace, WorkspaceAgent>.workspace() = this.first
524537

525-
fun getBackgroundHostName(
526-
url: URL,
527-
workspaceName: String,
528-
): String = getHostName(url, workspaceName) + "--bg"
538+
private fun Pair<Workspace, WorkspaceAgent>.agent() = this.second
529539
}
530540
}

src/main/kotlin/com/coder/toolbox/sdk/CoderRestClient.kt

+9-5
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,10 @@ import com.coder.toolbox.sdk.v2.models.CreateWorkspaceBuildRequest
1313
import com.coder.toolbox.sdk.v2.models.Template
1414
import com.coder.toolbox.sdk.v2.models.User
1515
import com.coder.toolbox.sdk.v2.models.Workspace
16+
import com.coder.toolbox.sdk.v2.models.WorkspaceAgent
1617
import com.coder.toolbox.sdk.v2.models.WorkspaceBuild
1718
import com.coder.toolbox.sdk.v2.models.WorkspaceResource
19+
import com.coder.toolbox.sdk.v2.models.WorkspaceStatus
1820
import com.coder.toolbox.sdk.v2.models.WorkspaceTransition
1921
import com.coder.toolbox.util.CoderHostnameVerifier
2022
import com.coder.toolbox.util.coderSocketFactory
@@ -190,15 +192,17 @@ open class CoderRestClient(
190192
}
191193

192194
/**
193-
* Retrieves all the agent names for all workspaces, including those that
194-
* are off. Meant to be used when configuring SSH.
195+
* Maps the list of workspaces to the associated agents.
195196
*/
196-
suspend fun agentNames(workspaces: List<Workspace>): Set<String> {
197+
suspend fun groupByAgents(workspaces: List<Workspace>): Set<Pair<Workspace, WorkspaceAgent>> {
197198
// It is possible for there to be resources with duplicate names so we
198199
// need to use a set.
199200
return workspaces.flatMap { ws ->
200-
resources(ws).filter { it.agents != null }.flatMap { it.agents!! }.map {
201-
"${ws.name}.${it.name}"
201+
when (ws.latestBuild.status) {
202+
WorkspaceStatus.RUNNING -> ws.latestBuild.resources
203+
else -> resources(ws)
204+
}.filter { it.agents != null }.flatMap { it.agents!! }.map {
205+
ws to it
202206
}
203207
}.toSet()
204208
}

src/main/kotlin/com/coder/toolbox/util/CoderProtocolHandler.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ open class CoderProtocolHandler(
162162
}
163163

164164
context.logger.info("Configuring Coder CLI...")
165-
cli.configSsh(restClient.agentNames(workspaces))
165+
cli.configSsh(restClient.groupByAgents(workspaces))
166166

167167
if (shouldWaitForAutoLogin) {
168168
isInitialized.waitForTrue()

src/main/kotlin/com/coder/toolbox/views/EnvironmentView.kt

+4-11
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package com.coder.toolbox.views
33
import com.coder.toolbox.cli.CoderCLIManager
44
import com.coder.toolbox.sdk.v2.models.Workspace
55
import com.coder.toolbox.sdk.v2.models.WorkspaceAgent
6-
import com.coder.toolbox.settings.ReadOnlyCoderSettings
76
import com.jetbrains.toolbox.api.remoteDev.environments.SshEnvironmentContentsView
87
import com.jetbrains.toolbox.api.remoteDev.ssh.SshConnectionInfo
98
import java.net.URL
@@ -17,16 +16,16 @@ import java.net.URL
1716
* SSH must be configured before this will work.
1817
*/
1918
class EnvironmentView(
20-
private val settings: ReadOnlyCoderSettings,
2119
private val url: URL,
20+
private val cli: CoderCLIManager,
2221
private val workspace: Workspace,
2322
private val agent: WorkspaceAgent,
2423
) : SshEnvironmentContentsView {
2524
override suspend fun getConnectionInfo(): SshConnectionInfo = object : SshConnectionInfo {
2625
/**
2726
* The host name generated by the cli manager for this workspace.
2827
*/
29-
override val host: String = resolveHost()
28+
override val host: String = cli.getHostname(url, workspace, agent)
3029

3130
/**
3231
* The port is ignored by the Coder proxy command.
@@ -36,12 +35,6 @@ class EnvironmentView(
3635
/**
3736
* The username is ignored by the Coder proxy command.
3837
*/
39-
override val userName: String? = "coder"
40-
38+
override val userName: String? = null
4139
}
42-
43-
private fun resolveHost(): String =
44-
if (settings.isSshWildcardConfigEnabled)
45-
CoderCLIManager.getWildcardHostname(url, workspace, agent)
46-
else CoderCLIManager.getHostname(url, workspace, agent)
47-
}
40+
}

0 commit comments

Comments
 (0)