Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@

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

### Fixed

- SSH connection will no longer fail with newer Coder deployments due to misconfiguration of hostname and proxy command.

## 0.1.5 - 2025-04-14

### Fixed
Expand Down
2 changes: 2 additions & 0 deletions src/main/kotlin/com/coder/toolbox/CoderRemoteEnvironment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ class CoderRemoteEnvironment(

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

fun asPairOfWorkspaceAndAgent(): Pair<Workspace, WorkspaceAgent> = Pair(workspace, agent)

private fun getAvailableActions(): List<ActionDescription> {
val actions = mutableListOf(
Action(context.i18n.ptrl("Open web terminal")) {
Expand Down
4 changes: 2 additions & 2 deletions src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ class CoderRemoteProvider(
// Reconfigure if environments changed.
if (lastEnvironments.size != resolvedEnvironments.size || lastEnvironments != resolvedEnvironments) {
context.logger.info("Workspaces have changed, reconfiguring CLI: $resolvedEnvironments")
cli.configSsh(resolvedEnvironments.map { it.name }.toSet())
cli.configSsh(resolvedEnvironments.map { it.asPairOfWorkspaceAndAgent() }.toSet())
}

environments.update {
Expand Down Expand Up @@ -149,7 +149,7 @@ class CoderRemoteProvider(
triggerSshConfig.onReceive { shouldTrigger ->
if (shouldTrigger) {
context.logger.trace("workspace poller waked up because it should reconfigure the ssh configurations")
cli.configSsh(lastEnvironments.map { it.name }.toSet())
cli.configSsh(lastEnvironments.map { it.asPairOfWorkspaceAndAgent() }.toSet())
}
}
}
Expand Down
58 changes: 33 additions & 25 deletions src/main/kotlin/com/coder/toolbox/cli/CoderCLIManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -223,11 +223,11 @@ class CoderCLIManager(
* This can take supported features for testing purposes only.
*/
fun configSsh(
workspaceNames: Set<String>,
wsWithAgents: Set<Pair<Workspace, WorkspaceAgent>>,
feats: Features = features,
) {
logger.info("Configuring SSH config at ${settings.sshConfigPath}")
writeSSHConfig(modifySSHConfig(readSSHConfig(), workspaceNames, feats))
writeSSHConfig(modifySSHConfig(readSSHConfig(), wsWithAgents, feats))
}

/**
Expand All @@ -249,13 +249,13 @@ class CoderCLIManager(
*/
private fun modifySSHConfig(
contents: String?,
workspaceNames: Set<String>,
wsWithAgents: Set<Pair<Workspace, WorkspaceAgent>>,
feats: Features,
): String? {
val host = deploymentURL.safeHost()
val startBlock = "# --- START CODER JETBRAINS TOOLBOX $host"
val endBlock = "# --- END CODER JETBRAINS TOOLBOX $host"
val isRemoving = workspaceNames.isEmpty()
val isRemoving = wsWithAgents.isEmpty()
val baseArgs =
listOfNotNull(
escape(localBinaryPath.toString()),
Expand Down Expand Up @@ -304,34 +304,39 @@ class CoderCLIManager(
.plus("\n\n")
.plus(
"""
Host ${getHostnamePrefix(deploymentURL)}-bg--*
Host ${getBackgroundHostnamePrefix(deploymentURL)}--*
ProxyCommand ${backgroundProxyArgs.joinToString(" ")} --ssh-host-prefix ${
getHostnamePrefix(
getBackgroundHostnamePrefix(
deploymentURL
)
}-bg-- %h
}-- %h
""".trimIndent()
.plus("\n" + options.prependIndent(" "))
.plus(extraConfig),
).replace("\n", System.lineSeparator()) +
System.lineSeparator() + endBlock
} else {
workspaceNames.joinToString(
wsWithAgents.joinToString(
System.lineSeparator(),
startBlock + System.lineSeparator(),
System.lineSeparator() + endBlock,
transform = {
"""
Host ${getHostName(deploymentURL, it)}
ProxyCommand ${proxyArgs.joinToString(" ")} $it
Host ${getHostname(deploymentURL, it.workspace(), it.agent())}
ProxyCommand ${proxyArgs.joinToString(" ")} ${getWsByOwner(it.workspace(), it.agent())}
""".trimIndent()
.plus("\n" + options.prependIndent(" "))
.plus(extraConfig)
.plus("\n")
.plus(
"""
Host ${getBackgroundHostName(deploymentURL, it)}
ProxyCommand ${backgroundProxyArgs.joinToString(" ")} $it
Host ${getBackgroundHostname(deploymentURL, it.workspace(), it.agent())}
ProxyCommand ${backgroundProxyArgs.joinToString(" ")} ${
getWsByOwner(
it.workspace(),
it.agent()
)
}
""".trimIndent()
.plus("\n" + options.prependIndent(" "))
.plus(extraConfig),
Expand Down Expand Up @@ -509,22 +514,25 @@ class CoderCLIManager(
companion object {
private val tokenRegex = "--token [^ ]+".toRegex()

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

private fun getBackgroundHostnamePrefix(url: URL): String = "coder-jetbrains-toolbox-${url.safeHost()}-bg"

fun getWildcardHostname(url: URL, ws: Workspace, agent: WorkspaceAgent): String =
"${getHostnamePrefix(url)}--${ws.ownerName}--${ws.name}.${agent.name}"

fun getWildcardHostname(url: URL, workspace: Workspace, agent: WorkspaceAgent): String =
"${getHostnamePrefix(url)}-bg--${workspace.name}.${agent.name}"
fun getHostname(url: URL, ws: Workspace, agent: WorkspaceAgent): String {
return "coder-jetbrains-toolbox--${ws.ownerName}--${ws.name}.${agent.name}--${url.safeHost()}"
}

fun getBackgroundHostname(url: URL, ws: Workspace, agent: WorkspaceAgent): String {
return "${getHostname(url, ws, agent)}--bg"
}

fun getHostname(url: URL, workspace: Workspace, agent: WorkspaceAgent) =
getHostName(url, "${workspace.name}.${agent.name}")
private fun getWsByOwner(ws: Workspace, agent: WorkspaceAgent): String = "${ws.ownerName}/${ws.name}.${agent.name}"

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

fun getBackgroundHostName(
url: URL,
workspaceName: String,
): String = getHostName(url, workspaceName) + "--bg"
private fun Pair<Workspace, WorkspaceAgent>.agent() = this.second
}
}
14 changes: 9 additions & 5 deletions src/main/kotlin/com/coder/toolbox/sdk/CoderRestClient.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@ import com.coder.toolbox.sdk.v2.models.CreateWorkspaceBuildRequest
import com.coder.toolbox.sdk.v2.models.Template
import com.coder.toolbox.sdk.v2.models.User
import com.coder.toolbox.sdk.v2.models.Workspace
import com.coder.toolbox.sdk.v2.models.WorkspaceAgent
import com.coder.toolbox.sdk.v2.models.WorkspaceBuild
import com.coder.toolbox.sdk.v2.models.WorkspaceResource
import com.coder.toolbox.sdk.v2.models.WorkspaceStatus
import com.coder.toolbox.sdk.v2.models.WorkspaceTransition
import com.coder.toolbox.util.CoderHostnameVerifier
import com.coder.toolbox.util.coderSocketFactory
Expand Down Expand Up @@ -190,15 +192,17 @@ open class CoderRestClient(
}

/**
* Retrieves all the agent names for all workspaces, including those that
* are off. Meant to be used when configuring SSH.
* Maps the list of workspaces to the associated agents.
*/
suspend fun agentNames(workspaces: List<Workspace>): Set<String> {
suspend fun groupByAgents(workspaces: List<Workspace>): Set<Pair<Workspace, WorkspaceAgent>> {
// It is possible for there to be resources with duplicate names so we
// need to use a set.
return workspaces.flatMap { ws ->
resources(ws).filter { it.agents != null }.flatMap { it.agents!! }.map {
"${ws.name}.${it.name}"
when (ws.latestBuild.status) {
WorkspaceStatus.RUNNING -> ws.latestBuild.resources
else -> resources(ws)
}.filter { it.agents != null }.flatMap { it.agents!! }.map {
ws to it
}
}.toSet()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ open class CoderProtocolHandler(
}

context.logger.info("Configuring Coder CLI...")
cli.configSsh(restClient.agentNames(workspaces))
cli.configSsh(restClient.groupByAgents(workspaces))

if (shouldWaitForAutoLogin) {
isInitialized.waitForTrue()
Expand Down
2 changes: 1 addition & 1 deletion src/main/kotlin/com/coder/toolbox/views/EnvironmentView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ class EnvironmentView(
/**
* The username is ignored by the Coder proxy command.
*/
override val userName: String? = "coder"
override val userName: String? = null

}

Expand Down
Loading
Loading