Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@

## Unreleased

### Added

- Add ability to customize filter for workspace connections view.
- Add owner column to connections view table.
- Add ability to connect to workspaces you don't own but have permissions for.

## 2.14.2 - 2024-09-23

### Changed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,11 @@ class CoderSettingsConfigurable : BoundConfigurable("Coder") {
.bindText(state::sshLogDirectory)
.comment(CoderGatewayBundle.message("gateway.connector.settings.ssh-log-directory.comment"))
}.layout(RowLayout.PARENT_GRID)
row(CoderGatewayBundle.message("gateway.connector.settings.workspace-filter.title")) {
textField().resizableColumn().align(AlignX.FILL)
.bindText(state::workspaceFilter)
.comment(CoderGatewayBundle.message("gateway.connector.settings.workspace-filter.comment"))
}.layout(RowLayout.PARENT_GRID)
}
}

Expand Down
46 changes: 33 additions & 13 deletions src/main/kotlin/com/coder/gateway/cli/CoderCLIManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
import com.coder.gateway.cli.ex.MissingVersionException
import com.coder.gateway.cli.ex.ResponseException
import com.coder.gateway.cli.ex.SSHConfigFormatException
import com.coder.gateway.sdk.v2.models.User
import com.coder.gateway.sdk.v2.models.Workspace
import com.coder.gateway.sdk.v2.models.WorkspaceAgent
import com.coder.gateway.settings.CoderSettings
import com.coder.gateway.settings.CoderSettingsState
import com.coder.gateway.util.CoderHostnameVerifier
Expand Down Expand Up @@ -219,11 +222,12 @@
* This can take supported features for testing purposes only.
*/
fun configSsh(
workspaceNames: Set<String>,
workspacesAndAgents: Set<Pair<Workspace, WorkspaceAgent>>,
currentUser: User,
feats: Features = features,
) {
logger.info("Configuring SSH config at ${settings.sshConfigPath}")
writeSSHConfig(modifySSHConfig(readSSHConfig(), workspaceNames, feats))
writeSSHConfig(modifySSHConfig(readSSHConfig(), workspacesAndAgents, feats, currentUser))
}

/**
Expand All @@ -245,8 +249,9 @@
*/
private fun modifySSHConfig(
contents: String?,
workspaceNames: Set<String>,
workspaceNames: Set<Pair<Workspace, WorkspaceAgent>>,
feats: Features,
currentUser: User,
): String? {
val host = deploymentURL.safeHost()
val startBlock = "# --- START CODER JETBRAINS $host"
Expand Down Expand Up @@ -287,8 +292,8 @@
System.lineSeparator() + endBlock,
transform = {
"""
Host ${getHostName(deploymentURL, it)}
ProxyCommand ${proxyArgs.joinToString(" ")} $it
Host ${getHostName(deploymentURL, it.first, currentUser, it.second)}
ProxyCommand ${proxyArgs.joinToString(" ")} ${getWorkspaceParts(it.first, it.second)}
ConnectTimeout 0
StrictHostKeyChecking no
UserKnownHostsFile /dev/null
Expand All @@ -299,8 +304,8 @@
.plus("\n")
.plus(
"""
Host ${getBackgroundHostName(deploymentURL, it)}
ProxyCommand ${backgroundProxyArgs.joinToString(" ")} $it
Host ${getHostName(deploymentURL, it.first, currentUser, it.second)}--bg
ProxyCommand ${backgroundProxyArgs.joinToString(" ")} ${getWorkspaceParts(it.first, it.second)}
ConnectTimeout 0
StrictHostKeyChecking no
UserKnownHostsFile /dev/null
Expand Down Expand Up @@ -478,21 +483,36 @@

private val tokenRegex = "--token [^ ]+".toRegex()

/**
* This function returns the ssh host name generated for connecting to the workspace.
*/
@JvmStatic
fun getHostName(
url: URL,
workspaceName: String,
): String = "coder-jetbrains--$workspaceName--${url.safeHost()}"
workspace: Workspace,
currentUser: User,
agent: WorkspaceAgent,
): String =
if (currentUser.username == workspace.ownerName) {
"coder-jetbrains--${workspace.name}.${agent.name}--${url.safeHost()}"
} else {
"coder-jetbrains--${workspace.ownerName}--${workspace.name}.${agent.name}--${url.safeHost()}"
}


/**
* This function returns the identifier for the workspace to pass to the
* coder ssh proxy command.
*/
@JvmStatic
fun getBackgroundHostName(
url: URL,
workspaceName: String,
): String = getHostName(url, workspaceName) + "--bg"
fun getWorkspaceParts(
workspace: Workspace,
agent: WorkspaceAgent,
): String = "${workspace.ownerName}/${workspace.name}.${agent.name}"

@JvmStatic
fun getBackgroundHostName(
hostname: String,
): String = hostname + "--bg"

Check notice on line 516 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
}
}
15 changes: 4 additions & 11 deletions src/main/kotlin/com/coder/gateway/sdk/CoderRestClient.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,7 @@ import com.coder.gateway.sdk.convertors.OSConverter
import com.coder.gateway.sdk.convertors.UUIDConverter
import com.coder.gateway.sdk.ex.APIResponseException
import com.coder.gateway.sdk.v2.CoderV2RestFacade
import com.coder.gateway.sdk.v2.models.BuildInfo
import com.coder.gateway.sdk.v2.models.CreateWorkspaceBuildRequest
import com.coder.gateway.sdk.v2.models.Template
import com.coder.gateway.sdk.v2.models.User
import com.coder.gateway.sdk.v2.models.Workspace
import com.coder.gateway.sdk.v2.models.WorkspaceBuild
import com.coder.gateway.sdk.v2.models.WorkspaceResource
import com.coder.gateway.sdk.v2.models.WorkspaceTransition
import com.coder.gateway.sdk.v2.models.*
import com.coder.gateway.settings.CoderSettings
import com.coder.gateway.settings.CoderSettingsState
import com.coder.gateway.util.CoderHostnameVerifier
Expand Down Expand Up @@ -166,7 +159,7 @@ open class CoderRestClient(
* @throws [APIResponseException].
*/
fun workspaces(): List<Workspace> {
val workspacesResponse = retroRestClient.workspaces("owner:me").execute()
val workspacesResponse = retroRestClient.workspaces(settings.workspaceFilter).execute()
if (!workspacesResponse.isSuccessful) {
throw APIResponseException("retrieve workspaces", url, workspacesResponse)
}
Expand All @@ -178,12 +171,12 @@ open class CoderRestClient(
* Retrieves all the agent names for all workspaces, including those that
* are off. Meant to be used when configuring SSH.
*/
fun agentNames(workspaces: List<Workspace>): Set<String> {
fun withAgents(workspaces: List<Workspace>): Set<Pair<Workspace, WorkspaceAgent>> {
// It is possible for there to be resources with duplicate names so we
// need to use a set.
return workspaces.flatMap { ws ->
ws.latestBuild.resources.ifEmpty { resources(ws) }.filter { it.agents != null }.flatMap { it.agents!! }.map {
"${ws.name}.${it.name}"
ws to it
}
}.toSet()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ data class Workspace(
@Json(name = "latest_build") val latestBuild: WorkspaceBuild,
@Json(name = "outdated") val outdated: Boolean,
@Json(name = "name") val name: String,
@Json(name = "owner_name") val ownerName: String,
)

/**
Expand Down
8 changes: 8 additions & 0 deletions src/main/kotlin/com/coder/gateway/settings/CoderSettings.kt
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@ open class CoderSettingsState(
open var defaultURL: String = "",
// Value for --log-dir.
open var sshLogDirectory: String = "",
// Default filter for fetching workspaces
open var workspaceFilter: String = "owner:me"
)

/**
Expand Down Expand Up @@ -135,6 +137,12 @@ open class CoderSettings(
val enableDownloads: Boolean
get() = state.enableDownloads

/**
* The filter to apply when fetching workspaces (default is owner:me)
*/
val workspaceFilter: String
get() = state.workspaceFilter

/**
* Whether falling back to the data directory is allowed if the binary
* directory is not writable.
Expand Down
5 changes: 1 addition & 4 deletions src/main/kotlin/com/coder/gateway/util/Dialogs.kt
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,13 @@
* A dialog wrapper around CoderWorkspaceStepView.
*/
private class CoderWorkspaceStepDialog(
name: String,
private val state: CoderWorkspacesStepSelection,
) : DialogWrapper(true) {
private val view = CoderWorkspaceProjectIDEStepView(showTitle = false)

init {
init()
title = CoderGatewayBundle.message("gateway.connector.view.coder.remoteproject.choose.text", name)
title = CoderGatewayBundle.message("gateway.connector.view.coder.remoteproject.choose.text", CoderCLIManager.getWorkspaceParts(state.workspace, state.agent))

Check warning on line 41 in src/main/kotlin/com/coder/gateway/util/Dialogs.kt

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Incorrect string capitalization

String 'Choose IDE and project for workspace {0}' is not properly capitalized. It should have title capitalization
}

override fun show() {
Expand Down Expand Up @@ -71,7 +70,6 @@
}

fun askIDE(
name: String,
agent: WorkspaceAgent,
workspace: Workspace,
cli: CoderCLIManager,
Expand All @@ -82,7 +80,6 @@
ApplicationManager.getApplication().invokeAndWait {
val dialog =
CoderWorkspaceStepDialog(
name,
CoderWorkspacesStepSelection(agent, workspace, cli, client, workspaces),
)
data = dialog.showAndGetData()
Expand Down
6 changes: 3 additions & 3 deletions src/main/kotlin/com/coder/gateway/util/LinkHandler.kt
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ open class LinkHandler(
}

indicator?.invoke("Configuring Coder CLI...")
cli.configSsh(client.agentNames(workspaces))
cli.configSsh(workspacesAndAgents = client.withAgents(workspaces), currentUser = client.me)

val name = "${workspace.name}.${agent.name}"
val openDialog =
Expand All @@ -116,14 +116,14 @@ open class LinkHandler(
parameters.folder().isNullOrBlank()

return if (openDialog) {
askIDE(name, agent, workspace, cli, client, workspaces) ?: throw MissingArgumentException("IDE selection aborted; unable to connect")
askIDE(agent, workspace, cli, client, workspaces) ?: throw MissingArgumentException("IDE selection aborted; unable to connect")
} else {
// Check that both the domain and the redirected domain are
// allowlisted. If not, check with the user whether to proceed.
verifyDownloadLink(parameters)
WorkspaceProjectIDE.fromInputs(
name = name,
hostname = CoderCLIManager.getHostName(deploymentURL.toURL(), name),
hostname = CoderCLIManager.getHostName(deploymentURL.toURL(), workspace, client.me, agent),
projectPath = parameters.folder(),
ideProductCode = parameters.ideProductCode(),
ideBuildNumber = parameters.ideBuildNumber(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -184,23 +184,22 @@ class CoderWorkspaceProjectIDEStepView(

// We use this when returning the connection params from data().
state = data

val name = "${data.workspace.name}.${data.agent.name}"
val name = CoderCLIManager.getWorkspaceParts(data.workspace, data.agent)
logger.info("Initializing workspace step for $name")

val homeDirectory = data.agent.expandedDirectory ?: data.agent.directory
tfProject.text = if (homeDirectory.isNullOrBlank()) "/home" else homeDirectory
titleLabel.text = CoderGatewayBundle.message("gateway.connector.view.coder.remoteproject.choose.text", name)
titleLabel.isVisible = showTitle
terminalLink.url = data.client.url.withPath("/me/$name/terminal").toString()
terminalLink.url = data.client.url.withPath("/$name/terminal").toString()

ideResolvingJob =
cs.launch(ModalityState.current().asContextElement()) {
try {
logger.info("Configuring Coder CLI...")
cbIDE.renderer = IDECellRenderer("Configuring Coder CLI...")
withContext(Dispatchers.IO) {
data.cliManager.configSsh(data.client.agentNames(data.workspaces))
data.cliManager.configSsh(data.client.withAgents(data.workspaces), data.client.me)
}

val ides =
Expand All @@ -215,7 +214,7 @@ class CoderWorkspaceProjectIDEStepView(
} else {
IDECellRenderer(CoderGatewayBundle.message("gateway.connector.view.coder.connect-ssh"))
}
val executor = createRemoteExecutor(CoderCLIManager.getBackgroundHostName(data.client.url, name))
val executor = createRemoteExecutor(CoderCLIManager.getHostName(data.client.url, data.workspace, data.client.me, data.agent) + "--bg")

if (ComponentValidator.getInstance(tfProject).isEmpty) {
logger.info("Installing remote path validator...")
Expand Down Expand Up @@ -338,7 +337,7 @@ class CoderWorkspaceProjectIDEStepView(
workspace: Workspace,
agent: WorkspaceAgent,
): List<IdeWithStatus> {
val name = "${workspace.name}.${agent.name}"
val name = CoderCLIManager.getWorkspaceParts(workspace, agent)
logger.info("Retrieving available IDEs for $name...")
val workspaceOS =
if (agent.operatingSystem != null && agent.architecture != null) {
Expand Down Expand Up @@ -406,7 +405,7 @@ class CoderWorkspaceProjectIDEStepView(
val name = "${state.workspace.name}.${state.agent.name}"
selectedIDE.withWorkspaceProject(
name = name,
hostname = CoderCLIManager.getHostName(state.client.url, name),
hostname = CoderCLIManager.getHostName(state.client.url, state.workspace, state.client.me, state.agent),
projectPath = tfProject.text,
deploymentURL = state.client.url,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import com.intellij.openapi.application.asContextElement
import com.intellij.openapi.components.service
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.observable.properties.ObservableMutableProperty

Check warning on line 36 in src/main/kotlin/com/coder/gateway/views/steps/CoderWorkspacesStepView.kt

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Unused import directive

Unused import directive
import com.intellij.openapi.rd.util.launchUnderBackgroundProgress
import com.intellij.openapi.ui.panel.ComponentPanelBuilder
import com.intellij.openapi.ui.setEmptyState
Expand Down Expand Up @@ -89,7 +90,7 @@
private data class CoderWorkspacesFormFields(
var coderURL: String = "",
var token: Pair<String, Source>? = null,
var useExistingToken: Boolean = false,
var useExistingToken: Boolean = false
)

/**
Expand Down Expand Up @@ -751,7 +752,7 @@
override fun data(): CoderWorkspacesStepSelection {
val selected = tableOfWorkspaces.selectedObject
return withoutNull(client, cliManager, selected?.agent, selected?.workspace) { client, cli, agent, workspace ->
val name = "${workspace.name}.${agent.name}"
val name = CoderCLIManager.getWorkspaceParts(workspace, agent)
logger.info("Returning data for $name")
CoderWorkspacesStepSelection(
agent = agent,
Expand Down Expand Up @@ -783,6 +784,7 @@
ListTableModel<WorkspaceAgentListModel>(
WorkspaceIconColumnInfo(""),
WorkspaceNameColumnInfo("Name"),
WorkspaceOwnerColumnInfo("Owner"),
WorkspaceTemplateNameColumnInfo("Template"),
WorkspaceVersionColumnInfo("Version"),
WorkspaceStatusColumnInfo("Status"),
Expand Down Expand Up @@ -849,6 +851,36 @@
}
}

private class WorkspaceOwnerColumnInfo(columnName: String) : ColumnInfo<WorkspaceAgentListModel, String>(columnName) {
override fun valueOf(item: WorkspaceAgentListModel?): String? = item?.workspace?.ownerName

override fun getComparator(): Comparator<WorkspaceAgentListModel> = Comparator { a, b ->
a.workspace.ownerName.compareTo(b.workspace.ownerName, ignoreCase = true)
}

override fun getRenderer(item: WorkspaceAgentListModel?): TableCellRenderer {
return object : DefaultTableCellRenderer() {
override fun getTableCellRendererComponent(
table: JTable,
value: Any,
isSelected: Boolean,
hasFocus: Boolean,
row: Int,
column: Int,
): Component {
super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column)
if (value is String) {
text = value
}

font = RelativeFont.BOLD.derive(table.tableHeader.font)
border = JBUI.Borders.empty(0, 8)
return this
}
}
}
}

private class WorkspaceTemplateNameColumnInfo(columnName: String) : ColumnInfo<WorkspaceAgentListModel, String>(columnName) {
override fun valueOf(item: WorkspaceAgentListModel?): String? = item?.workspace?.templateName

Expand Down Expand Up @@ -879,7 +911,7 @@
}

private class WorkspaceVersionColumnInfo(columnName: String) : ColumnInfo<WorkspaceAgentListModel, String>(columnName) {
override fun valueOf(workspace: WorkspaceAgentListModel?): String? = if (workspace == null) {

Check warning on line 914 in src/main/kotlin/com/coder/gateway/views/steps/CoderWorkspacesStepView.kt

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Redundant nullable return type

'valueOf' always returns non-null type
"Unknown"
} else if (workspace.workspace.outdated) {
"Outdated"
Expand Down
Loading
Loading