Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Coder Gateway Plugin
# Coder Gateway Plugin

[!["Join us on
Discord"](https://img.shields.io/badge/join-us%20on%20Discord-gray.svg?longCache=true&logo=discord&colorB=purple)](https://discord.gg/coder)
Expand Down Expand Up @@ -87,7 +87,9 @@ The properties listed define the plugin itself or configure the [gradle-intellij

### Testing

No functional or UI tests are available yet.
Run tests with `./gradlew test`. By default this will test against
`https://dev.coder.com` but you can set `CODER_GATEWAY_TEST_DEPLOYMENT` to a URL
of your choice or to `mock` to use mocks only.

### Code Monitoring

Expand Down Expand Up @@ -127,7 +129,8 @@ In the `.github/workflows` directory, you can find definitions for the following
- Triggered on `Publish release` event.
- Updates `CHANGELOG.md` file with the content provided with the release note.
- Publishes the plugin to JetBrains Marketplace using the provided `PUBLISH_TOKEN`.
- Sets publish channel depending on the plugin version, i.e. `1.0.0-beta` -> `beta` channel. For now, both `main` and `eap` branches are published on default release channel.
- Sets publish channel depending on the plugin version, i.e. `1.0.0-beta` -> `beta` channel. For now, both `main`
and `eap` branches are published on default release channel.
- Patches the Changelog and commits.

### Release flow
Expand Down
82 changes: 47 additions & 35 deletions src/main/kotlin/com/coder/gateway/sdk/CoderCLIManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import java.io.BufferedInputStream
import java.io.FileInputStream
import java.io.FileNotFoundException
import java.net.HttpURLConnection
import java.net.IDN
import java.net.URL
import java.nio.file.Files
import java.nio.file.Path
Expand Down Expand Up @@ -34,9 +35,13 @@ class CoderCLIManager @JvmOverloads constructor(deployment: URL, destinationDir:
deployment.port,
"/bin/$binaryName"
)
val subdir = if (deployment.port > 0) "${deployment.host}-${deployment.port}" else deployment.host
// Convert IDN to ASCII in case the file system cannot support the
// necessary character set.
val host = IDN.toASCII(deployment.host, IDN.ALLOW_UNASSIGNED)
val subdir = if (deployment.port > 0) "${host}-${deployment.port}" else host
localBinaryPath = destinationDir.resolve(subdir).resolve(binaryName)
}

/**
* Return the name of the binary (with extension) for the provided OS and
* architecture.
Expand Down Expand Up @@ -80,57 +85,60 @@ class CoderCLIManager @JvmOverloads constructor(deployment: URL, destinationDir:
conn.setRequestProperty("If-None-Match", "\"$etag\"")
}
conn.setRequestProperty("Accept-Encoding", "gzip")
conn.connect()
logger.info("GET ${conn.responseCode} $remoteBinaryUrl")
when (conn.responseCode) {
HttpURLConnection.HTTP_OK -> {
logger.info("Downloading binary to ${localBinaryPath.toAbsolutePath()}")
Files.createDirectories(localBinaryPath.parent)
conn.inputStream.use {
Files.copy(
if (conn.contentEncoding == "gzip") GZIPInputStream(it) else it,
localBinaryPath,
StandardCopyOption.REPLACE_EXISTING,
)
}
if (getOS() != OS.WINDOWS) {
Files.setPosixFilePermissions(
localBinaryPath,
PosixFilePermissions.fromString("rwxr-x---")
)

try {
conn.connect()
logger.info("GET ${conn.responseCode} $remoteBinaryUrl")
when (conn.responseCode) {
HttpURLConnection.HTTP_OK -> {
logger.info("Downloading binary to ${localBinaryPath.toAbsolutePath()}")
Files.createDirectories(localBinaryPath.parent)
conn.inputStream.use {
Files.copy(
if (conn.contentEncoding == "gzip") GZIPInputStream(it) else it,
localBinaryPath,
StandardCopyOption.REPLACE_EXISTING,
)
}
if (getOS() != OS.WINDOWS) {
Files.setPosixFilePermissions(
localBinaryPath,
PosixFilePermissions.fromString("rwxr-x---")
)
}
return true
}
conn.disconnect()
return true
}

HttpURLConnection.HTTP_NOT_MODIFIED -> {
logger.info("Using cached binary at ${localBinaryPath.toAbsolutePath()}")
conn.disconnect()
return false
HttpURLConnection.HTTP_NOT_MODIFIED -> {
logger.info("Using cached binary at ${localBinaryPath.toAbsolutePath()}")
return false
}
}
} finally {
conn.disconnect()
}
conn.disconnect()
throw Exception("Unable to download $remoteBinaryUrl (got response code `${conn.responseCode}`)")
throw ResponseException("Unexpected response from $remoteBinaryUrl", conn.responseCode)
}

/**
* Return the entity tag for the binary on disk, if any.
*/
@Suppress("ControlFlowWithEmptyBody")
private fun getBinaryETag(): String? {
try {
return try {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👀 this is a cool thing that I didn't know was a thing!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same!! Thank goodness for IDE suggestions lol

val md = MessageDigest.getInstance("SHA-1")
val fis = FileInputStream(localBinaryPath.toFile())
val dis = DigestInputStream(BufferedInputStream(fis), md)
fis.use {
while (dis.read() != -1) {
}
}
return HexBinaryAdapter().marshal(md.digest()).lowercase()
HexBinaryAdapter().marshal(md.digest()).lowercase()
} catch (e: FileNotFoundException) {
return null
null
} catch (e: Exception) {
logger.warn("Unable to calculate hash for ${localBinaryPath.toAbsolutePath()}", e)
return null
null
}
}

Expand All @@ -152,10 +160,12 @@ class CoderCLIManager @JvmOverloads constructor(deployment: URL, destinationDir:
}

/**
* Execute the binary with the provided arguments.
*
* @return The command's output.
* Return the binary version.
*/
fun version(): String {
return exec("version")
}

private fun exec(vararg args: String): String {
val stdout = ProcessExecutor()
.command(localBinaryPath.toString(), *args)
Expand Down Expand Up @@ -243,3 +253,5 @@ class Environment(private val env: Map<String, String> = emptyMap()) {
return System.getenv(name)
}
}

class ResponseException(message: String, val code: Int) : Exception(message)
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import com.coder.gateway.sdk.CoderSemVer
import com.coder.gateway.sdk.IncompatibleVersionException
import com.coder.gateway.sdk.InvalidVersionException
import com.coder.gateway.sdk.OS
import com.coder.gateway.sdk.ResponseException
import com.coder.gateway.sdk.TemplateIconDownloader
import com.coder.gateway.sdk.ex.AuthenticationResponseException
import com.coder.gateway.sdk.ex.TemplateResponseException
Expand Down Expand Up @@ -444,6 +445,9 @@ class CoderWorkspacesStepView(val setNextButtonEnabled: (Boolean) -> Unit) : Cod
}
try {
cliManager.downloadCLI()
} catch (e: ResponseException) {
logger.error("Download failed with response code ${e.code}", e)
return@launchUnderBackgroundProgress
} catch (e: Exception) {
logger.error("Failed to download Coder CLI", e)
return@launchUnderBackgroundProgress
Expand Down
Loading