Skip to content

Commit bb4b643

Browse files
committed
chore: simplify uri handling implementation (2)
Refactored code around workspace and agent resolving.
1 parent 7f74048 commit bb4b643

File tree

3 files changed

+177
-120
lines changed

3 files changed

+177
-120
lines changed

src/main/kotlin/com/coder/toolbox/CoderToolboxContext.kt

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,4 +58,24 @@ data class CoderToolboxContext(
5858
i18n.ptrl("OK")
5959
)
6060
}
61+
62+
suspend fun logAndShowError(title: String, error: String, exception: Exception) {
63+
logger.error(exception, error)
64+
ui.showSnackbar(
65+
UUID.randomUUID().toString(),
66+
i18n.pnotr(title),
67+
i18n.pnotr(error),
68+
i18n.ptrl("OK")
69+
)
70+
}
71+
72+
suspend fun logAndShowWarning(title: String, warning: String) {
73+
logger.warn(warning)
74+
ui.showSnackbar(
75+
UUID.randomUUID().toString(),
76+
i18n.pnotr(title),
77+
i18n.pnotr(warning),
78+
i18n.ptrl("OK")
79+
)
80+
}
6181
}

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -192,12 +192,12 @@ open class CoderRestClient(
192192
}
193193

194194
/**
195-
* Maps the list of workspaces to the associated agents.
195+
* Maps the available workspaces to the associated agents.
196196
*/
197-
suspend fun groupByAgents(workspaces: List<Workspace>): Set<Pair<Workspace, WorkspaceAgent>> {
197+
suspend fun workspacesByAgents(): Set<Pair<Workspace, WorkspaceAgent>> {
198198
// It is possible for there to be resources with duplicate names so we
199199
// need to use a set.
200-
return workspaces.flatMap { ws ->
200+
return workspaces().flatMap { ws ->
201201
when (ws.latestBuild.status) {
202202
WorkspaceStatus.RUNNING -> ws.latestBuild.resources
203203
else -> resources(ws)

src/main/kotlin/com/coder/toolbox/util/CoderProtocolHandler.kt

Lines changed: 154 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -57,85 +57,13 @@ open class CoderProtocolHandler(
5757

5858
val deploymentURL = resolveDeploymentUrl(params) ?: return
5959
val token = resolveToken(params) ?: return
60-
// TODO: Show a dropdown and ask for the workspace if missing. Right now it's not possible because dialogs are quite limited
61-
val workspaceName = resolveWorkspace(params) ?: return
60+
val workspaceName = resolveWorkspaceName(params) ?: return
6261
val restClient = buildRestClient(deploymentURL, token) ?: return
62+
val workspace = restClient.workspaces().matchName(workspaceName, deploymentURL) ?: return
63+
val agent = resolveAgent(params, workspace) ?: return
6364

64-
val workspaces = restClient.workspaces()
65-
val workspace = workspaces.firstOrNull { it.name == workspaceName }
66-
if (workspace == null) {
67-
context.logger.error("There is no workspace with name $workspaceName on $deploymentURL")
68-
context.showErrorPopup(MissingArgumentException("Can't handle URI because workspace with name $workspaceName does not exist"))
69-
return
70-
}
71-
72-
when (workspace.latestBuild.status) {
73-
WorkspaceStatus.PENDING, WorkspaceStatus.STARTING ->
74-
if (restClient.waitForReady(workspace) != true) {
75-
context.logger.error("$workspaceName from $deploymentURL could not be ready on time")
76-
context.showErrorPopup(MissingArgumentException("Can't handle URI because workspace $workspaceName could not be ready on time"))
77-
return
78-
}
79-
80-
WorkspaceStatus.STOPPING, WorkspaceStatus.STOPPED,
81-
WorkspaceStatus.CANCELING, WorkspaceStatus.CANCELED -> {
82-
if (settings.disableAutostart) {
83-
context.logger.warn("$workspaceName from $deploymentURL is not started and autostart is disabled.")
84-
context.showInfoPopup(
85-
context.i18n.pnotr("$workspaceName is not running"),
86-
context.i18n.ptrl("Can't handle URI because workspace is not running and autostart is disabled. Please start the workspace manually and execute the URI again."),
87-
context.i18n.ptrl("OK")
88-
)
89-
return
90-
}
91-
92-
try {
93-
restClient.startWorkspace(workspace)
94-
} catch (e: Exception) {
95-
context.logger.error(
96-
e,
97-
"$workspaceName from $deploymentURL could not be started while handling URI"
98-
)
99-
context.showErrorPopup(MissingArgumentException("Can't handle URI because an error was encountered while trying to start workspace $workspaceName"))
100-
return
101-
}
102-
if (restClient.waitForReady(workspace) != true) {
103-
context.logger.error("$workspaceName from $deploymentURL could not be started on time")
104-
context.showErrorPopup(MissingArgumentException("Can't handle URI because workspace $workspaceName could not be started on time"))
105-
return
106-
}
107-
}
108-
109-
WorkspaceStatus.FAILED, WorkspaceStatus.DELETING, WorkspaceStatus.DELETED -> {
110-
context.logger.error("Unable to connect to $workspaceName from $deploymentURL")
111-
context.showErrorPopup(MissingArgumentException("Can't handle URI because because we're unable to connect to workspace $workspaceName"))
112-
return
113-
}
114-
115-
WorkspaceStatus.RUNNING -> Unit // All is well
116-
}
117-
118-
// TODO: Show a dropdown and ask for an agent if missing.
119-
val agent: WorkspaceAgent
120-
try {
121-
agent = getMatchingAgent(params, workspace)
122-
} catch (e: IllegalArgumentException) {
123-
context.logger.error(e, "Can't resolve an agent for workspace $workspaceName from $deploymentURL")
124-
context.showErrorPopup(
125-
MissingArgumentException(
126-
"Can't handle URI because we can't resolve an agent for workspace $workspaceName from $deploymentURL",
127-
e
128-
)
129-
)
130-
return
131-
}
132-
val status = WorkspaceAndAgentStatus.from(workspace, agent)
133-
134-
if (!status.ready()) {
135-
context.logger.error("Agent ${agent.name} for workspace $workspaceName from $deploymentURL is not ready")
136-
context.showErrorPopup(MissingArgumentException("Can't handle URI because agent ${agent.name} for workspace $workspaceName from $deploymentURL is not ready"))
137-
return
138-
}
65+
if (!prepareWorkspace(workspace, restClient, workspaceName, deploymentURL)) return
66+
if (!ensureAgentIsReady(workspace, agent)) return
13967

14068
val cli = ensureCLI(
14169
context,
@@ -150,7 +78,7 @@ open class CoderProtocolHandler(
15078
}
15179

15280
context.logger.info("Configuring Coder CLI...")
153-
cli.configSsh(restClient.groupByAgents(workspaces))
81+
cli.configSsh(restClient.workspacesByAgents())
15482

15583
if (shouldWaitForAutoLogin) {
15684
isInitialized.waitForTrue()
@@ -247,7 +175,7 @@ open class CoderProtocolHandler(
247175
return token
248176
}
249177

250-
private suspend fun resolveWorkspace(params: Map<String, String>): String? {
178+
private suspend fun resolveWorkspaceName(params: Map<String, String>): String? {
251179
val workspace = params.workspace()
252180
if (workspace.isNullOrBlank()) {
253181
context.logAndShowError(CAN_T_HANDLE_URI_TITLE, "Query parameter \"$WORKSPACE\" is missing from URI")
@@ -279,6 +207,153 @@ open class CoderProtocolHandler(
279207
return client
280208
}
281209

210+
private suspend fun List<Workspace>.matchName(workspaceName: String, deploymentURL: String): Workspace? {
211+
val workspace = this.firstOrNull { it.name == workspaceName }
212+
if (workspace == null) {
213+
context.logAndShowError(
214+
CAN_T_HANDLE_URI_TITLE,
215+
"There is no workspace with name $workspaceName on $deploymentURL"
216+
)
217+
return null
218+
}
219+
return workspace
220+
}
221+
222+
private suspend fun prepareWorkspace(
223+
workspace: Workspace,
224+
restClient: CoderRestClient,
225+
workspaceName: String,
226+
deploymentURL: String
227+
): Boolean {
228+
when (workspace.latestBuild.status) {
229+
WorkspaceStatus.PENDING, WorkspaceStatus.STARTING ->
230+
if (!restClient.waitForReady(workspace)) {
231+
context.logAndShowError(
232+
CAN_T_HANDLE_URI_TITLE,
233+
"$workspaceName from $deploymentURL could not be ready on time"
234+
)
235+
return false
236+
}
237+
238+
WorkspaceStatus.STOPPING, WorkspaceStatus.STOPPED,
239+
WorkspaceStatus.CANCELING, WorkspaceStatus.CANCELED -> {
240+
if (settings.disableAutostart) {
241+
context.logAndShowWarning(
242+
CAN_T_HANDLE_URI_TITLE,
243+
"$workspaceName from $deploymentURL is not running and autostart is disabled"
244+
)
245+
return false
246+
}
247+
248+
try {
249+
restClient.startWorkspace(workspace)
250+
} catch (e: Exception) {
251+
context.logAndShowError(
252+
CAN_T_HANDLE_URI_TITLE,
253+
"$workspaceName from $deploymentURL could not be started",
254+
e
255+
)
256+
return false
257+
}
258+
259+
if (!restClient.waitForReady(workspace)) {
260+
context.logAndShowError(
261+
CAN_T_HANDLE_URI_TITLE,
262+
"$workspaceName from $deploymentURL could not be started on time",
263+
)
264+
return false
265+
}
266+
}
267+
268+
WorkspaceStatus.FAILED, WorkspaceStatus.DELETING, WorkspaceStatus.DELETED -> {
269+
context.logAndShowError(
270+
CAN_T_HANDLE_URI_TITLE,
271+
"Unable to connect to $workspaceName from $deploymentURL"
272+
)
273+
return false
274+
}
275+
276+
WorkspaceStatus.RUNNING -> return true // All is well
277+
}
278+
return true
279+
}
280+
281+
private suspend fun resolveAgent(
282+
params: Map<String, String>,
283+
workspace: Workspace
284+
): WorkspaceAgent? {
285+
try {
286+
return getMatchingAgent(params, workspace)
287+
} catch (e: IllegalArgumentException) {
288+
context.logAndShowError(
289+
CAN_T_HANDLE_URI_TITLE,
290+
"Can't resolve an agent for workspace ${workspace.name}",
291+
e
292+
)
293+
return null
294+
}
295+
}
296+
297+
/**
298+
* Return the agent matching the provided agent ID or name in the parameters.
299+
*
300+
* @throws [IllegalArgumentException]
301+
*/
302+
private suspend fun getMatchingAgent(
303+
parameters: Map<String, String?>,
304+
workspace: Workspace,
305+
): WorkspaceAgent? {
306+
val agents = workspace.latestBuild.resources.filter { it.agents != null }.flatMap { it.agents!! }
307+
if (agents.isEmpty()) {
308+
context.logAndShowError(CAN_T_HANDLE_URI_TITLE, "The workspace \"${workspace.name}\" has no agents")
309+
return null
310+
}
311+
312+
// If the agent is missing and the workspace has only one, use that.
313+
// Prefer the ID over the name if both are set.
314+
val agent =
315+
if (!parameters.agentID().isNullOrBlank()) {
316+
agents.firstOrNull { it.id.toString() == parameters.agentID() }
317+
} else if (agents.size == 1) {
318+
agents.first()
319+
} else {
320+
null
321+
}
322+
323+
if (agent == null) {
324+
if (!parameters.agentID().isNullOrBlank()) {
325+
context.logAndShowError(
326+
CAN_T_HANDLE_URI_TITLE,
327+
"The workspace \"${workspace.name}\" does not have an agent with ID \"${parameters.agentID()}\""
328+
)
329+
return null
330+
} else {
331+
context.logAndShowError(
332+
CAN_T_HANDLE_URI_TITLE,
333+
"Unable to determine which agent to connect to; \"$AGENT_ID\" must be set because the workspace \"${workspace.name}\" has more than one agent"
334+
)
335+
return null
336+
}
337+
}
338+
return agent
339+
}
340+
341+
private suspend fun ensureAgentIsReady(
342+
workspace: Workspace,
343+
agent: WorkspaceAgent
344+
): Boolean {
345+
val status = WorkspaceAndAgentStatus.from(workspace, agent)
346+
347+
if (!status.ready()) {
348+
context.logAndShowError(
349+
CAN_T_HANDLE_URI_TITLE,
350+
"Agent ${agent.name} for workspace ${workspace.name} is not ready"
351+
)
352+
return false
353+
}
354+
return true
355+
}
356+
282357
private suspend fun CoderRestClient.waitForReady(workspace: Workspace): Boolean {
283358
var status = workspace.latestBuild.status
284359
try {
@@ -346,44 +421,6 @@ internal fun resolveRedirects(url: URL): URL {
346421
throw Exception("Too many redirects")
347422
}
348423

349-
/**
350-
* Return the agent matching the provided agent ID or name in the parameters.
351-
*
352-
* @throws [IllegalArgumentException]
353-
*/
354-
internal fun getMatchingAgent(
355-
parameters: Map<String, String?>,
356-
workspace: Workspace,
357-
): WorkspaceAgent {
358-
val agents = workspace.latestBuild.resources.filter { it.agents != null }.flatMap { it.agents!! }
359-
if (agents.isEmpty()) {
360-
throw IllegalArgumentException("The workspace \"${workspace.name}\" has no agents")
361-
}
362-
363-
// If the agent is missing and the workspace has only one, use that.
364-
// Prefer the ID over the name if both are set.
365-
val agent =
366-
if (!parameters.agentID().isNullOrBlank()) {
367-
agents.firstOrNull { it.id.toString() == parameters.agentID() }
368-
} else if (agents.size == 1) {
369-
agents.first()
370-
} else {
371-
null
372-
}
373-
374-
if (agent == null) {
375-
if (!parameters.agentID().isNullOrBlank()) {
376-
throw IllegalArgumentException("The workspace \"${workspace.name}\" does not have an agent with ID \"${parameters.agentID()}\"")
377-
} else {
378-
throw MissingArgumentException(
379-
"Unable to determine which agent to connect to; \"$AGENT_ID\" must be set because the workspace \"${workspace.name}\" has more than one agent",
380-
)
381-
}
382-
}
383-
384-
return agent
385-
}
386-
387424
private suspend fun CoderToolboxContext.showErrorPopup(error: Throwable) {
388425
popupPluginMainPage()
389426
this.ui.showErrorInfoPopup(error)

0 commit comments

Comments
 (0)