22
33package com.coder.gateway
44
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
315import 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
378import com.intellij.openapi.components.service
389import 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
4210import com.jetbrains.gateway.api.ConnectionRequestor
4311import com.jetbrains.gateway.api.GatewayConnectionHandle
4412import 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- }
9313
9414// CoderGatewayConnectionProvider handles connecting via a Gateway link such as
9515// jetbrains-gateway://connect#type=coder.
@@ -101,204 +21,14 @@ class CoderGatewayConnectionProvider : GatewayConnectionProvider {
10121requestor : ConnectionRequestor ,
10222 ):GatewayConnectionHandle ? {
10323CoderRemoteConnectionHandle ().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
20727 }
20828 }
20929return null
21030 }
21131
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-
30232override fun isApplicable (parameters : Map <String ,String >):Boolean {
30333return parameters.isCoder()
30434 }
@@ -307,51 +37,3 @@ class CoderGatewayConnectionProvider : GatewayConnectionProvider {
30737val logger= Logger .getInstance(CoderGatewayConnectionProvider ::class .java.simpleName)
30838 }
30939}
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)