Skip to content

Commit 4560aa1

Browse files
committed
Allow configuring CLI directory separately from data
This is so admins can download the CLI to some restricted location (like ProgramFiles).
1 parent f3663a4 commit 4560aa1

File tree

6 files changed

+153
-61
lines changed

6 files changed

+153
-61
lines changed

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,10 +60,10 @@ class CoderSettingsConfigurable : BoundConfigurable("Coder") {
6060
}.layout(RowLayout.PARENT_GRID)
6161
row {
6262
cell() // For alignment.
63-
checkBox(CoderGatewayBundle.message("gateway.connector.settings.enable-binary-data-fallback.title"))
64-
.bindSelected(state::enableCLIDataFallback)
63+
checkBox(CoderGatewayBundle.message("gateway.connector.settings.enable-binary-directory-fallback.title"))
64+
.bindSelected(state::enableBinaryDirectoryFallback)
6565
.comment(
66-
CoderGatewayBundle.message("gateway.connector.settings.enable-binary-data-fallback.comment")
66+
CoderGatewayBundle.message("gateway.connector.settings.enable-binary-directory-fallback.comment")
6767
)
6868
}.layout(RowLayout.PARENT_GRID)
6969
}

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

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
package com.coder.gateway.sdk
22

33
import com.coder.gateway.models.WorkspaceAgentModel
4+
import com.coder.gateway.services.CoderSettingsState
45
import com.coder.gateway.views.steps.CoderWorkspacesStepView
56
import com.google.gson.Gson
67
import com.google.gson.JsonSyntaxException
78
import com.intellij.openapi.diagnostic.Logger
9+
import com.intellij.openapi.progress.ProgressIndicator
810
import org.zeroturnaround.exec.ProcessExecutor
911
import java.io.BufferedInputStream
1012
import java.io.FileInputStream
@@ -427,6 +429,68 @@ class CoderCLIManager @JvmOverloads constructor(
427429
fun getHostName(url: URL, ws: WorkspaceAgentModel): String {
428430
return "coder-jetbrains--${ws.name}--${getSafeHost(url)}"
429431
}
432+
433+
/**
434+
* Do as much as possible to get a valid, up-to-date CLI.
435+
*/
436+
@JvmStatic
437+
@JvmOverloads
438+
fun ensureCLI(
439+
deploymentURL: URL,
440+
buildVersion: String,
441+
settings: CoderSettingsState,
442+
indicator: ProgressIndicator? = null,
443+
): CoderCLIManager {
444+
val dataDir =
445+
if (settings.dataDirectory.isBlank()) getDataDir()
446+
else Path.of(settings.dataDirectory).toAbsolutePath()
447+
val binDir =
448+
if (settings.binaryDirectory.isBlank()) null
449+
else Path.of(settings.binaryDirectory).toAbsolutePath()
450+
451+
val cli = CoderCLIManager(deploymentURL, dataDir, binDir, settings.binarySource)
452+
453+
// Short-circuit if we already have the expected version. This
454+
// lets us bypass the 304 which is slower and may not be
455+
// supported if the binary is downloaded from alternate sources.
456+
// For CLIs without the JSON output flag we will fall back to
457+
// the 304 method.
458+
val cliMatches = cli.matchesVersion(buildVersion)
459+
if (cliMatches == true) {
460+
return cli
461+
}
462+
463+
// If downloads are enabled download the new version.
464+
if (settings.enableDownloads) {
465+
indicator?.text = "Downloading Coder CLI..."
466+
try {
467+
cli.downloadCLI()
468+
return cli
469+
} catch (e: java.nio.file.AccessDeniedException) {
470+
// Might be able to fall back.
471+
if (binDir == null || binDir == dataDir || !settings.enableBinaryDirectoryFallback) {
472+
throw e
473+
}
474+
}
475+
}
476+
477+
// Try falling back to the data directory.
478+
val dataCLI = CoderCLIManager(deploymentURL, dataDir, null, settings.binarySource)
479+
val dataCLIMatches = dataCLI.matchesVersion(buildVersion)
480+
if (dataCLIMatches == true) {
481+
return dataCLI
482+
}
483+
484+
if (settings.enableDownloads) {
485+
indicator?.text = "Downloading Coder CLI..."
486+
dataCLI.downloadCLI()
487+
return dataCLI
488+
}
489+
490+
// Prefer the binary directory unless the data directory has a
491+
// working binary and the binary directory does not.
492+
return if (cliMatches == null && dataCLIMatches != null) dataCLI else cli
493+
}
430494
}
431495
}
432496

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ class CoderSettingsState : PersistentStateComponent<CoderSettingsState> {
1717
var binaryDirectory: String = ""
1818
var dataDirectory: String = ""
1919
var enableDownloads: Boolean = true
20-
var enableCLIDataFallback: Boolean = false
20+
var enableBinaryDirectoryFallback: Boolean = false
2121
override fun getState(): CoderSettingsState {
2222
return this
2323
}

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

Lines changed: 6 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ import com.intellij.openapi.application.ApplicationManager
3333
import com.intellij.openapi.application.ModalityState
3434
import com.intellij.openapi.components.service
3535
import com.intellij.openapi.diagnostic.Logger
36-
import com.intellij.openapi.progress.ProgressIndicator
3736
import com.intellij.openapi.rd.util.launchUnderBackgroundProgress
3837
import com.intellij.openapi.ui.panel.ComponentPanelBuilder
3938
import com.intellij.openapi.ui.setEmptyState
@@ -81,7 +80,6 @@ import java.awt.font.TextAttribute.UNDERLINE_ON
8180
import java.net.ConnectException
8281
import java.net.SocketTimeoutException
8382
import java.net.URL
84-
import java.nio.file.Path
8583
import javax.swing.Icon
8684
import javax.swing.JCheckBox
8785
import javax.swing.JTable
@@ -459,7 +457,12 @@ class CoderWorkspacesStepView(val setNextButtonEnabled: (Boolean) -> Unit) : Cod
459457
canBeCancelled = false,
460458
isIndeterminate = true
461459
) {
462-
val cli = getCLIManager(deploymentURL, this.indicator)
460+
val cli = CoderCLIManager.ensureCLI(
461+
deploymentURL,
462+
clientService.buildVersion,
463+
settings,
464+
this.indicator,
465+
)
463466
try {
464467
this.indicator.text = "Authenticating client..."
465468
authenticate(deploymentURL, token.first)
@@ -519,58 +522,6 @@ class CoderWorkspacesStepView(val setNextButtonEnabled: (Boolean) -> Unit) : Cod
519522
}
520523
}
521524

522-
private fun getCLIManager(deploymentURL: URL, indicator: ProgressIndicator): CoderCLIManager {
523-
val dataDir =
524-
if (settings.dataDirectory.isBlank()) CoderCLIManager.getDataDir()
525-
else Path.of(settings.dataDirectory).toAbsolutePath()
526-
val cliDir =
527-
if (settings.binaryDirectory.isBlank()) null
528-
else Path.of(settings.binaryDirectory).toAbsolutePath()
529-
530-
val cli = CoderCLIManager(deploymentURL, dataDir, cliDir, settings.binarySource)
531-
532-
// Short-circuit if we already have the expected version. This
533-
// lets us bypass the 304 which is slower and may not be
534-
// supported if the binary is downloaded from alternate sources.
535-
// For CLIs without the JSON output flag we will fall back to
536-
// the 304 method.
537-
val cliMatches = cli.matchesVersion(clientService.buildVersion)
538-
if (cliMatches == true) {
539-
return cli
540-
}
541-
542-
// If downloads are enabled download the new version.
543-
if (settings.enableDownloads) {
544-
indicator.text = "Downloading Coder CLI..."
545-
try {
546-
cli.downloadCLI()
547-
return cli
548-
} catch (e: java.nio.file.AccessDeniedException) {
549-
// Might be able to fall back.
550-
if (cliDir != null && !cliDir.startsWith(dataDir) && settings.enableCLIDataFallback) {
551-
throw e
552-
}
553-
}
554-
}
555-
556-
// Try falling back to the data directory.
557-
val dataCLI = CoderCLIManager(deploymentURL, dataDir, null, settings.binarySource)
558-
if (dataCLI.matchesVersion(clientService.buildVersion) == true) {
559-
return dataCLI
560-
}
561-
562-
if (settings.enableDownloads) {
563-
indicator.text = "Downloading Coder CLI..."
564-
dataCLI.downloadCLI()
565-
return dataCLI
566-
}
567-
568-
// In the case where both directories do not have a matching binary
569-
// prefer whichever does have a binary, if any, and prefer the CLI
570-
// directory if they both have out-of-date versions.
571-
return if (cliMatches != null) cli else dataCLI
572-
}
573-
574525
/**
575526
* Open a dialog for providing the token. Show any existing token so the
576527
* user can validate it if a previous connection failed. If we are not

src/main/resources/messages/CoderGatewayBundle.properties

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,8 @@ gateway.connector.settings.binary-destination.title=CLI directory:
6868
gateway.connector.settings.binary-destination.comment=Directories are created \
6969
here that store the CLI for each domain to which the plugin connects. \
7070
Defaults to the data directory.
71-
gateway.connector.settings.enable-binary-data-fallback.title=Fall back to data directory
72-
gateway.connector.settings.enable-binary-data-fallback.comment=Checking this \
71+
gateway.connector.settings.enable-binary-directory-fallback.title=Fall back to data directory
72+
gateway.connector.settings.enable-binary-directory-fallback.comment=Checking this \
7373
box will allow the plugin to fall back to the data directory when the CLI \
7474
directory is not writable.
7575
gateway.connector.no-details="The error did not provide any further details"

src/test/groovy/CoderCLIManagerTest.groovy

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.coder.gateway.sdk
22

3+
import com.coder.gateway.services.CoderSettingsState
34
import com.google.gson.JsonSyntaxException
45
import com.sun.net.httpserver.HttpExchange
56
import com.sun.net.httpserver.HttpHandler
@@ -537,4 +538,80 @@ class CoderCLIManagerTest extends Specification {
537538
expect:
538539
ccm.localBinaryPath.getParent() == dir.resolve("test.coder.invalid")
539540
}
541+
542+
@IgnoreIf({ os.windows })
543+
def "use a separate cli dir"() {
544+
given:
545+
def (srv, url) = mockServer()
546+
def dataDir = tmpdir.resolve("data-dir")
547+
def binDir = tmpdir.resolve("bin-dir")
548+
def mainCCM = new CoderCLIManager(new URL(url), dataDir, binDir)
549+
def fallbackCCM = new CoderCLIManager(new URL(url), dataDir)
550+
551+
when:
552+
def settings = new CoderSettingsState()
553+
settings.binaryDirectory = binDir.toAbsolutePath()
554+
settings.dataDirectory = dataDir.toAbsolutePath()
555+
settings.enableDownloads = download
556+
settings.enableBinaryDirectoryFallback = enableFallback
557+
Files.createDirectories(mainCCM.localBinaryPath.parent)
558+
if (version != null) {
559+
mainCCM.localBinaryPath.toFile().text = """#!/bin/sh\necho '{"version": "$version"}'"""
560+
mainCCM.localBinaryPath.toFile().setExecutable(true)
561+
}
562+
mainCCM.localBinaryPath.parent.toFile().setWritable(writable)
563+
if (fallback != null) {
564+
Files.createDirectories(fallbackCCM.localBinaryPath.parent)
565+
fallbackCCM.localBinaryPath.toFile().text = """#!/bin/sh\necho '{"version": "$fallback"}'"""
566+
fallbackCCM.localBinaryPath.toFile().setExecutable(true)
567+
}
568+
def ccm
569+
try {
570+
ccm = CoderCLIManager.ensureCLI(new URL(url), build, settings)
571+
} catch (Exception e) {
572+
ccm = e
573+
}
574+
575+
then:
576+
// The binaries will exist if either they already existed or were downloaded.
577+
mainCCM.localBinaryPath.toFile().exists() == (version != null || (download && writable))
578+
fallbackCCM.localBinaryPath.toFile().exists() == (fallback != null || (download && !writable && enableFallback))
579+
// We will get an error if we tried to download to the binary directory and it was not writable and we could
580+
// not fall back to the data directory, and we should be using whichever already had the most recent
581+
// binary, managed to download the most recent binary, or the binary directory if neither were able.
582+
download && !writable && !enableFallback && version != build
583+
? ccm instanceof AccessDeniedException
584+
: ccm.localBinaryPath.parent.parent == (enableFallback && !writable && version != build &&
585+
(download || fallback == build || (fallback != null && version == null)) ? dataDir : binDir)
586+
587+
cleanup:
588+
srv.stop(0)
589+
mainCCM.localBinaryPath.parent.toFile().setWritable(true) // So it can get cleaned up.
590+
591+
where:
592+
version | fallback | build | writable | download | enableFallback
593+
594+
// CLI is writable.
595+
null | null | "1.0.0" | true | true | true
596+
null | null | "1.0.0" | true | false | true
597+
"1.0.1" | null | "1.0.0" | true | true | true
598+
"1.0.1" | null | "1.0.0" | true | false | true
599+
"1.0.0" | null | "1.0.0" | true | false | true
600+
601+
// CLI is *not* writable and fallback is disabled.
602+
null | null | "1.0.0" | false | true | false
603+
null | null | "1.0.0" | false | false | false
604+
"1.0.1" | null | "1.0.0" | false | true | false
605+
"1.0.1" | null | "1.0.0" | false | false | false
606+
"1.0.0" | null | "1.0.0" | false | false | false
607+
608+
// CLI is *not* writable and fallback is enabled.
609+
null | null | "1.0.0" | false | true | true
610+
null | null | "1.0.0" | false | false | true
611+
"1.0.1" | "1.0.1" | "1.0.0" | false | true | true
612+
"1.0.1" | "1.0.2" | "1.0.0" | false | false | true
613+
null | "1.0.2" | "1.0.0" | false | false | true
614+
"1.0.0" | null | "1.0.0" | false | false | true
615+
"1.0.1" | "1.0.0" | "1.0.0" | false | false | true
616+
}
540617
}

0 commit comments

Comments
 (0)