Skip to content

Commit e412686

Browse files
committed
Merge branch 'main' into impl-support-for-oauth
2 parents 0d502db + e23c432 commit e412686

19 files changed

Lines changed: 1016 additions & 359 deletions

File tree

CHANGELOG.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,14 @@
44

55
### Added
66

7+
- `Binary destination` can now point directly to an executable, used as-is; otherwise it is treated as a base directory
8+
as before
79
- support for OAuth2
810

9-
### Changed
11+
### Removed
1012

13+
- support for enabling or disabling the fallback to data dir. CLI resolution will always fall back on data dir if binary
14+
destination is not configured.
1115
- redesigned the Settings page, all the options are now grouped in a couple of sections for easy navigation
1216

1317
## 0.8.6 - 2026-03-05
@@ -21,7 +25,8 @@
2125
### Added
2226

2327
- support for configuring the SSH connection timeout, defaults to 10 seconds
24-
- enhanced IDE resolution by supporting latest EAP, latest release, latest installed labels with clear fallback behavior in URI handlers
28+
- enhanced IDE resolution by supporting latest EAP, latest release, latest installed labels with clear fallback behavior
29+
in URI handlers
2530

2631
### Fixed
2732

README.md

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -447,16 +447,16 @@ storage paths. The options can be configured from the plugin's main Workspaces p
447447
- `Binary source` specifies the source URL or relative path from which the Coder CLI should be downloaded.
448448
If a relative path is provided, it is resolved against the deployment domain.
449449

450-
- `Enable downloads` allows automatic downloading of the CLI if the current version is missing or outdated.
450+
- `Enable downloads` allows automatic downloading of the CLI if the current version is missing or outdated. Enabled by
451+
default.
451452

452-
- `Binary directory` specifies the directory where CLI binaries are stored. If omitted, it defaults to the data
453-
directory.
453+
- `Binary destination` specifies where the CLI binary is placed. This can be a path to an existing
454+
executable (used as-is) or a base directory (the CLI is placed under a host-specific subdirectory).
455+
If blank, the data directory is used. Supports `~` and `$HOME` expansion.
454456

455-
- `Enable binary directory fallback` if enabled, falls back to the data directory when the specified binary
456-
directory is not writable.
457-
458-
- `Data directory` directory where plugin-specific data such as session tokens and binaries are stored if not
459-
overridden by the binary directory setting.
457+
- `Data directory` directory where deployment-specific data such as session tokens and CLI binaries
458+
are stored. Each deployment gets a host-specific subdirectory (e.g. `coder.example.com`). Supports `~` and `$HOME`
459+
expansion.
460460

461461
- `Header command` command that outputs additional HTTP headers. Each line of output must be in the format key=value.
462462
The environment variable CODER_URL will be available to the command process.
@@ -471,6 +471,25 @@ storage paths. The options can be configured from the plugin's main Workspaces p
471471
Helpful for customers that have their own in-house dashboards. Defaults to the Coder deployment templates page.
472472
This setting supports `$workspaceOwner` as placeholder with the replacing value being the username that logged in.
473473

474+
#### How CLI resolution works
475+
476+
When connecting to a deployment the plugin ensures a compatible CLI binary is available.
477+
The binary location is resolved as follows:
478+
479+
- If **binary destination** points to an existing executable file, it is used as-is.
480+
- If **binary destination** is set but is not an executable file, it is treated as a base
481+
directory and the CLI is placed under a host-specific subdirectory (e.g.
482+
`<binary destination>/coder.example.com/<default-cli-name>`).
483+
- If **binary destination** is not set, the data directory is used instead.
484+
485+
Once the binary location is resolved:
486+
487+
1. If a CLI already exists there and its version matches the deployment, it is used immediately.
488+
2. Otherwise, if **downloads are enabled**, the plugin downloads the matching version to that location.
489+
Any download error is reported to the user.
490+
3. If **downloads are disabled** and the CLI exists but its version does not match, the stale
491+
CLI is used with a warning. If no CLI exists at all, an error is raised.
492+
474493
### TLS settings
475494

476495
The following options control the secure communication behavior of the plugin with Coder deployment and its available

gradle/libs.versions.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ ksp = "2.3.6"
1313
retrofit = "3.0.0"
1414
changelog = "2.5.0"
1515
gettext = "0.7.0"
16-
plugin-structure = "3.321"
16+
plugin-structure = "3.323"
1717
mockk = "1.14.9"
1818
detekt = "1.23.8"
1919
bouncycastle = "1.83"

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

Lines changed: 19 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -45,16 +45,17 @@ internal data class Version(
4545

4646

4747
/**
48-
* Do as much as possible to get a valid, up-to-date CLI.
48+
* Best effort to get an up-to-date CLI.
4949
*
50-
* 1. Read the binary directory for the provided URL.
51-
* 2. Abort if we already have an up-to-date version.
52-
* 3. Download the binary using an ETag.
53-
* 4. Abort if we get a 304 (covers cases where the binary is older and does not
54-
* have a version command).
55-
* 5. Download on top of the existing binary.
56-
* 6. Since the binary directory can be read-only, if downloading fails, start
57-
* from step 2 with the data directory.
50+
* 1. Create a CLI manager for the deployment URL.
51+
* 2. If the CLI version matches the build version, return it immediately.
52+
* 3. Otherwise, if downloads are enabled, attempt to download the CLI.
53+
* a. On success, return the CLI.
54+
* b. Any exception propagates to the user.
55+
* 4. If downloads are disabled:
56+
* a. [IllegalStateException] is raised if the CLI does not exist (look into binary destination if it was configured,
57+
* fallback to data dir otherwise)
58+
* b. Otherwise, warn the user and return the mismatched version.
5859
*/
5960
suspend fun ensureCLI(
6061
context: CoderToolboxContext,
@@ -84,36 +85,15 @@ suspend fun ensureCLI(
8485
// If downloads are enabled download the new version.
8586
if (settings.enableDownloads) {
8687
reportProgress("Downloading Coder CLI...")
87-
try {
88-
cli.download(buildVersion, showTextProgress)
89-
return cli
90-
} catch (e: java.nio.file.AccessDeniedException) {
91-
// Might be able to fall back to the data directory.
92-
val binPath = settings.binPath(deploymentURL)
93-
val dataDir = settings.dataDir(deploymentURL)
94-
if (binPath.parent == dataDir || !settings.enableBinaryDirectoryFallback) {
95-
throw e
96-
}
97-
}
98-
}
99-
100-
// Try falling back to the data directory.
101-
val dataCLI = CoderCLIManager(context, deploymentURL, true)
102-
val dataCLIMatches = dataCLI.matchesVersion(buildVersion)
103-
if (dataCLIMatches == true) {
104-
reportProgress("Local CLI version from data directory matches server version: $buildVersion")
105-
return dataCLI
88+
cli.download(buildVersion, showTextProgress)
89+
return cli
10690
}
10791

108-
if (settings.enableDownloads) {
109-
reportProgress("Downloading Coder CLI to the data directory...")
110-
dataCLI.download(buildVersion, showTextProgress)
111-
return dataCLI
92+
if (cliMatches == null) {
93+
throw IllegalStateException("Can't resolve Coder CLI and downloads are disabled")
11294
}
113-
114-
// Prefer the binary directory unless the data directory has a
115-
// working binary and the binary directory does not.
116-
return if (cliMatches == null && dataCLIMatches != null) dataCLI else cli
95+
reportProgress("Downloads are disabled, and a cached CLI is used which does not match the server version $buildVersion and could cause compatibility issues")
96+
return cli
11797
}
11898

11999
/**
@@ -132,16 +112,13 @@ data class Features(
132112
class CoderCLIManager(
133113
private val context: CoderToolboxContext,
134114
// The URL of the deployment this CLI is for.
135-
private val deploymentURL: URL,
136-
// If the binary directory is not writable, this can be used to force the
137-
// manager to download to the data directory instead.
138-
private val forceDownloadToData: Boolean = false,
115+
private val deploymentURL: URL
139116
) {
140117
private val downloader = createDownloadService()
141118
private val gpgVerifier = GPGVerifier(context)
142119

143120
val remoteBinaryURL: URL = context.settingsStore.binSource(deploymentURL)
144-
val localBinaryPath: Path = context.settingsStore.binPath(deploymentURL, forceDownloadToData)
121+
val localBinaryPath: Path = context.settingsStore.binPath(deploymentURL)
145122
val coderConfigPath: Path = context.settingsStore.dataDir(deploymentURL).resolve("config")
146123

147124
private fun createDownloadService(): CoderDownloadService {
@@ -154,7 +131,7 @@ class CoderCLIManager(
154131
.build()
155132

156133
val service = retrofit.create(CoderDownloadApi::class.java)
157-
return CoderDownloadService(context, service, deploymentURL, forceDownloadToData)
134+
return CoderDownloadService(context, service, deploymentURL)
158135
}
159136

160137
/**

src/main/kotlin/com/coder/toolbox/cli/downloader/CoderDownloadService.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,17 +38,17 @@ private val SUPPORTED_BIN_MIME_TYPES = listOf(
3838
"application/x-ms-dos-executable",
3939
"application/vnd.microsoft.portable-executable"
4040
)
41+
4142
/**
4243
* Handles the download steps of Coder CLI
4344
*/
4445
class CoderDownloadService(
4546
private val context: CoderToolboxContext,
4647
private val downloadApi: CoderDownloadApi,
47-
private val deploymentUrl: URL,
48-
forceDownloadToData: Boolean,
48+
private val deploymentUrl: URL
4949
) {
5050
private val remoteBinaryURL: URL = context.settingsStore.binSource(deploymentUrl)
51-
private val cliFinalDst: Path = context.settingsStore.binPath(deploymentUrl, forceDownloadToData)
51+
private val cliFinalDst: Path = context.settingsStore.binPath(deploymentUrl)
5252
private val cliTempDst: Path = cliFinalDst.resolveSibling("${cliFinalDst.name}.tmp")
5353

5454
suspend fun downloadCli(buildVersion: String, showTextProgress: (String) -> Unit): DownloadResult {

src/main/kotlin/com/coder/toolbox/settings/ReadOnlyCoderSettings.kt

Lines changed: 9 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,13 @@ interface ReadOnlyCoderSettings {
3535
val binarySource: String?
3636

3737
/**
38-
* Directories are created here that store the CLI for each domain to which
39-
* the plugin connects. Defaults to the data directory.
38+
* An absolute path to either a directory or an existing executable CLI binary.
39+
* When the path points to an existing executable file, it is used as the CLI
40+
* binary path directly. Otherwise, it is treated as a base directory under
41+
* which the CLI is placed in a host-specific subdirectory. Defaults to the
42+
* data directory when not set.
4043
*/
41-
val binaryDirectory: String?
44+
val binaryDestination: String?
4245

4346
/**
4447
* Controls whether we verify the cli signature
@@ -60,19 +63,14 @@ interface ReadOnlyCoderSettings {
6063
*/
6164
val defaultCliBinaryNameByOsAndArch: String
6265

63-
/**
64-
* Configurable CLI binary name with extension, dependent on OS and arch
65-
*/
66-
val binaryName: String
67-
6866
/**
6967
* Default CLI signature name based on OS and architecture
7068
*/
7169
val defaultSignatureNameByOsAndArch: String
7270

7371
/**
7472
* Where to save plugin data like the Coder binary (if not configured with
75-
* binaryDirectory) and the deployment URL and session token.
73+
* binaryDestination) and the deployment URL and session token.
7674
*/
7775
val dataDirectory: String?
7876

@@ -92,12 +90,6 @@ interface ReadOnlyCoderSettings {
9290
*/
9391
val enableDownloads: Boolean
9492

95-
/**
96-
* Whether to allow the plugin to fall back to the data directory when the
97-
* CLI directory is not writable.
98-
*/
99-
val enableBinaryDirectoryFallback: Boolean
100-
10193
/**
10294
* An external command that outputs additional HTTP headers added to all
10395
* requests. The command must output each header as `key=value` on its own
@@ -187,9 +179,9 @@ interface ReadOnlyCoderSettings {
187179
fun binSource(url: URL): URL
188180

189181
/**
190-
* To where the specified deployment should download the binary.
182+
* Returns a path to where the specified deployment should place the CLI binary.
191183
*/
192-
fun binPath(url: URL, forceDownloadToData: Boolean = false): Path
184+
fun binPath(url: URL): Path
193185

194186
/**
195187
* Return the URL and token from the config, if they exist.

src/main/kotlin/com/coder/toolbox/store/CoderSettingsStore.kt

Lines changed: 21 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -41,22 +41,19 @@ class CoderSettingsStore(
4141
override val defaultURL: String get() = store[DEFAULT_URL] ?: "https://dev.coder.com"
4242
override val useAppNameAsTitle: Boolean get() = store[APP_NAME_AS_TITLE]?.toBooleanStrictOrNull() ?: false
4343
override val binarySource: String? get() = store[BINARY_SOURCE]
44-
override val binaryDirectory: String? get() = store[BINARY_DIRECTORY]
44+
override val binaryDestination: String? get() = store[BINARY_DESTINATION] ?: store[BINARY_DIRECTORY]
4545
override val disableSignatureVerification: Boolean
4646
get() = store[DISABLE_SIGNATURE_VALIDATION]?.toBooleanStrictOrNull() ?: false
4747
override val fallbackOnCoderForSignatures: SignatureFallbackStrategy
4848
get() = SignatureFallbackStrategy.fromValue(store[FALLBACK_ON_CODER_FOR_SIGNATURES])
4949
override val httpClientLogLevel: HttpLoggingVerbosity
5050
get() = HttpLoggingVerbosity.fromValue(store[HTTP_CLIENT_LOG_LEVEL])
5151
override val defaultCliBinaryNameByOsAndArch: String get() = getCoderCLIForOS(getOS(), getArch())
52-
override val binaryName: String get() = store[BINARY_NAME] ?: getCoderCLIForOS(getOS(), getArch())
5352
override val defaultSignatureNameByOsAndArch: String get() = getCoderSignatureForOS(getOS(), getArch())
5453
override val dataDirectory: String? get() = store[DATA_DIRECTORY]
5554
override val globalDataDirectory: String get() = getDefaultGlobalDataDir().normalize().toString()
5655
override val globalConfigDir: String get() = getDefaultGlobalConfigDir().normalize().toString()
5756
override val enableDownloads: Boolean get() = store[ENABLE_DOWNLOADS]?.toBooleanStrictOrNull() ?: true
58-
override val enableBinaryDirectoryFallback: Boolean
59-
get() = store[ENABLE_BINARY_DIR_FALLBACK]?.toBooleanStrictOrNull() ?: false
6057
override val headerCommand: String? get() = store[HEADER_COMMAND]
6158
override val tls: ReadOnlyTLSSettings
6259
get() = TLSSettings(
@@ -125,21 +122,26 @@ class CoderSettingsStore(
125122
}
126123

127124
/**
128-
* To where the specified deployment should download the binary.
125+
* To where the specified deployment should place the CLI binary.
126+
*
127+
* Resolution logic:
128+
* 1. If [binaryDestination] is null/blank, return the deployment's data
129+
* directory with the default CLI binary name.
130+
* 2. If the expanded (~ and $HOME) [binaryDestination] is an existing executable file,
131+
* return it as-is.
132+
* 3. Otherwise, treat [binaryDestination] as a base directory and return a
133+
* host-specific subdirectory with the default CLI binary name.
129134
*/
130-
override fun binPath(
131-
url: URL,
132-
forceDownloadToData: Boolean,
133-
): Path {
134-
binaryDirectory.let {
135-
val dir =
136-
if (forceDownloadToData || it.isNullOrBlank()) {
137-
dataDir(url)
138-
} else {
139-
withHost(Path.of(expand(it)), url)
140-
}
141-
return dir.resolve(binaryName).toAbsolutePath()
135+
override fun binPath(url: URL): Path {
136+
val raw = binaryDestination?.takeIf { it.isNotBlank() } ?: return dataDir(url).resolve(
137+
defaultCliBinaryNameByOsAndArch
138+
).toAbsolutePath()
139+
140+
val dest = Path.of(expand(raw))
141+
if (Files.isRegularFile(dest) && Files.isExecutable(dest)) {
142+
return dest.toAbsolutePath()
142143
}
144+
return withHost(dest, url).resolve(defaultCliBinaryNameByOsAndArch).toAbsolutePath()
143145
}
144146

145147
/**
@@ -180,8 +182,8 @@ class CoderSettingsStore(
180182
store[BINARY_SOURCE] = source
181183
}
182184

183-
fun updateBinaryDirectory(dir: String) {
184-
store[BINARY_DIRECTORY] = dir
185+
fun updateBinaryDestination(dest: String) {
186+
store[BINARY_DESTINATION] = dest
185187
}
186188

187189
fun updateDataDirectory(dir: String) {
@@ -208,10 +210,6 @@ class CoderSettingsStore(
208210
store[HTTP_CLIENT_LOG_LEVEL] = level.toString()
209211
}
210212

211-
fun updateBinaryDirectoryFallback(shouldEnableBinDirFallback: Boolean) {
212-
store[ENABLE_BINARY_DIR_FALLBACK] = shouldEnableBinDirFallback.toString()
213-
}
214-
215213
fun updateHeaderCommand(cmd: String) {
216214
store[HEADER_COMMAND] = cmd
217215
}

src/main/kotlin/com/coder/toolbox/store/StoreKeys.kt

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,22 +10,21 @@ internal const val APP_NAME_AS_TITLE = "useAppNameAsTitle"
1010

1111
internal const val BINARY_SOURCE = "binarySource"
1212

13+
@Deprecated("Use BINARY_DESTINATION instead", replaceWith = ReplaceWith("BINARY_DESTINATION"))
1314
internal const val BINARY_DIRECTORY = "binaryDirectory"
1415

16+
internal const val BINARY_DESTINATION = "binaryDestination"
17+
1518
internal const val DISABLE_SIGNATURE_VALIDATION = "disableSignatureValidation"
1619

1720
internal const val FALLBACK_ON_CODER_FOR_SIGNATURES = "signatureFallbackStrategy"
1821

1922
internal const val HTTP_CLIENT_LOG_LEVEL = "httpClientLogLevel"
2023

21-
internal const val BINARY_NAME = "binaryName"
22-
2324
internal const val DATA_DIRECTORY = "dataDirectory"
2425

2526
internal const val ENABLE_DOWNLOADS = "enableDownloads"
2627

27-
internal const val ENABLE_BINARY_DIR_FALLBACK = "enableBinaryDirectoryFallback"
28-
2928
internal const val HEADER_COMMAND = "headerCommand"
3029

3130
internal const val TLS_CERT_PATH = "tlsCertPath"

0 commit comments

Comments
 (0)