|
2 | 2 |
|
3 | 3 | package com.coder.gateway
|
4 | 4 |
|
| 5 | +import com.coder.gateway.models.TokenSource |
| 6 | +import com.coder.gateway.sdk.CoderCLIManager |
| 7 | +import com.coder.gateway.sdk.CoderRestClient |
| 8 | +import com.coder.gateway.sdk.ex.AuthenticationResponseException |
| 9 | +import com.coder.gateway.sdk.toURL |
| 10 | +import com.coder.gateway.sdk.v2.models.toAgentModels |
| 11 | +import com.coder.gateway.sdk.withPath |
| 12 | +import com.coder.gateway.services.CoderSettingsState |
| 13 | +import com.intellij.openapi.components.service |
5 | 14 | import com.intellij.openapi.diagnostic.Logger
|
6 | 15 | import com.jetbrains.gateway.api.ConnectionRequestor
|
7 | 16 | import com.jetbrains.gateway.api.GatewayConnectionHandle
|
8 | 17 | import com.jetbrains.gateway.api.GatewayConnectionProvider
|
| 18 | +import java.net.URL |
9 | 19 |
|
| 20 | +// In addition to parameters `type`, these are the keys that we support in our |
| 21 | +// Gateway links. |
| 22 | +private const val URL = "url" |
| 23 | +private const val TOKEN = "token" |
| 24 | +private const val WORKSPACE = "workspace" |
| 25 | +private const val AGENT = "agent" |
| 26 | +private const val FOLDER = "folder" |
| 27 | +private const val IDE_DOWNLOAD_LINK = "ide_download_link" |
| 28 | +private const val IDE_PRODUCT_CODE = "ide_product_code" |
| 29 | +private const val IDE_BUILD_NUMBER = "ide_build_number" |
| 30 | +private const val IDE_PATH_ON_HOST = "ide_path_on_host" |
| 31 | + |
| 32 | +// CoderGatewayConnectionProvider handles connecting via a Gateway link such as |
| 33 | +// jetbrains-gateway://connect#type=coder. |
10 | 34 | class CoderGatewayConnectionProvider : GatewayConnectionProvider {
|
| 35 | + private val settings: CoderSettingsState = service() |
| 36 | + |
11 | 37 | override suspend fun connect(parameters: Map<String, String>, requestor: ConnectionRequestor): GatewayConnectionHandle? {
|
12 |
| - logger.debug("Launched Coder connection provider", parameters) |
13 |
| - CoderRemoteConnectionHandle().connect(parameters) |
| 38 | + CoderRemoteConnectionHandle().connect{ indicator -> |
| 39 | + logger.debug("Launched Coder connection provider", parameters) |
| 40 | + |
| 41 | + val deploymentURL = parameters[URL] |
| 42 | + ?: CoderRemoteConnectionHandle.ask("Enter the full URL of your Coder deployment") |
| 43 | + if (deploymentURL.isNullOrBlank()) { |
| 44 | + throw IllegalArgumentException("Query parameter \"$URL\" is missing") |
| 45 | + } |
| 46 | + |
| 47 | + val (client, username) = authenticate(deploymentURL.toURL(), parameters[TOKEN]) |
| 48 | + |
| 49 | + // TODO: If these are missing we could launch the wizard. |
| 50 | + val name = parameters[WORKSPACE] ?: throw IllegalArgumentException("Query parameter \"$WORKSPACE\" is missing") |
| 51 | + val agent = parameters[AGENT] ?: throw IllegalArgumentException("Query parameter \"$AGENT\" is missing") |
| 52 | + |
| 53 | + val workspaces = client.workspaces() |
| 54 | + val agents = workspaces.flatMap { it.toAgentModels() } |
| 55 | + val workspace = agents.firstOrNull { it.name == "$name.$agent" } |
| 56 | + ?: throw IllegalArgumentException("The agent $agent does not exist on the workspace $name or the workspace is off") |
| 57 | + |
| 58 | + // TODO: Turn on the workspace if it is off then wait for the agent |
| 59 | + // to be ready. Also, distinguish between whether the |
| 60 | + // workspace is off or the agent does not exist in the error |
| 61 | + // above instead of showing a combined error. |
| 62 | + |
| 63 | + val cli = CoderCLIManager.ensureCLI( |
| 64 | + deploymentURL.toURL(), |
| 65 | + client.buildInfo().version, |
| 66 | + settings, |
| 67 | + indicator, |
| 68 | + ) |
| 69 | + |
| 70 | + indicator.text = "Authenticating Coder CLI..." |
| 71 | + cli.login(client.token) |
| 72 | + |
| 73 | + indicator.text = "Configuring Coder CLI..." |
| 74 | + cli.configSsh(agents) |
| 75 | + |
| 76 | + // TODO: Ask for these if missing. Maybe we can reuse the second |
| 77 | + // step of the wizard? Could also be nice if we automatically used |
| 78 | + // the last IDE. |
| 79 | + if (parameters[IDE_PRODUCT_CODE].isNullOrBlank()) { |
| 80 | + throw IllegalArgumentException("Query parameter \"$IDE_PRODUCT_CODE\" is missing") |
| 81 | + } |
| 82 | + if (parameters[IDE_BUILD_NUMBER].isNullOrBlank()) { |
| 83 | + throw IllegalArgumentException("Query parameter \"$IDE_BUILD_NUMBER\" is missing") |
| 84 | + } |
| 85 | + if (parameters[IDE_PATH_ON_HOST].isNullOrBlank() && parameters[IDE_DOWNLOAD_LINK].isNullOrBlank()) { |
| 86 | + throw IllegalArgumentException("One of \"$IDE_PATH_ON_HOST\" or \"$IDE_DOWNLOAD_LINK\" is required") |
| 87 | + } |
| 88 | + |
| 89 | + // TODO: Ask for the project path if missing and validate the path. |
| 90 | + val folder = parameters[FOLDER] ?: throw IllegalArgumentException("Query parameter \"$FOLDER\" is missing") |
| 91 | + |
| 92 | + parameters |
| 93 | + .withWorkspaceHostname(CoderCLIManager.getHostName(deploymentURL.toURL(), workspace)) |
| 94 | + .withProjectPath(folder) |
| 95 | + .withWebTerminalLink(client.url.withPath("/@$username/$workspace.name/terminal").toString()) |
| 96 | + .withConfigDirectory(cli.coderConfigPath.toString()) |
| 97 | + .withName(name) |
| 98 | + } |
14 | 99 | return null
|
15 | 100 | }
|
16 | 101 |
|
| 102 | + /** |
| 103 | + * Return an authenticated Coder CLI and the user's name, asking for the |
| 104 | + * token as long as it continues to result in an authentication failure. |
| 105 | + */ |
| 106 | + private fun authenticate(deploymentURL: URL, queryToken: String?, lastToken: Pair<String, TokenSource>? = null): Pair<CoderRestClient, String> { |
| 107 | + // Use the token from the query, unless we already tried that. |
| 108 | + val isRetry = lastToken != null |
| 109 | + val token = if (!queryToken.isNullOrBlank() && !isRetry) |
| 110 | + Pair(queryToken, TokenSource.QUERY) |
| 111 | + else CoderRemoteConnectionHandle.askToken( |
| 112 | + deploymentURL, |
| 113 | + lastToken, |
| 114 | + isRetry, |
| 115 | + useExisting = true, |
| 116 | + ) |
| 117 | + if (token == null) { // User aborted. |
| 118 | + throw IllegalArgumentException("Unable to connect to $deploymentURL, $TOKEN is missing") |
| 119 | + } |
| 120 | + val client = CoderRestClient(deploymentURL, token.first) |
| 121 | + return try { |
| 122 | + Pair(client, client.me().username) |
| 123 | + } catch (ex: AuthenticationResponseException) { |
| 124 | + authenticate(deploymentURL, queryToken, token) |
| 125 | + } |
| 126 | + } |
| 127 | + |
17 | 128 | override fun isApplicable(parameters: Map<String, String>): Boolean {
|
18 | 129 | return parameters.areCoderType()
|
19 | 130 | }
|
|
0 commit comments