Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Store binary in data directory
The @jvmoverloads annotations make Kotlin generate overloads otherwise
the test code thinks the optional arguments are not optional.
  • Loading branch information
code-asher committed Apr 5, 2023
commit 6636f31cfb0a0d853548f63f73f338d288bd0ef7
47 changes: 29 additions & 18 deletions src/main/kotlin/com/coder/gateway/sdk/CoderCLIManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -22,44 +22,35 @@ import javax.xml.bind.annotation.adapters.HexBinaryAdapter
/**
* Manage the CLI for a single deployment.
*/
class CoderCLIManager(deployment: URL) {
class CoderCLIManager @JvmOverloads constructor(deployment: URL, destinationDir: Path = getDataDir()) {
private var remoteBinaryUrl: URL
var localBinaryPath: Path
private var binaryNamePrefix: String
private var destinationDir: Path

init {
// TODO: Should use a persistent path to avoid needing to download on
// each restart.
destinationDir = Path.of(System.getProperty("java.io.tmpdir"))
.resolve("coder-gateway").resolve(deployment.host)
val os = getOS()
binaryNamePrefix = getCoderCLIForOS(os, getArch())
val binaryName = if (os == OS.WINDOWS) "$binaryNamePrefix.exe" else binaryNamePrefix
val binaryName = getCoderCLIForOS(getOS(), getArch())
remoteBinaryUrl = URL(
deployment.protocol,
deployment.host,
deployment.port,
"/bin/$binaryName"
)
localBinaryPath = destinationDir.resolve(binaryName)
localBinaryPath = destinationDir.resolve(deployment.host).resolve(binaryName)
}

/**
* Return the name of the binary (sans extension) for the provided OS and
* Return the name of the binary (with extension) for the provided OS and
* architecture.
*/
private fun getCoderCLIForOS(os: OS?, arch: Arch?): String {
logger.info("Resolving binary for $os $arch")
if (os == null) {
logger.error("Could not resolve client OS and architecture, defaulting to WINDOWS AMD64")
return "coder-windows-amd64"
return "coder-windows-amd64.exe"
}
return when (os) {
OS.WINDOWS -> when (arch) {
Arch.AMD64 -> "coder-windows-amd64"
Arch.ARM64 -> "coder-windows-arm64"
else -> "coder-windows-amd64"
Arch.AMD64 -> "coder-windows-amd64.exe"
Arch.ARM64 -> "coder-windows-arm64.exe"
else -> "coder-windows-amd64.exe"
}

OS.LINUX -> when (arch) {
Expand Down Expand Up @@ -93,7 +84,7 @@ class CoderCLIManager(deployment: URL) {
when (conn.responseCode) {
200 -> {
logger.info("Downloading binary to ${localBinaryPath.toAbsolutePath()}")
Files.createDirectories(destinationDir)
Files.createDirectories(localBinaryPath.parent)
conn.inputStream.use {
Files.copy(
if (conn.contentEncoding == "gzip") GZIPInputStream(it) else it,
Expand Down Expand Up @@ -207,6 +198,7 @@ class CoderCLIManager(deployment: URL) {
* Return the config directory used by the CLI.
*/
@JvmStatic
@JvmOverloads
fun getConfigDir(env: Environment = Environment()): Path {
var dir = env.get("CODER_CONFIG_DIR")
if (!dir.isNullOrBlank()) {
Expand All @@ -226,6 +218,25 @@ class CoderCLIManager(deployment: URL) {
}
}
}

/**
* Return the data directory.
*/
@JvmStatic
@JvmOverloads
fun getDataDir(env: Environment = Environment()): Path {
return when (getOS()) {
OS.WINDOWS -> Paths.get(env.get("LOCALAPPDATA"), "coder-gateway")
OS.MAC -> Paths.get(env.get("HOME"), "Library/Application Support/coder-gateway")
else -> {
val dir = env.get("XDG_DATA_HOME")
if (!dir.isNullOrBlank()) {
return Paths.get(dir, "coder-gateway")
}
return Paths.get(env.get("HOME"), ".local/share/coder-gateway")
}
}
}
}
}

Expand Down
104 changes: 86 additions & 18 deletions src/test/groovy/CoderCLIManagerTest.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,38 @@ import java.nio.file.Path

@Unroll
class CoderCLIManagerTest extends spock.lang.Specification {
// TODO: Probably not good to depend on dev.coder.com being up...should use
// a mock? Or spin up a Coder deployment in CI?
/**
* Create a CLI manager pointing to the URL in the environment or to the
default URLs.
*
* @TODO: Implement a mock.
*/
def createCLIManager(Boolean alternate = false) {
var url = System.getenv("CODER_GATEWAY_TEST_DEPLOYMENT")
if (url == null) {
url = "https://dev.coder.com"
}
if (alternate) {
url = System.getenv("CODER_GATEWAY_TEST_DEPLOYMENT_ALT")
if (url == null) {
url = "https://oss.demo.coder.com"
}
}
var tmpdir = Path.of(System.getProperty("java.io.tmpdir")).resolve("coder-gateway-test")
return new CoderCLIManager(new URL(url), tmpdir)
}

def "defaults to a sub-directory in the data directory"() {
given:
def ccm = new CoderCLIManager(new URL("https://test.coder.invalid"))

expect:
ccm.localBinaryPath.getParent() == CoderCLIManager.getDataDir().resolve("test.coder.invalid")
}

def "downloads a working cli"() {
given:
def ccm = new CoderCLIManager(new URL("https://dev.coder.com"))
def ccm = createCLIManager()
ccm.localBinaryPath.getParent().toFile().deleteDir()

when:
Expand All @@ -25,7 +52,7 @@ class CoderCLIManagerTest extends spock.lang.Specification {

def "overwrites cli if incorrect version"() {
given:
def ccm = new CoderCLIManager(new URL("https://dev.coder.com"))
def ccm = createCLIManager()
Files.createDirectories(ccm.localBinaryPath.getParent())
ccm.localBinaryPath.toFile().write("cli")

Expand All @@ -39,7 +66,7 @@ class CoderCLIManagerTest extends spock.lang.Specification {

def "skips cli download if it already exists"() {
given:
def ccm = new CoderCLIManager(new URL("https://dev.coder.com"))
def ccm = createCLIManager()

when:
ccm.downloadCLI()
Expand All @@ -52,8 +79,8 @@ class CoderCLIManagerTest extends spock.lang.Specification {

def "does not clobber other deployments"() {
given:
def ccm1 = new CoderCLIManager(new URL("https://oss.demo.coder.com"))
def ccm2 = new CoderCLIManager(new URL("https://dev.coder.com"))
def ccm1 = createCLIManager(true)
def ccm2 = createCLIManager()

when:
ccm1.downloadCLI()
Expand All @@ -63,25 +90,27 @@ class CoderCLIManagerTest extends spock.lang.Specification {
ccm1.localBinaryPath != ccm2.localBinaryPath
}

def testEnv = [
"APPDATA" : "/tmp/coder-gateway-test/appdata",
"LOCALAPPDATA" : "/tmp/coder-gateway-test/localappdata",
"HOME" : "/tmp/coder-gateway-test/home",
"XDG_CONFIG_HOME" : "/tmp/coder-gateway-test/xdg-config",
"XDG_DATA_HOME" : "/tmp/coder-gateway-test/xdg-data",
"CODER_CONFIG_DIR": "",
]

/**
* Get a config dir using default environment variable values.
*/
def configDir(Map<String, String> env = [:]) {
return CoderCLIManager.getConfigDir(new Environment([
"APPDATA" : "/tmp/coder-gateway-test/appdata",
"HOME" : "/tmp/coder-gateway-test/home",
"XDG_CONFIG_HOME" : "/tmp/coder-gateway-test/xdg",
"CODER_CONFIG_DIR": "",
] + env))
return CoderCLIManager.getConfigDir(new Environment(testEnv + env))
}

// Mostly just a sanity check to make sure the default System.getenv runs
// without throwing any errors.
def "gets config dir"() {
when:
def dir = CoderCLIManager.getConfigDir(new Environment([
"CODER_CONFIG_DIR": "",
]))
def dir = CoderCLIManager.getConfigDir()

then:
dir.toString().contains("coderv2")
Expand All @@ -97,13 +126,13 @@ class CoderCLIManagerTest extends spock.lang.Specification {
}

@Requires({ os.linux })
def "gets config dir from XDG or HOME"() {
def "gets config dir from XDG_CONFIG_HOME or HOME"() {
expect:
Path.of(path) == configDir(env)

where:
env || path
[:] || "/tmp/coder-gateway-test/xdg/coderv2"
[:] || "/tmp/coder-gateway-test/xdg-config/coderv2"
["XDG_CONFIG_HOME": ""] || "/tmp/coder-gateway-test/home/.config/coderv2"
}

Expand All @@ -118,4 +147,43 @@ class CoderCLIManagerTest extends spock.lang.Specification {
expect:
Path.of("/tmp/coder-gateway-test/appdata/coderv2") == configDir()
}

/**
* Get a data dir using default environment variable values.
*/
def dataDir(Map<String, String> env = [:]) {
return CoderCLIManager.getDataDir(new Environment(testEnv + env))
}
// Mostly just a sanity check to make sure the default System.getenv runs
// without throwing any errors.
def "gets data dir"() {
when:
def dir = CoderCLIManager.getDataDir()

then:
dir.toString().contains("coder-gateway")
}

@Requires({ os.linux })
def "gets data dir from XDG_DATA_HOME or HOME"() {
expect:
Path.of(path) == dataDir(env)

where:
env || path
[:] || "/tmp/coder-gateway-test/xdg-data/coder-gateway"
["XDG_DATA_HOME": ""] || "/tmp/coder-gateway-test/home/.local/share/coder-gateway"
}

@Requires({ os.macOs })
def "gets data dir from HOME"() {
expect:
Path.of("/tmp/coder-gateway-test/home/Library/Application Support/coder-gateway") == dataDir()
}

@Requires({ os.windows })
def "gets data dir from LOCALAPPDATA"() {
expect:
Path.of("/tmp/coder-gateway-test/localappdata/coder-gateway") == dataDir()
}
}