Skip to content

Commit

Permalink
Handle Gateway links
Browse files Browse the repository at this point in the history
  • Loading branch information
code-asher committed Aug 7, 2023
1 parent 5569d46 commit 809f5b2
Show file tree
Hide file tree
Showing 6 changed files with 131 additions and 8 deletions.
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,16 @@ To manually install a local build:

Alternatively, `./gradlew clean runIde` will deploy a Gateway distribution (the one specified in `gradle.properties` - `platformVersion`) with the latest plugin changes deployed.

To simulate opening a workspace from the dashboard pass the Gateway link via `--args`. For example:

```
./gradlew clean runIDE --args="jetbrains-gateway://connect#type=coder&workspace=dev&agent=coder&folder=/home/coder&url=https://dev.coder.com&token=<redacted>&ide_product_code=IU&ide_build_number=223.8836.41&ide_download_link=https://download.jetbrains.com/idea/ideaIU-2022.3.3.tar.gz"
```

Alternatively, if you have separately built the plugin and already installed it
in a Gateway distribution you can launch that distribution with the URL as the
first argument (no `--args` in this case).

### Plugin Structure

```
Expand Down
115 changes: 113 additions & 2 deletions src/main/kotlin/com/coder/gateway/CoderGatewayConnectionProvider.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,129 @@

package com.coder.gateway

import com.coder.gateway.models.TokenSource
import com.coder.gateway.sdk.CoderCLIManager
import com.coder.gateway.sdk.CoderRestClient
import com.coder.gateway.sdk.ex.AuthenticationResponseException
import com.coder.gateway.sdk.toURL
import com.coder.gateway.sdk.v2.models.toAgentModels
import com.coder.gateway.sdk.withPath
import com.coder.gateway.services.CoderSettingsState
import com.intellij.openapi.components.service
import com.intellij.openapi.diagnostic.Logger
import com.jetbrains.gateway.api.ConnectionRequestor
import com.jetbrains.gateway.api.GatewayConnectionHandle
import com.jetbrains.gateway.api.GatewayConnectionProvider
import java.net.URL

// In addition to `type`, these are the keys that we support in our Gateway
// links.
private const val URL = "url"
private const val TOKEN = "token"
private const val WORKSPACE = "workspace"
private const val AGENT = "agent"
private const val FOLDER = "folder"
private const val IDE_DOWNLOAD_LINK = "ide_download_link"
private const val IDE_PRODUCT_CODE = "ide_product_code"
private const val IDE_BUILD_NUMBER = "ide_build_number"
private const val IDE_PATH_ON_HOST = "ide_path_on_host"

// CoderGatewayConnectionProvider handles connecting via a Gateway link such as
// jetbrains-gateway://connect#type=coder.
class CoderGatewayConnectionProvider : GatewayConnectionProvider {
private val settings: CoderSettingsState = service()

override suspend fun connect(parameters: Map<String, String>, requestor: ConnectionRequestor): GatewayConnectionHandle? {
logger.debug("Launched Coder connection provider", parameters)
CoderRemoteConnectionHandle().connect(parameters)
CoderRemoteConnectionHandle().connect{ indicator ->
logger.debug("Launched Coder connection provider", parameters)

val deploymentURL = parameters[URL]
?: CoderRemoteConnectionHandle.ask("Enter the full URL of your Coder deployment")
if (deploymentURL.isNullOrBlank()) {
throw IllegalArgumentException("Query parameter \"$URL\" is missing")
}

val (client, username) = authenticate(deploymentURL.toURL(), parameters[TOKEN])

// TODO: If these are missing we could launch the wizard.
val name = parameters[WORKSPACE] ?: throw IllegalArgumentException("Query parameter \"$WORKSPACE\" is missing")
val agent = parameters[AGENT] ?: throw IllegalArgumentException("Query parameter \"$AGENT\" is missing")

val workspaces = client.workspaces()
val agents = workspaces.flatMap { it.toAgentModels() }
val workspace = agents.firstOrNull { it.name == "$name.$agent" }
?: throw IllegalArgumentException("The agent $agent does not exist on the workspace $name or the workspace is off")

// TODO: Turn on the workspace if it is off then wait for the agent
// to be ready. Also, distinguish between whether the
// workspace is off or the agent does not exist in the error
// above instead of showing a combined error.

val cli = CoderCLIManager.ensureCLI(
deploymentURL.toURL(),
client.buildInfo().version,
settings,
indicator,
)

indicator.text = "Authenticating Coder CLI..."
cli.login(client.token)

indicator.text = "Configuring Coder CLI..."
cli.configSsh(agents)

// TODO: Ask for these if missing. Maybe we can reuse the second
// step of the wizard? Could also be nice if we automatically used
// the last IDE.
if (parameters[IDE_PRODUCT_CODE].isNullOrBlank()) {
throw IllegalArgumentException("Query parameter \"$IDE_PRODUCT_CODE\" is missing")
}
if (parameters[IDE_BUILD_NUMBER].isNullOrBlank()) {
throw IllegalArgumentException("Query parameter \"$IDE_BUILD_NUMBER\" is missing")
}
if (parameters[IDE_PATH_ON_HOST].isNullOrBlank() && parameters[IDE_DOWNLOAD_LINK].isNullOrBlank()) {
throw IllegalArgumentException("One of \"$IDE_PATH_ON_HOST\" or \"$IDE_DOWNLOAD_LINK\" is required")
}

// TODO: Ask for the project path if missing and validate the path.
val folder = parameters[FOLDER] ?: throw IllegalArgumentException("Query parameter \"$FOLDER\" is missing")

parameters
.withWorkspaceHostname(CoderCLIManager.getHostName(deploymentURL.toURL(), workspace))
.withProjectPath(folder)
.withWebTerminalLink(client.url.withPath("/@$username/$workspace.name/terminal").toString())
.withConfigDirectory(cli.coderConfigPath.toString())
.withName(name)
}
return null
}

/**
* Return an authenticated Coder CLI and the user's name, asking for the
* token as long as it continues to result in an authentication failure.
*/
private fun authenticate(deploymentURL: URL, queryToken: String?, lastToken: Pair<String, TokenSource>? = null): Pair<CoderRestClient, String> {
// Use the token from the query, unless we already tried that.
val isRetry = lastToken != null
val token = if (!queryToken.isNullOrBlank() && !isRetry)
Pair(queryToken, TokenSource.QUERY)
else CoderRemoteConnectionHandle.askToken(
deploymentURL,
lastToken,
isRetry,
useExisting = true,
)
if (token == null) { // User aborted.
throw IllegalArgumentException("Unable to connect to $deploymentURL, $TOKEN is missing")
}
val client = CoderRestClient(deploymentURL, token.first)
return try {
Pair(client, client.me().username)
} catch (ex: AuthenticationResponseException) {
authenticate(deploymentURL, queryToken, token)
}
}

override fun isApplicable(parameters: Map<String, String>): Boolean {
return parameters.areCoderType()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.components.service
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.rd.util.launchUnderBackgroundProgress
import com.intellij.openapi.ui.Messages
import com.intellij.openapi.ui.panel.ComponentPanelBuilder
Expand Down Expand Up @@ -44,11 +45,12 @@ import java.util.concurrent.TimeoutException
class CoderRemoteConnectionHandle {
private val recentConnectionsService = service<CoderRecentWorkspaceConnectionsService>()

suspend fun connect(parameters: Map<String, String>) {
logger.debug("Creating connection handle", parameters)
suspend fun connect(getParameters: (indicator: ProgressIndicator) -> Map<String, String>) {
val clientLifetime = LifetimeDefinition()
clientLifetime.launchUnderBackgroundProgress(CoderGatewayBundle.message("gateway.connector.coder.connection.provider.title"), canBeCancelled = true, isIndeterminate = true, project = null) {
try {
val parameters = getParameters(indicator)
logger.debug("Creating connection handle", parameters)
indicator.text = CoderGatewayBundle.message("gateway.connector.coder.connecting")
val context = suspendingRetryWithExponentialBackOff(
action = { attempt ->
Expand Down
2 changes: 1 addition & 1 deletion src/main/kotlin/com/coder/gateway/WorkspaceParams.kt
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ fun Map<String, String>.withName(name: String): Map<String, String> {


fun Map<String, String>.areCoderType(): Boolean {
return this[TYPE] == VALUE_FOR_TYPE && !this[CODER_WORKSPACE_HOSTNAME].isNullOrBlank() && !this[PROJECT_PATH].isNullOrBlank()
return this[TYPE] == VALUE_FOR_TYPE
}

fun Map<String, String>.toSshConfig(): SshConfig {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ class CoderGatewayRecentWorkspaceConnectionsView(private val setContentCallback:
icon(product.icon)
cell(ActionLink(connectionDetails.projectPath!!) {
cs.launch {
CoderRemoteConnectionHandle().connect(connectionDetails.toWorkspaceParams())
CoderRemoteConnectionHandle().connect{ connectionDetails.toWorkspaceParams() }
GatewayUI.getInstance().reset()
}
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -339,15 +339,15 @@ class CoderLocateRemoteProjectStepView(private val setNextButtonEnabled: (Boolea
return false
}
cs.launch {
CoderRemoteConnectionHandle().connect(
CoderRemoteConnectionHandle().connect{
selectedIDE
.toWorkspaceParams()
.withWorkspaceHostname(CoderCLIManager.getHostName(deploymentURL, selectedWorkspace))
.withProjectPath(tfProject.text)
.withWebTerminalLink("${terminalLink.url}")
.withConfigDirectory(wizardModel.configDirectory)
.withName(selectedWorkspace.name)
)
}
GatewayUI.getInstance().reset()
}
return true
Expand Down

0 comments on commit 809f5b2

Please sign in to comment.