diff --git a/CHANGELOG.md b/CHANGELOG.md index 177650e2..dbea5ef8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ ## Unreleased +### Added + +- support for disabling SSH wildcard config. + ## 2.22.3 - 2025-09-19 ### Fixed diff --git a/gradle.properties b/gradle.properties index edaf8108..26ae7b23 100644 --- a/gradle.properties +++ b/gradle.properties @@ -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 diff --git a/src/main/kotlin/com/coder/gateway/CoderSettingsConfigurable.kt b/src/main/kotlin/com/coder/gateway/CoderSettingsConfigurable.kt index 64a140b4..76096e98 100644 --- a/src/main/kotlin/com/coder/gateway/CoderSettingsConfigurable.kt +++ b/src/main/kotlin/com/coder/gateway/CoderSettingsConfigurable.kt @@ -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) diff --git a/src/main/kotlin/com/coder/gateway/cli/CoderCLIManager.kt b/src/main/kotlin/com/coder/gateway/cli/CoderCLIManager.kt index cfa7deef..74c3ee88 100644 --- a/src/main/kotlin/com/coder/gateway/cli/CoderCLIManager.kt +++ b/src/main/kotlin/com/coder/gateway/cli/CoderCLIManager.kt @@ -12,7 +12,6 @@ import com.coder.gateway.sdk.v2.models.User 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 @@ -129,7 +128,7 @@ class CoderCLIManager( // 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, @@ -373,7 +372,7 @@ class CoderCLIManager( SetEnv CODER_SSH_SESSION_TYPE=JetBrains """.trimIndent() val blockContent = - if (feats.wildcardSSH) { + if (settings.isSshWildcardConfigEnabled && feats.wildcardSSH) { startBlock + System.lineSeparator() + """ Host ${getHostPrefix()}--* @@ -622,7 +621,7 @@ class CoderCLIManager( 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, @@ -638,7 +637,7 @@ class CoderCLIManager( 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" diff --git a/src/main/kotlin/com/coder/gateway/models/RecentWorkspaceConnection.kt b/src/main/kotlin/com/coder/gateway/models/RecentWorkspaceConnection.kt index 17e03977..ba2eb719 100644 --- a/src/main/kotlin/com/coder/gateway/models/RecentWorkspaceConnection.kt +++ b/src/main/kotlin/com/coder/gateway/models/RecentWorkspaceConnection.kt @@ -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 @@ -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) @@ -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 } } diff --git a/src/main/kotlin/com/coder/gateway/models/WorkspaceProjectIDE.kt b/src/main/kotlin/com/coder/gateway/models/WorkspaceProjectIDE.kt index 287f1bd4..d1d33b08 100644 --- a/src/main/kotlin/com/coder/gateway/models/WorkspaceProjectIDE.kt +++ b/src/main/kotlin/com/coder/gateway/models/WorkspaceProjectIDE.kt @@ -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, diff --git a/src/main/kotlin/com/coder/gateway/settings/CoderSettings.kt b/src/main/kotlin/com/coder/gateway/settings/CoderSettings.kt index aa517746..a189df82 100644 --- a/src/main/kotlin/com/coder/gateway/settings/CoderSettings.kt +++ b/src/main/kotlin/com/coder/gateway/settings/CoderSettings.kt @@ -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 @@ -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. */ diff --git a/src/main/kotlin/com/coder/gateway/util/LinkHandler.kt b/src/main/kotlin/com/coder/gateway/util/LinkHandler.kt index aec1cd47..cb943552 100644 --- a/src/main/kotlin/com/coder/gateway/util/LinkHandler.kt +++ b/src/main/kotlin/com/coder/gateway/util/LinkHandler.kt @@ -74,7 +74,7 @@ open class LinkHandler( var workspace: Workspace var workspaces: List = emptyList() var workspacesAndAgents: Set> = emptySet() - if (cli.features.wildcardSSH) { + if (settings.isSshWildcardConfigEnabled && cli.features.wildcardSSH) { workspace = client.workspaceByOwnerAndName(owner, workspaceName) } else { workspaces = client.workspaces() diff --git a/src/main/kotlin/com/coder/gateway/util/Without.kt b/src/main/kotlin/com/coder/gateway/util/Without.kt index 8ba79ae0..946706a7 100644 --- a/src/main/kotlin/com/coder/gateway/util/Without.kt +++ b/src/main/kotlin/com/coder/gateway/util/Without.kt @@ -14,6 +14,22 @@ fun 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 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. diff --git a/src/main/kotlin/com/coder/gateway/views/CoderGatewayRecentWorkspaceConnectionsView.kt b/src/main/kotlin/com/coder/gateway/views/CoderGatewayRecentWorkspaceConnectionsView.kt index c679c451..1ec9596a 100644 --- a/src/main/kotlin/com/coder/gateway/views/CoderGatewayRecentWorkspaceConnectionsView.kt +++ b/src/main/kotlin/com/coder/gateway/views/CoderGatewayRecentWorkspaceConnectionsView.kt @@ -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 @@ -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()) @@ -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 @@ -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() } diff --git a/src/main/kotlin/com/coder/gateway/views/steps/CoderWorkspaceProjectIDEStepView.kt b/src/main/kotlin/com/coder/gateway/views/steps/CoderWorkspaceProjectIDEStepView.kt index 81f02b2a..e5d4616f 100644 --- a/src/main/kotlin/com/coder/gateway/views/steps/CoderWorkspaceProjectIDEStepView.kt +++ b/src/main/kotlin/com/coder/gateway/views/steps/CoderWorkspaceProjectIDEStepView.kt @@ -214,7 +214,7 @@ class CoderWorkspaceProjectIDEStepView( 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) @@ -237,7 +237,7 @@ class CoderWorkspaceProjectIDEStepView( 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 @@ -470,7 +470,11 @@ class CoderWorkspaceProjectIDEStepView( 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, ) diff --git a/src/main/resources/messages/CoderGatewayBundle.properties b/src/main/resources/messages/CoderGatewayBundle.properties index 7420b576..00afdbfa 100644 --- a/src/main/resources/messages/CoderGatewayBundle.properties +++ b/src/main/resources/messages/CoderGatewayBundle.properties @@ -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 \ diff --git a/src/test/kotlin/com/coder/gateway/cli/CoderCLIManagerTest.kt b/src/test/kotlin/com/coder/gateway/cli/CoderCLIManagerTest.kt index d83690b7..3869f9e7 100644 --- a/src/test/kotlin/com/coder/gateway/cli/CoderCLIManagerTest.kt +++ b/src/test/kotlin/com/coder/gateway/cli/CoderCLIManagerTest.kt @@ -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(