diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a13dc44..6ac894cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,13 @@ include agents in the initial workspaces query) and add them individually to the SSH config. In the future, we would like to use a wildcard host name to work around this issue. + + Additionally, be aware that the recents view is using the same query filter. + This means if you connect to a workspace, then change the filter such that the + workspace is excluded, you could cause the workspace to be deleted from the + recent connections even if the workspace still exists in actuality, as it + would no longer show up in the query which the plugin takes as its cue to + delete the connection. - Add owner column to connections view table. - Add agent name to the recent connections view. diff --git a/src/main/kotlin/com/coder/gateway/models/WorkspaceProjectIDE.kt b/src/main/kotlin/com/coder/gateway/models/WorkspaceProjectIDE.kt index c9ecd0b2..3b205554 100644 --- a/src/main/kotlin/com/coder/gateway/models/WorkspaceProjectIDE.kt +++ b/src/main/kotlin/com/coder/gateway/models/WorkspaceProjectIDE.kt @@ -16,6 +16,8 @@ import kotlin.io.path.name * workspace. */ class WorkspaceProjectIDE( + // Either `workspace.agent` for old connections or `user/workspace.agent` + // for new connections. val name: String, val hostname: String, val projectPath: String, diff --git a/src/main/kotlin/com/coder/gateway/util/LinkHandler.kt b/src/main/kotlin/com/coder/gateway/util/LinkHandler.kt index 506d3777..c201f08d 100644 --- a/src/main/kotlin/com/coder/gateway/util/LinkHandler.kt +++ b/src/main/kotlin/com/coder/gateway/util/LinkHandler.kt @@ -108,7 +108,6 @@ open class LinkHandler( indicator?.invoke("Configuring Coder CLI...") cli.configSsh(workspacesAndAgents = client.withAgents(workspaces), currentUser = client.me) - val name = "${workspace.name}.${agent.name}" val openDialog = parameters.ideProductCode().isNullOrBlank() || parameters.ideBuildNumber().isNullOrBlank() || @@ -122,7 +121,7 @@ open class LinkHandler( // allowlisted. If not, check with the user whether to proceed. verifyDownloadLink(parameters) WorkspaceProjectIDE.fromInputs( - name = name, + name = CoderCLIManager.getWorkspaceParts(workspace, agent), hostname = CoderCLIManager.getHostName(deploymentURL.toURL(), workspace, client.me, agent), projectPath = parameters.folder(), ideProductCode = parameters.ideProductCode(), diff --git a/src/main/kotlin/com/coder/gateway/views/CoderGatewayRecentWorkspaceConnectionsView.kt b/src/main/kotlin/com/coder/gateway/views/CoderGatewayRecentWorkspaceConnectionsView.kt index 59c3936c..027f291e 100644 --- a/src/main/kotlin/com/coder/gateway/views/CoderGatewayRecentWorkspaceConnectionsView.kt +++ b/src/main/kotlin/com/coder/gateway/views/CoderGatewayRecentWorkspaceConnectionsView.kt @@ -171,12 +171,16 @@ class CoderGatewayRecentWorkspaceConnectionsView(private val setContentCallback: } else { false } - val workspaceWithAgent = deployment?.items?.firstOrNull { it.workspace.name == workspaceName } + 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) + } 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(), @@ -244,7 +248,7 @@ class CoderGatewayRecentWorkspaceConnectionsView(private val setContentCallback: foreground = Color.GRAY } } - label(workspaceProjectIDE.name.replace(workspaceName + ".", "")).resizableColumn() + label(workspaceProjectIDE.name.replace("$workspaceName.", "")).resizableColumn() label(workspaceProjectIDE.ideName).applyToComponent { foreground = JBUI.CurrentTheme.ContextHelp.FOREGROUND font = ComponentPanelBuilder.getCommentFont(font) @@ -276,7 +280,10 @@ class CoderGatewayRecentWorkspaceConnectionsView(private val setContentCallback: } /** - * Get valid connections grouped by deployment and workspace. + * Get valid connections grouped by deployment and workspace name. The + * workspace name will be in the form `owner/workspace.agent`, without the agent + * 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>> = recentConnectionsService.getAllRecentConnections() // Validate and parse connections. @@ -351,10 +358,20 @@ class CoderGatewayRecentWorkspaceConnectionsView(private val setContentCallback: throw Exception("Unable to make request; token was not found in CLI config.") } + // This is purely to populate the current user, which is + // used to match workspaces that were not recorded with owner + // information. + val me = client.authenticate().username + // Delete connections that have no workspace. + // TODO: Deletion without confirmation seems sketchy. val items = client.workspaces().flatMap { it.toAgentList() } connectionsByWorkspace.forEach { (name, connections) -> - if (items.firstOrNull { it.workspace.name == name } == null) { + if (items.firstOrNull { + it.workspace.ownerName + "/" + 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})") connections.forEach { recentConnectionsService.removeConnection(it.toRecentWorkspaceConnection()) } } diff --git a/src/main/kotlin/com/coder/gateway/views/steps/CoderWorkspaceProjectIDEStepView.kt b/src/main/kotlin/com/coder/gateway/views/steps/CoderWorkspaceProjectIDEStepView.kt index 4aa05f96..8478e355 100644 --- a/src/main/kotlin/com/coder/gateway/views/steps/CoderWorkspaceProjectIDEStepView.kt +++ b/src/main/kotlin/com/coder/gateway/views/steps/CoderWorkspaceProjectIDEStepView.kt @@ -402,9 +402,8 @@ class CoderWorkspaceProjectIDEStepView( * Return the selected parameters. Throw if not configured. */ override fun data(): WorkspaceProjectIDE = withoutNull(cbIDE.selectedItem, state) { selectedIDE, state -> - val name = "${state.workspace.name}.${state.agent.name}" selectedIDE.withWorkspaceProject( - name = name, + name = CoderCLIManager.getWorkspaceParts(state.workspace, state.agent), hostname = CoderCLIManager.getHostName(state.client.url, state.workspace, state.client.me, state.agent), projectPath = tfProject.text, deploymentURL = state.client.url,