diff --git a/CHANGELOG.md b/CHANGELOG.md index 0019348e..8d260bae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,51 +1,65 @@ - - + + # coder-gateway Changelog ## [Unreleased] +### Added +- support for displaying workspace version +- support for managing the lifecycle of a workspace, i.e. start and stop and update workspace to the latest template version + +### Changed +- workspace panel is now updated every 5 seconds +- combinations of workspace names and agent names are now listed even when a workspace is down +- minimum supported Gateway build is now 222.3739.40 + +### Fixed +- terminal link for workspaces with a single agent +- no longer allow users to open a connection to a Windows or macOS workspace. It's not yet supported by Gateway ## [2.0.2] -### Added -- support for displaying working and non-working workspaces -- better support for Light and Dark themes in the "Status" column +### Added -### Fixed -- left panel is no longer visible when a new connection is triggered from Coder's "Recent Workspaces" panel. - This provides consistency with other plugins compatible with Gateway -- the "Select IDE and Project" button in the "Coder Workspaces" view is now disabled when no workspace is selected +- support for displaying working and non-working workspaces +- better support for Light and Dark themes in the "Status" column + +### Fixed + +- left panel is no longer visible when a new connection is triggered from Coder's "Recent Workspaces" panel. + This provides consistency with other plugins compatible with Gateway +- the "Select IDE and Project" button in the "Coder Workspaces" view is now disabled when no workspace is selected -### Changed +### Changed - the authentication view is now merged with the "Coder Workspaces" view allowing users to quickly change the host ## [2.0.1] ### Fixed -- `Recent Coder Workspaces` label overlaps with the search bar in the `Connections` view -- working workspaces are now listed when there are issues with resolving agents -- list only workspaces owned by the logged user - +- `Recent Coder Workspaces` label overlaps with the search bar in the `Connections` view +- working workspaces are now listed when there are issues with resolving agents +- list only workspaces owned by the logged user + ### Changed -- links to documentation now point to the latest Coder OSS -- simplified main action link text from `Connect to Coder Workspaces` to `Connect to Coder` +- links to documentation now point to the latest Coder OSS +- simplified main action link text from `Connect to Coder Workspaces` to `Connect to Coder` - minimum supported Gateway build is now 222.3739.24 ## [2.0.0] ### Added -- support for Gateway 2022.2 - +- support for Gateway 2022.2 + ### Changed -- Java 17 is now required to run the plugin +- Java 17 is now required to run the plugin - adapted the code to the new SSH API provided by Gateway ## [1.0.0] ### Added -- initial scaffold for Gateway plugin -- browser based authentication on Coder environments -- REST client for Coder V2 public API -- coder-cli orchestration for setting up the SSH configurations for Coder Workspaces -- basic panel to display live Coder Workspaces -- support for multi-agent Workspaces +- initial scaffold for Gateway plugin +- browser based authentication on Coder environments +- REST client for Coder V2 public API +- coder-cli orchestration for setting up the SSH configurations for Coder Workspaces +- basic panel to display live Coder Workspaces +- support for multi-agent Workspaces - Gateway SSH connection to a Coder Workspace \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 90e0c3ec..541b8aec 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,16 +3,16 @@ pluginGroup=com.coder.gateway pluginName=coder-gateway # SemVer format -> https://semver.org -pluginVersion=2.0.2 +pluginVersion=2.1.0 # See https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html # for insight into build numbers and IntelliJ Platform versions. -pluginSinceBuild=222.3739.24 +pluginSinceBuild=222.3739.40 pluginUntilBuild=222.* # IntelliJ Platform Properties -> https://github.com/JetBrains/gradle-intellij-plugin#intellij-platform-properties # Gateway available build versions https://www.jetbrains.com/intellij-repository/snapshots and https://www.jetbrains.com/intellij-repository/releases platformType=GW -platformVersion=222.3739.24-CUSTOM-SNAPSHOT -instrumentationCompiler=222.3739.24-CUSTOM-SNAPSHOT +platformVersion=222.3739.40-CUSTOM-SNAPSHOT +instrumentationCompiler=222.3739.40-CUSTOM-SNAPSHOT platformDownloadSources=true # Plugin Dependencies -> https://plugins.jetbrains.com/docs/intellij/plugin-dependencies.html # Example: platformPlugins = com.intellij.java, com.jetbrains.php:203.4449.22 diff --git a/src/main/kotlin/com/coder/gateway/icons/CoderIcons.kt b/src/main/kotlin/com/coder/gateway/icons/CoderIcons.kt index 2d084eb0..3f7a587f 100644 --- a/src/main/kotlin/com/coder/gateway/icons/CoderIcons.kt +++ b/src/main/kotlin/com/coder/gateway/icons/CoderIcons.kt @@ -9,6 +9,10 @@ object CoderIcons { val OPEN_TERMINAL = IconLoader.getIcon("open_terminal.svg", javaClass) + val RUN = IconLoader.getIcon("run.svg", javaClass) + val STOP = IconLoader.getIcon("stop.svg", javaClass) + val UPDATE = IconLoader.getIcon("update.svg", javaClass) + val WINDOWS = IconLoader.getIcon("windows.svg", javaClass) val MACOS = IconLoader.getIcon("macOS.svg", javaClass) val LINUX = IconLoader.getIcon("linux.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 990b4f43..a0afb910 100644 --- a/src/main/kotlin/com/coder/gateway/models/CoderWorkspacesWizardModel.kt +++ b/src/main/kotlin/com/coder/gateway/models/CoderWorkspacesWizardModel.kt @@ -4,6 +4,6 @@ data class CoderWorkspacesWizardModel( var coderURL: String = "https://localhost", var token: String = "", var buildVersion: String = "", - var workspaceAgents: List = mutableListOf(), + var localCliPath: String = "", 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 index 8979f73d..8ebdee2e 100644 --- a/src/main/kotlin/com/coder/gateway/models/WorkspaceAgentModel.kt +++ b/src/main/kotlin/com/coder/gateway/models/WorkspaceAgentModel.kt @@ -2,13 +2,17 @@ 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 +import java.util.UUID data class WorkspaceAgentModel( + val workspaceID: UUID, + val workspaceName: String, val name: String, + val templateID: UUID, val templateName: String, + val status: WorkspaceVersionStatus, val agentStatus: WorkspaceAgentStatus, + val lastBuildTransition: String, val agentOS: OS?, val agentArch: Arch?, val homeDirectory: String? diff --git a/src/main/kotlin/com/coder/gateway/models/WorkspaceVersionStatus.kt b/src/main/kotlin/com/coder/gateway/models/WorkspaceVersionStatus.kt new file mode 100644 index 00000000..73480670 --- /dev/null +++ b/src/main/kotlin/com/coder/gateway/models/WorkspaceVersionStatus.kt @@ -0,0 +1,14 @@ +package com.coder.gateway.models + +import com.coder.gateway.sdk.v2.models.Workspace + +enum class WorkspaceVersionStatus(val label: String) { + UPDATED("Up to date"), OUTDATED("Outdated"); + + companion object { + fun from(workspace: Workspace) = when (workspace.outdated) { + true -> OUTDATED + false -> UPDATED + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/coder/gateway/sdk/CoderCLIDownloader.kt b/src/main/kotlin/com/coder/gateway/sdk/CoderCLIDownloader.kt index 5f17e7ae..daf77f32 100644 --- a/src/main/kotlin/com/coder/gateway/sdk/CoderCLIDownloader.kt +++ b/src/main/kotlin/com/coder/gateway/sdk/CoderCLIDownloader.kt @@ -5,23 +5,20 @@ import java.io.InputStream import java.net.URL import java.nio.file.Files import java.nio.file.Path -import java.nio.file.Paths import java.nio.file.StandardCopyOption -class CoderCLIDownloader(private val buildVersion: String) { +class CoderCLIDownloader { - fun downloadCLI(url: URL, outputName: String, ext: String): Path { - val filename = if (ext.isBlank()) "${outputName}-$buildVersion" else "${outputName}-${buildVersion}.${ext}" - val cliPath = Paths.get(System.getProperty("java.io.tmpdir"), filename) + fun downloadCLI(url: URL, cliPath: Path): Boolean { if (Files.exists(cliPath)) { logger.info("${cliPath.toAbsolutePath()} already exists, skipping download") - return cliPath + return false } logger.info("Starting Coder CLI download to ${cliPath.toAbsolutePath()}") url.openStream().use { Files.copy(it as InputStream, cliPath, StandardCopyOption.REPLACE_EXISTING) } - return cliPath + return true } companion object { diff --git a/src/main/kotlin/com/coder/gateway/sdk/CoderCLIManager.kt b/src/main/kotlin/com/coder/gateway/sdk/CoderCLIManager.kt index 85a0b52f..d60d22b6 100644 --- a/src/main/kotlin/com/coder/gateway/sdk/CoderCLIManager.kt +++ b/src/main/kotlin/com/coder/gateway/sdk/CoderCLIManager.kt @@ -3,15 +3,20 @@ package com.coder.gateway.sdk import com.intellij.openapi.diagnostic.Logger import java.net.URL import java.nio.file.Path +import java.nio.file.Paths -class CoderCLIManager(private val url: URL, buildVersion: String) { - private val coderCLIDownloader = CoderCLIDownloader(buildVersion) +class CoderCLIManager(url: URL, buildVersion: String) { + var remoteCliPath: URL + var localCliPath: Path - fun download(): Path? { + init { val os = getOS() - val cliName = getCoderCLIForOS(os, getArch()) ?: return null - val cliNameWitExt = if (os == OS.WINDOWS) "$cliName.exe" else cliName - return coderCLIDownloader.downloadCLI(URL(url.protocol, url.host, url.port, "/bin/$cliNameWitExt"), cliName, if (os == OS.WINDOWS) "exe" else "") + val cliName = getCoderCLIForOS(os, getArch()) + val cliNameWithExt = if (os == OS.WINDOWS) "$cliName.exe" else cliName + val filename = if (os == OS.WINDOWS) "${cliName}-${buildVersion}.exe" else "${cliName}-${buildVersion}" + + remoteCliPath = URL(url.protocol, url.host, url.port, "/bin/$cliNameWithExt") + localCliPath = Paths.get(System.getProperty("java.io.tmpdir"), filename) } private fun getCoderCLIForOS(os: OS?, arch: Arch?): String? { @@ -25,12 +30,14 @@ class CoderCLIManager(private val url: URL, buildVersion: String) { Arch.ARM64 -> "coder-windows-arm64" else -> "coder-windows-amd64" } + OS.LINUX -> when (arch) { Arch.AMD64 -> "coder-linux-amd64" Arch.ARM64 -> "coder-linux-arm64" Arch.ARMV7 -> "coder-linux-armv7" else -> "coder-linux-amd64" } + OS.MAC -> when (arch) { Arch.AMD64 -> "coder-darwin-amd64" Arch.ARM64 -> "coder-darwin-arm64" diff --git a/src/main/kotlin/com/coder/gateway/sdk/CoderRestClientService.kt b/src/main/kotlin/com/coder/gateway/sdk/CoderRestClientService.kt index 0e37889c..be81a45a 100644 --- a/src/main/kotlin/com/coder/gateway/sdk/CoderRestClientService.kt +++ b/src/main/kotlin/com/coder/gateway/sdk/CoderRestClientService.kt @@ -2,13 +2,17 @@ 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.TemplateResponseException 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.CreateWorkspaceBuildRequest +import com.coder.gateway.sdk.v2.models.Template 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.coder.gateway.sdk.v2.models.WorkspaceBuild import com.google.gson.Gson import com.google.gson.GsonBuilder import com.intellij.openapi.components.Service @@ -20,8 +24,10 @@ import okhttp3.logging.HttpLoggingInterceptor import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory import java.net.CookieManager +import java.net.HttpURLConnection.HTTP_CREATED import java.net.URL import java.time.Instant +import java.util.UUID @Service(Service.Level.APP) class CoderRestClientService { @@ -49,7 +55,7 @@ class CoderRestClientService { .create() val interceptor = HttpLoggingInterceptor() - interceptor.setLevel(HttpLoggingInterceptor.Level.BODY) + interceptor.setLevel(HttpLoggingInterceptor.Level.BASIC) retroRestClient = Retrofit.Builder() .baseUrl(url.toString()) .client( @@ -97,17 +103,57 @@ class CoderRestClientService { } /** - * 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. + * Retrieves the workspace agents a template declares. + * 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() + fun workspaceAgentsByTemplate(workspace: Workspace): List { + val workspaceResourcesResponse = retroRestClient.templateVersionResources(workspace.latestBuild.templateVersionID).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() } } + + private fun template(templateID: UUID): Template { + val templateResponse = retroRestClient.template(templateID).execute() + if (!templateResponse.isSuccessful) { + throw TemplateResponseException("Failed to retrieve template with id: $templateID, reason: ${templateResponse.message()}") + } + return templateResponse.body()!! + } + + fun startWorkspace(workspaceID: UUID, workspaceName: String): WorkspaceBuild { + val buildRequest = CreateWorkspaceBuildRequest(null, "start", null, null, null) + val buildResponse = retroRestClient.createWorkspaceBuild(workspaceID, buildRequest).execute() + if (buildResponse.code() != HTTP_CREATED) { + throw WorkspaceResponseException("Failed to build workspace ${workspaceName}: ${buildResponse.code()}, reason: ${buildResponse.message()}") + } + + return buildResponse.body()!! + } + + fun stopWorkspace(workspaceID: UUID, workspaceName: String): WorkspaceBuild { + val buildRequest = CreateWorkspaceBuildRequest(null, "stop", null, null, null) + val buildResponse = retroRestClient.createWorkspaceBuild(workspaceID, buildRequest).execute() + if (buildResponse.code() != HTTP_CREATED) { + throw WorkspaceResponseException("Failed to stop workspace ${workspaceName}: ${buildResponse.code()}, reason: ${buildResponse.message()}") + } + + return buildResponse.body()!! + } + + fun updateWorkspace(workspaceID: UUID, workspaceName: String, lastWorkspaceTransition: String, templateID: UUID): WorkspaceBuild { + val template = template(templateID) + + val buildRequest = CreateWorkspaceBuildRequest(template.activeVersionID, lastWorkspaceTransition, null, null, null) + val buildResponse = retroRestClient.createWorkspaceBuild(workspaceID, buildRequest).execute() + if (buildResponse.code() != HTTP_CREATED) { + throw WorkspaceResponseException("Failed to update workspace ${workspaceName}: ${buildResponse.code()}, reason: ${buildResponse.message()}") + } + + return buildResponse.body()!! + } } \ 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 983611b0..e225c2b9 100644 --- a/src/main/kotlin/com/coder/gateway/sdk/ex/exceptions.kt +++ b/src/main/kotlin/com/coder/gateway/sdk/ex/exceptions.kt @@ -6,4 +6,6 @@ class AuthenticationResponseException(reason: String) : IOException(reason) class WorkspaceResponseException(reason: String) : IOException(reason) -class WorkspaceResourcesResponseException(reason: String) : IOException(reason) \ No newline at end of file +class WorkspaceResourcesResponseException(reason: String) : IOException(reason) + +class TemplateResponseException(reason: String) : IOException(reason) \ 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 0fcaa20e..37618b51 100644 --- a/src/main/kotlin/com/coder/gateway/sdk/v2/CoderV2RestFacade.kt +++ b/src/main/kotlin/com/coder/gateway/sdk/v2/CoderV2RestFacade.kt @@ -1,11 +1,16 @@ package com.coder.gateway.sdk.v2 import com.coder.gateway.sdk.v2.models.BuildInfo +import com.coder.gateway.sdk.v2.models.CreateWorkspaceBuildRequest +import com.coder.gateway.sdk.v2.models.Template import com.coder.gateway.sdk.v2.models.User import com.coder.gateway.sdk.v2.models.Workspace +import com.coder.gateway.sdk.v2.models.WorkspaceBuild import com.coder.gateway.sdk.v2.models.WorkspaceResource import retrofit2.Call +import retrofit2.http.Body import retrofit2.http.GET +import retrofit2.http.POST import retrofit2.http.Path import retrofit2.http.Query import java.util.UUID @@ -27,6 +32,15 @@ interface CoderV2RestFacade { @GET("api/v2/buildinfo") fun buildInfo(): Call - @GET("api/v2/workspacebuilds/{buildID}/resources") - fun workspaceResourceByBuild(@Path("buildID") build: UUID): Call> + @GET("api/v2/templateversions/{templateID}/resources") + fun templateVersionResources(@Path("templateID") templateID: UUID): Call> + + /** + * Queues a new build to occur for a workspace. + */ + @POST("api/v2/workspaces/{workspaceID}/builds") + fun createWorkspaceBuild(@Path("workspaceID") workspaceID: UUID, @Body createWorkspaceBuildRequest: CreateWorkspaceBuildRequest): Call + + @GET("api/v2/templates/{templateID}") + fun template(@Path("templateID") templateID: UUID): Call