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
Changes from all commits
Commits
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
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
@@ -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)
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
@@ -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<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
@@ -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
@@ -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<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
@@ -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

Original file line number Diff line number Diff line change
@@ -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<Workspace>()
private var workspaces = CollectionListModel<WorkspaceAgentModel>()
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()
Original file line number Diff line number Diff line change
@@ -1,23 +1,22 @@
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
import java.awt.Component
import javax.swing.JList
import javax.swing.ListCellRenderer

class WorkspaceCellRenderer : ListCellRenderer<Workspace> {
class WorkspaceCellRenderer : ListCellRenderer<WorkspaceAgentModel> {

override fun getListCellRendererComponent(list: JList<out Workspace>, workspace: Workspace, index: Int, isSelected: Boolean, cellHasFocus: Boolean): Component {
override fun getListCellRendererComponent(list: JList<out WorkspaceAgentModel>, workspace: WorkspaceAgentModel, index: Int, isSelected: Boolean, cellHasFocus: Boolean): Component {
return panel {
indent {
row {
@@ -44,32 +43,36 @@ class WorkspaceCellRenderer : ListCellRenderer<Workspace> {
}
}

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"
10 changes: 0 additions & 10 deletions src/main/resources/centos.svg

This file was deleted.

6 changes: 6 additions & 0 deletions src/main/resources/macOS.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 0 additions & 1 deletion src/main/resources/ubuntu.svg

This file was deleted.

6 changes: 6 additions & 0 deletions src/main/resources/unknown.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions src/main/resources/windows.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.