Skip to content
Merged
Show file tree
Hide file tree
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
### Added
- Add a setting for a command to run to get headers that will be set on all
requests to the Coder deployment.
- Support for Gateway 2023.3.

## 2.6.0 - 2023-09-06

Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
pluginGroup=com.coder.gateway
pluginName=coder-gateway
# SemVer format -> https://semver.org
pluginVersion=2.7.0
pluginVersion=2.8.0
# See https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html
# for insight into build numbers and IntelliJ Platform versions.
pluginSinceBuild=223.7571.70
Expand Down
20 changes: 12 additions & 8 deletions src/main/kotlin/com/coder/gateway/CoderRemoteConnectionHandle.kt
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import com.jetbrains.gateway.ssh.SshDeployFlowUtil
import com.jetbrains.gateway.ssh.SshMultistagePanelContext
import com.jetbrains.gateway.ssh.deploy.DeployException
import com.jetbrains.rd.util.lifetime.LifetimeDefinition
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import net.schmizz.sshj.common.SSHException
import net.schmizz.sshj.connection.ConnectionException
Expand All @@ -49,7 +50,7 @@ class CoderRemoteConnectionHandle {

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) {
clientLifetime.launchUnderBackgroundProgress(CoderGatewayBundle.message("gateway.connector.coder.connection.provider.title")) {
try {
val parameters = getParameters(indicator)
logger.debug("Creating connection handle", parameters)
Expand Down Expand Up @@ -78,7 +79,7 @@ class CoderRemoteConnectionHandle {
indicator.text = CoderGatewayBundle.message("gateway.connector.coder.connecting.failed.retry", humanizeDuration(remainingMs))
},
)
launch {
GlobalScope.launch {
logger.info("Deploying and starting IDE with $context")
// At this point JetBrains takes over with their own UI.
@Suppress("UnstableApiUsage") SshDeployFlowUtil.fullDeployCycle(
Expand Down Expand Up @@ -178,13 +179,16 @@ class CoderRemoteConnectionHandle {
}

/**
* Open a dialog for providing the token. Show any existing token so the
* user can validate it if a previous connection failed. If we are not
* retrying and the user has not checked the existing token box then open a
* browser to the auth page. If the user has checked the existing token box
* then populate the dialog with the token on disk (this will overwrite any
* Open a dialog for providing the token. Show any existing token so
* the user can validate it if a previous connection failed.
*
* If we are not retrying and the user has not checked the existing
* token box then also open a browser to the auth page.
*
* If the user has checked the existing token box then return the token
* on disk immediately and skip the dialog (this will overwrite any
* other existing token) unless this is a retry to avoid clobbering the
* token that just failed. Return the token submitted by the user.
* token that just failed.
*/
@JvmStatic
fun askToken(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,7 @@ import com.intellij.ui.DocumentAdapter
import com.intellij.ui.SearchTextField
import com.intellij.ui.components.ActionLink
import com.intellij.ui.components.JBScrollPane
import com.intellij.ui.dsl.builder.AlignX
import com.intellij.ui.dsl.builder.AlignY
import com.intellij.ui.dsl.builder.BottomGap
import com.intellij.ui.dsl.builder.RightGap
import com.intellij.ui.dsl.builder.TopGap
import com.intellij.ui.dsl.builder.panel
import com.intellij.ui.dsl.builder.*
import com.intellij.util.io.readText
import com.intellij.util.ui.JBFont
import com.intellij.util.ui.JBUI
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import com.coder.gateway.sdk.TemplateIconDownloader
import com.coder.gateway.sdk.ex.AuthenticationResponseException
import com.coder.gateway.sdk.ex.TemplateResponseException
import com.coder.gateway.sdk.ex.WorkspaceResponseException
import com.coder.gateway.sdk.isCancellation
import com.coder.gateway.sdk.toURL
import com.coder.gateway.sdk.v2.models.WorkspaceStatus
import com.coder.gateway.sdk.v2.models.toAgentModels
Expand Down Expand Up @@ -423,12 +424,7 @@ class CoderWorkspacesStepView(val setNextButtonEnabled: (Boolean) -> Unit) : Cod
tableOfWorkspaces.listTableModel.items = emptyList()

// Authenticate and load in a background process with progress.
// TODO: Make this cancelable.
return LifetimeDefinition().launchUnderBackgroundProgress(
CoderGatewayBundle.message("gateway.connector.view.coder.workspaces.cli.downloader.dialog.title"),
canBeCancelled = false,
isIndeterminate = true
) {
return LifetimeDefinition().launchUnderBackgroundProgress(CoderGatewayBundle.message("gateway.connector.view.coder.workspaces.cli.downloader.dialog.title")) {
try {
this.indicator.text = "Authenticating client..."
authenticate(deploymentURL, token.first)
Expand Down Expand Up @@ -456,51 +452,61 @@ class CoderWorkspacesStepView(val setNextButtonEnabled: (Boolean) -> Unit) : Cod
tableOfWorkspaces.setEmptyState(CoderGatewayBundle.message("gateway.connector.view.coder.workspaces.connect.text.connected", deploymentURL.host))
tfUrlComment?.text = CoderGatewayBundle.message("gateway.connector.view.coder.workspaces.connect.text.connected", deploymentURL.host)
} catch (e: Exception) {
val reason = e.message ?: CoderGatewayBundle.message("gateway.connector.view.workspaces.connect.no-reason")
val msg = when (e) {
is java.nio.file.AccessDeniedException -> CoderGatewayBundle.message("gateway.connector.view.workspaces.connect.access-denied", e.file)
is UnknownHostException -> CoderGatewayBundle.message("gateway.connector.view.workspaces.connect.unknown-host", e.message ?: deploymentURL.host)
is InvalidExitValueException -> CoderGatewayBundle.message("gateway.connector.view.workspaces.connect.unexpected-exit", e.exitValue)
is AuthenticationResponseException -> {
CoderGatewayBundle.message(
"gateway.connector.view.workspaces.connect.unauthorized",
deploymentURL,
)
}
is SocketTimeoutException -> {
CoderGatewayBundle.message(
"gateway.connector.view.workspaces.connect.timeout",
deploymentURL,
)
}
is ResponseException, is ConnectException -> {
CoderGatewayBundle.message(
"gateway.connector.view.workspaces.connect.download-failed",
reason,
)
if (isCancellation(e)) {
tfUrlComment?.text = CoderGatewayBundle.message("gateway.connector.view.coder.workspaces.connect.text.comment",
CoderGatewayBundle.message("gateway.connector.view.coder.workspaces.connect.text"))
tableOfWorkspaces.setEmptyState(CoderGatewayBundle.message(
"gateway.connector.view.workspaces.connect.canceled",
deploymentURL.host,
))
logger.info("Connection canceled due to ${e.javaClass.simpleName}")
} else {
val reason = e.message ?: CoderGatewayBundle.message("gateway.connector.view.workspaces.connect.no-reason")
val msg = when (e) {
is java.nio.file.AccessDeniedException -> CoderGatewayBundle.message("gateway.connector.view.workspaces.connect.access-denied", e.file)
is UnknownHostException -> CoderGatewayBundle.message("gateway.connector.view.workspaces.connect.unknown-host", e.message ?: deploymentURL.host)
is InvalidExitValueException -> CoderGatewayBundle.message("gateway.connector.view.workspaces.connect.unexpected-exit", e.exitValue)
is AuthenticationResponseException -> {
CoderGatewayBundle.message(
"gateway.connector.view.workspaces.connect.unauthorized",
deploymentURL,
)
}
is SocketTimeoutException -> {
CoderGatewayBundle.message(
"gateway.connector.view.workspaces.connect.timeout",
deploymentURL,
)
}
is ResponseException, is ConnectException -> {
CoderGatewayBundle.message(
"gateway.connector.view.workspaces.connect.download-failed",
reason,
)
}
is SSLHandshakeException -> {
CoderGatewayBundle.message(
"gateway.connector.view.workspaces.connect.ssl-error",
deploymentURL.host,
reason,
)
}
else -> reason
}
is SSLHandshakeException -> {
CoderGatewayBundle.message(
"gateway.connector.view.workspaces.connect.ssl-error",
deploymentURL.host,
reason,
)
// It would be nice to place messages directly into the table
// but it does not support wrapping or markup so place it in the
// comment field of the URL input instead.
tfUrlComment?.foreground = UIUtil.getErrorForeground()
tfUrlComment?.text = msg
tableOfWorkspaces.setEmptyState(CoderGatewayBundle.message(
"gateway.connector.view.workspaces.connect.failed",
deploymentURL.host,
))
logger.error(msg, e)

if (e is AuthenticationResponseException) {
cs.launch { onAuthFailure?.invoke() }
}
else -> reason
}
// It would be nice to place messages directly into the table
// but it does not support wrapping or markup so place it in the
// comment field of the URL input instead.
tfUrlComment?.foreground = UIUtil.getErrorForeground()
tfUrlComment?.text = msg
tableOfWorkspaces.setEmptyState(CoderGatewayBundle.message(
"gateway.connector.view.workspaces.connect.failed",
deploymentURL.host,
))
logger.error(msg, e)

if (e is AuthenticationResponseException) {
cs.launch { onAuthFailure?.invoke() }
}
}
}
Expand Down
3 changes: 2 additions & 1 deletion src/main/resources/messages/CoderGatewayBundle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ gateway.connector.action.text=Connect to Coder
gateway.connector.view.login.documentation.action=Learn more about Coder
gateway.connector.view.login.url.label=URL:
gateway.connector.view.login.existing-token.label=Use existing token
gateway.connector.view.login.existing-token.tooltip=Checking "{0}" will prevent the browser from being launched for generating a new token after pressing "{1}". Additionally, if a token is already configured for this URL via the CLI it will appear as the default and can be used as-is or replaced.
gateway.connector.view.login.existing-token.tooltip=Checking "{0}" will prevent the browser from being launched for generating a new token after pressing "{1}". Additionally, if a token is already configured for this URL via the CLI it will automatically be used.
gateway.connector.view.login.token.dialog=Paste your token here:
gateway.connector.view.login.token.label=Session Token:
gateway.connector.view.coder.workspaces.header.text=Coder Workspaces
Expand All @@ -26,6 +26,7 @@ gateway.connector.view.coder.workspaces.unsupported.os.info=Gateway supports onl
gateway.connector.view.coder.workspaces.invalid.coder.version=Could not parse Coder version {0}. Coder Gateway plugin might not be compatible with this version. <a href='https://coder.com/docs/v2/latest/ides/gateway#creating-a-new-jetbrains-gateway-connection'>Connect to a Coder workspace manually</a>
gateway.connector.view.coder.workspaces.unsupported.coder.version=Coder version {0} might not be compatible with this plugin version. <a href='https://coder.com/docs/v2/latest/ides/gateway#creating-a-new-jetbrains-gateway-connection'>Connect to a Coder workspace manually</a>
gateway.connector.view.workspaces.connect.failed=Connection to {0} failed. See above for details.
gateway.connector.view.workspaces.connect.canceled=Connection to {0} canceled.
gateway.connector.view.workspaces.connect.no-reason=No reason was provided.
gateway.connector.view.workspaces.connect.access-denied=Access denied to {0}.
gateway.connector.view.workspaces.connect.unknown-host=Unknown host {0}.
Expand Down