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 {
10121 requestor : ConnectionRequestor ,
10222 ): GatewayConnectionHandle ? {
10323 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
20727 }
20828 }
20929 return 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-
30232 override fun isApplicable (parameters : Map <String , String >): Boolean {
30333 return parameters.isCoder()
30434 }
@@ -307,51 +37,3 @@ class CoderGatewayConnectionProvider : GatewayConnectionProvider {
30737 val 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)
0 commit comments