Skip to content

Commit 7582da2

Browse files
vrubezhnyadietish
authored andcommitted
feat: Gateway: The workspaces listed in the "Connect to Dev Spaces" wizard should have their states updated automatically #23653
Fixes: eclipse-che/che#23653 Signed-off-by: Victor Rubezhny <[email protected]> Assisted by: OpenAI ChatGPT
1 parent 1869284 commit 7582da2

File tree

5 files changed

+350
-112
lines changed

5 files changed

+350
-112
lines changed

build.gradle.kts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,16 @@ dependencies {
4040
testImplementation("io.mockk:mockk:1.14.6")
4141
testImplementation("io.mockk:mockk-agent-jvm:1.14.6")
4242

43-
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")
44-
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.2")
43+
// Do NOT bundle kotlinx-coroutines: IntelliJ/Gateway provides its own version.
44+
// Adding another copy causes classloader conflicts, broken cancellation, and runtime errors.
45+
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.2") {
46+
// prevent coroutines-core from ending up inside the plugin
47+
exclude(group = "org.jetbrains.kotlinx", module = "kotlinx-coroutines-core")
48+
}
49+
configurations.all {
50+
// prevent coroutines-core from ending up inside the plugin
51+
exclude(group = "org.jetbrains.kotlinx", module = "kotlinx-coroutines-core")
52+
}
4553

4654
// IntelliJ Platform Gradle Plugin Dependencies Extension - read more: https://plugins.jetbrains.com/docs/intellij/tools-intellij-platform-gradle-plugin-dependencies-extension.html
4755
intellijPlatform {
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
/*
2+
* Copyright (c) 2025 Red Hat, Inc.
3+
* This program and the accompanying materials are made
4+
* available under the terms of the Eclipse Public License 2.0
5+
* which is available at https://www.eclipse.org/legal/epl-2.0/
6+
*
7+
* SPDX-License-Identifier: EPL-2.0
8+
*
9+
* Contributors:
10+
* Red Hat, Inc. - initial API and implementation
11+
*/
12+
package com.redhat.devtools.gateway.openshift
13+
14+
import com.intellij.openapi.application.EDT
15+
import io.kubernetes.client.openapi.ApiClient
16+
import io.kubernetes.client.util.Watch
17+
import kotlinx.coroutines.*
18+
19+
interface DevWorkspaceListener {
20+
fun onAdded(dw: DevWorkspace)
21+
fun onUpdated(dw: DevWorkspace)
22+
fun onDeleted(dw: DevWorkspace)
23+
}
24+
25+
class DevWorkspaceWatcher(
26+
private val namespace: String,
27+
private val createWatcher: (namespace: String, latestResourceVersion: String?) -> Watch<Any>,
28+
private val createFilter: (String) -> ((DevWorkspace) -> Boolean),
29+
private val listener: DevWorkspaceListener,
30+
private val scope: CoroutineScope,
31+
) {
32+
private var job: Job? = null
33+
34+
fun start(latestResourceVersion: String? = null) {
35+
job = scope.launch {
36+
watchLoop(latestResourceVersion)
37+
}
38+
}
39+
40+
fun stop() {
41+
job?.cancel()
42+
job = null
43+
}
44+
45+
private suspend fun watchLoop(latestResourceVersion: String? = null) {
46+
while (scope.isActive) {
47+
val watcher = createWatcher(namespace, latestResourceVersion)
48+
val matches = createFilter(namespace)
49+
50+
try {
51+
for (event in watcher) {
52+
if (!scope.isActive) break
53+
54+
val dw = DevWorkspace.from(event.`object`)
55+
withContext(Dispatchers.EDT) {
56+
when (event.type) {
57+
"ADDED" -> if(matches(dw)) listener.onAdded(dw)
58+
"MODIFIED" -> if(matches(dw)) listener.onUpdated(dw) else listener.onDeleted(dw)
59+
"DELETED" -> listener.onDeleted(dw)
60+
}
61+
}
62+
}
63+
} catch (_: Exception) {
64+
// connection dropped or closed — reconnect
65+
} finally {
66+
watcher.close()
67+
}
68+
69+
delay(100)
70+
}
71+
}
72+
}
73+
74+
class DevWorkspaceWatchManager(
75+
private val client: ApiClient,
76+
private val createWatcher: (String, String?) -> Watch<Any>,
77+
private val createFilter: (String) -> ((DevWorkspace) -> Boolean),
78+
private val listener: DevWorkspaceListener,
79+
private val scope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
80+
) {
81+
private val watchers = mutableListOf<DevWorkspaceWatcher>()
82+
83+
fun start(lastResourceVersions: Map<String, String?>? = null) {
84+
Projects(client).list().onEach { project ->
85+
val ns = Utils.getValue(project, arrayOf("metadata","name")) as String
86+
val w = DevWorkspaceWatcher(
87+
namespace = ns,
88+
createWatcher = createWatcher,
89+
createFilter = createFilter,
90+
listener = listener,
91+
scope = scope
92+
)
93+
watchers += w
94+
w.start(lastResourceVersions?.get(ns))
95+
}
96+
}
97+
98+
fun stop() {
99+
watchers.forEach { it.stop() }
100+
watchers.clear()
101+
}
102+
}
103+

src/main/kotlin/com/redhat/devtools/gateway/openshift/DevWorkspaces.kt

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ import java.io.IOException
2626
import java.util.concurrent.CancellationException
2727
import kotlin.time.Duration.Companion.seconds
2828

29+
data class DevWorkspaceListResult(
30+
val items: List<DevWorkspace>,
31+
val resourceVersion: String?
32+
)
33+
2934
class DevWorkspaces(private val client: ApiClient) {
3035
private val customApi = CustomObjectsApi(client)
3136

@@ -41,8 +46,8 @@ class DevWorkspaces(private val client: ApiClient) {
4146
}
4247

4348
@Throws(ApiException::class)
44-
fun list(namespace: String): List<DevWorkspace> {
45-
try {
49+
fun listWithResult(namespace: String): DevWorkspaceListResult {
50+
try {
4651
val response = customApi.listNamespacedCustomObject(
4752
"workspace.devfile.io",
4853
"v1alpha2",
@@ -52,12 +57,12 @@ class DevWorkspaces(private val client: ApiClient) {
5257

5358
val devWorkspaceTemplateMap = getDevWorkspaceTemplateMap(namespace)
5459
val dwItems = Utils.getValue(response, arrayOf("items")) as List<*>
55-
return dwItems
56-
.stream()
60+
val dwList = dwItems
5761
.map { dwItem -> DevWorkspace.from(dwItem) }
5862
.filter { isIdeaEditorBased(it, devWorkspaceTemplateMap) }
59-
.toList()
63+
val lastResourceVersion = (Utils.getValue(response, arrayOf("metadata", "resourceVersion")) as String?)
6064

65+
return DevWorkspaceListResult(dwList, lastResourceVersion)
6166
} catch (e: ApiException) {
6267
thisLogger().info(e.message)
6368

@@ -67,7 +72,7 @@ class DevWorkspaces(private val client: ApiClient) {
6772
// It doesn't make sense to show an error to the user in such cases,
6873
// so let's skip it silently.
6974
if ((response["code"] as Double) == 403.0) {
70-
return emptyList()
75+
return DevWorkspaceListResult(emptyList(), null)
7176
} else {
7277
// The error will be shown in the Gateway UI.
7378
thisLogger().error(e.message)
@@ -76,6 +81,11 @@ class DevWorkspaces(private val client: ApiClient) {
7681
}
7782
}
7883

84+
@Throws(ApiException::class)
85+
fun list(namespace: String): List<DevWorkspace> {
86+
return listWithResult(namespace).items
87+
}
88+
7989
fun isIdeaEditorBased(devWorkspace: DevWorkspace, devWorkspaceTemplateMap: Map<String, List<DevWorkspaceTemplate>>): Boolean {
8090
// Quick editor ID check
8191
val segment = devWorkspace.cheEditor?.split("/")?.getOrNull(1)
@@ -98,6 +108,16 @@ class DevWorkspaces(private val client: ApiClient) {
98108
}
99109
}
100110

111+
// Creates a filter for the Idea-based DevWorkspaces
112+
fun createFilter(
113+
namespace: String
114+
): (DevWorkspace) -> Boolean {
115+
val templateMap = getDevWorkspaceTemplateMap(namespace)
116+
return { dw: DevWorkspace ->
117+
isIdeaEditorBased(dw, templateMap)
118+
}
119+
}
120+
101121
fun get(namespace: String, name: String): DevWorkspace {
102122
val dwObj = customApi.getNamespacedCustomObject(
103123
"workspace.devfile.io",
@@ -155,7 +175,7 @@ class DevWorkspaces(private val client: ApiClient) {
155175
return withTimeoutOrNull(timeout.seconds) {
156176
while (true) {
157177
checkCancelled?.invoke()
158-
val watcher = createWatcher(namespace, "metadata.name=$name")
178+
val watcher = createWatcher(namespace, "metadata.name=$name", latestResourceVersion = null)
159179
try {
160180
while (true) {
161181
checkCancelled?.invoke()
@@ -178,7 +198,7 @@ class DevWorkspaces(private val client: ApiClient) {
178198
watcher.close()
179199
}
180200

181-
// Watch ended because server closed the stream retry a new watch
201+
// Watch ended because server closed the stream - retry a new watch
182202
delay(200)
183203
}
184204

@@ -199,7 +219,7 @@ class DevWorkspaces(private val client: ApiClient) {
199219
return withTimeoutOrNull(timeout.seconds) {
200220
while (true) {
201221
checkCancelled?.invoke()
202-
val watcher = createWatcher(namespace, "metadata.name=$name")
222+
val watcher = createWatcher(namespace, "metadata.name=$name", latestResourceVersion = null)
203223
try {
204224
while (true) {
205225
checkCancelled?.invoke()
@@ -218,7 +238,7 @@ class DevWorkspaces(private val client: ApiClient) {
218238
watcher.close()
219239
}
220240

221-
// Watch ended because server closed the stream retry a new watch
241+
// Watch ended because server closed the stream - retry a new watch
222242
delay(200)
223243
}
224244

@@ -250,7 +270,7 @@ class DevWorkspaces(private val client: ApiClient) {
250270

251271
// Example:
252272
// https://github.com/kubernetes-client/java/blob/master/examples/examples-release-20/src/main/java/io/kubernetes/client/examples/WatchExample.java
253-
private fun createWatcher(namespace: String, fieldSelector: String = "", labelSelector: String = ""): Watch<Any> {
273+
fun createWatcher(namespace: String, fieldSelector: String = "", labelSelector: String = "", latestResourceVersion: String? = null): Watch<Any> {
254274
return Watch.createWatch(
255275
client,
256276
customApi.listNamespacedCustomObject(
@@ -260,6 +280,7 @@ class DevWorkspaces(private val client: ApiClient) {
260280
"devworkspaces"
261281
).fieldSelector(fieldSelector)
262282
.labelSelector(labelSelector)
283+
.resourceVersion(latestResourceVersion)
263284
.watch(true)
264285
.buildCall(null),
265286
object : TypeToken<Watch.Response<Any>>() {}.type

src/main/kotlin/com/redhat/devtools/gateway/view/steps/DevSpacesServerStepView.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2024 Red Hat, Inc.
2+
* Copyright (c) 2024-2025 Red Hat, Inc.
33
* This program and the accompanying materials are made
44
* available under the terms of the Eclipse Public License 2.0
55
* which is available at https://www.eclipse.org/legal/epl-2.0/
@@ -66,6 +66,7 @@ class DevSpacesServerStepView(
6666
addItemListener(::onClusterSelected)
6767
PasteClipboardMenu.addTo(this.editor.editorComponent as JTextField)
6868
}
69+
6970
override val component = panel {
7071
row {
7172
label(DevSpacesBundle.message("connector.wizard_step.openshift_connection.title")).applyToComponent {
@@ -141,7 +142,7 @@ class DevSpacesServerStepView(
141142
ProgressManager.getInstance().runProcessWithProgressSynchronously(
142143
{
143144
try {
144-
Projects(client).list()
145+
Projects(client).isAuthenticated()
145146
success = true
146147
} catch (e: Exception) {
147148
Dialogs.error(e.message(), "Connection failed")

0 commit comments

Comments
 (0)