Skip to content

Commit a0d581c

Browse files
committed
Consolidate connection errors
This way we can display the same errors on the recent connections and "Connect to Coder" panels. Normally an authentication error is only detected when getting me(), but on the recents page that is just a wasted call, so refactor the errors to allow detecting a 401 on any request, which is probably better than relying on only me() anyway.
1 parent 7b735a0 commit a0d581c

14 files changed

+233
-189
lines changed

src/main/kotlin/com/coder/gateway/CoderGatewayConnectionProvider.kt

+3-3
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import com.coder.gateway.models.token
2323
import com.coder.gateway.models.url
2424
import com.coder.gateway.models.workspace
2525
import com.coder.gateway.sdk.CoderRestClient
26-
import com.coder.gateway.sdk.ex.AuthenticationResponseException
26+
import com.coder.gateway.sdk.ex.APIResponseException
2727
import com.coder.gateway.sdk.v2.models.Workspace
2828
import com.coder.gateway.sdk.v2.models.WorkspaceAgent
2929
import com.coder.gateway.sdk.v2.models.WorkspaceStatus
@@ -244,9 +244,9 @@ class CoderGatewayConnectionProvider : GatewayConnectionProvider {
244244
return try {
245245
client.authenticate()
246246
client
247-
} catch (ex: AuthenticationResponseException) {
247+
} catch (ex: APIResponseException) {
248248
// If doing token auth we can ask and try again.
249-
if (settings.requireTokenAuth) {
249+
if (settings.requireTokenAuth && ex.isUnauthorized) {
250250
authenticate(deploymentURL, queryToken, token)
251251
} else {
252252
throw ex

src/main/kotlin/com/coder/gateway/CoderRemoteConnectionHandle.kt

+4-4
Original file line numberDiff line numberDiff line change
@@ -241,10 +241,10 @@ class CoderRemoteConnectionHandle {
241241
} else {
242242
// Look on disk in case we already have a token, either in
243243
// the deployment's config or the global config.
244-
val token = settings.token(url.toString())
245-
if (token != null && token.first != existingToken) {
246-
logger.info("Injecting token for $url from ${token.second}")
247-
return token
244+
val tryToken = settings.token(url)
245+
if (tryToken != null && tryToken.first != existingToken) {
246+
logger.info("Injecting token for $url from ${tryToken.second}")
247+
return tryToken
248248
}
249249
}
250250
}

src/main/kotlin/com/coder/gateway/models/WorkspaceProjectIDE.kt

+5-4
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import com.jetbrains.gateway.ssh.deploy.TransferProgressTracker
1616
import com.jetbrains.gateway.ssh.util.validateIDEInstallPath
1717
import org.zeroturnaround.exec.ProcessExecutor
1818
import java.net.URI
19+
import java.net.URL
1920
import java.nio.file.Path
2021
import java.time.Duration
2122
import java.time.LocalDateTime
@@ -39,7 +40,7 @@ class WorkspaceProjectIDE(
3940
var idePathOnHost: String?,
4041
val downloadSource: String?,
4142
// These are used in the recent connections window.
42-
val deploymentURL: String,
43+
val deploymentURL: URL,
4344
var lastOpened: String?, // Null if never opened.
4445
) {
4546
val ideName = "${ideProductCode.productCode}-$ideBuildNumber"
@@ -250,7 +251,7 @@ class WorkspaceProjectIDE(
250251
ideBuildNumber = ideBuildNumber,
251252
downloadSource = downloadSource,
252253
idePathOnHost = idePathOnHost,
253-
deploymentURL = deploymentURL,
254+
deploymentURL = deploymentURL.toString(),
254255
lastOpened = lastOpened,
255256
)
256257
}
@@ -295,7 +296,7 @@ class WorkspaceProjectIDE(
295296
ideBuildNumber = ideBuildNumber,
296297
idePathOnHost = idePathOnHost,
297298
downloadSource = downloadSource,
298-
deploymentURL = deploymentURL,
299+
deploymentURL = URL(deploymentURL),
299300
lastOpened = lastOpened,
300301
)
301302
}
@@ -359,7 +360,7 @@ fun IdeWithStatus.withWorkspaceProject(
359360
name: String,
360361
hostname: String,
361362
projectPath: String,
362-
deploymentURL: String,
363+
deploymentURL: URL,
363364
): WorkspaceProjectIDE {
364365
return WorkspaceProjectIDE(
365366
name = name,

src/main/kotlin/com/coder/gateway/sdk/CoderRestClient.kt

+23-26
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,7 @@ import com.coder.gateway.sdk.convertors.ArchConverter
66
import com.coder.gateway.sdk.convertors.InstantConverter
77
import com.coder.gateway.sdk.convertors.OSConverter
88
import com.coder.gateway.sdk.convertors.UUIDConverter
9-
import com.coder.gateway.sdk.ex.AuthenticationResponseException
10-
import com.coder.gateway.sdk.ex.TemplateResponseException
11-
import com.coder.gateway.sdk.ex.WorkspaceResponseException
9+
import com.coder.gateway.sdk.ex.APIResponseException
1210
import com.coder.gateway.sdk.v2.CoderV2RestFacade
1311
import com.coder.gateway.sdk.v2.models.BuildInfo
1412
import com.coder.gateway.sdk.v2.models.CreateWorkspaceBuildRequest
@@ -138,19 +136,11 @@ open class CoderRestClient(
138136
.build().create(CoderV2RestFacade::class.java)
139137
}
140138

141-
private fun <T> error(
142-
action: String,
143-
res: retrofit2.Response<T>,
144-
): String {
145-
val details = res.errorBody()?.charStream()?.use { it.readText() } ?: "no details provided"
146-
return "Unable to $action: url=$url, code=${res.code()}, details=$details"
147-
}
148-
149139
/**
150140
* Authenticate and load information about the current user and the build
151141
* version.
152142
*
153-
* @throws [AuthenticationResponseException] if authentication failed.
143+
* @throws [APIResponseException].
154144
*/
155145
fun authenticate(): User {
156146
me = me()
@@ -160,25 +150,25 @@ open class CoderRestClient(
160150

161151
/**
162152
* Retrieve the current user.
163-
* @throws [AuthenticationResponseException] if authentication failed.
153+
* @throws [APIResponseException].
164154
*/
165155
fun me(): User {
166156
val userResponse = retroRestClient.me().execute()
167157
if (!userResponse.isSuccessful) {
168-
throw AuthenticationResponseException(error("authenticate", userResponse))
158+
throw APIResponseException("authenticate", url, userResponse)
169159
}
170160

171161
return userResponse.body()!!
172162
}
173163

174164
/**
175165
* Retrieves the available workspaces created by the user.
176-
* @throws WorkspaceResponseException if workspaces could not be retrieved.
166+
* @throws [ApiResponseException].
177167
*/
178168
fun workspaces(): List<Workspace> {
179169
val workspacesResponse = retroRestClient.workspaces("owner:me").execute()
180170
if (!workspacesResponse.isSuccessful) {
181-
throw WorkspaceResponseException(error("retrieve workspaces", workspacesResponse))
171+
throw APIResponseException("retrieve workspaces", url, workspacesResponse)
182172
}
183173

184174
return workspacesResponse.body()!!.workspaces
@@ -203,48 +193,56 @@ open class CoderRestClient(
203193
* does not include agents when the workspace is off so this can be used to
204194
* get them instead, just like `coder config-ssh` does (otherwise we risk
205195
* removing hosts from the SSH config when they are off).
196+
* @throws [ApiResponseException].
206197
*/
207198
fun resources(workspace: Workspace): List<WorkspaceResource> {
208199
val resourcesResponse = retroRestClient.templateVersionResources(workspace.latestBuild.templateVersionID).execute()
209200
if (!resourcesResponse.isSuccessful) {
210-
throw WorkspaceResponseException(error("retrieve resources for ${workspace.name}", resourcesResponse))
201+
throw APIResponseException("retrieve resources for ${workspace.name}", url, resourcesResponse)
211202
}
212203
return resourcesResponse.body()!!
213204
}
214205

215206
fun buildInfo(): BuildInfo {
216207
val buildInfoResponse = retroRestClient.buildInfo().execute()
217208
if (!buildInfoResponse.isSuccessful) {
218-
throw java.lang.IllegalStateException(error("retrieve build information", buildInfoResponse))
209+
throw APIResponseException("retrieve build information", url, buildInfoResponse)
219210
}
220211
return buildInfoResponse.body()!!
221212
}
222213

214+
/**
215+
* @throws [ApiResponseException].
216+
*/
223217
private fun template(templateID: UUID): Template {
224218
val templateResponse = retroRestClient.template(templateID).execute()
225219
if (!templateResponse.isSuccessful) {
226-
throw TemplateResponseException(error("retrieve template with ID $templateID", templateResponse))
220+
throw APIResponseException("retrieve template with ID $templateID", url, templateResponse)
227221
}
228222
return templateResponse.body()!!
229223
}
230224

225+
/**
226+
* @throws [ApiResponseException].
227+
*/
231228
fun startWorkspace(workspace: Workspace): WorkspaceBuild {
232229
val buildRequest = CreateWorkspaceBuildRequest(null, WorkspaceTransition.START)
233230
val buildResponse = retroRestClient.createWorkspaceBuild(workspace.id, buildRequest).execute()
234231
if (buildResponse.code() != HttpURLConnection.HTTP_CREATED) {
235-
throw WorkspaceResponseException(error("start workspace ${workspace.name}", buildResponse))
232+
throw APIResponseException("start workspace ${workspace.name}", url, buildResponse)
236233
}
237-
238234
return buildResponse.body()!!
239235
}
240236

237+
/**
238+
* @throws [ApiResponseException].
239+
*/
241240
fun stopWorkspace(workspace: Workspace): WorkspaceBuild {
242241
val buildRequest = CreateWorkspaceBuildRequest(null, WorkspaceTransition.STOP)
243242
val buildResponse = retroRestClient.createWorkspaceBuild(workspace.id, buildRequest).execute()
244243
if (buildResponse.code() != HttpURLConnection.HTTP_CREATED) {
245-
throw WorkspaceResponseException(error("stop workspace ${workspace.name}", buildResponse))
244+
throw APIResponseException("stop workspace ${workspace.name}", url, buildResponse)
246245
}
247-
248246
return buildResponse.body()!!
249247
}
250248

@@ -256,17 +254,16 @@ open class CoderRestClient(
256254
* 2. The agent gets a new ID and token on each START build. Many template
257255
* authors are not diligent about making sure the agent gets restarted
258256
* with this information when we do two START builds in a row.
257+
* @throws [ApiResponseException].
259258
*/
260259
fun updateWorkspace(workspace: Workspace): WorkspaceBuild {
261260
val template = template(workspace.templateID)
262-
263261
val buildRequest =
264262
CreateWorkspaceBuildRequest(template.activeVersionID, WorkspaceTransition.START)
265263
val buildResponse = retroRestClient.createWorkspaceBuild(workspace.id, buildRequest).execute()
266264
if (buildResponse.code() != HttpURLConnection.HTTP_CREATED) {
267-
throw WorkspaceResponseException(error("update workspace ${workspace.name}", buildResponse))
265+
throw APIResponseException("update workspace ${workspace.name}", url, buildResponse)
268266
}
269-
270267
return buildResponse.body()!!
271268
}
272269

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package com.coder.gateway.sdk.ex
2+
3+
import java.io.IOException
4+
import java.net.HttpURLConnection
5+
import java.net.URL
6+
7+
class APIResponseException(action: String, url: URL, res: retrofit2.Response<*>) :
8+
IOException(
9+
"Unable to $action: url=$url, code=${res.code()}, details=${
10+
res.errorBody()?.charStream()?.use {
11+
it.readText()
12+
} ?: "no details provided"}",
13+
) {
14+
val isUnauthorized = res.code() == HttpURLConnection.HTTP_UNAUTHORIZED
15+
}

src/main/kotlin/com/coder/gateway/sdk/ex/Exceptions.kt

-9
This file was deleted.

src/main/kotlin/com/coder/gateway/settings/CoderSettings.kt

+5-9
Original file line numberDiff line numberDiff line change
@@ -178,26 +178,22 @@ open class CoderSettings(
178178
/**
179179
* Given a deployment URL, try to find a token for it if required.
180180
*/
181-
fun token(url: String): Pair<String, Source>? {
181+
fun token(deploymentURL: URL): Pair<String, Source>? {
182182
// No need to bother if we do not need token auth anyway.
183183
if (!requireTokenAuth) {
184184
return null
185185
}
186186
// Try the deployment's config directory. This could exist if someone
187187
// has entered a URL that they are not currently connected to, but have
188188
// connected to in the past.
189-
try {
190-
val (_, deploymentToken) = readConfig(dataDir(url.toURL()).resolve("config"))
191-
if (!deploymentToken.isNullOrBlank()) {
192-
return deploymentToken to Source.DEPLOYMENT_CONFIG
193-
}
194-
} catch (ex: Exception) {
195-
// URL is invalid.
189+
val (_, deploymentToken) = readConfig(dataDir(deploymentURL).resolve("config"))
190+
if (!deploymentToken.isNullOrBlank()) {
191+
return deploymentToken to Source.DEPLOYMENT_CONFIG
196192
}
197193
// Try the global config directory, in case they previously set up the
198194
// CLI with this URL.
199195
val (configUrl, configToken) = readConfig(coderConfigDir)
200-
if (configUrl == url && !configToken.isNullOrBlank()) {
196+
if (configUrl == deploymentURL.toString() && !configToken.isNullOrBlank()) {
201197
return configToken to Source.CONFIG
202198
}
203199
return null
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package com.coder.gateway.util
2+
3+
import com.coder.gateway.CoderGatewayBundle
4+
import com.coder.gateway.cli.ex.ResponseException
5+
import com.coder.gateway.sdk.ex.APIResponseException
6+
import org.zeroturnaround.exec.InvalidExitValueException
7+
import java.net.ConnectException
8+
import java.net.SocketTimeoutException
9+
import java.net.URL
10+
import java.net.UnknownHostException
11+
import javax.net.ssl.SSLHandshakeException
12+
13+
fun humanizeConnectionError(deploymentURL: URL, requireTokenAuth: Boolean, e: Exception): String {
14+
val reason = e.message ?: CoderGatewayBundle.message("gateway.connector.view.workspaces.connect.no-reason")
15+
return when (e) {
16+
is java.nio.file.AccessDeniedException ->
17+
CoderGatewayBundle.message(
18+
"gateway.connector.view.workspaces.connect.access-denied",
19+
e.file,
20+
)
21+
is UnknownHostException ->
22+
CoderGatewayBundle.message(
23+
"gateway.connector.view.workspaces.connect.unknown-host",
24+
e.message ?: deploymentURL.host,
25+
)
26+
is InvalidExitValueException ->
27+
CoderGatewayBundle.message(
28+
"gateway.connector.view.workspaces.connect.unexpected-exit",
29+
e.exitValue,
30+
)
31+
is APIResponseException -> {
32+
if (e.isUnauthorized) {
33+
CoderGatewayBundle.message(
34+
if (requireTokenAuth) {
35+
"gateway.connector.view.workspaces.connect.unauthorized-token"
36+
} else {
37+
"gateway.connector.view.workspaces.connect.unauthorized-other"
38+
},
39+
deploymentURL,
40+
)
41+
} else {
42+
reason
43+
}
44+
}
45+
is SocketTimeoutException -> {
46+
CoderGatewayBundle.message(
47+
"gateway.connector.view.workspaces.connect.timeout",
48+
deploymentURL,
49+
)
50+
}
51+
is ResponseException, is ConnectException -> {
52+
CoderGatewayBundle.message(
53+
"gateway.connector.view.workspaces.connect.download-failed",
54+
reason,
55+
)
56+
}
57+
is SSLHandshakeException -> {
58+
CoderGatewayBundle.message(
59+
"gateway.connector.view.workspaces.connect.ssl-error",
60+
deploymentURL.host,
61+
reason,
62+
)
63+
}
64+
else -> reason
65+
}
66+
}

0 commit comments

Comments
 (0)