Skip to content

Commit ea83e1d

Browse files
authored
Merge pull request #64 from coder/impl-workspace-lifecycle-management
Impl workspace lifecycle management
2 parents 4de8c50 + 0f5aa11 commit ea83e1d

25 files changed

+556
-115
lines changed

CHANGELOG.md

Lines changed: 39 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,65 @@
1-
<!-- Keep a Changelog guide -> https://keepachangelog.com -->
2-
1+
<!-- Keep a Changelog guide -> https://keepachangelog.com -->
2+
33
# coder-gateway Changelog
44

55
## [Unreleased]
6+
### Added
7+
- support for displaying workspace version
8+
- support for managing the lifecycle of a workspace, i.e. start and stop and update workspace to the latest template version
9+
10+
### Changed
11+
- workspace panel is now updated every 5 seconds
12+
- combinations of workspace names and agent names are now listed even when a workspace is down
13+
- minimum supported Gateway build is now 222.3739.40
14+
15+
### Fixed
16+
- terminal link for workspaces with a single agent
17+
- no longer allow users to open a connection to a Windows or macOS workspace. It's not yet supported by Gateway
618

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

21+
### Added
1222

13-
### Fixed
14-
- left panel is no longer visible when a new connection is triggered from Coder's "Recent Workspaces" panel.
15-
This provides consistency with other plugins compatible with Gateway
16-
- the "Select IDE and Project" button in the "Coder Workspaces" view is now disabled when no workspace is selected
23+
- support for displaying working and non-working workspaces
24+
- better support for Light and Dark themes in the "Status" column
25+
26+
### Fixed
27+
28+
- left panel is no longer visible when a new connection is triggered from Coder's "Recent Workspaces" panel.
29+
This provides consistency with other plugins compatible with Gateway
30+
- the "Select IDE and Project" button in the "Coder Workspaces" view is now disabled when no workspace is selected
1731

1832

19-
### Changed
33+
### Changed
2034
- the authentication view is now merged with the "Coder Workspaces" view allowing users to quickly change the host
2135

2236
## [2.0.1]
2337
### Fixed
24-
- `Recent Coder Workspaces` label overlaps with the search bar in the `Connections` view
25-
- working workspaces are now listed when there are issues with resolving agents
26-
- list only workspaces owned by the logged user
27-
38+
- `Recent Coder Workspaces` label overlaps with the search bar in the `Connections` view
39+
- working workspaces are now listed when there are issues with resolving agents
40+
- list only workspaces owned by the logged user
41+
2842

2943
### Changed
30-
- links to documentation now point to the latest Coder OSS
31-
- simplified main action link text from `Connect to Coder Workspaces` to `Connect to Coder`
44+
- links to documentation now point to the latest Coder OSS
45+
- simplified main action link text from `Connect to Coder Workspaces` to `Connect to Coder`
3246
- minimum supported Gateway build is now 222.3739.24
3347

3448
## [2.0.0]
3549
### Added
36-
- support for Gateway 2022.2
37-
50+
- support for Gateway 2022.2
51+
3852

3953
### Changed
40-
- Java 17 is now required to run the plugin
54+
- Java 17 is now required to run the plugin
4155
- adapted the code to the new SSH API provided by Gateway
4256

4357
## [1.0.0]
4458
### Added
45-
- initial scaffold for Gateway plugin
46-
- browser based authentication on Coder environments
47-
- REST client for Coder V2 public API
48-
- coder-cli orchestration for setting up the SSH configurations for Coder Workspaces
49-
- basic panel to display live Coder Workspaces
50-
- support for multi-agent Workspaces
59+
- initial scaffold for Gateway plugin
60+
- browser based authentication on Coder environments
61+
- REST client for Coder V2 public API
62+
- coder-cli orchestration for setting up the SSH configurations for Coder Workspaces
63+
- basic panel to display live Coder Workspaces
64+
- support for multi-agent Workspaces
5165
- Gateway SSH connection to a Coder Workspace

gradle.properties

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,16 @@
33
pluginGroup=com.coder.gateway
44
pluginName=coder-gateway
55
# SemVer format -> https://semver.org
6-
pluginVersion=2.0.2
6+
pluginVersion=2.1.0
77
# See https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html
88
# for insight into build numbers and IntelliJ Platform versions.
9-
pluginSinceBuild=222.3739.24
9+
pluginSinceBuild=222.3739.40
1010
pluginUntilBuild=222.*
1111
# IntelliJ Platform Properties -> https://github.com/JetBrains/gradle-intellij-plugin#intellij-platform-properties
1212
# Gateway available build versions https://www.jetbrains.com/intellij-repository/snapshots and https://www.jetbrains.com/intellij-repository/releases
1313
platformType=GW
14-
platformVersion=222.3739.24-CUSTOM-SNAPSHOT
15-
instrumentationCompiler=222.3739.24-CUSTOM-SNAPSHOT
14+
platformVersion=222.3739.40-CUSTOM-SNAPSHOT
15+
instrumentationCompiler=222.3739.40-CUSTOM-SNAPSHOT
1616
platformDownloadSources=true
1717
# Plugin Dependencies -> https://plugins.jetbrains.com/docs/intellij/plugin-dependencies.html
1818
# Example: platformPlugins = com.intellij.java, com.jetbrains.php:203.4449.22

src/main/kotlin/com/coder/gateway/icons/CoderIcons.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ object CoderIcons {
99

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

12+
val RUN = IconLoader.getIcon("run.svg", javaClass)
13+
val STOP = IconLoader.getIcon("stop.svg", javaClass)
14+
val UPDATE = IconLoader.getIcon("update.svg", javaClass)
15+
1216
val WINDOWS = IconLoader.getIcon("windows.svg", javaClass)
1317
val MACOS = IconLoader.getIcon("macOS.svg", javaClass)
1418
val LINUX = IconLoader.getIcon("linux.svg", javaClass)

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,6 @@ data class CoderWorkspacesWizardModel(
44
var coderURL: String = "https://localhost",
55
var token: String = "",
66
var buildVersion: String = "",
7-
var workspaceAgents: List<WorkspaceAgentModel> = mutableListOf(),
7+
var localCliPath: String = "",
88
var selectedWorkspace: WorkspaceAgentModel? = null
99
)

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,17 @@ package com.coder.gateway.models
22

33
import com.coder.gateway.sdk.Arch
44
import com.coder.gateway.sdk.OS
5-
import com.coder.gateway.sdk.v2.models.ProvisionerJobStatus
6-
import com.coder.gateway.sdk.v2.models.WorkspaceBuildTransition
5+
import java.util.UUID
76

87
data class WorkspaceAgentModel(
8+
val workspaceID: UUID,
9+
val workspaceName: String,
910
val name: String,
11+
val templateID: UUID,
1012
val templateName: String,
13+
val status: WorkspaceVersionStatus,
1114
val agentStatus: WorkspaceAgentStatus,
15+
val lastBuildTransition: String,
1216
val agentOS: OS?,
1317
val agentArch: Arch?,
1418
val homeDirectory: String?
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package com.coder.gateway.models
2+
3+
import com.coder.gateway.sdk.v2.models.Workspace
4+
5+
enum class WorkspaceVersionStatus(val label: String) {
6+
UPDATED("Up to date"), OUTDATED("Outdated");
7+
8+
companion object {
9+
fun from(workspace: Workspace) = when (workspace.outdated) {
10+
true -> OUTDATED
11+
false -> UPDATED
12+
}
13+
}
14+
}

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

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,20 @@ import java.io.InputStream
55
import java.net.URL
66
import java.nio.file.Files
77
import java.nio.file.Path
8-
import java.nio.file.Paths
98
import java.nio.file.StandardCopyOption
109

11-
class CoderCLIDownloader(private val buildVersion: String) {
10+
class CoderCLIDownloader {
1211

13-
fun downloadCLI(url: URL, outputName: String, ext: String): Path {
14-
val filename = if (ext.isBlank()) "${outputName}-$buildVersion" else "${outputName}-${buildVersion}.${ext}"
15-
val cliPath = Paths.get(System.getProperty("java.io.tmpdir"), filename)
12+
fun downloadCLI(url: URL, cliPath: Path): Boolean {
1613
if (Files.exists(cliPath)) {
1714
logger.info("${cliPath.toAbsolutePath()} already exists, skipping download")
18-
return cliPath
15+
return false
1916
}
2017
logger.info("Starting Coder CLI download to ${cliPath.toAbsolutePath()}")
2118
url.openStream().use {
2219
Files.copy(it as InputStream, cliPath, StandardCopyOption.REPLACE_EXISTING)
2320
}
24-
return cliPath
21+
return true
2522
}
2623

2724
companion object {

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

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,20 @@ package com.coder.gateway.sdk
33
import com.intellij.openapi.diagnostic.Logger
44
import java.net.URL
55
import java.nio.file.Path
6+
import java.nio.file.Paths
67

7-
class CoderCLIManager(private val url: URL, buildVersion: String) {
8-
private val coderCLIDownloader = CoderCLIDownloader(buildVersion)
8+
class CoderCLIManager(url: URL, buildVersion: String) {
9+
var remoteCliPath: URL
10+
var localCliPath: Path
911

10-
fun download(): Path? {
12+
init {
1113
val os = getOS()
12-
val cliName = getCoderCLIForOS(os, getArch()) ?: return null
13-
val cliNameWitExt = if (os == OS.WINDOWS) "$cliName.exe" else cliName
14-
return coderCLIDownloader.downloadCLI(URL(url.protocol, url.host, url.port, "/bin/$cliNameWitExt"), cliName, if (os == OS.WINDOWS) "exe" else "")
14+
val cliName = getCoderCLIForOS(os, getArch())
15+
val cliNameWithExt = if (os == OS.WINDOWS) "$cliName.exe" else cliName
16+
val filename = if (os == OS.WINDOWS) "${cliName}-${buildVersion}.exe" else "${cliName}-${buildVersion}"
17+
18+
remoteCliPath = URL(url.protocol, url.host, url.port, "/bin/$cliNameWithExt")
19+
localCliPath = Paths.get(System.getProperty("java.io.tmpdir"), filename)
1520
}
1621

1722
private fun getCoderCLIForOS(os: OS?, arch: Arch?): String? {
@@ -25,12 +30,14 @@ class CoderCLIManager(private val url: URL, buildVersion: String) {
2530
Arch.ARM64 -> "coder-windows-arm64"
2631
else -> "coder-windows-amd64"
2732
}
33+
2834
OS.LINUX -> when (arch) {
2935
Arch.AMD64 -> "coder-linux-amd64"
3036
Arch.ARM64 -> "coder-linux-arm64"
3137
Arch.ARMV7 -> "coder-linux-armv7"
3238
else -> "coder-linux-amd64"
3339
}
40+
3441
OS.MAC -> when (arch) {
3542
Arch.AMD64 -> "coder-darwin-amd64"
3643
Arch.ARM64 -> "coder-darwin-arm64"

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

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,17 @@ package com.coder.gateway.sdk
22

33
import com.coder.gateway.sdk.convertors.InstantConverter
44
import com.coder.gateway.sdk.ex.AuthenticationResponseException
5+
import com.coder.gateway.sdk.ex.TemplateResponseException
56
import com.coder.gateway.sdk.ex.WorkspaceResourcesResponseException
67
import com.coder.gateway.sdk.ex.WorkspaceResponseException
78
import com.coder.gateway.sdk.v2.CoderV2RestFacade
89
import com.coder.gateway.sdk.v2.models.BuildInfo
10+
import com.coder.gateway.sdk.v2.models.CreateWorkspaceBuildRequest
11+
import com.coder.gateway.sdk.v2.models.Template
912
import com.coder.gateway.sdk.v2.models.User
1013
import com.coder.gateway.sdk.v2.models.Workspace
1114
import com.coder.gateway.sdk.v2.models.WorkspaceAgent
15+
import com.coder.gateway.sdk.v2.models.WorkspaceBuild
1216
import com.google.gson.Gson
1317
import com.google.gson.GsonBuilder
1418
import com.intellij.openapi.components.Service
@@ -20,8 +24,10 @@ import okhttp3.logging.HttpLoggingInterceptor
2024
import retrofit2.Retrofit
2125
import retrofit2.converter.gson.GsonConverterFactory
2226
import java.net.CookieManager
27+
import java.net.HttpURLConnection.HTTP_CREATED
2328
import java.net.URL
2429
import java.time.Instant
30+
import java.util.UUID
2531

2632
@Service(Service.Level.APP)
2733
class CoderRestClientService {
@@ -49,7 +55,7 @@ class CoderRestClientService {
4955
.create()
5056

5157
val interceptor = HttpLoggingInterceptor()
52-
interceptor.setLevel(HttpLoggingInterceptor.Level.BODY)
58+
interceptor.setLevel(HttpLoggingInterceptor.Level.BASIC)
5359
retroRestClient = Retrofit.Builder()
5460
.baseUrl(url.toString())
5561
.client(
@@ -97,17 +103,57 @@ class CoderRestClientService {
97103
}
98104

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

111117
return workspaceResourcesResponse.body()!!.flatMap { it.agents ?: emptyList() }
112118
}
119+
120+
private fun template(templateID: UUID): Template {
121+
val templateResponse = retroRestClient.template(templateID).execute()
122+
if (!templateResponse.isSuccessful) {
123+
throw TemplateResponseException("Failed to retrieve template with id: $templateID, reason: ${templateResponse.message()}")
124+
}
125+
return templateResponse.body()!!
126+
}
127+
128+
fun startWorkspace(workspaceID: UUID, workspaceName: String): WorkspaceBuild {
129+
val buildRequest = CreateWorkspaceBuildRequest(null, "start", null, null, null)
130+
val buildResponse = retroRestClient.createWorkspaceBuild(workspaceID, buildRequest).execute()
131+
if (buildResponse.code() != HTTP_CREATED) {
132+
throw WorkspaceResponseException("Failed to build workspace ${workspaceName}: ${buildResponse.code()}, reason: ${buildResponse.message()}")
133+
}
134+
135+
return buildResponse.body()!!
136+
}
137+
138+
fun stopWorkspace(workspaceID: UUID, workspaceName: String): WorkspaceBuild {
139+
val buildRequest = CreateWorkspaceBuildRequest(null, "stop", null, null, null)
140+
val buildResponse = retroRestClient.createWorkspaceBuild(workspaceID, buildRequest).execute()
141+
if (buildResponse.code() != HTTP_CREATED) {
142+
throw WorkspaceResponseException("Failed to stop workspace ${workspaceName}: ${buildResponse.code()}, reason: ${buildResponse.message()}")
143+
}
144+
145+
return buildResponse.body()!!
146+
}
147+
148+
fun updateWorkspace(workspaceID: UUID, workspaceName: String, lastWorkspaceTransition: String, templateID: UUID): WorkspaceBuild {
149+
val template = template(templateID)
150+
151+
val buildRequest = CreateWorkspaceBuildRequest(template.activeVersionID, lastWorkspaceTransition, null, null, null)
152+
val buildResponse = retroRestClient.createWorkspaceBuild(workspaceID, buildRequest).execute()
153+
if (buildResponse.code() != HTTP_CREATED) {
154+
throw WorkspaceResponseException("Failed to update workspace ${workspaceName}: ${buildResponse.code()}, reason: ${buildResponse.message()}")
155+
}
156+
157+
return buildResponse.body()!!
158+
}
113159
}

src/main/kotlin/com/coder/gateway/sdk/ex/exceptions.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,6 @@ class AuthenticationResponseException(reason: String) : IOException(reason)
66

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

9-
class WorkspaceResourcesResponseException(reason: String) : IOException(reason)
9+
class WorkspaceResourcesResponseException(reason: String) : IOException(reason)
10+
11+
class TemplateResponseException(reason: String) : IOException(reason)

0 commit comments

Comments
 (0)