Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
e3bd944
Impl: update the workspace panel every 5 seconds
fioan89 Aug 11, 2022
9ed6d9e
Fix: show the workspaces for which the agent can't be retrieved
fioan89 Aug 11, 2022
5a87781
Impl: workspace version status
fioan89 Aug 11, 2022
fd3a4be
Fix: render workspace name as bold
fioan89 Aug 11, 2022
45c17f4
Impl: add table action toolbar
fioan89 Aug 12, 2022
55e14ab
Fix: enable table antialiasing
fioan89 Aug 12, 2022
cce4f39
Use strings for workspace actions from localization bundle
fioan89 Aug 12, 2022
eb89d2c
Fix: enable or disable the workspace actions depending on the workspa…
fioan89 Aug 12, 2022
86654c3
Impl: add REST calls for starting/stopping workspaces
fioan89 Aug 12, 2022
f9db23e
Impl: add support for starting&stopping a workspace
fioan89 Aug 12, 2022
7d7c58c
Fix: consistent icon theming for start/stop workspace actions
fioan89 Aug 15, 2022
d1b1ba0
Log only request and response lines
fioan89 Aug 15, 2022
53577a7
Fix: terminal link for workspaces with a single agent
fioan89 Aug 15, 2022
43a1f6f
Upgrade to latest Gateway build
fioan89 Aug 15, 2022
869e99c
Fix: poll the workspaces every 5 seconds
fioan89 Aug 15, 2022
e2f8fe6
Fix: retrieve agents from the template
fioan89 Aug 16, 2022
f66debc
Fix: enable/disable start/stop actions after they were triggered
fioan89 Aug 17, 2022
2a9a5ba
Impl: REST calls to update a workspace to latest template
fioan89 Aug 17, 2022
8636fe7
Impl: action to update workspace to the latest template version
fioan89 Aug 17, 2022
cdde8e4
Fix: no such host is known errors
fioan89 Aug 17, 2022
b75685a
Next version is 2.1.0
fioan89 Aug 17, 2022
fc0016b
Fix: workspaces are now being refreshed when coming back from IDE&Pro…
fioan89 Aug 18, 2022
0f5aa11
Fix: disable the next button whe user selects a Windows or a macOS wo…
fioan89 Aug 18, 2022
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
64 changes: 39 additions & 25 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,51 +1,65 @@
<!-- Keep a Changelog guide -> https://keepachangelog.com -->
<!-- Keep a Changelog guide -> https://keepachangelog.com -->

# coder-gateway Changelog

## [Unreleased]
### Added
- support for displaying workspace version
- support for managing the lifecycle of a workspace, i.e. start and stop and update workspace to the latest template version

### Changed
- workspace panel is now updated every 5 seconds
- combinations of workspace names and agent names are now listed even when a workspace is down
- minimum supported Gateway build is now 222.3739.40

### Fixed
- terminal link for workspaces with a single agent
- no longer allow users to open a connection to a Windows or macOS workspace. It's not yet supported by Gateway

## [2.0.2]
### Added
- support for displaying working and non-working workspaces
- better support for Light and Dark themes in the "Status" column

### Added

### Fixed
- left panel is no longer visible when a new connection is triggered from Coder's "Recent Workspaces" panel.
This provides consistency with other plugins compatible with Gateway
- the "Select IDE and Project" button in the "Coder Workspaces" view is now disabled when no workspace is selected
- support for displaying working and non-working workspaces
- better support for Light and Dark themes in the "Status" column

### Fixed

- left panel is no longer visible when a new connection is triggered from Coder's "Recent Workspaces" panel.
This provides consistency with other plugins compatible with Gateway
- the "Select IDE and Project" button in the "Coder Workspaces" view is now disabled when no workspace is selected


### Changed
### Changed
- the authentication view is now merged with the "Coder Workspaces" view allowing users to quickly change the host

## [2.0.1]
### Fixed
- `Recent Coder Workspaces` label overlaps with the search bar in the `Connections` view
- working workspaces are now listed when there are issues with resolving agents
- list only workspaces owned by the logged user
- `Recent Coder Workspaces` label overlaps with the search bar in the `Connections` view
- working workspaces are now listed when there are issues with resolving agents
- list only workspaces owned by the logged user


### Changed
- links to documentation now point to the latest Coder OSS
- simplified main action link text from `Connect to Coder Workspaces` to `Connect to Coder`
- links to documentation now point to the latest Coder OSS
- simplified main action link text from `Connect to Coder Workspaces` to `Connect to Coder`
- minimum supported Gateway build is now 222.3739.24

## [2.0.0]
### Added
- support for Gateway 2022.2
- support for Gateway 2022.2


### Changed
- Java 17 is now required to run the plugin
- Java 17 is now required to run the plugin
- adapted the code to the new SSH API provided by Gateway

## [1.0.0]
### Added
- initial scaffold for Gateway plugin
- browser based authentication on Coder environments
- REST client for Coder V2 public API
- coder-cli orchestration for setting up the SSH configurations for Coder Workspaces
- basic panel to display live Coder Workspaces
- support for multi-agent Workspaces
- initial scaffold for Gateway plugin
- browser based authentication on Coder environments
- REST client for Coder V2 public API
- coder-cli orchestration for setting up the SSH configurations for Coder Workspaces
- basic panel to display live Coder Workspaces
- support for multi-agent Workspaces
- Gateway SSH connection to a Coder Workspace
8 changes: 4 additions & 4 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,16 @@
pluginGroup=com.coder.gateway
pluginName=coder-gateway
# SemVer format -> https://semver.org
pluginVersion=2.0.2
pluginVersion=2.1.0
# See https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html
# for insight into build numbers and IntelliJ Platform versions.
pluginSinceBuild=222.3739.24
pluginSinceBuild=222.3739.40
pluginUntilBuild=222.*
# IntelliJ Platform Properties -> https://github.com/JetBrains/gradle-intellij-plugin#intellij-platform-properties
# Gateway available build versions https://www.jetbrains.com/intellij-repository/snapshots and https://www.jetbrains.com/intellij-repository/releases
platformType=GW
platformVersion=222.3739.24-CUSTOM-SNAPSHOT
instrumentationCompiler=222.3739.24-CUSTOM-SNAPSHOT
platformVersion=222.3739.40-CUSTOM-SNAPSHOT
instrumentationCompiler=222.3739.40-CUSTOM-SNAPSHOT
platformDownloadSources=true
# Plugin Dependencies -> https://plugins.jetbrains.com/docs/intellij/plugin-dependencies.html
# Example: platformPlugins = com.intellij.java, com.jetbrains.php:203.4449.22
Expand Down
4 changes: 4 additions & 0 deletions src/main/kotlin/com/coder/gateway/icons/CoderIcons.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ object CoderIcons {

val OPEN_TERMINAL = IconLoader.getIcon("open_terminal.svg", javaClass)

val RUN = IconLoader.getIcon("run.svg", javaClass)
val STOP = IconLoader.getIcon("stop.svg", javaClass)
val UPDATE = IconLoader.getIcon("update.svg", javaClass)

val WINDOWS = IconLoader.getIcon("windows.svg", javaClass)
val MACOS = IconLoader.getIcon("macOS.svg", javaClass)
val LINUX = IconLoader.getIcon("linux.svg", javaClass)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@ data class CoderWorkspacesWizardModel(
var coderURL: String = "https://localhost",
var token: String = "",
var buildVersion: String = "",
var workspaceAgents: List<WorkspaceAgentModel> = mutableListOf(),
var localCliPath: String = "",
var selectedWorkspace: WorkspaceAgentModel? = null
)
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,17 @@ package com.coder.gateway.models

import com.coder.gateway.sdk.Arch
import com.coder.gateway.sdk.OS
import com.coder.gateway.sdk.v2.models.ProvisionerJobStatus
import com.coder.gateway.sdk.v2.models.WorkspaceBuildTransition
import java.util.UUID

data class WorkspaceAgentModel(
val workspaceID: UUID,
val workspaceName: String,
val name: String,
val templateID: UUID,
val templateName: String,
val status: WorkspaceVersionStatus,
val agentStatus: WorkspaceAgentStatus,
val lastBuildTransition: String,
val agentOS: OS?,
val agentArch: Arch?,
val homeDirectory: String?
Expand Down
14 changes: 14 additions & 0 deletions src/main/kotlin/com/coder/gateway/models/WorkspaceVersionStatus.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.coder.gateway.models

import com.coder.gateway.sdk.v2.models.Workspace

enum class WorkspaceVersionStatus(val label: String) {
UPDATED("Up to date"), OUTDATED("Outdated");

companion object {
fun from(workspace: Workspace) = when (workspace.outdated) {
true -> OUTDATED
false -> UPDATED
}
}
}
11 changes: 4 additions & 7 deletions src/main/kotlin/com/coder/gateway/sdk/CoderCLIDownloader.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,20 @@ import java.io.InputStream
import java.net.URL
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
import java.nio.file.StandardCopyOption

class CoderCLIDownloader(private val buildVersion: String) {
class CoderCLIDownloader {

fun downloadCLI(url: URL, outputName: String, ext: String): Path {
val filename = if (ext.isBlank()) "${outputName}-$buildVersion" else "${outputName}-${buildVersion}.${ext}"
val cliPath = Paths.get(System.getProperty("java.io.tmpdir"), filename)
fun downloadCLI(url: URL, cliPath: Path): Boolean {
if (Files.exists(cliPath)) {
logger.info("${cliPath.toAbsolutePath()} already exists, skipping download")
return cliPath
return false
}
logger.info("Starting Coder CLI download to ${cliPath.toAbsolutePath()}")
url.openStream().use {
Files.copy(it as InputStream, cliPath, StandardCopyOption.REPLACE_EXISTING)
}
return cliPath
return true
}

companion object {
Expand Down
19 changes: 13 additions & 6 deletions src/main/kotlin/com/coder/gateway/sdk/CoderCLIManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,20 @@ package com.coder.gateway.sdk
import com.intellij.openapi.diagnostic.Logger
import java.net.URL
import java.nio.file.Path
import java.nio.file.Paths

class CoderCLIManager(private val url: URL, buildVersion: String) {
private val coderCLIDownloader = CoderCLIDownloader(buildVersion)
class CoderCLIManager(url: URL, buildVersion: String) {
var remoteCliPath: URL
var localCliPath: Path

fun download(): Path? {
init {
val os = getOS()
val cliName = getCoderCLIForOS(os, getArch()) ?: return null
val cliNameWitExt = if (os == OS.WINDOWS) "$cliName.exe" else cliName
return coderCLIDownloader.downloadCLI(URL(url.protocol, url.host, url.port, "/bin/$cliNameWitExt"), cliName, if (os == OS.WINDOWS) "exe" else "")
val cliName = getCoderCLIForOS(os, getArch())
val cliNameWithExt = if (os == OS.WINDOWS) "$cliName.exe" else cliName
val filename = if (os == OS.WINDOWS) "${cliName}-${buildVersion}.exe" else "${cliName}-${buildVersion}"

remoteCliPath = URL(url.protocol, url.host, url.port, "/bin/$cliNameWithExt")
localCliPath = Paths.get(System.getProperty("java.io.tmpdir"), filename)
}

private fun getCoderCLIForOS(os: OS?, arch: Arch?): String? {
Expand All @@ -25,12 +30,14 @@ class CoderCLIManager(private val url: URL, buildVersion: String) {
Arch.ARM64 -> "coder-windows-arm64"
else -> "coder-windows-amd64"
}

OS.LINUX -> when (arch) {
Arch.AMD64 -> "coder-linux-amd64"
Arch.ARM64 -> "coder-linux-arm64"
Arch.ARMV7 -> "coder-linux-armv7"
else -> "coder-linux-amd64"
}

OS.MAC -> when (arch) {
Arch.AMD64 -> "coder-darwin-amd64"
Arch.ARM64 -> "coder-darwin-arm64"
Expand Down
56 changes: 51 additions & 5 deletions src/main/kotlin/com/coder/gateway/sdk/CoderRestClientService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,17 @@ package com.coder.gateway.sdk

import com.coder.gateway.sdk.convertors.InstantConverter
import com.coder.gateway.sdk.ex.AuthenticationResponseException
import com.coder.gateway.sdk.ex.TemplateResponseException
import com.coder.gateway.sdk.ex.WorkspaceResourcesResponseException
import com.coder.gateway.sdk.ex.WorkspaceResponseException
import com.coder.gateway.sdk.v2.CoderV2RestFacade
import com.coder.gateway.sdk.v2.models.BuildInfo
import com.coder.gateway.sdk.v2.models.CreateWorkspaceBuildRequest
import com.coder.gateway.sdk.v2.models.Template
import com.coder.gateway.sdk.v2.models.User
import com.coder.gateway.sdk.v2.models.Workspace
import com.coder.gateway.sdk.v2.models.WorkspaceAgent
import com.coder.gateway.sdk.v2.models.WorkspaceBuild
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.intellij.openapi.components.Service
Expand All @@ -20,8 +24,10 @@ import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.net.CookieManager
import java.net.HttpURLConnection.HTTP_CREATED
import java.net.URL
import java.time.Instant
import java.util.UUID

@Service(Service.Level.APP)
class CoderRestClientService {
Expand Down Expand Up @@ -49,7 +55,7 @@ class CoderRestClientService {
.create()

val interceptor = HttpLoggingInterceptor()
interceptor.setLevel(HttpLoggingInterceptor.Level.BODY)
interceptor.setLevel(HttpLoggingInterceptor.Level.BASIC)
retroRestClient = Retrofit.Builder()
.baseUrl(url.toString())
.client(
Expand Down Expand Up @@ -97,17 +103,57 @@ class CoderRestClientService {
}

/**
* Retrieves the workspace agents. A workspace is a collection of objects like, VMs, containers, cloud DBs, etc...
* Agents run on compute hosts like VMs or containers.
* Retrieves the workspace agents a template declares.
* A workspace is a collection of objects like, VMs, containers, cloud DBs, etc...Agents run on compute hosts like VMs or containers.
*
* @throws WorkspaceResourcesResponseException if workspace resources could not be retrieved.
*/
fun workspaceAgents(workspace: Workspace): List<WorkspaceAgent> {
val workspaceResourcesResponse = retroRestClient.workspaceResourceByBuild(workspace.latestBuild.id).execute()
fun workspaceAgentsByTemplate(workspace: Workspace): List<WorkspaceAgent> {
val workspaceResourcesResponse = retroRestClient.templateVersionResources(workspace.latestBuild.templateVersionID).execute()
if (!workspaceResourcesResponse.isSuccessful) {
throw WorkspaceResourcesResponseException("Could not retrieve agents for ${workspace.name} workspace :${workspaceResourcesResponse.code()}, reason: ${workspaceResourcesResponse.message()}")
}

return workspaceResourcesResponse.body()!!.flatMap { it.agents ?: emptyList() }
}

private fun template(templateID: UUID): Template {
val templateResponse = retroRestClient.template(templateID).execute()
if (!templateResponse.isSuccessful) {
throw TemplateResponseException("Failed to retrieve template with id: $templateID, reason: ${templateResponse.message()}")
}
return templateResponse.body()!!
}

fun startWorkspace(workspaceID: UUID, workspaceName: String): WorkspaceBuild {
val buildRequest = CreateWorkspaceBuildRequest(null, "start", null, null, null)
val buildResponse = retroRestClient.createWorkspaceBuild(workspaceID, buildRequest).execute()
if (buildResponse.code() != HTTP_CREATED) {
throw WorkspaceResponseException("Failed to build workspace ${workspaceName}: ${buildResponse.code()}, reason: ${buildResponse.message()}")
}

return buildResponse.body()!!
}

fun stopWorkspace(workspaceID: UUID, workspaceName: String): WorkspaceBuild {
val buildRequest = CreateWorkspaceBuildRequest(null, "stop", null, null, null)
val buildResponse = retroRestClient.createWorkspaceBuild(workspaceID, buildRequest).execute()
if (buildResponse.code() != HTTP_CREATED) {
throw WorkspaceResponseException("Failed to stop workspace ${workspaceName}: ${buildResponse.code()}, reason: ${buildResponse.message()}")
}

return buildResponse.body()!!
}

fun updateWorkspace(workspaceID: UUID, workspaceName: String, lastWorkspaceTransition: String, templateID: UUID): WorkspaceBuild {
val template = template(templateID)

val buildRequest = CreateWorkspaceBuildRequest(template.activeVersionID, lastWorkspaceTransition, null, null, null)
val buildResponse = retroRestClient.createWorkspaceBuild(workspaceID, buildRequest).execute()
if (buildResponse.code() != HTTP_CREATED) {
throw WorkspaceResponseException("Failed to update workspace ${workspaceName}: ${buildResponse.code()}, reason: ${buildResponse.message()}")
}

return buildResponse.body()!!
}
}
4 changes: 3 additions & 1 deletion src/main/kotlin/com/coder/gateway/sdk/ex/exceptions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,6 @@ class AuthenticationResponseException(reason: String) : IOException(reason)

class WorkspaceResponseException(reason: String) : IOException(reason)

class WorkspaceResourcesResponseException(reason: String) : IOException(reason)
class WorkspaceResourcesResponseException(reason: String) : IOException(reason)

class TemplateResponseException(reason: String) : IOException(reason)
18 changes: 16 additions & 2 deletions src/main/kotlin/com/coder/gateway/sdk/v2/CoderV2RestFacade.kt
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
package com.coder.gateway.sdk.v2

import com.coder.gateway.sdk.v2.models.BuildInfo
import com.coder.gateway.sdk.v2.models.CreateWorkspaceBuildRequest
import com.coder.gateway.sdk.v2.models.Template
import com.coder.gateway.sdk.v2.models.User
import com.coder.gateway.sdk.v2.models.Workspace
import com.coder.gateway.sdk.v2.models.WorkspaceBuild
import com.coder.gateway.sdk.v2.models.WorkspaceResource
import retrofit2.Call
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.POST
import retrofit2.http.Path
import retrofit2.http.Query
import java.util.UUID
Expand All @@ -27,6 +32,15 @@ interface CoderV2RestFacade {
@GET("api/v2/buildinfo")
fun buildInfo(): Call<BuildInfo>

@GET("api/v2/workspacebuilds/{buildID}/resources")
fun workspaceResourceByBuild(@Path("buildID") build: UUID): Call<List<WorkspaceResource>>
@GET("api/v2/templateversions/{templateID}/resources")
fun templateVersionResources(@Path("templateID") templateID: UUID): Call<List<WorkspaceResource>>

/**
* Queues a new build to occur for a workspace.
*/
@POST("api/v2/workspaces/{workspaceID}/builds")
fun createWorkspaceBuild(@Path("workspaceID") workspaceID: UUID, @Body createWorkspaceBuildRequest: CreateWorkspaceBuildRequest): Call<WorkspaceBuild>

@GET("api/v2/templates/{templateID}")
fun template(@Path("templateID") templateID: UUID): Call<Template>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.coder.gateway.sdk.v2.models

import com.google.gson.annotations.SerializedName
import java.util.UUID

data class CreateParameterRequest(
@SerializedName("copy_from_parameter") val cloneID: UUID?,
@SerializedName("name") val name: String,
@SerializedName("source_value") val sourceValue: String,
@SerializedName("source_scheme") val sourceScheme: String,
@SerializedName("destination_scheme") val destinationScheme: String
)
Loading