Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
8 changes: 5 additions & 3 deletions src/main/kotlin/com/coder/gateway/settings/CoderSettings.kt
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,10 @@ open class CoderSettings(
private val state: CoderSettingsState,
// The location of the SSH config. Defaults to ~/.ssh/config.
val sshConfigPath: Path = Path.of(System.getProperty("user.home")).resolve(".ssh/config"),
// Env allows overriding the default environment.
// Overrides the default environment (for tests).
private val env: Environment = Environment(),
// Overrides the default binary name (for tests).
private val binaryName: String? = null,
) {
val tls = CoderTLSSettings(state)
val enableDownloads: Boolean
Expand Down Expand Up @@ -68,10 +70,10 @@ open class CoderSettings(
* To where the specified deployment should download the binary.
*/
fun binPath(url: URL, forceDownloadToData: Boolean = false): Path {
val binaryName = getCoderCLIForOS(getOS(), getArch())
val name = binaryName ?: getCoderCLIForOS(getOS(), getArch())
val dir = if (forceDownloadToData || state.binaryDirectory.isBlank()) dataDir(url)
else withHost(Path.of(expand(state.binaryDirectory)), url)
return dir.resolve(binaryName).toAbsolutePath()
return dir.resolve(name).toAbsolutePath()
}

/**
Expand Down
170 changes: 91 additions & 79 deletions src/test/kotlin/com/coder/gateway/cli/CoderCLIManagerTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -32,23 +32,34 @@ import kotlin.test.assertNotEquals
import kotlin.test.assertTrue

internal class CoderCLIManagerTest {
private fun mkbin(version: String): String {
return listOf("#!/bin/sh", """echo '{"version": "$version"}'""")
.joinToString("\n")
/**
* Return the contents of a script that contains the string.
*/
private fun mkbin(str: String): String {
return if (getOS() == OS.WINDOWS) {
// Must use a .bat extension for this to work.
listOf("@echo off", str)
} else {
listOf("#!/bin/sh", str)
}.joinToString(System.lineSeparator())
}

/**
* Return the contents of a script that outputs JSON containing the version.
*/
private fun mkbinVersion(version: String): String {
return mkbin(echo("""{"version": "$version"}"""))
}

private fun mockServer(errorCode: Int = 0, version: String? = null): Pair<HttpServer, URL> {
val srv = HttpServer.create(InetSocketAddress(0), 0)
srv.createContext("/") {exchange ->
var code = HttpURLConnection.HTTP_OK
// TODO: Is there some simple way to create an executable file on
// Windows without having to execute something to generate said
// executable or having to commit one to the repo?
var response = mkbin(version ?: "${srv.address.port}.0.0")
var response = mkbinVersion(version ?: "${srv.address.port}.0.0")
val eTags = exchange.requestHeaders["If-None-Match"]
if (exchange.requestURI.path == "/bin/override") {
code = HttpURLConnection.HTTP_OK
response = mkbin("0.0.0")
response = mkbinVersion("0.0.0")
} else if (!exchange.requestURI.path.startsWith("/bin/coder-")) {
code = HttpURLConnection.HTTP_NOT_FOUND
response = "not found"
Expand Down Expand Up @@ -109,8 +120,7 @@ internal class CoderCLIManagerTest {

val (srv, url) = mockServer()
val ccm = CoderCLIManager(url, CoderSettings(CoderSettingsState(
dataDirectory = tmpdir.resolve("cli-dir-fail-to-write").toString()))
)
dataDirectory = tmpdir.resolve("cli-dir-fail-to-write").toString())))

ccm.localBinaryPath.parent.toFile().mkdirs()
ccm.localBinaryPath.parent.toFile().setWritable(false)
Expand All @@ -135,8 +145,7 @@ internal class CoderCLIManagerTest {
}

val ccm = CoderCLIManager(url.toURL(), CoderSettings(CoderSettingsState(
dataDirectory = tmpdir.resolve("real-cli").toString()))
)
dataDirectory = tmpdir.resolve("real-cli").toString())))

assertTrue(ccm.download())
assertDoesNotThrow { ccm.version() }
Expand All @@ -154,27 +163,19 @@ internal class CoderCLIManagerTest {
fun testDownloadMockCLI() {
val (srv, url) = mockServer()
var ccm = CoderCLIManager(url, CoderSettings(CoderSettingsState(
dataDirectory = tmpdir.resolve("mock-cli").toString()))
)
dataDirectory = tmpdir.resolve("mock-cli").toString()),
binaryName = "coder.bat"))

assertEquals(true, ccm.download())

// The mock does not serve a binary that works on Windows so do not
// actually execute. Checking the contents works just as well as proof
// that the binary was correctly downloaded anyway.
assertContains(ccm.localBinaryPath.toFile().readText(), url.port.toString())
if (getOS() != OS.WINDOWS) {
assertEquals(SemVer(url.port.toLong(), 0, 0), ccm.version())
}
assertEquals(SemVer(url.port.toLong(), 0, 0), ccm.version())

// It should skip the second attempt.
assertEquals(false, ccm.download())

// Should use the source override.
ccm = CoderCLIManager(url, CoderSettings(CoderSettingsState(
binarySource = "/bin/override",
dataDirectory = tmpdir.resolve("mock-cli").toString()))
)
dataDirectory = tmpdir.resolve("mock-cli").toString())))

assertEquals(true, ccm.download())
assertContains(ccm.localBinaryPath.toFile().readText(), "0.0.0")
Expand All @@ -185,8 +186,7 @@ internal class CoderCLIManagerTest {
@Test
fun testRunNonExistentBinary() {
val ccm = CoderCLIManager(URL("https://foo"), CoderSettings(CoderSettingsState(
dataDirectory = tmpdir.resolve("does-not-exist").toString()))
)
dataDirectory = tmpdir.resolve("does-not-exist").toString())))

assertFailsWith(
exceptionClass = ProcessInitException::class,
Expand All @@ -197,8 +197,7 @@ internal class CoderCLIManagerTest {
fun testOverwitesWrongVersion() {
val (srv, url) = mockServer()
val ccm = CoderCLIManager(url, CoderSettings(CoderSettingsState(
dataDirectory = tmpdir.resolve("overwrite-cli").toString()))
)
dataDirectory = tmpdir.resolve("overwrite-cli").toString())))

ccm.localBinaryPath.parent.toFile().mkdirs()
ccm.localBinaryPath.toFile().writeText("cli")
Expand Down Expand Up @@ -325,9 +324,7 @@ internal class CoderCLIManagerTest {
sshConfigPath = tmpdir.resolve("configured$it.conf"))
settings.sshConfigPath.parent.toFile().mkdirs()
Path.of("src/test/fixtures/inputs").resolve("$it.conf").toFile().copyTo(
settings.sshConfigPath.toFile(),
true,
)
settings.sshConfigPath.toFile(), true)

val ccm = CoderCLIManager(URL("https://test.coder.invalid"), settings)

Expand All @@ -345,41 +342,60 @@ internal class CoderCLIManagerTest {

tests.forEach {
val ccm = CoderCLIManager(URL("https://test.coder.invalid"), CoderSettings(CoderSettingsState(
headerCommand = it))
)
headerCommand = it)))

assertFailsWith(
exceptionClass = Exception::class,
block = { ccm.configSsh(listOf("foo", "bar")) })
}
}

@Test
fun testFailVersionParse() {
if (getOS() == OS.WINDOWS) {
return // Cannot execute mock binaries on Windows.
/**
* Return an echo command for the OS.
*/
private fun echo(str: String): String {
return if (getOS() == OS.WINDOWS) {
"echo $str"
} else {
"echo '$str'"
}
}

/**
* Return an exit command for the OS.
*/
private fun exit(code: Number): String {
return if (getOS() == OS.WINDOWS) {
"exit /b $code"
} else {
"exit $code"
}
}

@Test
fun testFailVersionParse() {
val tests = mapOf(
null to ProcessInitException::class,
"""echo '{"foo": true, "baz": 1}'""" to MissingVersionException::class,
"""echo '{"version: '""" to JsonSyntaxException::class,
"""echo '{"version": "invalid"}'""" to InvalidVersionException::class,
"exit 0" to MissingVersionException::class,
"exit 1" to InvalidExitValueException::class,
null to ProcessInitException::class,
echo("""{"foo": true, "baz": 1}""") to MissingVersionException::class,
echo("""{"version: """) to JsonSyntaxException::class,
echo("""{"version": "invalid"}""") to InvalidVersionException::class,
exit(0) to MissingVersionException::class,
exit(1) to InvalidExitValueException::class,
)

val ccm = CoderCLIManager(URL("https://test.coder.parse-fail.invalid"), CoderSettings(CoderSettingsState(
binaryDirectory = tmpdir.resolve("bad-version").toString()))
)
binaryDirectory = tmpdir.resolve("bad-version").toString()),
binaryName = "coder.bat"))
ccm.localBinaryPath.parent.toFile().mkdirs()

tests.forEach {
if (it.key == null) {
ccm.localBinaryPath.toFile().deleteRecursively()
} else {
ccm.localBinaryPath.toFile().writeText("#!/bin/sh\n${it.key}")
ccm.localBinaryPath.toFile().setExecutable(true)
ccm.localBinaryPath.toFile().writeText(mkbin(it.key!!))
if (getOS() != OS.WINDOWS) {
ccm.localBinaryPath.toFile().setExecutable(true)
}
}
assertFailsWith(
exceptionClass = it.value,
Expand All @@ -389,39 +405,37 @@ internal class CoderCLIManagerTest {

@Test
fun testMatchesVersion() {
if (getOS() == OS.WINDOWS) {
return
}

val test = listOf(
Triple(null, "v1.0.0", null),
Triple("""echo '{"version": "v1.0.0"}'""", "v1.0.0", true),
Triple("""echo '{"version": "v1.0.0"}'""", "v1.0.0-devel+b5b5b5b5", true),
Triple("""echo '{"version": "v1.0.0-devel+b5b5b5b5"}'""", "v1.0.0-devel+b5b5b5b5", true),
Triple("""echo '{"version": "v1.0.0-devel+b5b5b5b5"}'""", "v1.0.0", true),
Triple("""echo '{"version": "v1.0.0-devel+b5b5b5b5"}'""", "v1.0.0-devel+c6c6c6c6", true),
Triple("""echo '{"version": "v1.0.0-prod+b5b5b5b5"}'""", "v1.0.0-devel+b5b5b5b5", true),
Triple("""echo '{"version": "v1.0.0"}'""", "v1.0.1", false),
Triple("""echo '{"version": "v1.0.0"}'""", "v1.1.0", false),
Triple("""echo '{"version": "v1.0.0"}'""", "v2.0.0", false),
Triple("""echo '{"version": "v1.0.0"}'""", "v0.0.0", false),
Triple("""echo '{"version": ""}'""", "v1.0.0", null),
Triple("""echo '{"version": "v1.0.0"}'""", "", null),
Triple("""echo '{"version'""", "v1.0.0", null),
Triple("""exit 0""", "v1.0.0", null),
Triple("""exit 1""", "v1.0.0", null))
Triple(echo("""{"version": "v1.0.0"}"""), "v1.0.0", true),
Triple(echo("""{"version": "v1.0.0"}"""), "v1.0.0-devel+b5b5b5b5", true),
Triple(echo("""{"version": "v1.0.0-devel+b5b5b5b5"}"""), "v1.0.0-devel+b5b5b5b5", true),
Triple(echo("""{"version": "v1.0.0-devel+b5b5b5b5"}"""), "v1.0.0", true),
Triple(echo("""{"version": "v1.0.0-devel+b5b5b5b5"}"""), "v1.0.0-devel+c6c6c6c6", true),
Triple(echo("""{"version": "v1.0.0-prod+b5b5b5b5"}"""), "v1.0.0-devel+b5b5b5b5", true),
Triple(echo("""{"version": "v1.0.0"}"""), "v1.0.1", false),
Triple(echo("""{"version": "v1.0.0"}"""), "v1.1.0", false),
Triple(echo("""{"version": "v1.0.0"}"""), "v2.0.0", false),
Triple(echo("""{"version": "v1.0.0"}"""), "v0.0.0", false),
Triple(echo("""{"version": ""}"""), "v1.0.0", null),
Triple(echo("""{"version": "v1.0.0"}"""), "", null),
Triple(echo("""{"version"""), "v1.0.0", null),
Triple(exit(0), "v1.0.0", null),
Triple(exit(1), "v1.0.0", null))

val ccm = CoderCLIManager(URL("https://test.coder.matches-version.invalid"), CoderSettings(CoderSettingsState(
binaryDirectory = tmpdir.resolve("matches-version").toString()))
)
binaryDirectory = tmpdir.resolve("matches-version").toString()),
binaryName = "coder.bat"))
ccm.localBinaryPath.parent.toFile().mkdirs()

test.forEach {
if (it.first == null) {
ccm.localBinaryPath.toFile().deleteRecursively()
} else {
ccm.localBinaryPath.toFile().writeText("#!/bin/sh\n${it.first}")
ccm.localBinaryPath.toFile().setExecutable(true)
ccm.localBinaryPath.toFile().writeText(mkbin(it.first!!))
if (getOS() != OS.WINDOWS) {
ccm.localBinaryPath.toFile().setExecutable(true)
}
}

assertEquals(it.third, ccm.matchesVersion(it.second), it.first)
Expand All @@ -446,7 +460,9 @@ internal class CoderCLIManagerTest {
@Test
fun testEnsureCLI() {
if (getOS() == OS.WINDOWS) {
return // Cannot execute mock binaries on Windows and setWritable() works differently.
// TODO: setWritable() does not work the same way on Windows but we
// should test what we can.
return
}

val tests = listOf(
Expand Down Expand Up @@ -490,7 +506,7 @@ internal class CoderCLIManagerTest {
// Create a binary in the regular location.
if (it.version != null) {
settings.binPath(url).parent.toFile().mkdirs()
settings.binPath(url).toFile().writeText(mkbin(it.version))
settings.binPath(url).toFile().writeText(mkbinVersion(it.version))
settings.binPath(url).toFile().setExecutable(true)
}

Expand All @@ -503,7 +519,7 @@ internal class CoderCLIManagerTest {
// Create a binary in the fallback location.
if (it.fallbackVersion != null) {
settings.binPath(url, true).parent.toFile().mkdirs()
settings.binPath(url, true).toFile().writeText(mkbin(it.fallbackVersion))
settings.binPath(url, true).toFile().writeText(mkbinVersion(it.fallbackVersion))
settings.binPath(url, true).toFile().setExecutable(true)
}

Expand Down Expand Up @@ -553,10 +569,6 @@ internal class CoderCLIManagerTest {

@Test
fun testFeatures() {
if (getOS() == OS.WINDOWS) {
return // Cannot execute mock binaries on Windows.
}

val tests = listOf(
Pair("2.5.0", Features(true)),
Pair("4.9.0", Features(true)),
Expand All @@ -567,8 +579,8 @@ internal class CoderCLIManagerTest {
tests.forEach {
val (srv, url) = mockServer(version = it.first)
val ccm = CoderCLIManager(url, CoderSettings(CoderSettingsState(
dataDirectory = tmpdir.resolve("features").toString()))
)
dataDirectory = tmpdir.resolve("features").toString()),
binaryName = "coder.bat"))
assertEquals(true, ccm.download())
assertEquals(it.second, ccm.features, "version: ${it.first}")

Expand Down
Loading