Skip to content

Add header command setting #303

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Sep 22, 2023
Merged
Prev Previous commit
Next Next commit
Inject header command into proxy command
  • Loading branch information
code-asher committed Sep 19, 2023
commit 761303e7be02f09a7cb25551758f712b17a97b8f
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ class CoderGatewayConnectionProvider : GatewayConnectionProvider {
cli.login(client.token)

indicator.text = "Configuring Coder CLI..."
cli.configSsh(workspaces.flatMap { it.toAgentModels() })
cli.configSsh(workspaces.flatMap { it.toAgentModels() }, settings.headerCommand)

// TODO: Ask for these if missing. Maybe we can reuse the second
// step of the wizard? Could also be nice if we automatically used
Expand Down
28 changes: 24 additions & 4 deletions src/main/kotlin/com/coder/gateway/sdk/CoderCLIManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -179,8 +179,9 @@ class CoderCLIManager @JvmOverloads constructor(
/**
* Configure SSH to use this binary.
*/
fun configSsh(workspaces: List<WorkspaceAgentModel>) {
writeSSHConfig(modifySSHConfig(readSSHConfig(), workspaces))
@JvmOverloads
fun configSsh(workspaces: List<WorkspaceAgentModel>, headerCommand: String? = null) {
writeSSHConfig(modifySSHConfig(readSSHConfig(), workspaces, headerCommand))
}

/**
Expand All @@ -194,16 +195,35 @@ class CoderCLIManager @JvmOverloads constructor(
}
}

/**
* Escape a command argument by wrapping it in double quotes and escaping
* any double quotes in the argument. For example, echo "test" becomes
* "echo \"test\"".
*/
private fun escape(s: String): String {
return "\"" + s.replace("\"", "\\\"") + "\""
}

/**
* Given an existing SSH config modify it to add or remove the config for
* this deployment and return the modified config or null if it does not
* need to be modified.
*/
private fun modifySSHConfig(contents: String?, workspaces: List<WorkspaceAgentModel>): String? {
private fun modifySSHConfig(
contents: String?,
workspaces: List<WorkspaceAgentModel>,
headerCommand: String?,
): String? {
val host = getSafeHost(deploymentURL)
val startBlock = "# --- START CODER JETBRAINS $host"
val endBlock = "# --- END CODER JETBRAINS $host"
val isRemoving = workspaces.isEmpty()
val proxyArgs = listOfNotNull(
escape(localBinaryPath.toString()),
"--global-config", escape(coderConfigPath.toString()),
if (!headerCommand.isNullOrBlank()) "--header-command" else null,
if (!headerCommand.isNullOrBlank()) escape(headerCommand) else null,
"ssh", "--stdio")
val blockContent = workspaces.joinToString(
System.lineSeparator(),
startBlock + System.lineSeparator(),
Expand All @@ -212,7 +232,7 @@ class CoderCLIManager @JvmOverloads constructor(
"""
Host ${getHostName(deploymentURL, it)}
HostName coder.${it.name}
ProxyCommand "$localBinaryPath" --global-config "$coderConfigPath" ssh --stdio ${it.name}
ProxyCommand ${proxyArgs.joinToString(" ")} ${it.name}
ConnectTimeout 0
StrictHostKeyChecking no
UserKnownHostsFile /dev/null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -614,7 +614,7 @@ class CoderWorkspacesStepView(val setNextButtonEnabled: (Boolean) -> Unit) : Cod
poller?.cancel()

logger.info("Configuring Coder CLI...")
cli.configSsh(tableOfWorkspaces.items)
cli.configSsh(tableOfWorkspaces.items, settings.headerCommand)

// The config directory can be used to pull the URL and token in
// order to query this workspace's status in other flows, for
Expand Down
10 changes: 10 additions & 0 deletions src/test/fixtures/outputs/header-command.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# --- START CODER JETBRAINS test.coder.invalid
Host coder-jetbrains--header--test.coder.invalid
HostName coder.header
ProxyCommand "/tmp/coder-gateway/test.coder.invalid/coder-linux-amd64" --global-config "/tmp/coder-gateway/test.coder.invalid/config" --header-command "my-header-command \"test\"" ssh --stdio header
ConnectTimeout 0
StrictHostKeyChecking no
UserKnownHostsFile /dev/null
LogLevel ERROR
SetEnv CODER_SSH_SESSION_TYPE=JetBrains
# --- END CODER JETBRAINS test.coder.invalid
29 changes: 15 additions & 14 deletions src/test/groovy/CoderCLIManagerTest.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -398,7 +398,7 @@ class CoderCLIManagerTest extends Specification {
.replace("/tmp/coder-gateway/test.coder.invalid/coder-linux-amd64", ccm.localBinaryPath.toString())

when:
ccm.configSsh(workspaces.collect { DataGen.workspace(it) })
ccm.configSsh(workspaces.collect { DataGen.workspace(it) }, headerCommand)

then:
sshConfigPath.toFile().text == expectedConf
Expand All @@ -410,19 +410,20 @@ class CoderCLIManagerTest extends Specification {
sshConfigPath.toFile().text == Path.of("src/test/fixtures/inputs").resolve(remove + ".conf").toFile().text

where:
workspaces | input | output | remove
["foo", "bar"] | null | "multiple-workspaces" | "blank"
["foo-bar"] | "blank" | "append-blank" | "blank"
["foo-bar"] | "blank-newlines" | "append-blank-newlines" | "blank"
["foo-bar"] | "existing-end" | "replace-end" | "no-blocks"
["foo-bar"] | "existing-end-no-newline" | "replace-end-no-newline" | "no-blocks"
["foo-bar"] | "existing-middle" | "replace-middle" | "no-blocks"
["foo-bar"] | "existing-middle-and-unrelated" | "replace-middle-ignore-unrelated" | "no-related-blocks"
["foo-bar"] | "existing-only" | "replace-only" | "blank"
["foo-bar"] | "existing-start" | "replace-start" | "no-blocks"
["foo-bar"] | "no-blocks" | "append-no-blocks" | "no-blocks"
["foo-bar"] | "no-related-blocks" | "append-no-related-blocks" | "no-related-blocks"
["foo-bar"] | "no-newline" | "append-no-newline" | "no-blocks"
workspaces | input | output | remove | headerCommand
["foo", "bar"] | null | "multiple-workspaces" | "blank" | null
["foo-bar"] | "blank" | "append-blank" | "blank" | null
["foo-bar"] | "blank-newlines" | "append-blank-newlines" | "blank" | null
["foo-bar"] | "existing-end" | "replace-end" | "no-blocks" | null
["foo-bar"] | "existing-end-no-newline" | "replace-end-no-newline" | "no-blocks" | null
["foo-bar"] | "existing-middle" | "replace-middle" | "no-blocks" | null
["foo-bar"] | "existing-middle-and-unrelated" | "replace-middle-ignore-unrelated" | "no-related-blocks" | null
["foo-bar"] | "existing-only" | "replace-only" | "blank" | null
["foo-bar"] | "existing-start" | "replace-start" | "no-blocks" | null
["foo-bar"] | "no-blocks" | "append-no-blocks" | "no-blocks" | null
["foo-bar"] | "no-related-blocks" | "append-no-related-blocks" | "no-related-blocks" | null
["foo-bar"] | "no-newline" | "append-no-newline" | "no-blocks" | null
["header"] | null | "header-command" | "blank" | "my-header-command \"test\""
}

def "fails if config is malformed"() {
Expand Down