Skip to content

Impl support for multi agent workspaces #26

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Jun 28, 2022
Merged
5 changes: 3 additions & 2 deletions src/main/kotlin/com/coder/gateway/icons/CoderIcons.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@ object CoderIcons {

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

val UBUNTU = IconLoader.getIcon("ubuntu.svg", javaClass)
val CENTOS = IconLoader.getIcon("centos.svg", javaClass)
val WINDOWS = IconLoader.getIcon("windows.svg", javaClass)
val MACOS = IconLoader.getIcon("macOS.svg", javaClass)
val LINUX = IconLoader.getIcon("linux.svg", javaClass)
val UNKNOWN = IconLoader.getIcon("unknown.svg", javaClass)

val GREEN_CIRCLE = IconLoader.getIcon("green_circle.svg", javaClass)
val GRAY_CIRCLE = IconLoader.getIcon("gray_circle.svg", javaClass)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
package com.coder.gateway.models

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

data class CoderWorkspacesWizardModel(
var coderURL: String = "https://localhost",
var token: String = "",
var buildVersion: String = "",
var workspaces: List<Workspace> = mutableListOf(),
var selectedWorkspace: Workspace? = null
var workspaceAgents: List<WorkspaceAgentModel> = mutableListOf(),
var selectedWorkspace: WorkspaceAgentModel? = null
)
16 changes: 16 additions & 0 deletions src/main/kotlin/com/coder/gateway/models/WorkspaceAgentModel.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
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

data class WorkspaceAgentModel(
val name: String,

val jobStatus: ProvisionerJobStatus,
val buildTransition: WorkspaceBuildTransition,

val agentOS: OS?,
val agentArch: Arch?
)
17 changes: 17 additions & 0 deletions src/main/kotlin/com/coder/gateway/sdk/CoderRestClientService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ 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.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.User
import com.coder.gateway.sdk.v2.models.Workspace
import com.coder.gateway.sdk.v2.models.WorkspaceAgent
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.intellij.openapi.components.Service
Expand Down Expand Up @@ -93,4 +95,19 @@ class CoderRestClientService {
}
return buildInfoResponse.body()!!
}

/**
* 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.
*
* @throws WorkspaceResourcesResponseException if workspace resources could not be retrieved.
*/
fun workspaceAgents(workspace: Workspace): List<WorkspaceAgent> {
val workspaceResourcesResponse = retroRestClient.workspaceResourceByBuild(workspace.latestBuild.id).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() }
}
}
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 @@ -4,4 +4,6 @@ import java.io.IOException

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

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

class WorkspaceResourcesResponseException(reason: String) : IOException(reason)
57 changes: 35 additions & 22 deletions src/main/kotlin/com/coder/gateway/sdk/os.kt
Original file line number Diff line number Diff line change
@@ -1,35 +1,48 @@
package com.coder.gateway.sdk

fun getOS(): OS? {
val os = System.getProperty("os.name").toLowerCase()
return when {
os.contains("win", true) -> {
OS.WINDOWS
}
os.contains("nix", true) || os.contains("nux", true) || os.contains("aix", true) -> {
OS.LINUX
}
os.contains("mac", true) -> {
OS.MAC
}
else -> null
}
return OS.from(System.getProperty("os.name"))
}

fun getArch(): Arch? {
val arch = System.getProperty("os.arch").toLowerCase()
return when {
arch.contains("amd64", true) || arch.contains("x86_64", true) -> Arch.AMD64
arch.contains("arm64", true) || arch.contains("aarch64", true) -> Arch.ARM64
arch.contains("armv7", true) -> Arch.ARMV7
else -> null
}
return Arch.from(System.getProperty("os.arch").toLowerCase())
}

enum class OS {
WINDOWS, LINUX, MAC
WINDOWS, LINUX, MAC;

companion object {
fun from(os: String): OS? {
return when {
os.contains("win", true) -> {
WINDOWS
}

os.contains("nix", true) || os.contains("nux", true) || os.contains("aix", true) -> {
LINUX
}

os.contains("mac", true) -> {
MAC
}

else -> null
}
}
}
}

enum class Arch {
AMD64, ARM64, ARMV7
AMD64, ARM64, ARMV7;

companion object {
fun from(arch: String): Arch? {
return when {
arch.contains("amd64", true) || arch.contains("x86_64", true) -> AMD64
arch.contains("arm64", true) || arch.contains("aarch64", true) -> ARM64
arch.contains("armv7", true) -> ARMV7
else -> null
}
}
}
}
6 changes: 6 additions & 0 deletions src/main/kotlin/com/coder/gateway/sdk/v2/CoderV2RestFacade.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@ package com.coder.gateway.sdk.v2
import com.coder.gateway.sdk.v2.models.BuildInfo
import com.coder.gateway.sdk.v2.models.User
import com.coder.gateway.sdk.v2.models.Workspace
import com.coder.gateway.sdk.v2.models.WorkspaceResource
import retrofit2.Call
import retrofit2.http.GET
import retrofit2.http.Path
import java.util.UUID

interface CoderV2RestFacade {

Expand All @@ -22,4 +25,7 @@ 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>>
}
24 changes: 24 additions & 0 deletions src/main/kotlin/com/coder/gateway/sdk/v2/models/WorkspaceAgent.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.coder.gateway.sdk.v2.models

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

data class WorkspaceAgent(
@SerializedName("id") val id: UUID,
@SerializedName("created_at") val createdAt: Instant,
@SerializedName("updated_at") val updatedAt: Instant,
@SerializedName("first_connected_at") val firstConnectedAt: Instant?,
@SerializedName("last_connected_at") val lastConnectedAt: Instant?,
@SerializedName("disconnected_at") val disconnectedAt: Instant?,
@SerializedName("status") val status: String,
@SerializedName("name") val name: String,
@SerializedName("resource_id") val resourceID: UUID,
@SerializedName("instance_id") val instanceID: String,
@SerializedName("architecture") val architecture: String,
@SerializedName("environment_variables") val envVariables: Map<String, String>,
@SerializedName("operating_system") val operatingSystem: String,
@SerializedName("startup_script") val startupScript: String,
@SerializedName("directory") val directory: String,
@SerializedName("apps") val apps: List<WorkspaceApp>
)
11 changes: 11 additions & 0 deletions src/main/kotlin/com/coder/gateway/sdk/v2/models/WorkspaceApp.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.coder.gateway.sdk.v2.models

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

data class WorkspaceApp(
@SerializedName("id") val id: UUID,
@SerializedName("name") val name: String,
@SerializedName("command") val command: String?,
@SerializedName("icon") val icon: String?,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.coder.gateway.sdk.v2.models

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

data class WorkspaceResource(
@SerializedName("id") val id: UUID,
@SerializedName("created_at") val createdAt: Instant,
@SerializedName("job_id") val jobID: UUID,
@SerializedName("workspace_transition") val workspaceTransition: String,
@SerializedName("type") val type: String,
@SerializedName("name") val name: String,
@SerializedName("agents") val agents: List<WorkspaceAgent>?
)
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ package com.coder.gateway.views.steps
import com.coder.gateway.CoderGatewayBundle
import com.coder.gateway.icons.CoderIcons
import com.coder.gateway.models.CoderWorkspacesWizardModel
import com.coder.gateway.sdk.Arch
import com.coder.gateway.sdk.CoderRestClientService
import com.coder.gateway.sdk.OS
import com.coder.gateway.views.LazyBrowserLink
import com.intellij.ide.IdeBundle
import com.intellij.openapi.Disposable
Expand All @@ -25,6 +27,9 @@ import com.intellij.util.ui.JBFont
import com.intellij.util.ui.UIUtil
import com.jetbrains.gateway.api.GatewayUI
import com.jetbrains.gateway.ssh.CachingProductsJsonWrapper
import com.jetbrains.gateway.ssh.DeployTargetOS
import com.jetbrains.gateway.ssh.DeployTargetOS.OSArch
import com.jetbrains.gateway.ssh.DeployTargetOS.OSKind
import com.jetbrains.gateway.ssh.IdeStatus
import com.jetbrains.gateway.ssh.IdeWithStatus
import com.jetbrains.gateway.ssh.IntelliJPlatformProduct
Expand Down Expand Up @@ -111,7 +116,7 @@ class CoderLocateRemoteProjectStepView(private val disableNextAction: () -> Unit

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

private fun toDeployedOS(os: OS, arch: Arch): DeployTargetOS {
return when (os) {
OS.LINUX -> when (arch) {
Arch.AMD64 -> DeployTargetOS(OSKind.Linux, OSArch.X86_64)
Arch.ARM64 -> DeployTargetOS(OSKind.Linux, OSArch.Aarch64)
Arch.ARMV7 -> DeployTargetOS(OSKind.Linux, OSArch.Unknown)
}

OS.WINDOWS -> when (arch) {
Arch.AMD64 -> DeployTargetOS(OSKind.Windows, OSArch.X86_64)
Arch.ARM64 -> DeployTargetOS(OSKind.Windows, OSArch.Aarch64)
Arch.ARMV7 -> DeployTargetOS(OSKind.Windows, OSArch.Unknown)
}

OS.MAC -> when (arch) {
Arch.AMD64 -> DeployTargetOS(OSKind.MacOs, OSArch.X86_64)
Arch.ARM64 -> DeployTargetOS(OSKind.MacOs, OSArch.Aarch64)
Arch.ARMV7 -> DeployTargetOS(OSKind.MacOs, OSArch.Unknown)
}
}
}

override fun onNext(wizardModel: CoderWorkspacesWizardModel): Boolean {
val selectedIDE = cbIDE.selectedItem ?: return false

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ package com.coder.gateway.views.steps
import com.coder.gateway.CoderGatewayBundle
import com.coder.gateway.icons.CoderIcons
import com.coder.gateway.models.CoderWorkspacesWizardModel
import com.coder.gateway.models.WorkspaceAgentModel
import com.coder.gateway.sdk.Arch
import com.coder.gateway.sdk.CoderRestClientService
import com.coder.gateway.sdk.v2.models.Workspace
import com.coder.gateway.sdk.OS
import com.intellij.ide.IdeBundle
import com.intellij.openapi.Disposable
import com.intellij.openapi.application.ApplicationManager
Expand All @@ -28,7 +30,7 @@ class CoderWorkspacesStepView : CoderWorkspacesWizardStep, Disposable {
private val cs = CoroutineScope(Dispatchers.Main)

private val coderClient: CoderRestClientService = ApplicationManager.getApplication().getService(CoderRestClientService::class.java)
private var workspaces = CollectionListModel<Workspace>()
private var workspaces = CollectionListModel<WorkspaceAgentModel>()
private var workspacesView = JBList(workspaces)

private lateinit var wizard: CoderWorkspacesWizardModel
Expand Down Expand Up @@ -60,7 +62,22 @@ class CoderWorkspacesStepView : CoderWorkspacesWizardStep, Disposable {
cs.launch {
val workspaceList = withContext(Dispatchers.IO) {
try {
coderClient.workspaces()
val workspaces = coderClient.workspaces()
return@withContext workspaces.flatMap { workspace ->
val agents = coderClient.workspaceAgents(workspace)
val shouldContainAgentName = agents.size > 1
agents.map { agent ->
val workspaceName = if (shouldContainAgentName) "${workspace.name}.${agent.name}" else workspace.name
WorkspaceAgentModel(
workspaceName,
workspace.latestBuild.job.status,
workspace.latestBuild.workspaceTransition,
OS.from(agent.operatingSystem),
Arch.from(agent.architecture)

)
}
}
} catch (e: Exception) {
logger.error("Could not retrieve workspaces for ${coderClient.me.username} on ${coderClient.coderURL}. Reason: $e")
emptyList()
Expand Down
Loading