Skip to content

Commit 2edaf7c

Browse files
authored
Merge pull request #26 from coder/impl-support-for-multi-agent-workspaces
Impl support for multi agent workspaces - add REST models for Workspace agents - add REST calls to retrieve the workspace resources - use custom exception & add docs - refactor: return the agents instead the full list of resources - add new icons for win/mac - add new view model for workspace - UI needs to present a combination of properties from two data models (Workspace and WorkspaceAgent) - implement support for workspaces with multiple agents - the hostname for the connection is `coder.WorkspaceName` if there is at most an agent. - whereas if there are multiple agents, the hostname becomes `coder.WorkspaceName.AgentName` - the OS and Arch are also retrieved from the agent. - removed specific Linux distribution icons - refactored the utilities around OS&Arch resolving - resolves #2 - use agent's OS and Arch to determine the supported Jetbrains products - the mechanism to determine the supported jetbrains products based on OS/Arch uses the os/arch properties from the agent data models. - if that is missing we fall back on the old way which opens an SSH connection and runs a few unix commands to determine the properties
2 parents 6da49cb + c06299d commit 2edaf7c

18 files changed

+216
-60
lines changed

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

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

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

12-
val UBUNTU = IconLoader.getIcon("ubuntu.svg", javaClass)
13-
val CENTOS = IconLoader.getIcon("centos.svg", javaClass)
12+
val WINDOWS = IconLoader.getIcon("windows.svg", javaClass)
13+
val MACOS = IconLoader.getIcon("macOS.svg", javaClass)
1414
val LINUX = IconLoader.getIcon("linux.svg", javaClass)
15+
val UNKNOWN = IconLoader.getIcon("unknown.svg", javaClass)
1516

1617
val GREEN_CIRCLE = IconLoader.getIcon("green_circle.svg", javaClass)
1718
val GRAY_CIRCLE = IconLoader.getIcon("gray_circle.svg", javaClass)
Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
package com.coder.gateway.models
22

3-
import com.coder.gateway.sdk.v2.models.Workspace
4-
53
data class CoderWorkspacesWizardModel(
64
var coderURL: String = "https://localhost",
75
var token: String = "",
86
var buildVersion: String = "",
9-
var workspaces: List<Workspace> = mutableListOf(),
10-
var selectedWorkspace: Workspace? = null
7+
var workspaceAgents: List<WorkspaceAgentModel> = mutableListOf(),
8+
var selectedWorkspace: WorkspaceAgentModel? = null
119
)
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package com.coder.gateway.models
2+
3+
import com.coder.gateway.sdk.Arch
4+
import com.coder.gateway.sdk.OS
5+
import com.coder.gateway.sdk.v2.models.ProvisionerJobStatus
6+
import com.coder.gateway.sdk.v2.models.WorkspaceBuildTransition
7+
8+
data class WorkspaceAgentModel(
9+
val name: String,
10+
11+
val jobStatus: ProvisionerJobStatus,
12+
val buildTransition: WorkspaceBuildTransition,
13+
14+
val agentOS: OS?,
15+
val agentArch: Arch?
16+
)

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@ 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.WorkspaceResourcesResponseException
56
import com.coder.gateway.sdk.ex.WorkspaceResponseException
67
import com.coder.gateway.sdk.v2.CoderV2RestFacade
78
import com.coder.gateway.sdk.v2.models.BuildInfo
89
import com.coder.gateway.sdk.v2.models.User
910
import com.coder.gateway.sdk.v2.models.Workspace
11+
import com.coder.gateway.sdk.v2.models.WorkspaceAgent
1012
import com.google.gson.Gson
1113
import com.google.gson.GsonBuilder
1214
import com.intellij.openapi.components.Service
@@ -93,4 +95,19 @@ class CoderRestClientService {
9395
}
9496
return buildInfoResponse.body()!!
9597
}
98+
99+
/**
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.
102+
*
103+
* @throws WorkspaceResourcesResponseException if workspace resources could not be retrieved.
104+
*/
105+
fun workspaceAgents(workspace: Workspace): List<WorkspaceAgent> {
106+
val workspaceResourcesResponse = retroRestClient.workspaceResourceByBuild(workspace.latestBuild.id).execute()
107+
if (!workspaceResourcesResponse.isSuccessful) {
108+
throw WorkspaceResourcesResponseException("Could not retrieve agents for ${workspace.name} workspace :${workspaceResourcesResponse.code()}, reason: ${workspaceResourcesResponse.message()}")
109+
}
110+
111+
return workspaceResourcesResponse.body()!!.flatMap { it.agents ?: emptyList() }
112+
}
96113
}

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,6 @@ import java.io.IOException
44

55
class AuthenticationResponseException(reason: String) : IOException(reason)
66

7-
class WorkspaceResponseException(reason: String) : IOException(reason)
7+
class WorkspaceResponseException(reason: String) : IOException(reason)
8+
9+
class WorkspaceResourcesResponseException(reason: String) : IOException(reason)
Lines changed: 35 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,48 @@
11
package com.coder.gateway.sdk
22

33
fun getOS(): OS? {
4-
val os = System.getProperty("os.name").toLowerCase()
5-
return when {
6-
os.contains("win", true) -> {
7-
OS.WINDOWS
8-
}
9-
os.contains("nix", true) || os.contains("nux", true) || os.contains("aix", true) -> {
10-
OS.LINUX
11-
}
12-
os.contains("mac", true) -> {
13-
OS.MAC
14-
}
15-
else -> null
16-
}
4+
return OS.from(System.getProperty("os.name"))
175
}
186

197
fun getArch(): Arch? {
20-
val arch = System.getProperty("os.arch").toLowerCase()
21-
return when {
22-
arch.contains("amd64", true) || arch.contains("x86_64", true) -> Arch.AMD64
23-
arch.contains("arm64", true) || arch.contains("aarch64", true) -> Arch.ARM64
24-
arch.contains("armv7", true) -> Arch.ARMV7
25-
else -> null
26-
}
8+
return Arch.from(System.getProperty("os.arch").toLowerCase())
279
}
2810

2911
enum class OS {
30-
WINDOWS, LINUX, MAC
12+
WINDOWS, LINUX, MAC;
13+
14+
companion object {
15+
fun from(os: String): OS? {
16+
return when {
17+
os.contains("win", true) -> {
18+
WINDOWS
19+
}
20+
21+
os.contains("nix", true) || os.contains("nux", true) || os.contains("aix", true) -> {
22+
LINUX
23+
}
24+
25+
os.contains("mac", true) -> {
26+
MAC
27+
}
28+
29+
else -> null
30+
}
31+
}
32+
}
3133
}
3234

3335
enum class Arch {
34-
AMD64, ARM64, ARMV7
36+
AMD64, ARM64, ARMV7;
37+
38+
companion object {
39+
fun from(arch: String): Arch? {
40+
return when {
41+
arch.contains("amd64", true) || arch.contains("x86_64", true) -> AMD64
42+
arch.contains("arm64", true) || arch.contains("aarch64", true) -> ARM64
43+
arch.contains("armv7", true) -> ARMV7
44+
else -> null
45+
}
46+
}
47+
}
3548
}

src/main/kotlin/com/coder/gateway/sdk/v2/CoderV2RestFacade.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,11 @@ package com.coder.gateway.sdk.v2
33
import com.coder.gateway.sdk.v2.models.BuildInfo
44
import com.coder.gateway.sdk.v2.models.User
55
import com.coder.gateway.sdk.v2.models.Workspace
6+
import com.coder.gateway.sdk.v2.models.WorkspaceResource
67
import retrofit2.Call
78
import retrofit2.http.GET
9+
import retrofit2.http.Path
10+
import java.util.UUID
811

912
interface CoderV2RestFacade {
1013

@@ -22,4 +25,7 @@ interface CoderV2RestFacade {
2225

2326
@GET("api/v2/buildinfo")
2427
fun buildInfo(): Call<BuildInfo>
28+
29+
@GET("api/v2/workspacebuilds/{buildID}/resources")
30+
fun workspaceResourceByBuild(@Path("buildID") build: UUID): Call<List<WorkspaceResource>>
2531
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.coder.gateway.sdk.v2.models
2+
3+
import com.google.gson.annotations.SerializedName
4+
import java.time.Instant
5+
import java.util.UUID
6+
7+
data class WorkspaceAgent(
8+
@SerializedName("id") val id: UUID,
9+
@SerializedName("created_at") val createdAt: Instant,
10+
@SerializedName("updated_at") val updatedAt: Instant,
11+
@SerializedName("first_connected_at") val firstConnectedAt: Instant?,
12+
@SerializedName("last_connected_at") val lastConnectedAt: Instant?,
13+
@SerializedName("disconnected_at") val disconnectedAt: Instant?,
14+
@SerializedName("status") val status: String,
15+
@SerializedName("name") val name: String,
16+
@SerializedName("resource_id") val resourceID: UUID,
17+
@SerializedName("instance_id") val instanceID: String,
18+
@SerializedName("architecture") val architecture: String,
19+
@SerializedName("environment_variables") val envVariables: Map<String, String>,
20+
@SerializedName("operating_system") val operatingSystem: String,
21+
@SerializedName("startup_script") val startupScript: String,
22+
@SerializedName("directory") val directory: String,
23+
@SerializedName("apps") val apps: List<WorkspaceApp>
24+
)
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.coder.gateway.sdk.v2.models
2+
3+
import com.google.gson.annotations.SerializedName
4+
import java.util.UUID
5+
6+
data class WorkspaceApp(
7+
@SerializedName("id") val id: UUID,
8+
@SerializedName("name") val name: String,
9+
@SerializedName("command") val command: String?,
10+
@SerializedName("icon") val icon: String?,
11+
)
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package com.coder.gateway.sdk.v2.models
2+
3+
import com.google.gson.annotations.SerializedName
4+
import java.time.Instant
5+
import java.util.UUID
6+
7+
data class WorkspaceResource(
8+
@SerializedName("id") val id: UUID,
9+
@SerializedName("created_at") val createdAt: Instant,
10+
@SerializedName("job_id") val jobID: UUID,
11+
@SerializedName("workspace_transition") val workspaceTransition: String,
12+
@SerializedName("type") val type: String,
13+
@SerializedName("name") val name: String,
14+
@SerializedName("agents") val agents: List<WorkspaceAgent>?
15+
)

src/main/kotlin/com/coder/gateway/views/steps/CoderLocateRemoteProjectStepView.kt

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ package com.coder.gateway.views.steps
33
import com.coder.gateway.CoderGatewayBundle
44
import com.coder.gateway.icons.CoderIcons
55
import com.coder.gateway.models.CoderWorkspacesWizardModel
6+
import com.coder.gateway.sdk.Arch
67
import com.coder.gateway.sdk.CoderRestClientService
8+
import com.coder.gateway.sdk.OS
79
import com.coder.gateway.views.LazyBrowserLink
810
import com.intellij.ide.IdeBundle
911
import com.intellij.openapi.Disposable
@@ -25,6 +27,9 @@ import com.intellij.util.ui.JBFont
2527
import com.intellij.util.ui.UIUtil
2628
import com.jetbrains.gateway.api.GatewayUI
2729
import com.jetbrains.gateway.ssh.CachingProductsJsonWrapper
30+
import com.jetbrains.gateway.ssh.DeployTargetOS
31+
import com.jetbrains.gateway.ssh.DeployTargetOS.OSArch
32+
import com.jetbrains.gateway.ssh.DeployTargetOS.OSKind
2833
import com.jetbrains.gateway.ssh.IdeStatus
2934
import com.jetbrains.gateway.ssh.IdeWithStatus
3035
import com.jetbrains.gateway.ssh.IntelliJPlatformProduct
@@ -111,7 +116,7 @@ class CoderLocateRemoteProjectStepView(private val disableNextAction: () -> Unit
111116

112117
cs.launch {
113118
logger.info("Retrieving available IDE's for ${selectedWorkspace.name} workspace...")
114-
val workspaceOS = withContext(Dispatchers.IO) {
119+
val workspaceOS = if (selectedWorkspace.agentOS != null && selectedWorkspace.agentArch != null) withContext(Dispatchers.IO) { toDeployedOS(selectedWorkspace.agentOS, selectedWorkspace.agentArch) } else withContext(Dispatchers.IO) {
115120
try {
116121
RemoteCredentialsHolder().apply {
117122
setHost("coder.${selectedWorkspace.name}")
@@ -149,6 +154,28 @@ class CoderLocateRemoteProjectStepView(private val disableNextAction: () -> Unit
149154
}
150155
}
151156

157+
private fun toDeployedOS(os: OS, arch: Arch): DeployTargetOS {
158+
return when (os) {
159+
OS.LINUX -> when (arch) {
160+
Arch.AMD64 -> DeployTargetOS(OSKind.Linux, OSArch.X86_64)
161+
Arch.ARM64 -> DeployTargetOS(OSKind.Linux, OSArch.Aarch64)
162+
Arch.ARMV7 -> DeployTargetOS(OSKind.Linux, OSArch.Unknown)
163+
}
164+
165+
OS.WINDOWS -> when (arch) {
166+
Arch.AMD64 -> DeployTargetOS(OSKind.Windows, OSArch.X86_64)
167+
Arch.ARM64 -> DeployTargetOS(OSKind.Windows, OSArch.Aarch64)
168+
Arch.ARMV7 -> DeployTargetOS(OSKind.Windows, OSArch.Unknown)
169+
}
170+
171+
OS.MAC -> when (arch) {
172+
Arch.AMD64 -> DeployTargetOS(OSKind.MacOs, OSArch.X86_64)
173+
Arch.ARM64 -> DeployTargetOS(OSKind.MacOs, OSArch.Aarch64)
174+
Arch.ARMV7 -> DeployTargetOS(OSKind.MacOs, OSArch.Unknown)
175+
}
176+
}
177+
}
178+
152179
override fun onNext(wizardModel: CoderWorkspacesWizardModel): Boolean {
153180
val selectedIDE = cbIDE.selectedItem ?: return false
154181

src/main/kotlin/com/coder/gateway/views/steps/CoderWorkspacesStepView.kt

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@ package com.coder.gateway.views.steps
33
import com.coder.gateway.CoderGatewayBundle
44
import com.coder.gateway.icons.CoderIcons
55
import com.coder.gateway.models.CoderWorkspacesWizardModel
6+
import com.coder.gateway.models.WorkspaceAgentModel
7+
import com.coder.gateway.sdk.Arch
68
import com.coder.gateway.sdk.CoderRestClientService
7-
import com.coder.gateway.sdk.v2.models.Workspace
9+
import com.coder.gateway.sdk.OS
810
import com.intellij.ide.IdeBundle
911
import com.intellij.openapi.Disposable
1012
import com.intellij.openapi.application.ApplicationManager
@@ -28,7 +30,7 @@ class CoderWorkspacesStepView : CoderWorkspacesWizardStep, Disposable {
2830
private val cs = CoroutineScope(Dispatchers.Main)
2931

3032
private val coderClient: CoderRestClientService = ApplicationManager.getApplication().getService(CoderRestClientService::class.java)
31-
private var workspaces = CollectionListModel<Workspace>()
33+
private var workspaces = CollectionListModel<WorkspaceAgentModel>()
3234
private var workspacesView = JBList(workspaces)
3335

3436
private lateinit var wizard: CoderWorkspacesWizardModel
@@ -60,7 +62,22 @@ class CoderWorkspacesStepView : CoderWorkspacesWizardStep, Disposable {
6062
cs.launch {
6163
val workspaceList = withContext(Dispatchers.IO) {
6264
try {
63-
coderClient.workspaces()
65+
val workspaces = coderClient.workspaces()
66+
return@withContext workspaces.flatMap { workspace ->
67+
val agents = coderClient.workspaceAgents(workspace)
68+
val shouldContainAgentName = agents.size > 1
69+
agents.map { agent ->
70+
val workspaceName = if (shouldContainAgentName) "${workspace.name}.${agent.name}" else workspace.name
71+
WorkspaceAgentModel(
72+
workspaceName,
73+
workspace.latestBuild.job.status,
74+
workspace.latestBuild.workspaceTransition,
75+
OS.from(agent.operatingSystem),
76+
Arch.from(agent.architecture)
77+
78+
)
79+
}
80+
}
6481
} catch (e: Exception) {
6582
logger.error("Could not retrieve workspaces for ${coderClient.me.username} on ${coderClient.coderURL}. Reason: $e")
6683
emptyList()

0 commit comments

Comments
 (0)