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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@

## Unreleased

### Fixed

- api keys are no longer created each time workspaces are polled

## 2.22.1 - 2025-07-30

### Added
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ pluginGroup=com.coder.gateway
artifactName=coder-gateway
pluginName=Coder
# SemVer format -> https://semver.org
pluginVersion=2.22.1
pluginVersion=2.22.2
# See https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html
# for insight into build numbers and IntelliJ Platform versions.
pluginSinceBuild=243.26574
Expand Down
1 change: 1 addition & 0 deletions src/main/kotlin/com/coder/gateway/cli/CoderCLIManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,7 @@

else -> {
val failure = result as VerificationResult.Failed
UnsignedBinaryExecutionDeniedException(result.error.message)

Check warning on line 277 in src/main/kotlin/com/coder/gateway/cli/CoderCLIManager.kt

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Throwable not thrown

Throwable instance 'UnsignedBinaryExecutionDeniedException' is not thrown
logger.error("Failed to verify signature for ${cliResult.dst}", failure.error)
}
}
Expand All @@ -288,6 +288,7 @@
return exec(
"login",
deploymentURL.toString(),
"--use-token-as-session",
"--token",
token,
"--global-config",
Expand Down Expand Up @@ -570,7 +571,7 @@
coderConfigPath.toString(),
"start",
"--yes",
workspaceOwner + "/" + workspaceName

Check notice on line 574 in src/main/kotlin/com/coder/gateway/cli/CoderCLIManager.kt

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

String concatenation that can be converted to string template

'String' concatenation can be converted to a template
)

if (feats.buildReason) {
Expand Down Expand Up @@ -612,7 +613,7 @@
/*
* This function returns the ssh-host-prefix used for Host entries.
*/
fun getHostPrefix(): String = "coder-jetbrains-${deploymentURL.safeHost()}"

Check notice on line 616 in src/main/kotlin/com/coder/gateway/cli/CoderCLIManager.kt

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Class member can have 'private' visibility

Function 'getHostPrefix' could be private

/**
* This function returns the ssh host name generated for connecting to the workspace.
Expand Down Expand Up @@ -668,7 +669,7 @@
}
// non-wildcard case
if (parts[0] == "coder-jetbrains") {
return hostname + "--bg"

Check notice on line 672 in src/main/kotlin/com/coder/gateway/cli/CoderCLIManager.kt

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

String concatenation that can be converted to string template

'String' concatenation can be converted to a template
}
// wildcard case
parts[0] += "-bg"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ package com.coder.gateway.views
import com.coder.gateway.CoderGatewayBundle
import com.coder.gateway.CoderGatewayConstants
import com.coder.gateway.CoderRemoteConnectionHandle
import com.coder.gateway.cli.CoderCLIManager
import com.coder.gateway.cli.ensureCLI
import com.coder.gateway.icons.CoderIcons
import com.coder.gateway.models.WorkspaceAgentListModel
Expand Down Expand Up @@ -75,8 +74,6 @@ data class DeploymentInfo(
var items: List<WorkspaceAgentListModel>? = null,
// Null if there have not been any errors yet.
var error: String? = null,
// Null if unable to ensure the CLI is downloaded.
var cli: CoderCLIManager? = null,
)

class CoderGatewayRecentWorkspaceConnectionsView(private val setContentCallback: (Component) -> Unit) :
Expand Down Expand Up @@ -178,13 +175,18 @@ class CoderGatewayRecentWorkspaceConnectionsView(private val setContentCallback:
val me = deployment?.client?.me?.username
val workspaceWithAgent = deployment?.items?.firstOrNull {
it.workspace.ownerName + "/" + it.workspace.name == workspaceName ||
(it.workspace.ownerName == me && it.workspace.name == workspaceName)
(it.workspace.ownerName == me && it.workspace.name == workspaceName)
}
val status =
if (deploymentError != null) {
Triple(UIUtil.getErrorForeground(), deploymentError, UIUtil.getBalloonErrorIcon())
} else if (workspaceWithAgent != null) {
val inLoadingState = listOf(WorkspaceStatus.STARTING, WorkspaceStatus.CANCELING, WorkspaceStatus.DELETING, WorkspaceStatus.STOPPING).contains(workspaceWithAgent.workspace.latestBuild.status)
val inLoadingState = listOf(
WorkspaceStatus.STARTING,
WorkspaceStatus.CANCELING,
WorkspaceStatus.DELETING,
WorkspaceStatus.STOPPING
).contains(workspaceWithAgent.workspace.latestBuild.status)

Triple(
workspaceWithAgent.status.statusColor(),
Expand All @@ -196,7 +198,11 @@ class CoderGatewayRecentWorkspaceConnectionsView(private val setContentCallback:
},
)
} else {
Triple(UIUtil.getContextHelpForeground(), "Querying workspace status...", AnimatedIcon.Default())
Triple(
UIUtil.getContextHelpForeground(),
"Querying workspace status...",
AnimatedIcon.Default()
)
}
val gap =
if (top) {
Expand All @@ -216,7 +222,13 @@ class CoderGatewayRecentWorkspaceConnectionsView(private val setContentCallback:
label("").resizableColumn().align(AlignX.FILL)
}.topGap(gap)

val enableLinks = listOf(WorkspaceStatus.STOPPED, WorkspaceStatus.CANCELED, WorkspaceStatus.FAILED, WorkspaceStatus.STARTING, WorkspaceStatus.RUNNING).contains(workspaceWithAgent?.workspace?.latestBuild?.status)
val enableLinks = listOf(
WorkspaceStatus.STOPPED,
WorkspaceStatus.CANCELED,
WorkspaceStatus.FAILED,
WorkspaceStatus.STARTING,
WorkspaceStatus.RUNNING
).contains(workspaceWithAgent?.workspace?.latestBuild?.status)

// We only display an API error on the first workspace rather than duplicating it on each workspace.
if (deploymentError == null || showError) {
Expand All @@ -236,9 +248,29 @@ class CoderGatewayRecentWorkspaceConnectionsView(private val setContentCallback:
if (enableLinks) {
cell(
ActionLink(workspaceProjectIDE.projectPathDisplay) {
withoutNull(deployment?.cli, workspaceWithAgent?.workspace) { cli, workspace ->
withoutNull(
deployment?.client,
workspaceWithAgent?.workspace
) { client, workspace ->
CoderRemoteConnectionHandle().connect {
if (listOf(WorkspaceStatus.STOPPED, WorkspaceStatus.CANCELED, WorkspaceStatus.FAILED).contains(workspace.latestBuild.status)) {
if (listOf(
WorkspaceStatus.STOPPED,
WorkspaceStatus.CANCELED,
WorkspaceStatus.FAILED
).contains(workspace.latestBuild.status)
) {
val cli = ensureCLI(
deploymentURL.toURL(),
client.buildInfo().version,
settings,
)
// We only need to log the cli in if we have token-based auth.
// Otherwise, we assume it is set up in the same way the plugin
// is with mTLS.
if (client.token != null) {
cli.login(client.token)
}

cli.startWorkspace(workspace.ownerName, workspace.name)
}
workspaceProjectIDE
Expand Down Expand Up @@ -289,33 +321,34 @@ class CoderGatewayRecentWorkspaceConnectionsView(private val setContentCallback:
* name, or just `workspace`, if the connection predates when we added owner
* information, in which case it belongs to the current user.
*/
private fun getConnectionsByDeployment(filter: Boolean): Map<String, Map<String, List<WorkspaceProjectIDE>>> = recentConnectionsService.getAllRecentConnections()
// Validate and parse connections.
.mapNotNull {
try {
it.toWorkspaceProjectIDE()
} catch (e: Exception) {
logger.warn("Removing invalid recent connection $it", e)
recentConnectionsService.removeConnection(it)
null
private fun getConnectionsByDeployment(filter: Boolean): Map<String, Map<String, List<WorkspaceProjectIDE>>> =
recentConnectionsService.getAllRecentConnections()
// Validate and parse connections.
.mapNotNull {
try {
it.toWorkspaceProjectIDE()
} catch (e: Exception) {
logger.warn("Removing invalid recent connection $it", e)
recentConnectionsService.removeConnection(it)
null
}
}
.filter { !filter || matchesFilter(it) }
// Group by the deployment.
.groupBy { it.deploymentURL.toString() }
// Group the connections in each deployment by workspace.
.mapValues { (_, connections) ->
connections
.groupBy { it.name.split(".", limit = 2).first() }
}
}
.filter { !filter || matchesFilter(it) }
// Group by the deployment.
.groupBy { it.deploymentURL.toString() }
// Group the connections in each deployment by workspace.
.mapValues { (_, connections) ->
connections
.groupBy { it.name.split(".", limit = 2).first() }
}

/**
* Return true if the connection matches the current filter.
*/
private fun matchesFilter(connection: WorkspaceProjectIDE): Boolean = filterString.let {
it.isNullOrBlank() ||
connection.hostname.lowercase(Locale.getDefault()).contains(it) ||
connection.projectPath.lowercase(Locale.getDefault()).contains(it)
connection.hostname.lowercase(Locale.getDefault()).contains(it) ||
connection.projectPath.lowercase(Locale.getDefault()).contains(it)
}

/**
Expand Down Expand Up @@ -362,19 +395,6 @@ class CoderGatewayRecentWorkspaceConnectionsView(private val setContentCallback:
throw Exception("Unable to make request; token was not found in CLI config.")
}

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

// We only need to log the cli in if we have token-based auth.
// Otherwise, we assume it is set up in the same way the plugin
// is with mTLS.
if (client.token != null) {
cli.login(client.token)
}

// This is purely to populate the current user, which is
// used to match workspaces that were not recorded with owner
// information.
Expand All @@ -386,7 +406,7 @@ class CoderGatewayRecentWorkspaceConnectionsView(private val setContentCallback:
connectionsByWorkspace.forEach { (name, connections) ->
if (items.firstOrNull {
it.workspace.ownerName + "/" + it.workspace.name == name ||
(it.workspace.ownerName == me && it.workspace.name == name)
(it.workspace.ownerName == me && it.workspace.name == name)
} == null
) {
logger.info("Removing recent connections for deleted workspace $name (found ${connections.size})")
Expand All @@ -395,7 +415,6 @@ class CoderGatewayRecentWorkspaceConnectionsView(private val setContentCallback:
}

deployment.client = client
deployment.cli = cli
deployment.items = items
deployment.error = null
} catch (e: Exception) {
Expand Down
Loading