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)