Skip to content

Commit f1eaba1

Browse files
committed
Refactor CLI manager to support multiple deployments
Currently multiple deployments will delete each other's binaries so this uses a directory for each deployment host to store the binaries. Add a catch for download errors; previously they would go unnoticed. Also refactor the download a little; instead of checking for the file's existence catch the appropriate error and set the permissions from Kotlin code rather than spawning chmod. Remove a chunk of code where we configure the CLI again before moving to the next step; we already do that when we first connect (plus it is missing some checks like making sure the CLI has actually been downloaded and is the right version, etc). This also makes it unnecessary to globally store the version and CLI path on the model since we now only do it in one spot. This is just a first step; in a future PR the config code will all be extracted out since we will want to configure every time we try to connect (rather than just in the initial setup step) since otherwise the recent connections can fail if you have configured a different deployment in the meantime (and probably reconnections would also fail if you already have an IDE open) or if you have restarted (since the binaries are currently in tmp). We will also need to configure ourselves since `config-ssh` does not support multiple deployments and we want to add an environment variable via SetEnv.
1 parent 548c725 commit f1eaba1

File tree

4 files changed

+287
-100
lines changed

4 files changed

+287
-100
lines changed

src/main/kotlin/com/coder/gateway/models/CoderWorkspacesWizardModel.kt

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@ package com.coder.gateway.models
33
data class CoderWorkspacesWizardModel(
44
var coderURL: String = "https://coder.example.com",
55
var token: String = "",
6-
var buildVersion: String = "",
7-
var localCliPath: String = "",
86
var selectedWorkspace: WorkspaceAgentModel? = null,
97
var useExistingToken: Boolean = false
108
)

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

Lines changed: 61 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -3,30 +3,43 @@ package com.coder.gateway.sdk
33
import com.intellij.openapi.diagnostic.Logger
44
import java.io.InputStream
55
import java.net.URL
6-
import java.nio.file.FileVisitOption
76
import java.nio.file.Files
87
import java.nio.file.Path
9-
import java.nio.file.Paths
10-
import java.nio.file.StandardCopyOption
8+
import java.nio.file.attribute.PosixFilePermissions
119

12-
class CoderCLIManager(url: URL, buildVersion: String) {
13-
var remoteCli: URL
14-
var localCli: Path
15-
private var cliNamePrefix: String
16-
private var tmpDir: String
17-
private var cliFileName: String
10+
/**
11+
* Manage the CLI for a single deployment.
12+
*/
13+
class CoderCLIManager(deployment: URL, buildVersion: String) {
14+
private var remoteBinaryUrl: URL
15+
var localBinaryPath: Path
16+
private var binaryNamePrefix: String
17+
private var destinationDir: Path
18+
private var localBinaryName: String
1819

1920
init {
21+
// TODO: Should use a persistent path to avoid needing to download on
22+
// each restart.
23+
destinationDir = Path.of(System.getProperty("java.io.tmpdir"))
24+
.resolve("coder-gateway").resolve(deployment.host)
2025
val os = getOS()
21-
cliNamePrefix = getCoderCLIForOS(os, getArch())
22-
val cliNameWithExt = if (os == OS.WINDOWS) "$cliNamePrefix.exe" else cliNamePrefix
23-
cliFileName = if (os == OS.WINDOWS) "${cliNamePrefix}-${buildVersion}.exe" else "${cliNamePrefix}-${buildVersion}"
24-
25-
remoteCli = URL(url.protocol, url.host, url.port, "/bin/$cliNameWithExt")
26-
tmpDir = System.getProperty("java.io.tmpdir")
27-
localCli = Paths.get(tmpDir, cliFileName)
26+
binaryNamePrefix = getCoderCLIForOS(os, getArch())
27+
val binaryName = if (os == OS.WINDOWS) "$binaryNamePrefix.exe" else binaryNamePrefix
28+
localBinaryName =
29+
if (os == OS.WINDOWS) "${binaryNamePrefix}-${buildVersion}.exe" else "${binaryNamePrefix}-${buildVersion}"
30+
remoteBinaryUrl = URL(
31+
deployment.protocol,
32+
deployment.host,
33+
deployment.port,
34+
"/bin/$binaryName"
35+
)
36+
localBinaryPath = destinationDir.resolve(localBinaryName)
2837
}
2938

39+
/**
40+
* Return the name of the binary (sans extension) for the provided OS and
41+
* architecture.
42+
*/
3043
private fun getCoderCLIForOS(os: OS?, arch: Arch?): String {
3144
logger.info("Resolving coder cli for $os $arch")
3245
if (os == null) {
@@ -55,31 +68,50 @@ class CoderCLIManager(url: URL, buildVersion: String) {
5568
}
5669
}
5770

71+
/**
72+
* Download the CLI from the deployment if necessary.
73+
*/
5874
fun downloadCLI(): Boolean {
59-
if (Files.exists(localCli)) {
60-
logger.info("${localCli.toAbsolutePath()} already exists, skipping download")
75+
Files.createDirectories(destinationDir)
76+
try {
77+
logger.info("Downloading Coder CLI to ${localBinaryPath.toAbsolutePath()}")
78+
remoteBinaryUrl.openStream().use {
79+
Files.copy(it as InputStream, localBinaryPath)
80+
}
81+
} catch (e: java.nio.file.FileAlreadyExistsException) {
82+
// This relies on the provided build version being the latest. It
83+
// must be freshly fetched immediately before downloading.
84+
// TODO: Use etags instead?
85+
logger.info("${localBinaryPath.toAbsolutePath()} already exists, skipping download")
6186
return false
6287
}
63-
logger.info("Starting Coder CLI download to ${localCli.toAbsolutePath()}")
64-
remoteCli.openStream().use {
65-
Files.copy(it as InputStream, localCli, StandardCopyOption.REPLACE_EXISTING)
88+
if (getOS() != OS.WINDOWS) {
89+
Files.setPosixFilePermissions(
90+
localBinaryPath,
91+
PosixFilePermissions.fromString("rwxr-x---")
92+
)
6693
}
6794
return true
6895
}
6996

97+
/**
98+
* Remove all versions of the CLI for this deployment that do not match the
99+
* current build version.
100+
*/
70101
fun removeOldCli() {
71-
val tmpPath = Path.of(tmpDir)
72-
if (Files.isReadable(tmpPath)) {
73-
Files.walk(tmpPath, 1).use {
74-
it.sorted().map { pt -> pt.toFile() }.filter { fl -> fl.name.contains(cliNamePrefix) && !fl.name.contains(cliFileName) }.forEach { fl ->
75-
logger.info("Removing $fl because it is an old coder cli")
76-
fl.delete()
77-
}
102+
if (Files.isReadable(destinationDir)) {
103+
Files.walk(destinationDir, 1).use {
104+
it.sorted().map { pt -> pt.toFile() }
105+
.filter { fl -> fl.name.contains(binaryNamePrefix) && fl.name != localBinaryName }
106+
.forEach { fl ->
107+
logger.info("Removing $fl because it is an old version")
108+
fl.delete()
109+
}
78110
}
79111
}
80112
}
81113

82114
companion object {
83115
val logger = Logger.getInstance(CoderCLIManager::class.java.simpleName)
84116
}
85-
}
117+
}

0 commit comments

Comments
 (0)