Skip to content

fix: show login screen when token expires during workspace polling #83

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 2 commits into from
Apr 14, 2025
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
11 changes: 8 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,21 @@

## Unreleased

### Fixed

- login screen is shown instead of an empty list of workspaces when token expired

## 0.1.4 - 2025-04-11

### Fixed

- SSH connection to a Workspace is no longer established only once
- authorization wizard automatically goes to a previous screen when an error is encountered during connection to Coder deployment
- SSH connection to a Workspace is no longer established only once
- authorization wizard automatically goes to a previous screen when an error is encountered during connection to Coder
deployment

### Changed

- action buttons on the token input step were swapped to achieve better keyboard navigation
- action buttons on the token input step were swapped to achieve better keyboard navigation
- URI `project_path` query parameter was renamed to `folder`

## 0.1.3 - 2025-04-09
Expand Down
28 changes: 18 additions & 10 deletions src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.coder.toolbox

import com.coder.toolbox.cli.CoderCLIManager
import com.coder.toolbox.sdk.CoderRestClient
import com.coder.toolbox.sdk.ex.APIResponseException
import com.coder.toolbox.sdk.v2.models.WorkspaceStatus
import com.coder.toolbox.util.CoderProtocolHandler
import com.coder.toolbox.util.DialogUi
Expand Down Expand Up @@ -60,7 +61,7 @@ class CoderRemoteProvider(
// If we have an error in the polling we store it here before going back to
// sign-in page, so we can display it there. This is mainly because there
// does not seem to be a mechanism to show errors on the environment list.
private var pollError: Exception? = null
private var errorBuffer = mutableListOf<Throwable>()

// On the first load, automatically log in if we can.
private var firstRun = true
Expand Down Expand Up @@ -141,14 +142,21 @@ class CoderRemoteProvider(
client.setupSession()
} else {
context.logger.error(ex, "workspace polling error encountered")
pollError = ex
errorBuffer.add(ex)
logout()
break
}
} catch (ex: APIResponseException) {
context.logger.error(ex, "error in contacting ${client.url} while polling the available workspaces")
errorBuffer.add(ex)
logout()
goToEnvironmentsPage()
break
} catch (ex: Exception) {
context.logger.error(ex, "workspace polling error encountered")
pollError = ex
errorBuffer.add(ex)
logout()
goToEnvironmentsPage()
break
}

Expand Down Expand Up @@ -300,15 +308,14 @@ class CoderRemoteProvider(
if (client == null) {
// When coming back to the application, authenticate immediately.
val autologin = shouldDoAutoLogin()
var autologinEx: Exception? = null
context.secrets.lastToken.let { lastToken ->
context.secrets.lastDeploymentURL.let { lastDeploymentURL ->
if (autologin && lastDeploymentURL.isNotBlank() && (lastToken.isNotBlank() || !settings.requireTokenAuth)) {
try {
AuthWizardState.goToStep(WizardStep.LOGIN)
return AuthWizardPage(context, true, ::onConnect)
} catch (ex: Exception) {
autologinEx = ex
errorBuffer.add(ex)
}
}
}
Expand All @@ -317,11 +324,12 @@ class CoderRemoteProvider(

// Login flow.
val authWizard = AuthWizardPage(context, false, ::onConnect)
// We might have tried and failed to automatically log in.
autologinEx?.let { authWizard.notify("Error logging in", it) }
// We might have navigated here due to a polling error.
pollError?.let { authWizard.notify("Error fetching workspaces", it) }

errorBuffer.forEach {
authWizard.notify("Error encountered", it)
}
// and now reset the errors, otherwise we show it every time on the screen
errorBuffer.clear()
return authWizard
}
return null
Expand All @@ -336,7 +344,7 @@ class CoderRemoteProvider(
// Currently we always remember, but this could be made an option.
context.secrets.rememberMe = true
this.client = client
pollError = null
errorBuffer.clear()
pollJob?.cancel()
pollJob = poll(client, cli)
goToEnvironmentsPage()
Expand Down
80 changes: 69 additions & 11 deletions src/main/kotlin/com/coder/toolbox/sdk/CoderRestClient.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import com.coder.toolbox.sdk.convertors.OSConverter
import com.coder.toolbox.sdk.convertors.UUIDConverter
import com.coder.toolbox.sdk.ex.APIResponseException
import com.coder.toolbox.sdk.v2.CoderV2RestFacade
import com.coder.toolbox.sdk.v2.models.ApiErrorResponse
import com.coder.toolbox.sdk.v2.models.BuildInfo
import com.coder.toolbox.sdk.v2.models.CreateWorkspaceBuildRequest
import com.coder.toolbox.sdk.v2.models.Template
Expand All @@ -24,6 +25,7 @@ import com.coder.toolbox.util.getOS
import com.squareup.moshi.Moshi
import okhttp3.Credentials
import okhttp3.OkHttpClient
import retrofit2.Response
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import java.net.HttpURLConnection
Expand Down Expand Up @@ -55,6 +57,7 @@ open class CoderRestClient(
private val pluginVersion: String = "development",
) {
private val settings = context.settingsStore.readOnly()
private lateinit var moshi: Moshi
private lateinit var httpClient: OkHttpClient
private lateinit var retroRestClient: CoderV2RestFacade

Expand All @@ -66,7 +69,7 @@ open class CoderRestClient(
}

fun setupSession() {
val moshi =
moshi =
Moshi.Builder()
.add(ArchConverter())
.add(InstantConverter())
Expand Down Expand Up @@ -152,7 +155,7 @@ open class CoderRestClient(
suspend fun me(): User {
val userResponse = retroRestClient.me()
if (!userResponse.isSuccessful) {
throw APIResponseException("authenticate", url, userResponse)
throw APIResponseException("authenticate", url, userResponse.code(), userResponse.parseErrorBody(moshi))
}

return userResponse.body()!!
Expand All @@ -165,7 +168,12 @@ open class CoderRestClient(
suspend fun workspaces(): List<Workspace> {
val workspacesResponse = retroRestClient.workspaces("owner:me")
if (!workspacesResponse.isSuccessful) {
throw APIResponseException("retrieve workspaces", url, workspacesResponse)
throw APIResponseException(
"retrieve workspaces",
url,
workspacesResponse.code(),
workspacesResponse.parseErrorBody(moshi)
)
}

return workspacesResponse.body()!!.workspaces
Expand All @@ -178,7 +186,12 @@ open class CoderRestClient(
suspend fun workspace(workspaceID: UUID): Workspace {
val workspacesResponse = retroRestClient.workspace(workspaceID)
if (!workspacesResponse.isSuccessful) {
throw APIResponseException("retrieve workspace", url, workspacesResponse)
throw APIResponseException(
"retrieve workspace",
url,
workspacesResponse.code(),
workspacesResponse.parseErrorBody(moshi)
)
}

return workspacesResponse.body()!!
Expand Down Expand Up @@ -209,15 +222,25 @@ open class CoderRestClient(
val resourcesResponse =
retroRestClient.templateVersionResources(workspace.latestBuild.templateVersionID)
if (!resourcesResponse.isSuccessful) {
throw APIResponseException("retrieve resources for ${workspace.name}", url, resourcesResponse)
throw APIResponseException(
"retrieve resources for ${workspace.name}",
url,
resourcesResponse.code(),
resourcesResponse.parseErrorBody(moshi)
)
}
return resourcesResponse.body()!!
}

suspend fun buildInfo(): BuildInfo {
val buildInfoResponse = retroRestClient.buildInfo()
if (!buildInfoResponse.isSuccessful) {
throw APIResponseException("retrieve build information", url, buildInfoResponse)
throw APIResponseException(
"retrieve build information",
url,
buildInfoResponse.code(),
buildInfoResponse.parseErrorBody(moshi)
)
}
return buildInfoResponse.body()!!
}
Expand All @@ -228,7 +251,12 @@ open class CoderRestClient(
private suspend fun template(templateID: UUID): Template {
val templateResponse = retroRestClient.template(templateID)
if (!templateResponse.isSuccessful) {
throw APIResponseException("retrieve template with ID $templateID", url, templateResponse)
throw APIResponseException(
"retrieve template with ID $templateID",
url,
templateResponse.code(),
templateResponse.parseErrorBody(moshi)
)
}
return templateResponse.body()!!
}
Expand All @@ -240,7 +268,12 @@ open class CoderRestClient(
val buildRequest = CreateWorkspaceBuildRequest(null, WorkspaceTransition.START)
val buildResponse = retroRestClient.createWorkspaceBuild(workspace.id, buildRequest)
if (buildResponse.code() != HttpURLConnection.HTTP_CREATED) {
throw APIResponseException("start workspace ${workspace.name}", url, buildResponse)
throw APIResponseException(
"start workspace ${workspace.name}",
url,
buildResponse.code(),
buildResponse.parseErrorBody(moshi)
)
}
return buildResponse.body()!!
}
Expand All @@ -251,7 +284,12 @@ open class CoderRestClient(
val buildRequest = CreateWorkspaceBuildRequest(null, WorkspaceTransition.STOP)
val buildResponse = retroRestClient.createWorkspaceBuild(workspace.id, buildRequest)
if (buildResponse.code() != HttpURLConnection.HTTP_CREATED) {
throw APIResponseException("stop workspace ${workspace.name}", url, buildResponse)
throw APIResponseException(
"stop workspace ${workspace.name}",
url,
buildResponse.code(),
buildResponse.parseErrorBody(moshi)
)
}
return buildResponse.body()!!
}
Expand All @@ -263,7 +301,12 @@ open class CoderRestClient(
val buildRequest = CreateWorkspaceBuildRequest(null, WorkspaceTransition.DELETE, false)
val buildResponse = retroRestClient.createWorkspaceBuild(workspace.id, buildRequest)
if (buildResponse.code() != HttpURLConnection.HTTP_CREATED) {
throw APIResponseException("delete workspace ${workspace.name}", url, buildResponse)
throw APIResponseException(
"delete workspace ${workspace.name}",
url,
buildResponse.code(),
buildResponse.parseErrorBody(moshi)
)
}
}

Expand All @@ -283,7 +326,12 @@ open class CoderRestClient(
CreateWorkspaceBuildRequest(template.activeVersionID, WorkspaceTransition.START)
val buildResponse = retroRestClient.createWorkspaceBuild(workspace.id, buildRequest)
if (buildResponse.code() != HttpURLConnection.HTTP_CREATED) {
throw APIResponseException("update workspace ${workspace.name}", url, buildResponse)
throw APIResponseException(
"update workspace ${workspace.name}",
url,
buildResponse.code(),
buildResponse.parseErrorBody(moshi)
)
}
return buildResponse.body()!!
}
Expand All @@ -296,3 +344,13 @@ open class CoderRestClient(
}
}
}

private fun Response<*>.parseErrorBody(moshi: Moshi): ApiErrorResponse? {
val errorBody = this.errorBody() ?: return null
return try {
val adapter = moshi.adapter(ApiErrorResponse::class.java)
adapter.fromJson(errorBody.string())
} catch (e: Exception) {
null
}
}
Loading
Loading