Skip to content

Commit ab45c41

Browse files
authored
Add extra SSH options setting (#382)
1 parent 3096448 commit ab45c41

File tree

7 files changed

+91
-23
lines changed

7 files changed

+91
-23
lines changed

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

+8
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package com.coder.gateway
22

33
import com.coder.gateway.services.CoderSettingsService
44
import com.coder.gateway.services.CoderSettingsStateService
5+
import com.coder.gateway.settings.CODER_SSH_CONFIG_OPTIONS
56
import com.coder.gateway.util.canCreateDirectory
67
import com.intellij.openapi.components.service
78
import com.intellij.openapi.options.BoundConfigurable
@@ -109,6 +110,13 @@ class CoderSettingsConfigurable : BoundConfigurable("Coder") {
109110
CoderGatewayBundle.message("gateway.connector.settings.disable-autostart.comment")
110111
)
111112
}.layout(RowLayout.PARENT_GRID)
113+
row(CoderGatewayBundle.message("gateway.connector.settings.ssh-config-options.title")) {
114+
textArea().resizableColumn().align(AlignX.FILL)
115+
.bindText(state::sshConfigOptions)
116+
.comment(
117+
CoderGatewayBundle.message("gateway.connector.settings.ssh-config-options.comment", CODER_SSH_CONFIG_OPTIONS)
118+
)
119+
}.layout(RowLayout.PARENT_GRID)
112120
}
113121
}
114122

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

+6-1
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,9 @@ class CoderCLIManager(
255255
if (settings.headerCommand.isNotBlank()) escapeSubcommand(settings.headerCommand) else null,
256256
"ssh", "--stdio",
257257
if (settings.disableAutostart && feats.disableAutostart) "--disable-autostart" else null)
258+
val extraConfig = if (settings.sshConfigOptions.isNotBlank()) {
259+
"\n" + settings.sshConfigOptions.prependIndent(" ")
260+
} else ""
258261
val blockContent = workspaceNames.joinToString(
259262
System.lineSeparator(),
260263
startBlock + System.lineSeparator(),
@@ -268,7 +271,9 @@ class CoderCLIManager(
268271
UserKnownHostsFile /dev/null
269272
LogLevel ERROR
270273
SetEnv CODER_SSH_SESSION_TYPE=JetBrains
271-
""".trimIndent().replace("\n", System.lineSeparator())
274+
""".trimIndent()
275+
.plus(extraConfig)
276+
.replace("\n", System.lineSeparator())
272277
})
273278

274279
if (contents == null) {

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

+10
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import java.nio.file.Files
1414
import java.nio.file.Path
1515
import java.nio.file.Paths
1616

17+
const val CODER_SSH_CONFIG_OPTIONS = "CODER_SSH_CONFIG_OPTIONS";
18+
1719
open class CoderSettingsState(
1820
// Used to download the Coder CLI which is necessary to proxy SSH
1921
// connections. The If-None-Match header will be set to the SHA1 of the CLI
@@ -57,6 +59,8 @@ open class CoderSettingsState(
5759
// around issues on macOS where it periodically wakes and Gateway
5860
// reconnects, keeping the workspace constantly up.
5961
open var disableAutostart: Boolean = getOS() == OS.MAC,
62+
// Extra SSH config options.
63+
open var sshConfigOptions: String = "",
6064
)
6165

6266
/**
@@ -113,6 +117,12 @@ open class CoderSettings(
113117
val disableAutostart: Boolean
114118
get() = state.disableAutostart
115119

120+
/**
121+
* Extra SSH config to append to each host block.
122+
*/
123+
val sshConfigOptions: String
124+
get() = state.sshConfigOptions.ifBlank { env.get(CODER_SSH_CONFIG_OPTIONS) }
125+
116126
/**
117127
* Where the specified deployment should put its data.
118128
*/

src/main/resources/messages/CoderGatewayBundle.properties

+6
Original file line numberDiff line numberDiff line change
@@ -117,3 +117,9 @@ gateway.connector.settings.disable-autostart.comment=Checking this box will \
117117
cause the plugin to configure the CLI with --disable-autostart. You must go \
118118
through the IDE selection again for the plugin to reconfigure the CLI with \
119119
this setting.
120+
gateway.connector.settings.ssh-config-options.title=SSH config options
121+
gateway.connector.settings.ssh-config-options.comment=Extra SSH config options \
122+
to use when connecting to a workspace. This text will be appended as-is to \
123+
the SSH configuration block for each workspace. If left blank the \
124+
environment variable {0} will be used, if set.
125+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# --- START CODER JETBRAINS test.coder.invalid
2+
Host coder-jetbrains--extra--test.coder.invalid
3+
ProxyCommand /tmp/coder-gateway/test.coder.invalid/coder-linux-amd64 --global-config /tmp/coder-gateway/test.coder.invalid/config ssh --stdio extra
4+
ConnectTimeout 0
5+
StrictHostKeyChecking no
6+
UserKnownHostsFile /dev/null
7+
LogLevel ERROR
8+
SetEnv CODER_SSH_SESSION_TYPE=JetBrains
9+
ServerAliveInterval 5
10+
ServerAliveCountMax 3
11+
# --- END CODER JETBRAINS test.coder.invalid

src/test/kotlin/com/coder/gateway/cli/CoderCLIManagerTest.kt

+35-22
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@ package com.coder.gateway.cli
33
import com.coder.gateway.cli.ex.MissingVersionException
44
import com.coder.gateway.cli.ex.ResponseException
55
import com.coder.gateway.cli.ex.SSHConfigFormatException
6-
import com.coder.gateway.settings.CoderSettingsState
6+
import com.coder.gateway.settings.CODER_SSH_CONFIG_OPTIONS
77
import com.coder.gateway.settings.CoderSettings
8+
import com.coder.gateway.settings.CoderSettingsState
9+
import com.coder.gateway.settings.Environment
810
import com.coder.gateway.util.InvalidVersionException
911
import com.coder.gateway.util.OS
1012
import com.coder.gateway.util.SemVer
@@ -238,34 +240,43 @@ internal class CoderCLIManagerTest {
238240
val input: String?,
239241
val output: String,
240242
val remove: String,
241-
val headerCommand: String?,
243+
val headerCommand: String = "",
242244
val disableAutostart: Boolean = false,
243-
val features: Features? = null,
245+
val features: Features = Features(),
246+
val extraConfig: String = "",
247+
val env: Environment = Environment(),
244248
)
245249

246250
@Test
247251
fun testConfigureSSH() {
252+
val extraConfig = listOf(
253+
"ServerAliveInterval 5",
254+
"ServerAliveCountMax 3").joinToString(System.lineSeparator())
248255
val tests = listOf(
249-
SSHTest(listOf("foo", "bar"), null,"multiple-workspaces", "blank", null),
250-
SSHTest(listOf("foo", "bar"), null,"multiple-workspaces", "blank", null),
251-
SSHTest(listOf("foo-bar"), "blank", "append-blank", "blank", null),
252-
SSHTest(listOf("foo-bar"), "blank-newlines", "append-blank-newlines", "blank", null),
253-
SSHTest(listOf("foo-bar"), "existing-end", "replace-end", "no-blocks", null),
254-
SSHTest(listOf("foo-bar"), "existing-end-no-newline", "replace-end-no-newline", "no-blocks", null),
255-
SSHTest(listOf("foo-bar"), "existing-middle", "replace-middle", "no-blocks", null),
256-
SSHTest(listOf("foo-bar"), "existing-middle-and-unrelated", "replace-middle-ignore-unrelated", "no-related-blocks", null),
257-
SSHTest(listOf("foo-bar"), "existing-only", "replace-only", "blank", null),
258-
SSHTest(listOf("foo-bar"), "existing-start", "replace-start", "no-blocks", null),
259-
SSHTest(listOf("foo-bar"), "no-blocks", "append-no-blocks", "no-blocks", null),
260-
SSHTest(listOf("foo-bar"), "no-related-blocks", "append-no-related-blocks", "no-related-blocks", null),
261-
SSHTest(listOf("foo-bar"), "no-newline", "append-no-newline", "no-blocks", null),
256+
SSHTest(listOf("foo", "bar"), null, "multiple-workspaces", "blank"),
257+
SSHTest(listOf("foo", "bar"), null, "multiple-workspaces", "blank"),
258+
SSHTest(listOf("foo-bar"), "blank", "append-blank", "blank"),
259+
SSHTest(listOf("foo-bar"), "blank-newlines", "append-blank-newlines", "blank"),
260+
SSHTest(listOf("foo-bar"), "existing-end", "replace-end", "no-blocks"),
261+
SSHTest(listOf("foo-bar"), "existing-end-no-newline", "replace-end-no-newline", "no-blocks"),
262+
SSHTest(listOf("foo-bar"), "existing-middle", "replace-middle", "no-blocks"),
263+
SSHTest(listOf("foo-bar"), "existing-middle-and-unrelated", "replace-middle-ignore-unrelated", "no-related-blocks"),
264+
SSHTest(listOf("foo-bar"), "existing-only", "replace-only", "blank"),
265+
SSHTest(listOf("foo-bar"), "existing-start", "replace-start", "no-blocks"),
266+
SSHTest(listOf("foo-bar"), "no-blocks", "append-no-blocks", "no-blocks"),
267+
SSHTest(listOf("foo-bar"), "no-related-blocks", "append-no-related-blocks", "no-related-blocks"),
268+
SSHTest(listOf("foo-bar"), "no-newline", "append-no-newline", "no-blocks"),
262269
if (getOS() == OS.WINDOWS) {
263270
SSHTest(listOf("header"), null, "header-command-windows", "blank", """"C:\Program Files\My Header Command\HeaderCommand.exe" --url="%CODER_URL%" --test="foo bar"""")
264271
} else {
265272
SSHTest(listOf("header"), null, "header-command", "blank", "my-header-command --url=\"\$CODER_URL\" --test=\"foo bar\" --literal='\$CODER_URL'")
266273
},
267-
SSHTest(listOf("foo"), null, "disable-autostart", "blank", null, true, Features(true)),
268-
SSHTest(listOf("foo"), null, "no-disable-autostart", "blank", null, true, Features(false)),
274+
SSHTest(listOf("foo"), null, "disable-autostart", "blank", "", true, Features(true)),
275+
SSHTest(listOf("foo"), null, "no-disable-autostart", "blank", "", true, Features(false)),
276+
SSHTest(listOf("extra"), null, "extra-config", "blank",
277+
extraConfig = extraConfig),
278+
SSHTest(listOf("extra"), null, "extra-config", "blank",
279+
env = Environment(mapOf(CODER_SSH_CONFIG_OPTIONS to extraConfig))),
269280
)
270281

271282
val newlineRe = "\r?\n".toRegex()
@@ -274,8 +285,10 @@ internal class CoderCLIManagerTest {
274285
val settings = CoderSettings(CoderSettingsState(
275286
disableAutostart = it.disableAutostart,
276287
dataDirectory = tmpdir.resolve("configure-ssh").toString(),
277-
headerCommand = it.headerCommand ?: ""),
278-
sshConfigPath = tmpdir.resolve(it.input + "_to_" + it.output + ".conf"))
288+
headerCommand = it.headerCommand,
289+
sshConfigOptions = it.extraConfig),
290+
sshConfigPath = tmpdir.resolve(it.input + "_to_" + it.output + ".conf"),
291+
env = it.env)
279292

280293
val ccm = CoderCLIManager(URL("https://test.coder.invalid"), settings)
281294

@@ -295,12 +308,12 @@ internal class CoderCLIManagerTest {
295308
.replace("/tmp/coder-gateway/test.coder.invalid/coder-linux-amd64", escape(ccm.localBinaryPath.toString()))
296309

297310
// Add workspaces.
298-
ccm.configSsh(it.workspaces.toSet(), it.features ?: Features())
311+
ccm.configSsh(it.workspaces.toSet(), it.features)
299312

300313
assertEquals(expectedConf, settings.sshConfigPath.toFile().readText())
301314

302315
// Remove configuration.
303-
ccm.configSsh(emptySet(), it.features ?: Features())
316+
ccm.configSsh(emptySet(), it.features)
304317

305318
// Remove is the configuration we expect after removing.
306319
assertEquals(

src/test/kotlin/com/coder/gateway/settings/CoderSettingsTest.kt

+15
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,21 @@ internal class CoderSettingsTest {
170170
assertEquals(Pair("http://test.gateway.coder.com$expected", "fake-token"), got)
171171
}
172172

173+
@Test
174+
fun testSSHConfigOptions() {
175+
var settings = CoderSettings(CoderSettingsState(sshConfigOptions = "ssh config options from state"))
176+
assertEquals("ssh config options from state", settings.sshConfigOptions)
177+
178+
settings = CoderSettings(CoderSettingsState(),
179+
env = Environment(mapOf(CODER_SSH_CONFIG_OPTIONS to "ssh config options from env")))
180+
assertEquals("ssh config options from env", settings.sshConfigOptions)
181+
182+
// State has precedence.
183+
settings = CoderSettings(CoderSettingsState(sshConfigOptions = "ssh config options from state"),
184+
env = Environment(mapOf(CODER_SSH_CONFIG_OPTIONS to "ssh config options from env")))
185+
assertEquals("ssh config options from state", settings.sshConfigOptions)
186+
}
187+
173188
@Test
174189
fun testSettings() {
175190
// Make sure the remaining settings are being conveyed.

0 commit comments

Comments
 (0)