2
2
3
3
package com.coder.gateway
4
4
5
- import com.coder.gateway.cli.CoderCLIManager
6
- import com.coder.gateway.cli.ensureCLI
7
- import com.coder.gateway.models.AGENT_ID
8
- import com.coder.gateway.models.AGENT_NAME
9
- import com.coder.gateway.models.TOKEN
10
- import com.coder.gateway.models.URL
11
- import com.coder.gateway.models.WORKSPACE
12
- import com.coder.gateway.models.WorkspaceAndAgentStatus
13
- import com.coder.gateway.models.WorkspaceProjectIDE
14
- import com.coder.gateway.models.agentID
15
- import com.coder.gateway.models.agentName
16
- import com.coder.gateway.models.folder
17
- import com.coder.gateway.models.ideBuildNumber
18
- import com.coder.gateway.models.ideDownloadLink
19
- import com.coder.gateway.models.idePathOnHost
20
- import com.coder.gateway.models.ideProductCode
21
- import com.coder.gateway.models.isCoder
22
- import com.coder.gateway.models.token
23
- import com.coder.gateway.models.url
24
- import com.coder.gateway.models.workspace
25
- import com.coder.gateway.sdk.CoderRestClient
26
- import com.coder.gateway.sdk.ex.APIResponseException
27
- import com.coder.gateway.sdk.v2.models.Workspace
28
- import com.coder.gateway.sdk.v2.models.WorkspaceAgent
29
- import com.coder.gateway.sdk.v2.models.WorkspaceStatus
30
- import com.coder.gateway.services.CoderRestClientService
31
5
import com.coder.gateway.services.CoderSettingsService
32
- import com.coder.gateway.settings.Source
33
- import com.coder.gateway.util.toURL
34
- import com.coder.gateway.views.steps.CoderWorkspaceProjectIDEStepView
35
- import com.coder.gateway.views.steps.CoderWorkspacesStepSelection
36
- import com.intellij.openapi.application.ApplicationManager
6
+ import com.coder.gateway.util.handleLink
7
+ import com.coder.gateway.util.isCoder
37
8
import com.intellij.openapi.components.service
38
9
import com.intellij.openapi.diagnostic.Logger
39
- import com.intellij.openapi.ui.DialogWrapper
40
- import com.intellij.ui.dsl.builder.panel
41
- import com.intellij.util.ui.JBUI
42
10
import com.jetbrains.gateway.api.ConnectionRequestor
43
11
import com.jetbrains.gateway.api.GatewayConnectionHandle
44
12
import com.jetbrains.gateway.api.GatewayConnectionProvider
45
- import javax.swing.JComponent
46
- import javax.swing.border.Border
47
-
48
- /* *
49
- * A dialog wrapper around CoderWorkspaceStepView.
50
- */
51
- class CoderWorkspaceStepDialog (
52
- name : String ,
53
- private val state : CoderWorkspacesStepSelection ,
54
- ) : DialogWrapper(true ) {
55
- private val view = CoderWorkspaceProjectIDEStepView (showTitle = false )
56
-
57
- init {
58
- init ()
59
- title = CoderGatewayBundle .message(" gateway.connector.view.coder.remoteproject.choose.text" , name)
60
- }
61
-
62
- override fun show () {
63
- view.init (state)
64
- view.onPrevious = { close(1 ) }
65
- view.onNext = { close(0 ) }
66
- super .show()
67
- view.dispose()
68
- }
69
-
70
- fun showAndGetData (): WorkspaceProjectIDE ? {
71
- if (showAndGet()) {
72
- return view.data()
73
- }
74
- return null
75
- }
76
-
77
- override fun createContentPaneBorder (): Border {
78
- return JBUI .Borders .empty()
79
- }
80
-
81
- override fun createCenterPanel (): JComponent {
82
- return view
83
- }
84
-
85
- override fun createSouthPanel (): JComponent {
86
- // The plugin provides its own buttons.
87
- // TODO: Is it more idiomatic to handle buttons out here?
88
- return panel {}.apply {
89
- border = JBUI .Borders .empty()
90
- }
91
- }
92
- }
93
13
94
14
// CoderGatewayConnectionProvider handles connecting via a Gateway link such as
95
15
// jetbrains-gateway://connect#type=coder.
@@ -101,204 +21,14 @@ class CoderGatewayConnectionProvider : GatewayConnectionProvider {
101
21
requestor : ConnectionRequestor ,
102
22
): GatewayConnectionHandle ? {
103
23
CoderRemoteConnectionHandle ().connect { indicator ->
104
- logger.debug(" Launched Coder connection provider" , parameters)
105
-
106
- val deploymentURL =
107
- parameters.url()
108
- ? : CoderRemoteConnectionHandle .ask(" Enter the full URL of your Coder deployment" )
109
- if (deploymentURL.isNullOrBlank()) {
110
- throw IllegalArgumentException (" Query parameter \" $URL \" is missing" )
111
- }
112
-
113
- val client = authenticate(deploymentURL, parameters.token())
114
-
115
- // TODO: If the workspace is missing we could launch the wizard.
116
- val workspaceName = parameters.workspace() ? : throw IllegalArgumentException (" Query parameter \" $WORKSPACE \" is missing" )
117
-
118
- val workspaces = client.workspaces()
119
- val workspace =
120
- workspaces.firstOrNull {
121
- it.name == workspaceName
122
- } ? : throw IllegalArgumentException (" The workspace $workspaceName does not exist" )
123
-
124
- when (workspace.latestBuild.status) {
125
- WorkspaceStatus .PENDING , WorkspaceStatus .STARTING ->
126
- // TODO: Wait for the workspace to turn on.
127
- throw IllegalArgumentException (
128
- " The workspace \" $workspaceName \" is ${workspace.latestBuild.status.toString().lowercase()} ; please wait then try again" ,
129
- )
130
- WorkspaceStatus .STOPPING , WorkspaceStatus .STOPPED ,
131
- WorkspaceStatus .CANCELING , WorkspaceStatus .CANCELED ,
132
- ->
133
- // TODO: Turn on the workspace.
134
- throw IllegalArgumentException (
135
- " The workspace \" $workspaceName \" is ${workspace.latestBuild.status.toString().lowercase()} ; please start the workspace and try again" ,
136
- )
137
- WorkspaceStatus .FAILED , WorkspaceStatus .DELETING , WorkspaceStatus .DELETED ->
138
- throw IllegalArgumentException (
139
- " The workspace \" $workspaceName \" is ${workspace.latestBuild.status.toString().lowercase()} ; unable to connect" ,
140
- )
141
- WorkspaceStatus .RUNNING -> Unit // All is well
142
- }
143
-
144
- // TODO: Show a dropdown and ask for an agent if missing.
145
- val agent = getMatchingAgent(parameters, workspace)
146
- val status = WorkspaceAndAgentStatus .from(workspace, agent)
147
-
148
- if (status.pending()) {
149
- // TODO: Wait for the agent to be ready.
150
- throw IllegalArgumentException (
151
- " The agent \" ${agent.name} \" is ${status.toString().lowercase()} ; please wait then try again" ,
152
- )
153
- } else if (! status.ready()) {
154
- throw IllegalArgumentException (" The agent \" ${agent.name} \" is ${status.toString().lowercase()} ; unable to connect" )
155
- }
156
-
157
- val cli =
158
- ensureCLI(
159
- deploymentURL.toURL(),
160
- client.buildInfo().version,
161
- settings,
162
- indicator,
163
- )
164
-
165
- // We only need to log in if we are using token-based auth.
166
- if (client.token != = null ) {
167
- indicator.text = " Authenticating Coder CLI..."
168
- cli.login(client.token)
169
- }
170
-
171
- indicator.text = " Configuring Coder CLI..."
172
- cli.configSsh(client.agentNames(workspaces))
173
-
174
- val name = " ${workspace.name} .${agent.name} "
175
- val openDialog =
176
- parameters.ideProductCode().isNullOrBlank() ||
177
- parameters.ideBuildNumber().isNullOrBlank() ||
178
- (parameters.idePathOnHost().isNullOrBlank() && parameters.ideDownloadLink().isNullOrBlank()) ||
179
- parameters.folder().isNullOrBlank()
180
-
181
- if (openDialog) {
182
- var data: WorkspaceProjectIDE ? = null
183
- ApplicationManager .getApplication().invokeAndWait {
184
- val dialog =
185
- CoderWorkspaceStepDialog (
186
- name,
187
- CoderWorkspacesStepSelection (agent, workspace, cli, client, workspaces),
188
- )
189
- data = dialog.showAndGetData()
190
- }
191
- data ? : throw Exception (" IDE selection aborted; unable to connect" )
192
- } else {
193
- // Check that both the domain and the redirected domain are
194
- // allowlisted. If not, check with the user whether to proceed.
195
- verifyDownloadLink(parameters)
196
- WorkspaceProjectIDE .fromInputs(
197
- name = name,
198
- hostname = CoderCLIManager .getHostName(deploymentURL.toURL(), name),
199
- projectPath = parameters.folder(),
200
- ideProductCode = parameters.ideProductCode(),
201
- ideBuildNumber = parameters.ideBuildNumber(),
202
- idePathOnHost = parameters.idePathOnHost(),
203
- downloadSource = parameters.ideDownloadLink(),
204
- deploymentURL = deploymentURL,
205
- lastOpened = null , // Have not opened yet.
206
- )
24
+ logger.debug(" Launched Coder link handler" , parameters)
25
+ handleLink(parameters, settings) {
26
+ indicator.text = it
207
27
}
208
28
}
209
29
return null
210
30
}
211
31
212
- /* *
213
- * Return an authenticated Coder CLI, asking for the token as long as it
214
- * continues to result in an authentication failure and token authentication
215
- * is required.
216
- */
217
- private fun authenticate (
218
- deploymentURL : String ,
219
- queryToken : String? ,
220
- lastToken : Pair <String , Source >? = null,
221
- ): CoderRestClient {
222
- val token =
223
- if (settings.requireTokenAuth) {
224
- // Use the token from the query, unless we already tried that.
225
- val isRetry = lastToken != null
226
- if (! queryToken.isNullOrBlank() && ! isRetry) {
227
- Pair (queryToken, Source .QUERY )
228
- } else {
229
- CoderRemoteConnectionHandle .askToken(
230
- deploymentURL.toURL(),
231
- lastToken,
232
- isRetry,
233
- useExisting = true ,
234
- settings,
235
- )
236
- }
237
- } else {
238
- null
239
- }
240
- if (settings.requireTokenAuth && token == null ) { // User aborted.
241
- throw IllegalArgumentException (" Unable to connect to $deploymentURL , query parameter \" $TOKEN \" is missing" )
242
- }
243
- val client = CoderRestClientService (deploymentURL.toURL(), token?.first)
244
- return try {
245
- client.authenticate()
246
- client
247
- } catch (ex: APIResponseException ) {
248
- // If doing token auth we can ask and try again.
249
- if (settings.requireTokenAuth && ex.isUnauthorized) {
250
- authenticate(deploymentURL, queryToken, token)
251
- } else {
252
- throw ex
253
- }
254
- }
255
- }
256
-
257
- /* *
258
- * Check that the link is allowlisted. If not, confirm with the user.
259
- */
260
- private fun verifyDownloadLink (parameters : Map <String , String >) {
261
- val link = parameters.ideDownloadLink()
262
- if (link.isNullOrBlank()) {
263
- return // Nothing to verify
264
- }
265
-
266
- val url =
267
- try {
268
- link.toURL()
269
- } catch (ex: Exception ) {
270
- throw IllegalArgumentException (" $link is not a valid URL" )
271
- }
272
-
273
- val (allowlisted, https, linkWithRedirect) =
274
- try {
275
- CoderRemoteConnectionHandle .isAllowlisted(url)
276
- } catch (e: Exception ) {
277
- throw IllegalArgumentException (" Unable to verify $url : $e " )
278
- }
279
- if (allowlisted && https) {
280
- return
281
- }
282
-
283
- val comment =
284
- if (allowlisted) {
285
- " The download link is from a non-allowlisted URL"
286
- } else if (https) {
287
- " The download link is not using HTTPS"
288
- } else {
289
- " The download link is from a non-allowlisted URL and is not using HTTPS"
290
- }
291
-
292
- if (! CoderRemoteConnectionHandle .confirm(
293
- " Confirm download URL" ,
294
- " $comment . Would you like to proceed?" ,
295
- linkWithRedirect,
296
- )
297
- ) {
298
- throw IllegalArgumentException (" $linkWithRedirect is not allowlisted" )
299
- }
300
- }
301
-
302
32
override fun isApplicable (parameters : Map <String , String >): Boolean {
303
33
return parameters.isCoder()
304
34
}
@@ -307,51 +37,3 @@ class CoderGatewayConnectionProvider : GatewayConnectionProvider {
307
37
val logger = Logger .getInstance(CoderGatewayConnectionProvider ::class .java.simpleName)
308
38
}
309
39
}
310
-
311
- /* *
312
- * Return the agent matching the provided agent ID or name in the parameters.
313
- * The name is ignored if the ID is set. If neither was supplied and the
314
- * workspace has only one agent, return that. Otherwise throw an error.
315
- *
316
- * @throws [MissingArgumentException, IllegalArgumentException]
317
- */
318
- fun getMatchingAgent (
319
- parameters : Map <String , String ?>,
320
- workspace : Workspace ,
321
- ): WorkspaceAgent {
322
- val agents = workspace.latestBuild.resources.filter { it.agents != null }.flatMap { it.agents!! }
323
- if (agents.isEmpty()) {
324
- throw IllegalArgumentException (" The workspace \" ${workspace.name} \" has no agents" )
325
- }
326
-
327
- // If the agent is missing and the workspace has only one, use that.
328
- // Prefer the ID over the name if both are set.
329
- val agent =
330
- if (! parameters.agentID().isNullOrBlank()) {
331
- agents.firstOrNull { it.id.toString() == parameters.agentID() }
332
- } else if (! parameters.agentName().isNullOrBlank()) {
333
- agents.firstOrNull { it.name == parameters.agentName() }
334
- } else if (agents.size == 1 ) {
335
- agents.first()
336
- } else {
337
- null
338
- }
339
-
340
- if (agent == null ) {
341
- if (! parameters.agentID().isNullOrBlank()) {
342
- throw IllegalArgumentException (" The workspace \" ${workspace.name} \" does not have an agent with ID \" ${parameters.agentID()} \" " )
343
- } else if (! parameters.agentName().isNullOrBlank()) {
344
- throw IllegalArgumentException (
345
- " The workspace \" ${workspace.name} \" does not have an agent named \" ${parameters.agentName()} \" " ,
346
- )
347
- } else {
348
- throw MissingArgumentException (
349
- " Unable to determine which agent to connect to; one of \" $AGENT_NAME \" or \" $AGENT_ID \" must be set because the workspace \" ${workspace.name} \" has more than one agent" ,
350
- )
351
- }
352
- }
353
-
354
- return agent
355
- }
356
-
357
- class MissingArgumentException (message : String ) : IllegalArgumentException(message)
0 commit comments