diff --git a/src/main/kotlin/com/coder/gateway/icons/CoderIcons.kt b/src/main/kotlin/com/coder/gateway/icons/CoderIcons.kt index 3fac2af3..2d084eb0 100644 --- a/src/main/kotlin/com/coder/gateway/icons/CoderIcons.kt +++ b/src/main/kotlin/com/coder/gateway/icons/CoderIcons.kt @@ -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) diff --git a/src/main/kotlin/com/coder/gateway/models/CoderWorkspacesWizardModel.kt b/src/main/kotlin/com/coder/gateway/models/CoderWorkspacesWizardModel.kt index 9b865644..990b4f43 100644 --- a/src/main/kotlin/com/coder/gateway/models/CoderWorkspacesWizardModel.kt +++ b/src/main/kotlin/com/coder/gateway/models/CoderWorkspacesWizardModel.kt @@ -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 = mutableListOf(), - var selectedWorkspace: Workspace? = null + var workspaceAgents: List = mutableListOf(), + var selectedWorkspace: WorkspaceAgentModel? = null ) \ No newline at end of file diff --git a/src/main/kotlin/com/coder/gateway/models/WorkspaceAgentModel.kt b/src/main/kotlin/com/coder/gateway/models/WorkspaceAgentModel.kt new file mode 100644 index 00000000..e8c5e157 --- /dev/null +++ b/src/main/kotlin/com/coder/gateway/models/WorkspaceAgentModel.kt @@ -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? +) diff --git a/src/main/kotlin/com/coder/gateway/sdk/CoderRestClientService.kt b/src/main/kotlin/com/coder/gateway/sdk/CoderRestClientService.kt index c1d79115..786a1512 100644 --- a/src/main/kotlin/com/coder/gateway/sdk/CoderRestClientService.kt +++ b/src/main/kotlin/com/coder/gateway/sdk/CoderRestClientService.kt @@ -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 @@ -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 { + 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() } + } } \ No newline at end of file diff --git a/src/main/kotlin/com/coder/gateway/sdk/ex/exceptions.kt b/src/main/kotlin/com/coder/gateway/sdk/ex/exceptions.kt index d24e338c..983611b0 100644 --- a/src/main/kotlin/com/coder/gateway/sdk/ex/exceptions.kt +++ b/src/main/kotlin/com/coder/gateway/sdk/ex/exceptions.kt @@ -4,4 +4,6 @@ import java.io.IOException class AuthenticationResponseException(reason: String) : IOException(reason) -class WorkspaceResponseException(reason: String) : IOException(reason) \ No newline at end of file +class WorkspaceResponseException(reason: String) : IOException(reason) + +class WorkspaceResourcesResponseException(reason: String) : IOException(reason) \ No newline at end of file diff --git a/src/main/kotlin/com/coder/gateway/sdk/os.kt b/src/main/kotlin/com/coder/gateway/sdk/os.kt index df342173..c8682b32 100644 --- a/src/main/kotlin/com/coder/gateway/sdk/os.kt +++ b/src/main/kotlin/com/coder/gateway/sdk/os.kt @@ -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 + } + } + } } \ No newline at end of file diff --git a/src/main/kotlin/com/coder/gateway/sdk/v2/CoderV2RestFacade.kt b/src/main/kotlin/com/coder/gateway/sdk/v2/CoderV2RestFacade.kt index 3355d427..9ef91d52 100644 --- a/src/main/kotlin/com/coder/gateway/sdk/v2/CoderV2RestFacade.kt +++ b/src/main/kotlin/com/coder/gateway/sdk/v2/CoderV2RestFacade.kt @@ -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 { @@ -22,4 +25,7 @@ interface CoderV2RestFacade { @GET("api/v2/buildinfo") fun buildInfo(): Call + + @GET("api/v2/workspacebuilds/{buildID}/resources") + fun workspaceResourceByBuild(@Path("buildID") build: UUID): Call> } \ No newline at end of file diff --git a/src/main/kotlin/com/coder/gateway/sdk/v2/models/WorkspaceAgent.kt b/src/main/kotlin/com/coder/gateway/sdk/v2/models/WorkspaceAgent.kt new file mode 100644 index 00000000..18ed6777 --- /dev/null +++ b/src/main/kotlin/com/coder/gateway/sdk/v2/models/WorkspaceAgent.kt @@ -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, + @SerializedName("operating_system") val operatingSystem: String, + @SerializedName("startup_script") val startupScript: String, + @SerializedName("directory") val directory: String, + @SerializedName("apps") val apps: List +) \ No newline at end of file diff --git a/src/main/kotlin/com/coder/gateway/sdk/v2/models/WorkspaceApp.kt b/src/main/kotlin/com/coder/gateway/sdk/v2/models/WorkspaceApp.kt new file mode 100644 index 00000000..736dd813 --- /dev/null +++ b/src/main/kotlin/com/coder/gateway/sdk/v2/models/WorkspaceApp.kt @@ -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?, +) diff --git a/src/main/kotlin/com/coder/gateway/sdk/v2/models/WorkspaceResource.kt b/src/main/kotlin/com/coder/gateway/sdk/v2/models/WorkspaceResource.kt new file mode 100644 index 00000000..8b993232 --- /dev/null +++ b/src/main/kotlin/com/coder/gateway/sdk/v2/models/WorkspaceResource.kt @@ -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? +) diff --git a/src/main/kotlin/com/coder/gateway/views/steps/CoderLocateRemoteProjectStepView.kt b/src/main/kotlin/com/coder/gateway/views/steps/CoderLocateRemoteProjectStepView.kt index 63a854bd..7944d224 100644 --- a/src/main/kotlin/com/coder/gateway/views/steps/CoderLocateRemoteProjectStepView.kt +++ b/src/main/kotlin/com/coder/gateway/views/steps/CoderLocateRemoteProjectStepView.kt @@ -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 @@ -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 @@ -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}") @@ -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 diff --git a/src/main/kotlin/com/coder/gateway/views/steps/CoderWorkspacesStepView.kt b/src/main/kotlin/com/coder/gateway/views/steps/CoderWorkspacesStepView.kt index 822fef7b..76ca28d0 100644 --- a/src/main/kotlin/com/coder/gateway/views/steps/CoderWorkspacesStepView.kt +++ b/src/main/kotlin/com/coder/gateway/views/steps/CoderWorkspacesStepView.kt @@ -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 @@ -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() + private var workspaces = CollectionListModel() private var workspacesView = JBList(workspaces) private lateinit var wizard: CoderWorkspacesWizardModel @@ -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() diff --git a/src/main/kotlin/com/coder/gateway/views/steps/WorkspaceCellRenderer.kt b/src/main/kotlin/com/coder/gateway/views/steps/WorkspaceCellRenderer.kt index e7170a6c..e89773ef 100644 --- a/src/main/kotlin/com/coder/gateway/views/steps/WorkspaceCellRenderer.kt +++ b/src/main/kotlin/com/coder/gateway/views/steps/WorkspaceCellRenderer.kt @@ -1,13 +1,12 @@ package com.coder.gateway.views.steps -import com.coder.gateway.icons.CoderIcons.CENTOS +import com.coder.gateway.icons.CoderIcons import com.coder.gateway.icons.CoderIcons.GRAY_CIRCLE import com.coder.gateway.icons.CoderIcons.GREEN_CIRCLE -import com.coder.gateway.icons.CoderIcons.LINUX import com.coder.gateway.icons.CoderIcons.RED_CIRCLE -import com.coder.gateway.icons.CoderIcons.UBUNTU +import com.coder.gateway.models.WorkspaceAgentModel +import com.coder.gateway.sdk.OS import com.coder.gateway.sdk.v2.models.ProvisionerJobStatus -import com.coder.gateway.sdk.v2.models.Workspace import com.coder.gateway.sdk.v2.models.WorkspaceBuildTransition import com.intellij.ui.dsl.builder.panel import com.intellij.util.ui.JBFont @@ -15,9 +14,9 @@ import java.awt.Component import javax.swing.JList import javax.swing.ListCellRenderer -class WorkspaceCellRenderer : ListCellRenderer { +class WorkspaceCellRenderer : ListCellRenderer { - override fun getListCellRendererComponent(list: JList, workspace: Workspace, index: Int, isSelected: Boolean, cellHasFocus: Boolean): Component { + override fun getListCellRendererComponent(list: JList, workspace: WorkspaceAgentModel, index: Int, isSelected: Boolean, cellHasFocus: Boolean): Component { return panel { indent { row { @@ -44,32 +43,36 @@ class WorkspaceCellRenderer : ListCellRenderer { } } - private fun iconForImageTag(workspace: Workspace) = when (workspace.templateName) { - "ubuntu" -> UBUNTU - "centos" -> CENTOS - else -> LINUX + private fun iconForImageTag(workspace: WorkspaceAgentModel) = when (workspace?.agentOS) { + OS.LINUX -> CoderIcons.LINUX + OS.WINDOWS -> CoderIcons.WINDOWS + OS.MAC -> CoderIcons.MACOS + else -> CoderIcons.UNKNOWN } - private fun iconForStatus(workspace: Workspace) = when (workspace.latestBuild.job.status) { - ProvisionerJobStatus.SUCCEEDED -> if (workspace.latestBuild.workspaceTransition == WorkspaceBuildTransition.START) GREEN_CIRCLE else RED_CIRCLE - ProvisionerJobStatus.RUNNING -> when (workspace.latestBuild.workspaceTransition) { + private fun iconForStatus(workspace: WorkspaceAgentModel) = when (workspace.jobStatus) { + ProvisionerJobStatus.SUCCEEDED -> if (workspace.buildTransition == WorkspaceBuildTransition.START) GREEN_CIRCLE else RED_CIRCLE + ProvisionerJobStatus.RUNNING -> when (workspace.buildTransition) { WorkspaceBuildTransition.START, WorkspaceBuildTransition.STOP, WorkspaceBuildTransition.DELETE -> GRAY_CIRCLE } + else -> RED_CIRCLE } - private fun labelForStatus(workspace: Workspace) = when (workspace.latestBuild.job.status) { + private fun labelForStatus(workspace: WorkspaceAgentModel) = when (workspace.jobStatus) { ProvisionerJobStatus.PENDING -> "◍ Queued" - ProvisionerJobStatus.RUNNING -> when (workspace.latestBuild.workspaceTransition) { + ProvisionerJobStatus.RUNNING -> when (workspace.buildTransition) { WorkspaceBuildTransition.START -> "⦿ Starting" WorkspaceBuildTransition.STOP -> "◍ Stopping" WorkspaceBuildTransition.DELETE -> "⦸ Deleting" } - ProvisionerJobStatus.SUCCEEDED -> when (workspace.latestBuild.workspaceTransition) { + + ProvisionerJobStatus.SUCCEEDED -> when (workspace.buildTransition) { WorkspaceBuildTransition.START -> "⦿ Running" WorkspaceBuildTransition.STOP -> "◍ Stopped" WorkspaceBuildTransition.DELETE -> "⦸ Deleted" } + ProvisionerJobStatus.CANCELING -> "◍ Canceling action" ProvisionerJobStatus.CANCELED -> "◍ Canceled action" ProvisionerJobStatus.FAILED -> "ⓧ Failed" diff --git a/src/main/resources/centos.svg b/src/main/resources/centos.svg deleted file mode 100644 index 62da80c5..00000000 --- a/src/main/resources/centos.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/src/main/resources/macOS.svg b/src/main/resources/macOS.svg new file mode 100644 index 00000000..e0c6cbd4 --- /dev/null +++ b/src/main/resources/macOS.svg @@ -0,0 +1,6 @@ + + + + + diff --git a/src/main/resources/ubuntu.svg b/src/main/resources/ubuntu.svg deleted file mode 100644 index 0150e343..00000000 --- a/src/main/resources/ubuntu.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/main/resources/unknown.svg b/src/main/resources/unknown.svg new file mode 100644 index 00000000..1f8cd754 --- /dev/null +++ b/src/main/resources/unknown.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/main/resources/windows.svg b/src/main/resources/windows.svg new file mode 100644 index 00000000..71da3d35 --- /dev/null +++ b/src/main/resources/windows.svg @@ -0,0 +1,5 @@ + + + + +