Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit a6efd99

Browse files
Add header command setting (#303)
* Add header command setting * Inject header command into proxy command * Add headers to API requests * Update changelog with header command * Add --info to CI tests It seems the default is to say a test fails but give no explanation as to why. * Matche Coder CLI behavior for escaping to SSH config
1 parent 8223fc9 commit a6efd99

27 files changed

+257
-43
lines changed

‎.github/workflows/build.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ jobs:
3232
- uses: gradle/wrapper-validation-action@v1.1.0
3333

3434
# Run tests
35-
- run: ./gradlew test
35+
- run: ./gradlew test --info
3636

3737
# Collect Tests Result of failed tests
3838
- if: ${{ failure() }}

‎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+
- Add a setting for a command to run to get headers that will be set on all
9+
requests to the Coder deployment.
10+
711
## 2.6.0 - 2023年09月06日
812

913
### Added

‎gradle.properties‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
pluginGroup=com.coder.gateway
44
pluginName=coder-gateway
55
# SemVer format -> https://semver.org
6-
pluginVersion=2.6.0
6+
pluginVersion=2.7.0
77
# See https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html
88
# for insight into build numbers and IntelliJ Platform versions.
99
pluginSinceBuild=223.7571.70

‎src/main/kotlin/com/coder/gateway/CoderGatewayConnectionProvider.kt‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ class CoderGatewayConnectionProvider : GatewayConnectionProvider {
100100
cli.login(client.token)
101101

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

105105
// TODO: Ask for these if missing. Maybe we can reuse the second
106106
// step of the wizard? Could also be nice if we automatically used
@@ -150,7 +150,7 @@ class CoderGatewayConnectionProvider : GatewayConnectionProvider {
150150
if (token == null) { // User aborted.
151151
throw IllegalArgumentException("Unable to connect to $deploymentURL, $TOKEN is missing")
152152
}
153-
val client = CoderRestClient(deploymentURL, token.first)
153+
val client = CoderRestClient(deploymentURL, token.first, settings.headerCommand)
154154
return try {
155155
Pair(client, client.me().username)
156156
} catch (ex: AuthenticationResponseException) {

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,13 @@ class CoderSettingsConfigurable : BoundConfigurable("Coder") {
6666
CoderGatewayBundle.message("gateway.connector.settings.enable-binary-directory-fallback.comment")
6767
)
6868
}.layout(RowLayout.PARENT_GRID)
69+
row(CoderGatewayBundle.message("gateway.connector.settings.header-command.title")) {
70+
textField().resizableColumn().align(AlignX.FILL)
71+
.bindText(state::headerCommand)
72+
.comment(
73+
CoderGatewayBundle.message("gateway.connector.settings.header-command.comment")
74+
)
75+
}.layout(RowLayout.PARENT_GRID)
6976
}
7077
}
7178

‎src/main/kotlin/com/coder/gateway/sdk/CoderCLIManager.kt‎

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -179,8 +179,9 @@ class CoderCLIManager @JvmOverloads constructor(
179179
/**
180180
* Configure SSH to use this binary.
181181
*/
182-
fun configSsh(workspaces: List<WorkspaceAgentModel>) {
183-
writeSSHConfig(modifySSHConfig(readSSHConfig(), workspaces))
182+
@JvmOverloads
183+
fun configSsh(workspaces: List<WorkspaceAgentModel>, headerCommand: String? = null) {
184+
writeSSHConfig(modifySSHConfig(readSSHConfig(), workspaces, headerCommand))
184185
}
185186

186187
/**
@@ -199,11 +200,21 @@ class CoderCLIManager @JvmOverloads constructor(
199200
* this deployment and return the modified config or null if it does not
200201
* need to be modified.
201202
*/
202-
private fun modifySSHConfig(contents: String?, workspaces: List<WorkspaceAgentModel>): String? {
203+
private fun modifySSHConfig(
204+
contents: String?,
205+
workspaces: List<WorkspaceAgentModel>,
206+
headerCommand: String?,
207+
): String? {
203208
val host = getSafeHost(deploymentURL)
204209
val startBlock = "# --- START CODER JETBRAINS $host"
205210
val endBlock = "# --- END CODER JETBRAINS $host"
206211
val isRemoving = workspaces.isEmpty()
212+
val proxyArgs = listOfNotNull(
213+
escape(localBinaryPath.toString()),
214+
"--global-config", escape(coderConfigPath.toString()),
215+
if (!headerCommand.isNullOrBlank()) "--header-command" else null,
216+
if (!headerCommand.isNullOrBlank()) escape(headerCommand) else null,
217+
"ssh", "--stdio")
207218
val blockContent = workspaces.joinToString(
208219
System.lineSeparator(),
209220
startBlock + System.lineSeparator(),
@@ -212,7 +223,7 @@ class CoderCLIManager @JvmOverloads constructor(
212223
"""
213224
Host ${getHostName(deploymentURL, it)}
214225
HostName coder.${it.name}
215-
ProxyCommand "$localBinaryPath" --global-config "$coderConfigPath" ssh --stdio ${it.name}
226+
ProxyCommand ${proxyArgs.joinToString("")} ${it.name}
216227
ConnectTimeout 0
217228
StrictHostKeyChecking no
218229
UserKnownHostsFile /dev/null
@@ -495,6 +506,24 @@ class CoderCLIManager @JvmOverloads constructor(
495506
// working binary and the binary directory does not.
496507
return if (cliMatches == null && dataCLIMatches != null) dataCLI else cli
497508
}
509+
510+
/**
511+
* Escape a command argument to be used in the ProxyCommand of an SSH
512+
* config. Surround with double quotes if the argument contains
513+
* whitespace and escape any existing double quotes.
514+
*
515+
* Throws if the argument is invalid.
516+
*/
517+
@JvmStatic
518+
fun escape(s: String): String {
519+
if (s.contains("\n")) {
520+
throw Exception("argument cannot contain newlines")
521+
}
522+
if (s.contains(" ") || s.contains("\t")) {
523+
return "\"" + s.replace("\"", "\\\"") + "\""
524+
}
525+
return s.replace("\"", "\\\"")
526+
}
498527
}
499528
}
500529

‎src/main/kotlin/com/coder/gateway/sdk/CoderRestClientService.kt‎

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import com.intellij.openapi.extensions.PluginId
2020
import com.intellij.openapi.util.SystemInfo
2121
import okhttp3.OkHttpClient
2222
import okhttp3.logging.HttpLoggingInterceptor
23+
import org.zeroturnaround.exec.ProcessExecutor
2324
import retrofit2.Retrofit
2425
import retrofit2.converter.gson.GsonConverterFactory
2526
import java.net.HttpURLConnection.HTTP_CREATED
@@ -41,16 +42,16 @@ class CoderRestClientService {
4142
*
4243
* @throws [AuthenticationResponseException] if authentication failed.
4344
*/
44-
fun initClientSession(url: URL, token: String): User {
45-
client = CoderRestClient(url, token)
45+
fun initClientSession(url: URL, token: String, headerCommand:String?): User {
46+
client = CoderRestClient(url, token, headerCommand)
4647
me = client.me()
4748
buildVersion = client.buildInfo().version
4849
isReady = true
4950
return me
5051
}
5152
}
5253

53-
class CoderRestClient(var url: URL, var token: String) {
54+
class CoderRestClient(var url: URL, var token: String, varheaderCommand:String?) {
5455
private var httpClient: OkHttpClient
5556
private var retroRestClient: CoderV2RestFacade
5657

@@ -61,6 +62,16 @@ class CoderRestClient(var url: URL, var token: String) {
6162
httpClient = OkHttpClient.Builder()
6263
.addInterceptor { it.proceed(it.request().newBuilder().addHeader("Coder-Session-Token", token).build()) }
6364
.addInterceptor { it.proceed(it.request().newBuilder().addHeader("User-Agent", "Coder Gateway/${pluginVersion.version} (${SystemInfo.getOsNameAndVersion()}; ${SystemInfo.OS_ARCH})").build()) }
65+
.addInterceptor {
66+
var request = it.request()
67+
val headers = getHeaders(url, headerCommand)
68+
if (headers.size > 0) {
69+
val builder = request.newBuilder()
70+
headers.forEach { h -> builder.addHeader(h.key, h.value) }
71+
request = builder.build()
72+
}
73+
it.proceed(request)
74+
}
6475
// this should always be last if we want to see previous interceptors logged
6576
.addInterceptor(HttpLoggingInterceptor().apply { setLevel(HttpLoggingInterceptor.Level.BASIC) })
6677
.build()
@@ -141,4 +152,45 @@ class CoderRestClient(var url: URL, var token: String) {
141152

142153
return buildResponse.body()!!
143154
}
155+
156+
companion object {
157+
private val newlineRegex = "\r?\n".toRegex()
158+
private val endingNewlineRegex = "\r?\n$".toRegex()
159+
160+
// TODO: This really only needs to be a private function, but
161+
// unfortunately it is not possible to test the client because it fails
162+
// on the plugin manager core call and I do not know how to fix it. So,
163+
// for now make this static and test it directly instead.
164+
@JvmStatic
165+
fun getHeaders(url: URL, headerCommand: String?): Map<String, String> {
166+
if (headerCommand.isNullOrBlank()) {
167+
return emptyMap()
168+
}
169+
val (shell, caller) = when (getOS()) {
170+
OS.WINDOWS -> Pair("cmd.exe", "/c")
171+
else -> Pair("sh", "-c")
172+
}
173+
return ProcessExecutor()
174+
.command(shell, caller, headerCommand)
175+
.environment("CODER_URL", url.toString())
176+
.exitValues(0)
177+
.readOutput(true)
178+
.execute()
179+
.outputUTF8()
180+
.replaceFirst(endingNewlineRegex, "")
181+
.split(newlineRegex)
182+
.associate {
183+
// Header names cannot be blank or contain whitespace and
184+
// the Coder CLI requires that there be an equals sign (the
185+
// value can be blank though). The second case is taken
186+
// care of by the destructure here, as it will throw if
187+
// there are not enough parts.
188+
val (name, value) = it.split("=", limit=2)
189+
if (name.contains(" ") || name == "") {
190+
throw Exception("\"$name\" is not a valid header name")
191+
}
192+
name to value
193+
}
194+
}
195+
}
144196
}

‎src/main/kotlin/com/coder/gateway/services/CoderSettingsState.kt‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ class CoderSettingsState : PersistentStateComponent<CoderSettingsState> {
1818
var dataDirectory: String = ""
1919
var enableDownloads: Boolean = true
2020
var enableBinaryDirectoryFallback: Boolean = false
21+
var headerCommand: String = ""
2122
override fun getState(): CoderSettingsState {
2223
return this
2324
}

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import com.coder.gateway.sdk.toURL
1313
import com.coder.gateway.sdk.v2.models.WorkspaceStatus
1414
import com.coder.gateway.sdk.v2.models.toAgentModels
1515
import com.coder.gateway.services.CoderRecentWorkspaceConnectionsService
16+
import com.coder.gateway.services.CoderSettingsState
1617
import com.coder.gateway.toWorkspaceParams
1718
import com.intellij.icons.AllIcons
1819
import com.intellij.ide.BrowserUtil
@@ -72,6 +73,7 @@ data class DeploymentInfo(
7273
)
7374

7475
class CoderGatewayRecentWorkspaceConnectionsView(private val setContentCallback: (Component) -> Unit) : GatewayRecentConnections, Disposable {
76+
private val settings: CoderSettingsState = service()
7577
private val recentConnectionsService = service<CoderRecentWorkspaceConnectionsService>()
7678
private val cs = CoroutineScope(Dispatchers.Main)
7779

@@ -259,7 +261,7 @@ class CoderGatewayRecentWorkspaceConnectionsView(private val setContentCallback:
259261
deployments[dir] ?: try {
260262
val url = Path.of(dir).resolve("url").readText()
261263
val token = Path.of(dir).resolve("session").readText()
262-
DeploymentInfo(CoderRestClient(url.toURL(), token))
264+
DeploymentInfo(CoderRestClient(url.toURL(), token, settings.headerCommand))
263265
} catch (e: Exception) {
264266
logger.error("Unable to create client from $dir", e)
265267
DeploymentInfo(error = "Error trying to read $dir: ${e.message}")

‎src/main/kotlin/com/coder/gateway/views/steps/CoderWorkspacesStepView.kt‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -527,7 +527,7 @@ class CoderWorkspacesStepView(val setNextButtonEnabled: (Boolean) -> Unit) : Cod
527527
*/
528528
private fun authenticate(url: URL, token: String) {
529529
logger.info("Authenticating to $url...")
530-
clientService.initClientSession(url, token)
530+
clientService.initClientSession(url, token, settings.headerCommand)
531531

532532
try {
533533
logger.info("Checking compatibility with Coder version ${clientService.buildVersion}...")
@@ -614,7 +614,7 @@ class CoderWorkspacesStepView(val setNextButtonEnabled: (Boolean) -> Unit) : Cod
614614
poller?.cancel()
615615

616616
logger.info("Configuring Coder CLI...")
617-
cli.configSsh(tableOfWorkspaces.items)
617+
cli.configSsh(tableOfWorkspaces.items, settings.headerCommand)
618618

619619
// The config directory can be used to pull the URL and token in
620620
// order to query this workspace's status in other flows, for

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /