diff --git a/.idea/misc.xml b/.idea/misc.xml index b4a9ecaf473601..49dcfc6ccde83d 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -4,6 +4,8 @@ + + diff --git a/components/ide/jetbrains/toolbox/build.gradle.kts b/components/ide/jetbrains/toolbox/build.gradle.kts index 5fed0431406d43..5d0329d66c87b0 100644 --- a/components/ide/jetbrains/toolbox/build.gradle.kts +++ b/components/ide/jetbrains/toolbox/build.gradle.kts @@ -1,3 +1,7 @@ +// Copyright (c) 2024 Gitpod GmbH. All rights reserved. +// Licensed under the GNU Affero General Public License (AGPL). +// See License.AGPL.txt in the project root for license information. + import com.github.jk1.license.filter.ExcludeTransitiveDependenciesFilter import com.github.jk1.license.render.JsonReportRenderer import org.jetbrains.intellij.pluginRepository.PluginRepositoryFactory @@ -34,7 +38,7 @@ dependencies { implementation("com.connectrpc:connect-kotlin:0.6.0") // Java specific dependencies. implementation("com.connectrpc:connect-kotlin-google-java-ext:0.6.0") - implementation("com.google.protobuf:protobuf-java:4.26.0") + implementation("com.google.protobuf:protobuf-java:4.27.2") // WebSocket compileOnly("javax.websocket:javax.websocket-api:1.1") compileOnly("org.eclipse.jetty.websocket:websocket-api:9.4.54.v20240208") diff --git a/components/ide/jetbrains/toolbox/src/main/kotlin/io/gitpod/toolbox/auth/GitpodAuthManager.kt b/components/ide/jetbrains/toolbox/src/main/kotlin/io/gitpod/toolbox/auth/GitpodAuthManager.kt index d7148bd98a7f52..a9af1f1f815979 100644 --- a/components/ide/jetbrains/toolbox/src/main/kotlin/io/gitpod/toolbox/auth/GitpodAuthManager.kt +++ b/components/ide/jetbrains/toolbox/src/main/kotlin/io/gitpod/toolbox/auth/GitpodAuthManager.kt @@ -60,19 +60,44 @@ class GitpodAuthManager { manager.addEventListener { when (it.type) { AuthEvent.Type.LOGIN -> { - logger.info("account ${it.accountId} logged in") + logger.debug("account ${it.accountId} logged in") + resetCurrentAccount(it.accountId) loginListeners.forEach { it() } } + AuthEvent.Type.LOGOUT -> { - logger.info("account ${it.accountId} logged out") + logger.debug("account ${it.accountId} logged out") + resetCurrentAccount(it.accountId) logoutListeners.forEach { it() } } } } } + private fun resetCurrentAccount(accountId: String) { + val account = manager.accountsWithStatus.find { it.account.id == accountId }?.account ?: return + logger.debug("reset settings for ${account.getHost()}") + Utils.gitpodSettings.resetSettings(account.getHost()) + } + fun getCurrentAccount(): GitpodAccount? { - return manager.accountsWithStatus.firstOrNull()?.account + return manager.accountsWithStatus.find { it.account.getHost() === Utils.gitpodSettings.gitpodHost }?.account + } + + fun loginWithHost(host: String): Boolean { + if (getCurrentAccount()?.getHost() == host) { + // TODO: validate token is still available + return true + } + val account = manager.accountsWithStatus.find { it.account.getHost() == host }?.account + if (account != null) { + Utils.gitpodSettings.gitpodHost = host + loginListeners.forEach { it() } + // TODO: validate token is still available + return true + } + Utils.openUrl(this.getOAuthLoginUrl(host)) + return false } fun logout() { @@ -135,48 +160,12 @@ class GitpodAccount( private val name: String, private val host: String ) : Account { - private val orgSelectedListeners: MutableList<(String) -> Unit> = mutableListOf() - private val logger = LoggerFactory.getLogger(javaClass) override fun getId() = id override fun getFullName() = name fun getCredentials() = credentials fun getHost() = host - private fun getStoreKey(key: String) = "USER:${id}:${key}" - - var organizationId: String? - get() = Utils.settingStore[getStoreKey("ORG")] - set(value){ - if (value == null) { - return - } - Utils.settingStore[getStoreKey("ORG")] = value - orgSelectedListeners.forEach { it(value) } - } - - var preferEditor: String? - get() = Utils.settingStore[getStoreKey("EDITOR")] - set(value){ - if (value == null) { - return - } - Utils.settingStore[getStoreKey("EDITOR")] = value - } - - var preferWorkspaceClass: String? - get() = Utils.settingStore[getStoreKey("WS_CLS")] - set(value){ - if (value == null) { - return - } - Utils.settingStore[getStoreKey("WS_CLS")] = value - } - - fun onOrgSelected(listener: (String) -> Unit) { - orgSelectedListeners.add(listener) - } - fun encode(): String { return Json.encodeToString(this) } diff --git a/components/ide/jetbrains/toolbox/src/main/kotlin/io/gitpod/toolbox/auth/GitpodOrganizationPage.kt b/components/ide/jetbrains/toolbox/src/main/kotlin/io/gitpod/toolbox/auth/GitpodOrganizationPage.kt index 09f0cd8e6bc92e..5a9efbf80de4a0 100644 --- a/components/ide/jetbrains/toolbox/src/main/kotlin/io/gitpod/toolbox/auth/GitpodOrganizationPage.kt +++ b/components/ide/jetbrains/toolbox/src/main/kotlin/io/gitpod/toolbox/auth/GitpodOrganizationPage.kt @@ -29,11 +29,11 @@ class GitpodOrganizationPage(val authManager: GitpodAuthManager, val publicApi: val options = mutableListOf() options.addAll(organizations.map { org -> MenuItem(org.name, null, null) { - authManager.getCurrentAccount()?.organizationId = org.id + Utils.gitpodSettings.organizationId = org.id Utils.toolboxUi.hideUiPage(this) } }) - val orgName = organizations.find { it.id == authManager.getCurrentAccount()?.organizationId }?.name ?: "" + val orgName = organizations.find { it.id == Utils.gitpodSettings.organizationId }?.name ?: "" AutocompleteTextField("Organization", orgName, options, 1.0f) { if (it.isNullOrEmpty()) { ValidationResult.Invalid("Organization is required") diff --git a/components/ide/jetbrains/toolbox/src/main/kotlin/io/gitpod/toolbox/gateway/GitpodRemoteProvider.kt b/components/ide/jetbrains/toolbox/src/main/kotlin/io/gitpod/toolbox/gateway/GitpodRemoteProvider.kt index f26f4d514a5bd8..3e9af2060151b6 100644 --- a/components/ide/jetbrains/toolbox/src/main/kotlin/io/gitpod/toolbox/gateway/GitpodRemoteProvider.kt +++ b/components/ide/jetbrains/toolbox/src/main/kotlin/io/gitpod/toolbox/gateway/GitpodRemoteProvider.kt @@ -16,12 +16,13 @@ import io.gitpod.toolbox.auth.GitpodLoginPage import io.gitpod.toolbox.auth.GitpodOrganizationPage import io.gitpod.toolbox.components.GitpodIcon import io.gitpod.toolbox.components.SimpleButton +import io.gitpod.toolbox.service.ConnectParams import io.gitpod.toolbox.service.GitpodPublicApiManager import io.gitpod.toolbox.service.Utils +import io.gitpod.toolbox.service.getConnectParams import kotlinx.coroutines.launch import org.slf4j.LoggerFactory import java.net.URI -import java.net.URLEncoder class GitpodRemoteProvider( private val consumer: RemoteEnvironmentConsumer, @@ -36,24 +37,29 @@ class GitpodRemoteProvider( private val environmentMap = mutableMapOf() private val openInToolboxUriHandler = GitpodOpenInToolboxUriHandler { connectParams -> + if (!authManger.loginWithHost(connectParams.gitpodHost)) { + // TODO: store connectParams locally so that user doesn't need another dashboard click? + return@GitpodOpenInToolboxUriHandler + } Utils.toolboxUi.showPluginEnvironmentsPage() - setEnvironmentVisibility(connectParams.workspaceId) + setEnvironmentVisibility(connectParams) } // TODO: multiple host support - private fun setEnvironmentVisibility(workspaceId: String) { + private fun setEnvironmentVisibility(connectParams: ConnectParams) { + val workspaceId = connectParams.workspaceId logger.info("setEnvironmentVisibility $workspaceId") Utils.toolboxUi.showWindow() val env = environmentMap[workspaceId] if (env != null) { env.markActive() - Utils.clientHelper.setAutoConnectOnEnvironmentReady(workspaceId, "GO-233.15026.17", "/workspace/empty") + Utils.clientHelper.setAutoConnectOnEnvironmentReady(connectParams.uniqueID, "GO-241.18034.61", "/workspace/template-golang-cli") } else { - GitpodRemoteProviderEnvironment(authManger, workspaceId, publicApi).apply { + GitpodRemoteProviderEnvironment(authManger, connectParams, publicApi).apply { environmentMap[workspaceId] = this this.markActive() consumer.consumeEnvironments(listOf(this)) - Utils.clientHelper.setAutoConnectOnEnvironmentReady(workspaceId, "GO-233.15026.17", "/workspace/empty") + Utils.clientHelper.setAutoConnectOnEnvironmentReady(workspaceId, "GO-241.18034.61", "/workspace/template-golang-cli") } } } @@ -76,21 +82,14 @@ class GitpodRemoteProvider( if (workspaces.isEmpty()) { return@collect } - // TODO: Remove me - workspaces.forEach{ - val host = URLEncoder.encode("https://exp-migration.preview.gitpod-dev.com", "UTF-8") - val workspaceId = URLEncoder.encode(it.id, "UTF-8") - val debugWorkspace = "false" - val newUri = "jetbrains://gateway/io.gitpod.toolbox.gateway/open-in-toolbox?host=${host}&workspaceId=${workspaceId}&debugWorkspace=${debugWorkspace}" - logger.info("workspace ${it.id} $newUri") - } consumer.consumeEnvironments(workspaces.map { - val env = environmentMap[it.id] + val connectParams = it.getConnectParams() + val env = environmentMap[connectParams.uniqueID] if (env != null) { env } else { - val newEnv = GitpodRemoteProviderEnvironment(authManger, it.id, publicApi) - environmentMap[it.id] = newEnv + val newEnv = GitpodRemoteProviderEnvironment(authManger, connectParams, publicApi) + environmentMap[connectParams.uniqueID] = newEnv newEnv } }) @@ -99,9 +98,9 @@ class GitpodRemoteProvider( } private fun startup() { - val account = authManger.getCurrentAccount() ?: return + authManger.getCurrentAccount() ?: return publicApi.setup() - val orgId = account.organizationId + val orgId = Utils.gitpodSettings.organizationId logger.info("user logged in, current selected org: $orgId") if (orgId != null) { Utils.dataManager.startWatchWorkspaces(publicApi) @@ -111,14 +110,19 @@ class GitpodRemoteProvider( Utils.toolboxUi.showUiPage(organizationPage) } } - authManger.getCurrentAccount()?.onOrgSelected { - Utils.dataManager.startWatchWorkspaces(publicApi) + Utils.gitpodSettings.onSettingsChanged { key, _ -> + when (key) { + GitpodSettings.SettingKey.ORGANIZATION_ID.name -> { + Utils.dataManager.startWatchWorkspaces(publicApi) + } + } } } override fun getOverrideUiPage(): UiPage? { - logger.info("getOverrideUiPage") - authManger.getCurrentAccount() ?: return loginPage + val account = authManger.getCurrentAccount() + logger.info("get override ui page for ${account?.getHost()}") + account ?: return loginPage return null } diff --git a/components/ide/jetbrains/toolbox/src/main/kotlin/io/gitpod/toolbox/gateway/GitpodRemoteProviderEnvironment.kt b/components/ide/jetbrains/toolbox/src/main/kotlin/io/gitpod/toolbox/gateway/GitpodRemoteProviderEnvironment.kt index 4b1be52c5cb404..6eaa89334bfb55 100644 --- a/components/ide/jetbrains/toolbox/src/main/kotlin/io/gitpod/toolbox/gateway/GitpodRemoteProviderEnvironment.kt +++ b/components/ide/jetbrains/toolbox/src/main/kotlin/io/gitpod/toolbox/gateway/GitpodRemoteProviderEnvironment.kt @@ -14,6 +14,7 @@ import io.gitpod.publicapi.v1.WorkspaceOuterClass import io.gitpod.publicapi.v1.WorkspaceOuterClass.WorkspacePhase import io.gitpod.toolbox.auth.GitpodAuthManager import io.gitpod.toolbox.components.SimpleButton +import io.gitpod.toolbox.service.ConnectParams import io.gitpod.toolbox.service.GitpodPublicApiManager import io.gitpod.toolbox.service.Utils import kotlinx.coroutines.channels.BufferOverflow @@ -24,7 +25,7 @@ import java.util.concurrent.CompletableFuture class GitpodRemoteProviderEnvironment( private val authManager: GitpodAuthManager, - private val workspaceId: String, + private val connectParams: ConnectParams, private val publicApi: GitpodPublicApiManager, ) : AbstractRemoteProviderEnvironment() { private val logger = LoggerFactory.getLogger(javaClass) @@ -32,7 +33,7 @@ class GitpodRemoteProviderEnvironment( private val contentsViewFuture: CompletableFuture = CompletableFuture.completedFuture( GitpodSSHEnvironmentContentsView( authManager, - workspaceId, + connectParams, publicApi, ) ) @@ -53,9 +54,9 @@ class GitpodRemoteProviderEnvironment( } init { - logger.info("==================GitpodRemoteProviderEnvironment.init $workspaceId") + logger.info("==================GitpodRemoteProviderEnvironment.init ${connectParams.uniqueID}") Utils.coroutineScope.launch { - Utils.dataManager.watchWorkspaceStatus(workspaceId) { + Utils.dataManager.watchWorkspaceStatus(connectParams.workspaceId) { lastPhase = it.phase lastWSEnvState.tryEmit(WorkspaceEnvState(it.phase, isMarkActive)) } @@ -88,8 +89,8 @@ class GitpodRemoteProviderEnvironment( } } - override fun getId(): String = workspaceId - override fun getName(): String = workspaceId + override fun getId(): String = connectParams.uniqueID + override fun getName(): String = connectParams.resolvedWorkspaceId override fun getContentsView(): CompletableFuture = contentsViewFuture diff --git a/components/ide/jetbrains/toolbox/src/main/kotlin/io/gitpod/toolbox/gateway/GitpodSSHEnvironmentContentsView.kt b/components/ide/jetbrains/toolbox/src/main/kotlin/io/gitpod/toolbox/gateway/GitpodSSHEnvironmentContentsView.kt index daa087ce41a485..2deeecb09893ab 100644 --- a/components/ide/jetbrains/toolbox/src/main/kotlin/io/gitpod/toolbox/gateway/GitpodSSHEnvironmentContentsView.kt +++ b/components/ide/jetbrains/toolbox/src/main/kotlin/io/gitpod/toolbox/gateway/GitpodSSHEnvironmentContentsView.kt @@ -8,6 +8,7 @@ import com.jetbrains.toolbox.gateway.environments.ManualEnvironmentContentsView import com.jetbrains.toolbox.gateway.environments.SshEnvironmentContentsView import com.jetbrains.toolbox.gateway.ssh.SshConnectionInfo import io.gitpod.toolbox.auth.GitpodAuthManager +import io.gitpod.toolbox.service.ConnectParams import io.gitpod.toolbox.service.GitpodConnectionProvider import io.gitpod.toolbox.service.GitpodPublicApiManager import io.gitpod.toolbox.service.Utils @@ -17,7 +18,7 @@ import java.util.concurrent.CompletableFuture class GitpodSSHEnvironmentContentsView( private val authManager: GitpodAuthManager, - private val workspaceId: String, + private val connectParams: ConnectParams, private val publicApi: GitpodPublicApiManager, ) : SshEnvironmentContentsView, ManualEnvironmentContentsView { private var cancel = {} @@ -27,7 +28,7 @@ class GitpodSSHEnvironmentContentsView( override fun getConnectionInfo(): CompletableFuture { return Utils.coroutineScope.future { - val provider = GitpodConnectionProvider(authManager, workspaceId, publicApi) + val provider = GitpodConnectionProvider(authManager, connectParams, publicApi) val (connInfo, cancel) = provider.connect() this@GitpodSSHEnvironmentContentsView.cancel = cancel return@future connInfo diff --git a/components/ide/jetbrains/toolbox/src/main/kotlin/io/gitpod/toolbox/gateway/GitpodSettings.kt b/components/ide/jetbrains/toolbox/src/main/kotlin/io/gitpod/toolbox/gateway/GitpodSettings.kt new file mode 100644 index 00000000000000..b3d12f3ae764e5 --- /dev/null +++ b/components/ide/jetbrains/toolbox/src/main/kotlin/io/gitpod/toolbox/gateway/GitpodSettings.kt @@ -0,0 +1,51 @@ +// Copyright (c) 2024 Gitpod GmbH. All rights reserved. +// Licensed under the GNU Affero General Public License (AGPL). +// See License.AGPL.txt in the project root for license information. + +package io.gitpod.toolbox.gateway + +import io.gitpod.toolbox.service.Utils +import org.slf4j.LoggerFactory + +class GitpodSettings { + private val logger = LoggerFactory.getLogger(javaClass) + private val settingsChangedListeners: MutableList<(String, String) -> Unit> = mutableListOf() + + private fun getStoreKey(key: SettingKey) = "GITPOD_SETTINGS:${key.name}" + + private fun updateSetting(key: SettingKey, value: String) { + logger.debug("updateSetting ${key.name}=$value") + Utils.settingStore[getStoreKey(key)] = value + settingsChangedListeners.forEach { it(key.name, value) } + } + + fun onSettingsChanged(listener: (String, String) -> Unit) { + settingsChangedListeners.add(listener) + } + + var organizationId: String? + get() { + val value = Utils.settingStore[getStoreKey(SettingKey.ORGANIZATION_ID)] + return if (value.isNullOrBlank()) null else value + } + set(value) { + updateSetting(SettingKey.ORGANIZATION_ID, value ?: "") + } + + fun resetSettings(host: String = "gitpod.io") { + logger.info("=============reset for $host") + gitpodHost = host + organizationId = "" + } + + var gitpodHost: String + get() = Utils.settingStore[getStoreKey(SettingKey.GITPOD_HOST)] ?: "gitpod.io" + set(value) { + updateSetting(SettingKey.GITPOD_HOST, value) + } + + enum class SettingKey { + ORGANIZATION_ID, + GITPOD_HOST + } +} diff --git a/components/ide/jetbrains/toolbox/src/main/kotlin/io/gitpod/toolbox/service/DataManager.kt b/components/ide/jetbrains/toolbox/src/main/kotlin/io/gitpod/toolbox/service/DataManager.kt index 7e4feb841e6463..e2dfd042363fc4 100644 --- a/components/ide/jetbrains/toolbox/src/main/kotlin/io/gitpod/toolbox/service/DataManager.kt +++ b/components/ide/jetbrains/toolbox/src/main/kotlin/io/gitpod/toolbox/service/DataManager.kt @@ -83,3 +83,7 @@ class DataManager { } } } + +fun WorkspaceOuterClass.Workspace.getConnectParams(): ConnectParams { + return ConnectParams("exp-migration.preview.gitpod-dev.com", id, false) +} \ No newline at end of file diff --git a/components/ide/jetbrains/toolbox/src/main/kotlin/io/gitpod/toolbox/service/GitpodConnectionProvider.kt b/components/ide/jetbrains/toolbox/src/main/kotlin/io/gitpod/toolbox/service/GitpodConnectionProvider.kt index ea981091b8920f..41e324b2598a14 100644 --- a/components/ide/jetbrains/toolbox/src/main/kotlin/io/gitpod/toolbox/service/GitpodConnectionProvider.kt +++ b/components/ide/jetbrains/toolbox/src/main/kotlin/io/gitpod/toolbox/service/GitpodConnectionProvider.kt @@ -10,15 +10,18 @@ import com.jetbrains.toolbox.gateway.ssh.SshConnectionInfo import io.gitpod.publicapi.v1.WorkspaceOuterClass import io.gitpod.toolbox.auth.GitpodAuthManager import kotlinx.serialization.Serializable +import org.slf4j.LoggerFactory class GitpodConnectionProvider( private val authManager: GitpodAuthManager, - private val workspaceId: String, + private val connectParams: ConnectParams, private val publicApi: GitpodPublicApiManager, ) { + private val logger = LoggerFactory.getLogger(javaClass) private val activeConnections = ConcurrentHashMap() suspend fun connect(): Pair Unit> { + val workspaceId = connectParams.workspaceId val workspace = publicApi.getWorkspace(workspaceId).workspace val ownerTokenResp = publicApi.getWorkspaceOwnerToken(workspaceId) val account = authManager.getCurrentAccount() ?: throw Exception("No account found") diff --git a/components/ide/jetbrains/toolbox/src/main/kotlin/io/gitpod/toolbox/service/GitpodPublicApiManager.kt b/components/ide/jetbrains/toolbox/src/main/kotlin/io/gitpod/toolbox/service/GitpodPublicApiManager.kt index 504878a358d4cc..a4c34389e33ea6 100644 --- a/components/ide/jetbrains/toolbox/src/main/kotlin/io/gitpod/toolbox/service/GitpodPublicApiManager.kt +++ b/components/ide/jetbrains/toolbox/src/main/kotlin/io/gitpod/toolbox/service/GitpodPublicApiManager.kt @@ -50,7 +50,7 @@ class GitpodPublicApiManager(private val authManger: GitpodAuthManager) { } private val orgId: String - get() = account?.organizationId ?: throw IllegalStateException("Organization not selected") + get() = Utils.gitpodSettings.organizationId ?: throw IllegalStateException("Organization not selected") suspend fun listOrganizations(): List { val organizationApi = organizationApi ?: throw IllegalStateException("No client") diff --git a/components/ide/jetbrains/toolbox/src/main/kotlin/io/gitpod/toolbox/service/Utils.kt b/components/ide/jetbrains/toolbox/src/main/kotlin/io/gitpod/toolbox/service/Utils.kt index 05b785e6400084..067f8ce6b759c0 100644 --- a/components/ide/jetbrains/toolbox/src/main/kotlin/io/gitpod/toolbox/service/Utils.kt +++ b/components/ide/jetbrains/toolbox/src/main/kotlin/io/gitpod/toolbox/service/Utils.kt @@ -11,6 +11,7 @@ import com.jetbrains.toolbox.gateway.connection.ToolboxProxySettings import com.jetbrains.toolbox.gateway.ssh.validation.SshConnectionValidator import com.jetbrains.toolbox.gateway.ui.ObservablePropertiesFactory import com.jetbrains.toolbox.gateway.ui.ToolboxUi +import io.gitpod.toolbox.gateway.GitpodSettings import kotlinx.coroutines.CoroutineScope import okhttp3.OkHttpClient import java.net.Proxy @@ -26,6 +27,7 @@ object Utils { lateinit var observablePropertiesFactory: ObservablePropertiesFactory private set lateinit var proxySettings: ToolboxProxySettings private set + lateinit var gitpodSettings: GitpodSettings private set lateinit var dataManager: DataManager private set lateinit var toolboxUi: ToolboxUi private set @@ -45,6 +47,7 @@ object Utils { observablePropertiesFactory = serviceLocator.getService(ObservablePropertiesFactory::class.java) proxySettings = serviceLocator.getService(ToolboxProxySettings::class.java) dataManager = DataManager() + gitpodSettings = GitpodSettings() } fun openUrl(url: String) {