Skip to content

Commit aab5916

Browse files
authored
impl: add the option to disable ssh wildcard configuration (#584)
* impl: add the option to disable ssh wildcard configuration It will be used later by the Coder Settings view to allow users to enable or disable SSH hostname wildcard configuration. * impl: expose ssh wildcard config in the Settings page Updated the UI component to allow configuration by the user * impl: take into account wildcard configuration when generating the ssh config for Coder Gateway. Up until now we just checked if the Coder deployment supports this feature, but now users have to option to continue to use expanded hostnames in the ssh config. * fix: force CLI manager to use user settings CLIManager can be created with default settings (simplifies testing), among which the ssh wildcard config is enabled. But in reality the config can be disabled by the user. * impl: ability to start a recent workspace connection after ssh wildcard config was changed Currently, if a user starts a connection with wildcard enabled and then later on it disables the wildcard config then the recent connections becomes unusable because the hostnames are invalid. The issue can reproduce the other way around as well (start with wildcard ssh config disabled, start an IDE and then later on enable wildcard config) This commit addresses the issue by resolving the hostname on demand when the user wants to open the remote IDE from the recent connections panel. * chore: update README * chore: next version is 2.23.0 * fix: don't show twice the connection to the same workspace Connections started with two different hostnames (because of the ssh wildcard config) can be rendered twice in the Recent projects panel. With this commit we ignore the hostname and instead use the workspace name and deployment URL.
1 parent 9bff367 commit aab5916

File tree

13 files changed

+99
-30
lines changed

13 files changed

+99
-30
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44

55
## Unreleased
66

7+
### Added
8+
9+
- support for disabling SSH wildcard config.
10+
711
## 2.22.3 - 2025-09-19
812

913
### Fixed

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ pluginGroup=com.coder.gateway
55
artifactName=coder-gateway
66
pluginName=Coder
77
# SemVer format -> https://semver.org
8-
pluginVersion=2.22.3
8+
pluginVersion=2.23.0
99
# See https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html
1010
# for insight into build numbers and IntelliJ Platform versions.
1111
pluginSinceBuild=243.26574

src/main/kotlin/com/coder/gateway/CoderSettingsConfigurable.kt

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -120,13 +120,24 @@ class CoderSettingsConfigurable : BoundConfigurable("Coder") {
120120
CoderGatewayBundle.message("gateway.connector.settings.tls-alt-name.comment"),
121121
)
122122
}.layout(RowLayout.PARENT_GRID)
123-
row(CoderGatewayBundle.message("gateway.connector.settings.disable-autostart.heading")) {
124-
checkBox(CoderGatewayBundle.message("gateway.connector.settings.disable-autostart.title"))
125-
.bindSelected(state::disableAutostart)
126-
.comment(
127-
CoderGatewayBundle.message("gateway.connector.settings.disable-autostart.comment"),
128-
)
129-
}.layout(RowLayout.PARENT_GRID)
123+
group {
124+
row {
125+
cell() // For alignment.
126+
checkBox(CoderGatewayBundle.message("gateway.connector.settings.disable-autostart.title"))
127+
.bindSelected(state::disableAutostart)
128+
.comment(
129+
CoderGatewayBundle.message("gateway.connector.settings.disable-autostart.comment"),
130+
)
131+
}.layout(RowLayout.PARENT_GRID)
132+
row {
133+
cell() // For alignment.
134+
checkBox(CoderGatewayBundle.message("gateway.connector.settings.wildcard-config.title"))
135+
.bindSelected(state::isSshWildcardConfigEnabled)
136+
.comment(
137+
CoderGatewayBundle.message("gateway.connector.settings.wildcard-config.comment"),
138+
)
139+
}.layout(RowLayout.PARENT_GRID)
140+
}
130141
row(CoderGatewayBundle.message("gateway.connector.settings.ssh-config-options.title")) {
131142
textArea().resizableColumn().align(AlignX.FILL)
132143
.bindText(state::sshConfigOptions)

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

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import com.coder.gateway.sdk.v2.models.User
1212
import com.coder.gateway.sdk.v2.models.Workspace
1313
import com.coder.gateway.sdk.v2.models.WorkspaceAgent
1414
import com.coder.gateway.settings.CoderSettings
15-
import com.coder.gateway.settings.CoderSettingsState
1615
import com.coder.gateway.util.CoderHostnameVerifier
1716
import com.coder.gateway.util.DialogUi
1817
import com.coder.gateway.util.InvalidVersionException
@@ -129,7 +128,7 @@ class CoderCLIManager(
129128
// The URL of the deployment this CLI is for.
130129
private val deploymentURL: URL,
131130
// Plugin configuration.
132-
private val settings: CoderSettings = CoderSettings(CoderSettingsState()),
131+
private val settings: CoderSettings,
133132
// If the binary directory is not writable, this can be used to force the
134133
// manager to download to the data directory instead.
135134
private val forceDownloadToData: Boolean = false,
@@ -373,7 +372,7 @@ class CoderCLIManager(
373372
SetEnv CODER_SSH_SESSION_TYPE=JetBrains
374373
""".trimIndent()
375374
val blockContent =
376-
if (feats.wildcardSSH) {
375+
if (settings.isSshWildcardConfigEnabled && feats.wildcardSSH) {
377376
startBlock + System.lineSeparator() +
378377
"""
379378
Host ${getHostPrefix()}--*
@@ -622,7 +621,7 @@ class CoderCLIManager(
622621
workspace: Workspace,
623622
currentUser: User,
624623
agent: WorkspaceAgent,
625-
): String = if (features.wildcardSSH) {
624+
): String = if (settings.isSshWildcardConfigEnabled && features.wildcardSSH) {
626625
"${getHostPrefix()}--${workspace.ownerName}--${workspace.name}.${agent.name}"
627626
} else {
628627
// For a user's own workspace, we use the old syntax without a username for backwards compatibility,
@@ -638,7 +637,7 @@ class CoderCLIManager(
638637
workspace: Workspace,
639638
currentUser: User,
640639
agent: WorkspaceAgent,
641-
): String = if (features.wildcardSSH) {
640+
): String = if (settings.isSshWildcardConfigEnabled && features.wildcardSSH) {
642641
"${getHostPrefix()}-bg--${workspace.ownerName}--${workspace.name}.${agent.name}"
643642
} else {
644643
getHostName(workspace, currentUser, agent) + "--bg"

src/main/kotlin/com/coder/gateway/models/RecentWorkspaceConnection.kt

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,8 @@ class RecentWorkspaceConnection(
8282

8383
other as RecentWorkspaceConnection
8484

85-
if (coderWorkspaceHostname != other.coderWorkspaceHostname) return false
85+
if (name != other.name) return false
86+
if (deploymentURL != other.deploymentURL) return false
8687
if (projectPath != other.projectPath) return false
8788
if (ideProductCode != other.ideProductCode) return false
8889
if (ideBuildNumber != other.ideBuildNumber) return false
@@ -92,7 +93,8 @@ class RecentWorkspaceConnection(
9293

9394
override fun hashCode(): Int {
9495
var result = super.hashCode()
95-
result = 31 * result + (coderWorkspaceHostname?.hashCode() ?: 0)
96+
result = 31 * result + (name?.hashCode() ?: 0)
97+
result = 31 * result + (deploymentURL?.hashCode() ?: 0)
9698
result = 31 * result + (projectPath?.hashCode() ?: 0)
9799
result = 31 * result + (ideProductCode?.hashCode() ?: 0)
98100
result = 31 * result + (ideBuildNumber?.hashCode() ?: 0)
@@ -101,18 +103,21 @@ class RecentWorkspaceConnection(
101103
}
102104

103105
override fun compareTo(other: RecentWorkspaceConnection): Int {
104-
val i = other.coderWorkspaceHostname?.let { coderWorkspaceHostname?.compareTo(it) }
106+
val i = other.name?.let { name?.compareTo(it) }
105107
if (i != null && i != 0) return i
106108

107-
val j = other.projectPath?.let { projectPath?.compareTo(it) }
109+
val j = other.deploymentURL?.let { deploymentURL?.compareTo(it) }
108110
if (j != null && j != 0) return j
109111

110-
val k = other.ideProductCode?.let { ideProductCode?.compareTo(it) }
112+
val k = other.projectPath?.let { projectPath?.compareTo(it) }
111113
if (k != null && k != 0) return k
112114

113-
val l = other.ideBuildNumber?.let { ideBuildNumber?.compareTo(it) }
115+
val l = other.ideProductCode?.let { ideProductCode?.compareTo(it) }
114116
if (l != null && l != 0) return l
115117

118+
val m = other.ideBuildNumber?.let { ideBuildNumber?.compareTo(it) }
119+
if (m != null && m != 0) return m
120+
116121
return 0
117122
}
118123
}

src/main/kotlin/com/coder/gateway/models/WorkspaceProjectIDE.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ private val NON_STABLE_RELEASE_TYPES = setOf("EAP", "RC", "NIGHTLY", "PREVIEW")
1818
* Validated parameters for downloading and opening a project using an IDE on a
1919
* workspace.
2020
*/
21-
class WorkspaceProjectIDE(
21+
data class WorkspaceProjectIDE(
2222
// Either `workspace.agent` for old connections or `user/workspace.agent`
2323
// for new connections.
2424
val name: String,

src/main/kotlin/com/coder/gateway/settings/CoderSettings.kt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,12 @@ open class CoderSettingsState(
9898
// around issues on macOS where it periodically wakes and Gateway
9999
// reconnects, keeping the workspace constantly up.
100100
open var disableAutostart: Boolean = getOS() == OS.MAC,
101+
102+
/**
103+
* Whether SSH wildcard config is enabled
104+
*/
105+
open var isSshWildcardConfigEnabled: Boolean = true,
106+
101107
// Extra SSH config options.
102108
open var sshConfigOptions: String = "",
103109
// An external command to run in the directory of the IDE before connecting
@@ -199,6 +205,12 @@ open class CoderSettings(
199205
val disableAutostart: Boolean
200206
get() = state.disableAutostart
201207

208+
/**
209+
* Whether SSH wildcard config is enabled
210+
*/
211+
val isSshWildcardConfigEnabled: Boolean
212+
get() = state.isSshWildcardConfigEnabled
213+
202214
/**
203215
* Extra SSH config to append to each host block.
204216
*/

src/main/kotlin/com/coder/gateway/util/LinkHandler.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ open class LinkHandler(
7474
var workspace: Workspace
7575
var workspaces: List<Workspace> = emptyList()
7676
var workspacesAndAgents: Set<Pair<Workspace, WorkspaceAgent>> = emptySet()
77-
if (cli.features.wildcardSSH) {
77+
if (settings.isSshWildcardConfigEnabled && cli.features.wildcardSSH) {
7878
workspace = client.workspaceByOwnerAndName(owner, workspaceName)
7979
} else {
8080
workspaces = client.workspaces()

src/main/kotlin/com/coder/gateway/util/Without.kt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,22 @@ fun <A, Z> withoutNull(
1414
return block(a)
1515
}
1616

17+
/**
18+
* Run block with provided arguments after checking they are all non-null. This
19+
* is to enforce non-null values and should be used to signify developer error.
20+
*/
21+
fun <A, B, C, Z> withoutNull(
22+
a: A?,
23+
b: B?,
24+
c: C?,
25+
block: (a: A, b: B, c: C) -> Z,
26+
): Z {
27+
if (a == null || b == null || c == null) {
28+
throw Exception("Unexpected null value")
29+
}
30+
return block(a, b, c)
31+
}
32+
1733
/**
1834
* Run block with provided arguments after checking they are all non-null. This
1935
* is to enforce non-null values and should be used to signify developer error.

src/main/kotlin/com/coder/gateway/views/CoderGatewayRecentWorkspaceConnectionsView.kt

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package com.coder.gateway.views
55
import com.coder.gateway.CoderGatewayBundle
66
import com.coder.gateway.CoderGatewayConstants
77
import com.coder.gateway.CoderRemoteConnectionHandle
8+
import com.coder.gateway.cli.CoderCLIManager
89
import com.coder.gateway.cli.ensureCLI
910
import com.coder.gateway.icons.CoderIcons
1011
import com.coder.gateway.models.WorkspaceAgentListModel
@@ -177,6 +178,7 @@ class CoderGatewayRecentWorkspaceConnectionsView(private val setContentCallback:
177178
it.workspace.ownerName + "/" + it.workspace.name == workspaceName ||
178179
(it.workspace.ownerName == me && it.workspace.name == workspaceName)
179180
}
181+
180182
val status =
181183
if (deploymentError != null) {
182184
Triple(UIUtil.getErrorForeground(), deploymentError, UIUtil.getBalloonErrorIcon())
@@ -250,10 +252,11 @@ class CoderGatewayRecentWorkspaceConnectionsView(private val setContentCallback:
250252
ActionLink(workspaceProjectIDE.projectPathDisplay) {
251253
withoutNull(
252254
deployment?.client,
253-
workspaceWithAgent?.workspace
254-
) { client, workspace ->
255+
workspaceWithAgent?.workspace,
256+
workspaceWithAgent?.agent
257+
) { client, workspace, agent ->
255258
CoderRemoteConnectionHandle().connect {
256-
if (listOf(
259+
val cli = if (listOf(
257260
WorkspaceStatus.STOPPED,
258261
WorkspaceStatus.CANCELED,
259262
WorkspaceStatus.FAILED
@@ -272,8 +275,21 @@ class CoderGatewayRecentWorkspaceConnectionsView(private val setContentCallback:
272275
}
273276

274277
cli.startWorkspace(workspace.ownerName, workspace.name)
278+
cli
279+
} else {
280+
CoderCLIManager(deploymentURL.toURL(), settings)
275281
}
276-
workspaceProjectIDE
282+
// the ssh config could have changed in the meantime
283+
// so we want to make sure we use a proper hostname
284+
// depending on whether the ssh wildcard config
285+
// is enabled, otherwise the connection will fail.
286+
workspaceProjectIDE.copy(
287+
hostname = cli.getHostName(
288+
workspace,
289+
client.me(),
290+
agent
291+
)
292+
)
277293
}
278294
GatewayUI.getInstance().reset()
279295
}

0 commit comments

Comments
 (0)