Skip to content

Start workspaces by shelling out to CLI #518

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jan 3, 2025
Merged
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
15 changes: 15 additions & 0 deletions src/main/kotlin/com/coder/gateway/cli/CoderCLIManager.kt
Original file line number Diff line number Diff line change
@@ -451,6 +451,21 @@
return matches
}

/**
* Start a workspace.
*
* Throws if the command execution fails.
*/
fun startWorkspace(workspaceOwner: String, workspaceName: String): String {
return exec(
"--global-config",
coderConfigPath.toString(),
"start",
"--yes",
workspaceOwner+"/"+workspaceName,

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

GitHub Actions / Build

String concatenation that can be converted to string template

'String' concatenation can be converted to a template
)
}

private fun exec(vararg args: String): String {
val stdout =
ProcessExecutor()
@@ -521,6 +536,6 @@
@JvmStatic
fun getBackgroundHostName(
hostname: String,
): String = hostname + "--bg"

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

GitHub Actions / Build

String concatenation that can be converted to string template

'String' concatenation can be converted to a template
}
}
12 changes: 0 additions & 12 deletions src/main/kotlin/com/coder/gateway/sdk/CoderRestClient.kt
Original file line number Diff line number Diff line change
@@ -227,18 +227,6 @@ open class CoderRestClient(
return templateResponse.body()!!
}

/**
* @throws [APIResponseException].
*/
fun startWorkspace(workspace: Workspace): WorkspaceBuild {
val buildRequest = CreateWorkspaceBuildRequest(null, WorkspaceTransition.START)
val buildResponse = retroRestClient.createWorkspaceBuild(workspace.id, buildRequest).execute()
if (buildResponse.code() != HttpURLConnection.HTTP_CREATED) {
throw APIResponseException("start workspace ${workspace.name}", url, buildResponse)
}
return buildResponse.body()!!
}

/**
* @throws [APIResponseException].
*/
Original file line number Diff line number Diff line change
@@ -5,6 +5,8 @@ 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
import com.coder.gateway.models.WorkspaceProjectIDE
@@ -73,6 +75,8 @@ 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) :
@@ -232,10 +236,10 @@ class CoderGatewayRecentWorkspaceConnectionsView(private val setContentCallback:
if (enableLinks) {
cell(
ActionLink(workspaceProjectIDE.projectPathDisplay) {
withoutNull(deployment?.client, workspaceWithAgent?.workspace) { client, workspace ->
withoutNull(deployment?.cli, workspaceWithAgent?.workspace) { cli, workspace ->
CoderRemoteConnectionHandle().connect {
if (listOf(WorkspaceStatus.STOPPED, WorkspaceStatus.CANCELED, WorkspaceStatus.FAILED).contains(workspace.latestBuild.status)) {
client.startWorkspace(workspace)
cli.startWorkspace(workspace.ownerName, workspace.name)
}
workspaceProjectIDE
}
@@ -358,6 +362,19 @@ 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.
@@ -378,6 +395,7 @@ class CoderGatewayRecentWorkspaceConnectionsView(private val setContentCallback:
}

deployment.client = client
deployment.cli = cli
deployment.items = items
deployment.error = null
} catch (e: Exception) {
Original file line number Diff line number Diff line change
@@ -303,13 +303,13 @@
CoderIcons.RUN,
) {
override fun actionPerformed(p0: AnActionEvent) {
withoutNull(client, tableOfWorkspaces.selectedObject?.workspace) { c, workspace ->
withoutNull(cliManager, tableOfWorkspaces.selectedObject?.workspace) { cliManager, workspace ->
jobs[workspace.id]?.cancel()
jobs[workspace.id] =
cs.launch(ModalityState.current().asContextElement()) {
withContext(Dispatchers.IO) {
try {
c.startWorkspace(workspace)
cliManager.startWorkspace(workspace.ownerName, workspace.name)
loadWorkspaces()
} catch (e: Exception) {
logger.error("Could not start workspace ${workspace.name}", e)
@@ -659,7 +659,7 @@
cs.launch(ModalityState.current().asContextElement()) {
while (isActive) {
loadWorkspaces()
delay(5000)
delay(1000)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made this poll more frequently because so it picks up the "Starting" state faster while the CLI is starting the workspace. I'm not sure if there's a good way to switch to fast polling while the CLI is running and then switch back - feel free to make suggestions or commit to the branch!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we can redirect the output to somewhere (or nothing, since we are not using it) so we can return immediately from the exec and loadWorkspaces() will run right away to pick up the starting state as quickly as possible. Let me experiment for a bit and see. Code looks good to me though!

}
}
}
@@ -910,7 +910,7 @@
}

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

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

GitHub Actions / Build

Redundant nullable return type

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

Unchanged files with check annotations Beta

init {
init()
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

GitHub Actions / Build

Incorrect string capitalization

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