Skip to content

Commit 56909a3

Browse files
committed
Refactor connect flow somewhat
Mostly trying to reduce the direct uses of the local model in favor of passing in arguments so they will be easier to test (specifically askToken is pure now). Mention use of the local model in the functions where that is the case and where they use the model I use it for all things (rather than just the URL but pass in the token for example) since everything we need is already in the model. Also rename/suffix some functions to/with connect to match the button. Remove the progress updates to the indicator since they are indeterminate.
1 parent f311f16 commit 56909a3

File tree

3 files changed

+127
-93
lines changed

3 files changed

+127
-93
lines changed

src/main/kotlin/com/coder/gateway/CoderGatewayConnectionProvider.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ class CoderGatewayConnectionProvider : GatewayConnectionProvider {
2121

2222
override suspend fun connect(parameters: Map<String, String>, requestor: ConnectionRequestor): GatewayConnectionHandle? {
2323
val clientLifetime = LifetimeDefinition()
24+
// TODO: If this fails determine if it is an auth error and if so prompt
25+
// for a new token, configure the CLI, then try again.
2426
clientLifetime.launchUnderBackgroundProgress(CoderGatewayBundle.message("gateway.connector.coder.connection.provider.title"), canBeCancelled = true, isIndeterminate = true, project = null) {
2527
val context = SshMultistagePanelContext(parameters.toHostDeployInputs())
2628
logger.info("Deploying and starting IDE with $context")
@@ -43,4 +45,4 @@ class CoderGatewayConnectionProvider : GatewayConnectionProvider {
4345
companion object {
4446
val logger = Logger.getInstance(CoderGatewayConnectionProvider::class.java.simpleName)
4547
}
46-
}
48+
}

src/main/kotlin/com/coder/gateway/sdk/CoderCLIManager.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import javax.xml.bind.annotation.adapters.HexBinaryAdapter
2323
/**
2424
* Manage the CLI for a single deployment.
2525
*/
26-
class CoderCLIManager @JvmOverloads constructor(deployment: URL, destinationDir: Path = getDataDir()) {
26+
class CoderCLIManager @JvmOverloads constructor(private val deployment: URL, destinationDir: Path = getDataDir()) {
2727
private var remoteBinaryUrl: URL
2828
var localBinaryPath: Path
2929

@@ -143,10 +143,10 @@ class CoderCLIManager @JvmOverloads constructor(deployment: URL, destinationDir:
143143
}
144144

145145
/**
146-
* Use the provided credentials to authenticate the CLI.
146+
* Use the provided token to authenticate the CLI.
147147
*/
148-
fun login(url: String, token: String): String {
149-
return exec("login", url, "--token", token)
148+
fun login(token: String): String {
149+
return exec("login", deployment.toString(), "--token", token)
150150
}
151151

152152
/**

src/main/kotlin/com/coder/gateway/views/steps/CoderWorkspacesStepView.kt

Lines changed: 120 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,15 @@ import com.intellij.ui.RelativeFont
4444
import com.intellij.ui.ToolbarDecorator
4545
import com.intellij.ui.components.JBTextField
4646
import com.intellij.ui.components.dialog
47-
import com.intellij.ui.dsl.builder.*
47+
import com.intellij.ui.dsl.builder.AlignX
48+
import com.intellij.ui.dsl.builder.AlignY
49+
import com.intellij.ui.dsl.builder.BottomGap
50+
import com.intellij.ui.dsl.builder.RightGap
51+
import com.intellij.ui.dsl.builder.RowLayout
52+
import com.intellij.ui.dsl.builder.TopGap
53+
import com.intellij.ui.dsl.builder.bindSelected
54+
import com.intellij.ui.dsl.builder.bindText
55+
import com.intellij.ui.dsl.builder.panel
4856
import com.intellij.ui.table.TableView
4957
import com.intellij.util.ui.ColumnInfo
5058
import com.intellij.util.ui.JBFont
@@ -68,6 +76,7 @@ import java.awt.event.MouseMotionListener
6876
import java.awt.font.TextAttribute
6977
import java.awt.font.TextAttribute.UNDERLINE_ON
7078
import java.net.SocketTimeoutException
79+
import java.net.URL
7180
import javax.swing.Icon
7281
import javax.swing.JCheckBox
7382
import javax.swing.JTable
@@ -214,15 +223,13 @@ class CoderWorkspacesStepView(val setNextButtonEnabled: (Boolean) -> Unit) : Cod
214223
tfUrl = textField().resizableColumn().align(AlignX.FILL).gap(RightGap.SMALL)
215224
.bindText(localWizardModel::coderURL).applyToComponent {
216225
addActionListener {
217-
poller?.cancel()
218-
listTableModelOfWorkspaces.items = emptyList()
219-
askTokenAndOpenSession(true)
226+
// Reconnect when the enter key is pressed.
227+
askTokenAndConnect()
220228
}
221229
}.component
222230
button(CoderGatewayBundle.message("gateway.connector.view.coder.workspaces.connect.text")) {
223-
poller?.cancel()
224-
listTableModelOfWorkspaces.items = emptyList()
225-
askTokenAndOpenSession(true)
231+
// Reconnect when the connect button is pressed.
232+
askTokenAndConnect()
226233
}.applyToComponent {
227234
background = WelcomeScreenUIManager.getMainAssociatedComponentBackground()
228235
}
@@ -347,7 +354,7 @@ class CoderWorkspacesStepView(val setNextButtonEnabled: (Boolean) -> Unit) : Cod
347354
localWizardModel.token = token
348355
}
349356
if (!url.isNullOrBlank() && !token.isNullOrBlank()) {
350-
loginAndLoadWorkspaces(token, true)
357+
connect()
351358
}
352359
}
353360
updateWorkspaceActions()
@@ -397,105 +404,126 @@ class CoderWorkspacesStepView(val setNextButtonEnabled: (Boolean) -> Unit) : Cod
397404
ActivityTracker.getInstance().inc()
398405
}
399406

400-
private fun askTokenAndOpenSession(openBrowser: Boolean) {
401-
// force bindings to be filled
402-
component.apply()
403-
404-
val pastedToken = askToken(openBrowser)
407+
/**
408+
* Ask for a new token (regardless of whether we already have a token),
409+
* place it in the local model, then connect.
410+
*/
411+
private fun askTokenAndConnect(openBrowser: Boolean = true) {
412+
component.apply() // Force bindings to be filled.
413+
val pastedToken = askToken(
414+
localWizardModel.coderURL.toURL(),
415+
localWizardModel.token,
416+
openBrowser,
417+
localWizardModel.useExistingToken,
418+
)
405419
if (pastedToken.isNullOrBlank()) {
406-
return
420+
return // User aborted.
407421
}
408-
// False so that subsequent authentication failures do not keep opening
409-
// the browser as it was already opened earlier.
410-
loginAndLoadWorkspaces(pastedToken, false)
422+
localWizardModel.token = pastedToken
423+
// If the token ends up being invalid we will ask for it again; pass
424+
// false so we do not keep endlessly opening the browser.
425+
connect(false)
411426
}
412427

413-
private fun loginAndLoadWorkspaces(token: String, openBrowser: Boolean) {
414-
LifetimeDefinition().launchUnderBackgroundProgress(CoderGatewayBundle.message("gateway.connector.view.coder.workspaces.cli.downloader.dialog.title"), canBeCancelled = false, isIndeterminate = true) {
415-
this.indicator.apply {
416-
text = "Authenticating..."
417-
}
428+
/**
429+
* Connect to the deployment in the local model and if successful store the
430+
* URL and token for use as the default in subsequent launches then load
431+
* workspaces into the table and keep it updated with a poll.
432+
*
433+
* Existing workspaces will be immediately cleared before attempting to
434+
* connect to the new deployment.
435+
*
436+
* If the token is invalid abort and start over from askTokenAndConnect().
437+
*/
438+
private fun connect(openBrowser: Boolean = true) {
439+
// Clear out old deployment details.
440+
poller?.cancel()
441+
listTableModelOfWorkspaces.items = emptyList()
418442

443+
// Authenticate and load in a background process with progress.
444+
// TODO: Make this cancelable.
445+
LifetimeDefinition().launchUnderBackgroundProgress(
446+
CoderGatewayBundle.message("gateway.connector.view.coder.workspaces.cli.downloader.dialog.title"),
447+
canBeCancelled = false,
448+
isIndeterminate = true
449+
) {
419450
try {
420-
authenticate(token)
421-
} catch (e: AuthenticationResponseException) {
422-
logger.error("Unable to authenticate to ${localWizardModel.coderURL}; has your token expired?", e)
423-
askTokenAndOpenSession(openBrowser)
424-
return@launchUnderBackgroundProgress
425-
} catch (e: SocketTimeoutException) {
426-
logger.error("Unable to connect to ${localWizardModel.coderURL}; is it up?", e)
427-
return@launchUnderBackgroundProgress
428-
}
451+
this.indicator.text = "Authenticating client..."
452+
authenticate(localWizardModel.coderURL.toURL(), localWizardModel.token)
453+
// Remember these in order to default to them for future attempts.
454+
appPropertiesService.setValue(CODER_URL_KEY, localWizardModel.coderURL)
455+
appPropertiesService.setValue(SESSION_TOKEN, localWizardModel.token)
429456

430-
val cliManager = CoderCLIManager(localWizardModel.coderURL.toURL())
431-
localWizardModel.token = token
457+
this.indicator.text = "Retrieving workspaces..."
458+
loadWorkspaces()
432459

433-
this.indicator.apply {
434-
isIndeterminate = false
435-
text = "Retrieving Workspaces..."
436-
fraction = 0.1
437-
}
460+
this.indicator.text = "Downloading Coder CLI..."
461+
val cliManager = CoderCLIManager(localWizardModel.coderURL.toURL())
462+
cliManager.downloadCLI()
438463

439-
loadWorkspaces()
464+
this.indicator.text = "Authenticating Coder CLI..."
465+
cliManager.login(localWizardModel.token)
440466

441-
this.indicator.apply {
442-
isIndeterminate = false
443-
text = "Downloading Coder CLI..."
444-
fraction = 0.3
445-
}
446-
try {
447-
cliManager.downloadCLI()
467+
this.indicator.text = "Configuring SSH..."
468+
cliManager.configSsh()
469+
470+
updateWorkspaceActions()
471+
triggerWorkspacePolling(false)
472+
} catch (e: AuthenticationResponseException) {
473+
logger.error("Token was rejected by ${localWizardModel.coderURL}; has your token expired?", e)
474+
askTokenAndConnect(openBrowser) // Try again but no more opening browser windows.
475+
} catch (e: SocketTimeoutException) {
476+
logger.error("Unable to connect to ${localWizardModel.coderURL}; is it up?", e)
448477
} catch (e: ResponseException) {
449-
logger.error("Download failed with response code ${e.code}", e)
450-
return@launchUnderBackgroundProgress
451-
} catch (e: Exception) {
452478
logger.error("Failed to download Coder CLI", e)
453-
return@launchUnderBackgroundProgress
454-
}
455-
this.indicator.apply {
456-
text = "Logging in..."
457-
fraction = 0.5
458-
}
459-
cliManager.login(localWizardModel.coderURL, localWizardModel.token)
460-
461-
this.indicator.apply {
462-
text = "Configuring SSH..."
463-
fraction = 0.7
479+
} catch (e: Exception) {
480+
logger.error("Failed to configure connection to ${localWizardModel.coderURL}", e)
464481
}
465-
cliManager.configSsh()
466-
467-
this.indicator.fraction = 1.0
468-
updateWorkspaceActions()
469-
triggerWorkspacePolling(false)
470482
}
471483
}
472484

473-
private fun askToken(openBrowser: Boolean): String? {
474-
val getTokenUrl = localWizardModel.coderURL.toURL().withPath("/login?redirect=%2Fcli-auth")
475-
if (openBrowser && !localWizardModel.useExistingToken) {
485+
/**
486+
* Open a dialog for providing the token. Show the existing token so the
487+
* user can validate it if a previous connection failed. Open a browser to
488+
* the auth page if openBrowser is true and useExisting is false. If
489+
* useExisting is true then populate the dialog with the token on disk if
490+
* there is one and it matches the url (this will overwrite the provided
491+
* token). Return the token submitted by the user.
492+
*/
493+
private fun askToken(url: URL, token: String, openBrowser: Boolean, useExisting: Boolean): String? {
494+
var existingToken = token
495+
val getTokenUrl = url.withPath("/login?redirect=%2Fcli-auth")
496+
if (openBrowser && !useExisting) {
476497
BrowserUtil.browse(getTokenUrl)
477-
} else if (localWizardModel.useExistingToken) {
478-
val (url, token) = CoderCLIManager.readConfig()
479-
if (url == localWizardModel.coderURL && !token.isNullOrBlank()) {
498+
} else if (useExisting) {
499+
val (u, t) = CoderCLIManager.readConfig()
500+
if (url == u?.toURL() && !t.isNullOrBlank()) {
480501
logger.info("Injecting valid token from CLI config")
481-
localWizardModel.token = token
502+
existingToken = t
482503
}
483504
}
484505
var tokenFromUser: String? = null
485506
ApplicationManager.getApplication().invokeAndWait({
486507
lateinit var sessionTokenTextField: JBTextField
487-
488508
val panel = panel {
489509
row {
490-
browserLink(CoderGatewayBundle.message("gateway.connector.view.login.token.label"), getTokenUrl.toString())
491-
sessionTokenTextField = textField().bindText(localWizardModel::token).applyToComponent {
510+
browserLink(
511+
CoderGatewayBundle.message("gateway.connector.view.login.token.label"),
512+
getTokenUrl.toString()
513+
)
514+
sessionTokenTextField = textField().applyToComponent {
515+
text = existingToken
492516
minimumSize = Dimension(320, -1)
493517
}.component
494518
}
495519
}
496-
497520
AppIcon.getInstance().requestAttention(null, true)
498-
if (!dialog(CoderGatewayBundle.message("gateway.connector.view.login.token.dialog"), panel = panel, focusedComponent = sessionTokenTextField).showAndGet()) {
521+
if (!dialog(
522+
CoderGatewayBundle.message("gateway.connector.view.login.token.dialog"),
523+
panel = panel,
524+
focusedComponent = sessionTokenTextField
525+
).showAndGet()
526+
) {
499527
return@invokeAndWait
500528
}
501529
tokenFromUser = sessionTokenTextField.text
@@ -518,13 +546,13 @@ class CoderWorkspacesStepView(val setNextButtonEnabled: (Boolean) -> Unit) : Cod
518546
}
519547

520548
/**
521-
* Check that the token is valid for the URL in the wizard and throw if not.
522-
* On success store the URL and token and display warning banners if
523-
* versions do not match.
549+
* Authenticate the Coder client with the provided token and URL. On
550+
* failure throw an error. On success display warning banners if versions
551+
* do not match.
524552
*/
525-
private fun authenticate(token: String) {
526-
logger.info("Authenticating to ${localWizardModel.coderURL}...")
527-
coderClient.initClientSession(localWizardModel.coderURL.toURL(), token)
553+
private fun authenticate(url: URL, token: String) {
554+
logger.info("Authenticating to $url...")
555+
coderClient.initClientSession(url, token)
528556

529557
try {
530558
logger.info("Checking compatibility with Coder version ${coderClient.buildVersion}...")
@@ -534,7 +562,12 @@ class CoderWorkspacesStepView(val setNextButtonEnabled: (Boolean) -> Unit) : Cod
534562
logger.warn(e)
535563
notificationBanner.apply {
536564
component.isVisible = true
537-
showWarning(CoderGatewayBundle.message("gateway.connector.view.coder.workspaces.invalid.coder.version", coderClient.buildVersion))
565+
showWarning(
566+
CoderGatewayBundle.message(
567+
"gateway.connector.view.coder.workspaces.invalid.coder.version",
568+
coderClient.buildVersion
569+
)
570+
)
538571
}
539572
} catch (e: IncompatibleVersionException) {
540573
logger.warn(e)
@@ -545,12 +578,11 @@ class CoderWorkspacesStepView(val setNextButtonEnabled: (Boolean) -> Unit) : Cod
545578
}
546579

547580
logger.info("Authenticated successfully")
548-
549-
// Remember these in order to default to them for future attempts.
550-
appPropertiesService.setValue(CODER_URL_KEY, localWizardModel.coderURL)
551-
appPropertiesService.setValue(SESSION_TOKEN, token)
552581
}
553582

583+
/**
584+
* Request workspaces then update the table.
585+
*/
554586
private suspend fun loadWorkspaces() {
555587
val ws = withContext(Dispatchers.IO) {
556588
val timeBeforeRequestingWorkspaces = System.currentTimeMillis()

0 commit comments

Comments
 (0)