Skip to content
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@

## Unreleased

### Added

- support for disabling SSH wildcard config.

## 2.22.3 - 2025-09-19

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ pluginGroup=com.coder.gateway
artifactName=coder-gateway
pluginName=Coder
# SemVer format -> https://semver.org
pluginVersion=2.22.3
pluginVersion=2.23.0
# See https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html
# for insight into build numbers and IntelliJ Platform versions.
pluginSinceBuild=243.26574
Expand Down
25 changes: 18 additions & 7 deletions src/main/kotlin/com/coder/gateway/CoderSettingsConfigurable.kt
Original file line number Diff line number Diff line change
Expand Up @@ -120,13 +120,24 @@ class CoderSettingsConfigurable : BoundConfigurable("Coder") {
CoderGatewayBundle.message("gateway.connector.settings.tls-alt-name.comment"),
)
}.layout(RowLayout.PARENT_GRID)
row(CoderGatewayBundle.message("gateway.connector.settings.disable-autostart.heading")) {
checkBox(CoderGatewayBundle.message("gateway.connector.settings.disable-autostart.title"))
.bindSelected(state::disableAutostart)
.comment(
CoderGatewayBundle.message("gateway.connector.settings.disable-autostart.comment"),
)
}.layout(RowLayout.PARENT_GRID)
group {
row {
cell() // For alignment.
checkBox(CoderGatewayBundle.message("gateway.connector.settings.disable-autostart.title"))
.bindSelected(state::disableAutostart)
.comment(
CoderGatewayBundle.message("gateway.connector.settings.disable-autostart.comment"),
)
}.layout(RowLayout.PARENT_GRID)
row {
cell() // For alignment.
checkBox(CoderGatewayBundle.message("gateway.connector.settings.wildcard-config.title"))
.bindSelected(state::isSshWildcardConfigEnabled)
.comment(
CoderGatewayBundle.message("gateway.connector.settings.wildcard-config.comment"),
)
}.layout(RowLayout.PARENT_GRID)
}
row(CoderGatewayBundle.message("gateway.connector.settings.ssh-config-options.title")) {
textArea().resizableColumn().align(AlignX.FILL)
.bindText(state::sshConfigOptions)
Expand Down
9 changes: 4 additions & 5 deletions src/main/kotlin/com/coder/gateway/cli/CoderCLIManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
import com.coder.gateway.sdk.v2.models.Workspace
import com.coder.gateway.sdk.v2.models.WorkspaceAgent
import com.coder.gateway.settings.CoderSettings
import com.coder.gateway.settings.CoderSettingsState
import com.coder.gateway.util.CoderHostnameVerifier
import com.coder.gateway.util.DialogUi
import com.coder.gateway.util.InvalidVersionException
Expand Down Expand Up @@ -129,7 +128,7 @@
// The URL of the deployment this CLI is for.
private val deploymentURL: URL,
// Plugin configuration.
private val settings: CoderSettings = CoderSettings(CoderSettingsState()),
private val settings: CoderSettings,
// If the binary directory is not writable, this can be used to force the
// manager to download to the data directory instead.
private val forceDownloadToData: Boolean = false,
Expand Down Expand Up @@ -274,7 +273,7 @@

else -> {
val failure = result as VerificationResult.Failed
UnsignedBinaryExecutionDeniedException(result.error.message)

Check warning on line 276 in src/main/kotlin/com/coder/gateway/cli/CoderCLIManager.kt

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Throwable not thrown

Throwable instance 'UnsignedBinaryExecutionDeniedException' is not thrown
logger.error("Failed to verify signature for ${cliResult.dst}", failure.error)
}
}
Expand Down Expand Up @@ -373,7 +372,7 @@
SetEnv CODER_SSH_SESSION_TYPE=JetBrains
""".trimIndent()
val blockContent =
if (feats.wildcardSSH) {
if (settings.isSshWildcardConfigEnabled && feats.wildcardSSH) {
startBlock + System.lineSeparator() +
"""
Host ${getHostPrefix()}--*
Expand Down Expand Up @@ -571,7 +570,7 @@
coderConfigPath.toString(),
"start",
"--yes",
workspaceOwner + "/" + workspaceName

Check notice on line 573 in src/main/kotlin/com/coder/gateway/cli/CoderCLIManager.kt

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

String concatenation that can be converted to string template

'String' concatenation can be converted to a template
)

if (feats.buildReason) {
Expand Down Expand Up @@ -613,7 +612,7 @@
/*
* This function returns the ssh-host-prefix used for Host entries.
*/
fun getHostPrefix(): String = "coder-jetbrains-${deploymentURL.safeHost()}"

Check notice on line 615 in src/main/kotlin/com/coder/gateway/cli/CoderCLIManager.kt

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Class member can have 'private' visibility

Function 'getHostPrefix' could be private

/**
* This function returns the ssh host name generated for connecting to the workspace.
Expand All @@ -622,7 +621,7 @@
workspace: Workspace,
currentUser: User,
agent: WorkspaceAgent,
): String = if (features.wildcardSSH) {
): String = if (settings.isSshWildcardConfigEnabled && features.wildcardSSH) {
"${getHostPrefix()}--${workspace.ownerName}--${workspace.name}.${agent.name}"
} else {
// For a user's own workspace, we use the old syntax without a username for backwards compatibility,
Expand All @@ -638,7 +637,7 @@
workspace: Workspace,
currentUser: User,
agent: WorkspaceAgent,
): String = if (features.wildcardSSH) {
): String = if (settings.isSshWildcardConfigEnabled && features.wildcardSSH) {
"${getHostPrefix()}-bg--${workspace.ownerName}--${workspace.name}.${agent.name}"
} else {
getHostName(workspace, currentUser, agent) + "--bg"
Expand Down Expand Up @@ -669,7 +668,7 @@
}
// non-wildcard case
if (parts[0] == "coder-jetbrains") {
return hostname + "--bg"

Check notice on line 671 in src/main/kotlin/com/coder/gateway/cli/CoderCLIManager.kt

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

String concatenation that can be converted to string template

'String' concatenation can be converted to a template
}
// wildcard case
parts[0] += "-bg"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,8 @@ class RecentWorkspaceConnection(

other as RecentWorkspaceConnection

if (coderWorkspaceHostname != other.coderWorkspaceHostname) return false
if (name != other.name) return false
if (deploymentURL != other.deploymentURL) return false
if (projectPath != other.projectPath) return false
if (ideProductCode != other.ideProductCode) return false
if (ideBuildNumber != other.ideBuildNumber) return false
Expand All @@ -92,7 +93,8 @@ class RecentWorkspaceConnection(

override fun hashCode(): Int {
var result = super.hashCode()
result = 31 * result + (coderWorkspaceHostname?.hashCode() ?: 0)
result = 31 * result + (name?.hashCode() ?: 0)
result = 31 * result + (deploymentURL?.hashCode() ?: 0)
result = 31 * result + (projectPath?.hashCode() ?: 0)
result = 31 * result + (ideProductCode?.hashCode() ?: 0)
result = 31 * result + (ideBuildNumber?.hashCode() ?: 0)
Expand All @@ -101,18 +103,21 @@ class RecentWorkspaceConnection(
}

override fun compareTo(other: RecentWorkspaceConnection): Int {
val i = other.coderWorkspaceHostname?.let { coderWorkspaceHostname?.compareTo(it) }
val i = other.name?.let { name?.compareTo(it) }
if (i != null && i != 0) return i

val j = other.projectPath?.let { projectPath?.compareTo(it) }
val j = other.deploymentURL?.let { deploymentURL?.compareTo(it) }
if (j != null && j != 0) return j

val k = other.ideProductCode?.let { ideProductCode?.compareTo(it) }
val k = other.projectPath?.let { projectPath?.compareTo(it) }
if (k != null && k != 0) return k

val l = other.ideBuildNumber?.let { ideBuildNumber?.compareTo(it) }
val l = other.ideProductCode?.let { ideProductCode?.compareTo(it) }
if (l != null && l != 0) return l

val m = other.ideBuildNumber?.let { ideBuildNumber?.compareTo(it) }
if (m != null && m != 0) return m

return 0
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ private val NON_STABLE_RELEASE_TYPES = setOf("EAP", "RC", "NIGHTLY", "PREVIEW")
* Validated parameters for downloading and opening a project using an IDE on a
* workspace.
*/
class WorkspaceProjectIDE(
data class WorkspaceProjectIDE(
// Either `workspace.agent` for old connections or `user/workspace.agent`
// for new connections.
val name: String,
Expand Down
12 changes: 12 additions & 0 deletions src/main/kotlin/com/coder/gateway/settings/CoderSettings.kt
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,12 @@ open class CoderSettingsState(
// around issues on macOS where it periodically wakes and Gateway
// reconnects, keeping the workspace constantly up.
open var disableAutostart: Boolean = getOS() == OS.MAC,

/**
* Whether SSH wildcard config is enabled
*/
open var isSshWildcardConfigEnabled: Boolean = true,

// Extra SSH config options.
open var sshConfigOptions: String = "",
// An external command to run in the directory of the IDE before connecting
Expand Down Expand Up @@ -199,6 +205,12 @@ open class CoderSettings(
val disableAutostart: Boolean
get() = state.disableAutostart

/**
* Whether SSH wildcard config is enabled
*/
val isSshWildcardConfigEnabled: Boolean
get() = state.isSshWildcardConfigEnabled

/**
* Extra SSH config to append to each host block.
*/
Expand Down
2 changes: 1 addition & 1 deletion src/main/kotlin/com/coder/gateway/util/LinkHandler.kt
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,10 @@
indicator,
)

var workspace: Workspace

Check warning on line 74 in src/main/kotlin/com/coder/gateway/util/LinkHandler.kt

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Local 'var' is never modified and can be declared as 'val'

Variable is never modified, so it can be declared using 'val'
var workspaces: List<Workspace> = emptyList()
var workspacesAndAgents: Set<Pair<Workspace, WorkspaceAgent>> = emptySet()
if (cli.features.wildcardSSH) {
if (settings.isSshWildcardConfigEnabled && cli.features.wildcardSSH) {
workspace = client.workspaceByOwnerAndName(owner, workspaceName)
} else {
workspaces = client.workspaces()
Expand Down
16 changes: 16 additions & 0 deletions src/main/kotlin/com/coder/gateway/util/Without.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,22 @@ fun <A, Z> withoutNull(
return block(a)
}

/**
* Run block with provided arguments after checking they are all non-null. This
* is to enforce non-null values and should be used to signify developer error.
*/
fun <A, B, C, Z> withoutNull(
a: A?,
b: B?,
c: C?,
block: (a: A, b: B, c: C) -> Z,
): Z {
if (a == null || b == null || c == null) {
throw Exception("Unexpected null value")
}
return block(a, b, c)
}

/**
* Run block with provided arguments after checking they are all non-null. This
* is to enforce non-null values and should be used to signify developer error.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package com.coder.gateway.views
import com.coder.gateway.CoderGatewayBundle
import com.coder.gateway.CoderGatewayConstants
import com.coder.gateway.CoderRemoteConnectionHandle
import com.coder.gateway.cli.CoderCLIManager
import com.coder.gateway.cli.ensureCLI
import com.coder.gateway.icons.CoderIcons
import com.coder.gateway.models.WorkspaceAgentListModel
Expand Down Expand Up @@ -177,6 +178,7 @@ class CoderGatewayRecentWorkspaceConnectionsView(private val setContentCallback:
it.workspace.ownerName + "/" + it.workspace.name == workspaceName ||
(it.workspace.ownerName == me && it.workspace.name == workspaceName)
}

val status =
if (deploymentError != null) {
Triple(UIUtil.getErrorForeground(), deploymentError, UIUtil.getBalloonErrorIcon())
Expand Down Expand Up @@ -250,10 +252,11 @@ class CoderGatewayRecentWorkspaceConnectionsView(private val setContentCallback:
ActionLink(workspaceProjectIDE.projectPathDisplay) {
withoutNull(
deployment?.client,
workspaceWithAgent?.workspace
) { client, workspace ->
workspaceWithAgent?.workspace,
workspaceWithAgent?.agent
) { client, workspace, agent ->
CoderRemoteConnectionHandle().connect {
if (listOf(
val cli = if (listOf(
WorkspaceStatus.STOPPED,
WorkspaceStatus.CANCELED,
WorkspaceStatus.FAILED
Expand All @@ -272,8 +275,21 @@ class CoderGatewayRecentWorkspaceConnectionsView(private val setContentCallback:
}

cli.startWorkspace(workspace.ownerName, workspace.name)
cli
} else {
CoderCLIManager(deploymentURL.toURL(), settings)
}
workspaceProjectIDE
// the ssh config could have changed in the meantime
// so we want to make sure we use a proper hostname
// depending on whether the ssh wildcard config
// is enabled, otherwise the connection will fail.
workspaceProjectIDE.copy(
hostname = cli.getHostName(
workspace,
client.me(),
agent
)
)
}
GatewayUI.getInstance().reset()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@
logger.info("Configuring Coder CLI...")
cbIDE.renderer = IDECellRenderer("Configuring Coder CLI...")
withContext(Dispatchers.IO) {
if (data.cliManager.features.wildcardSSH) {
if (settings.isSshWildcardConfigEnabled && data.cliManager.features.wildcardSSH) {
data.cliManager.configSsh(emptySet(), data.client.me)
} else {
data.cliManager.configSsh(data.client.withAgents(data.workspaces), data.client.me)
Expand All @@ -237,7 +237,7 @@
IDECellRenderer(CoderGatewayBundle.message("gateway.connector.view.coder.connect-ssh"))
}
val executor = createRemoteExecutor(
CoderCLIManager(data.client.url).getBackgroundHostName(
CoderCLIManager(data.client.url, settings).getBackgroundHostName(
data.workspace,
data.client.me,
data.agent
Expand Down Expand Up @@ -291,7 +291,7 @@
)

// Check the provided setting to see if there's a default IDE to set.
val defaultIde = ides.find { it ->

Check notice on line 294 in src/main/kotlin/com/coder/gateway/views/steps/CoderWorkspaceProjectIDEStepView.kt

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Redundant lambda arrow

Redundant lambda arrow
// Using contains on the displayable version of the ide means they can be as specific or as vague as they want
// CL 2023.3.6 233.15619.8 -> a specific Clion build
// CL 2023.3.6 -> a specific Clion version
Expand Down Expand Up @@ -431,7 +431,7 @@
if (remainingInstalledIdes.size < installedIdes.size) {
logger.info(
"Skipping the following list of installed IDEs because there is already a released version " +
"available for download: ${(installedIdes - remainingInstalledIdes).joinToString { "${it.product.productCode} ${it.presentableVersion}" }}"

Check notice on line 434 in src/main/kotlin/com/coder/gateway/views/steps/CoderWorkspaceProjectIDEStepView.kt

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Argument could be converted to 'Set' to improve performance

The argument can be converted to 'Set' to improve performance
)
}
return remainingInstalledIdes.map { it.toIdeWithStatus() }.sorted() + availableIdes.map { it.toIdeWithStatus() }
Expand Down Expand Up @@ -470,7 +470,11 @@
override fun data(): WorkspaceProjectIDE = withoutNull(cbIDE.selectedItem, state) { selectedIDE, state ->
selectedIDE.withWorkspaceProject(
name = CoderCLIManager.getWorkspaceParts(state.workspace, state.agent),
hostname = CoderCLIManager(state.client.url).getHostName(state.workspace, state.client.me, state.agent),
hostname = CoderCLIManager(state.client.url, settings).getHostName(
state.workspace,
state.client.me,
state.agent
),
projectPath = tfProject.text,
deploymentURL = state.client.url,
)
Expand Down
4 changes: 3 additions & 1 deletion src/main/resources/messages/CoderGatewayBundle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -104,12 +104,14 @@ gateway.connector.settings.tls-alt-name.comment=Optionally set this to \
an alternate hostname used for verifying TLS connections. This is useful \
when the hostname used to connect to the Coder service does not match the \
hostname in the TLS certificate.
gateway.connector.settings.disable-autostart.heading=Autostart
gateway.connector.settings.disable-autostart.title=Disable autostart
gateway.connector.settings.disable-autostart.comment=Checking this box will \
cause the plugin to configure the CLI with --disable-autostart. You must go \
through the IDE selection again for the plugin to reconfigure the CLI with \
this setting.
gateway.connector.settings.wildcard-config.title=Enable SSH wildcard config
gateway.connector.settings.wildcard-config.comment=Enables or disables wildcard \
entries in the SSH configuration, which allows generic rules for matching multiple workspaces
gateway.connector.settings.ssh-config-options.title=SSH config options
gateway.connector.settings.ssh-config-options.comment=Extra SSH config options \
to use when connecting to a workspace. This text will be appended as-is to \
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ internal class CoderCLIManagerTest {
@Test
fun testServerInternalError() {
val (srv, url) = mockServer(HttpURLConnection.HTTP_INTERNAL_ERROR)
val ccm = CoderCLIManager(url)
val ccm = CoderCLIManager(url, CoderSettings(CoderSettingsState()))

val ex =
assertFailsWith(
Expand Down
Loading