Skip to content

Commit 09f7223

Browse files
vrubezhnyadietish
authored andcommitted
fix: Gateway plugin should list the workspaces created from a custom editor definition
The DevWorkspaceTemplate object is used to detect if the JetBrains Idea based editor was used to create a WS Fixes: eclipse-che/che#23610 Signed-off-by: Victor Rubezhny <vrubezhny@redhat.com>
1 parent 153d664 commit 09f7223

File tree

5 files changed

+209
-20
lines changed

5 files changed

+209
-20
lines changed

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

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@ data class DevWorkspace(
2828
return metadata.name
2929
}
3030

31+
val uid: String
32+
get() {
33+
return metadata.uid
34+
}
35+
3136
val started: Boolean
3237
get() {
3338
return spec.started
@@ -43,9 +48,9 @@ data class DevWorkspace(
4348
return status.running
4449
}
4550

46-
val editor: String
51+
val cheEditor: String?
4752
get() {
48-
return metadata.editor
53+
return metadata.cheEditor
4954
}
5055

5156
companion object {
@@ -70,7 +75,7 @@ data class DevWorkspace(
7075

7176
if (metadata.name != other.metadata.name) return false
7277
if (metadata.namespace != other.metadata.namespace) return false
73-
if (metadata.editor != other.metadata.editor) return false
78+
if (metadata.cheEditor != other.metadata.cheEditor) return false
7479

7580
return true
7681
}
@@ -86,18 +91,21 @@ data class DevWorkspace(
8691
data class DevWorkspaceObjectMeta(
8792
val name: String,
8893
val namespace: String,
89-
val editor: String
94+
val uid: String,
95+
val cheEditor: String?
9096
) {
9197
companion object {
9298
fun from(map: Any) = object {
9399
val name = Utils.getValue(map, arrayOf("name"))
94100
val namespace = Utils.getValue(map, arrayOf("namespace"))
95-
val editor = Utils.getValue(map, arrayOf("annotations", "che.eclipse.org/che-editor")) ?: "unknown"
101+
val uid = Utils.getValue(map, arrayOf("uid"))
102+
val cheEditor = Utils.getValue(map, arrayOf("annotations", "che.eclipse.org/che-editor"))
96103

97104
val data = DevWorkspaceObjectMeta(
98105
name as String,
99106
namespace as String,
100-
editor as String
107+
uid as String,
108+
cheEditor as String?
101109
)
102110
}.data
103111
}
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
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 java.util.Collections
15+
16+
data class DevWorkspaceTemplate (
17+
private val metadata: DevWorkspaceTemplateMetadata,
18+
private val spec: DevWorkspaceTemplateSpec,
19+
) {
20+
val namespace: String
21+
get() {
22+
return metadata.namespace
23+
}
24+
25+
val name: String
26+
get() {
27+
return metadata.name
28+
}
29+
30+
val ownerRefencesUids: List<String>
31+
get() {
32+
return metadata.ownerRefencesUids
33+
}
34+
35+
val components: Any
36+
get() {
37+
return spec.components
38+
}
39+
40+
val pluginRegistryUrl: String?
41+
get() {
42+
return metadata.pluginRegistryUrl
43+
}
44+
45+
46+
companion object {
47+
fun from(map: Any?) = object {
48+
val metadata = Utils.getValue(map, arrayOf("metadata")) ?: Collections.emptyMap<String, Any>()
49+
val spec = Utils.getValue(map, arrayOf("spec")) ?: Collections.emptyMap<String, Any>()
50+
51+
val data = DevWorkspaceTemplate(
52+
DevWorkspaceTemplateMetadata.from(metadata),
53+
DevWorkspaceTemplateSpec.from(spec)
54+
)
55+
}.data
56+
}
57+
58+
override fun equals(other: Any?): Boolean {
59+
if (this === other) return true
60+
if (javaClass != other?.javaClass) return false
61+
62+
other as DevWorkspaceTemplate
63+
64+
if (metadata.name != other.name) return false
65+
if (metadata.namespace != other.namespace) return false
66+
if (metadata.pluginRegistryUrl != other.pluginRegistryUrl) return false
67+
if (metadata.ownerRefencesUids != other.ownerRefencesUids) return false
68+
69+
return true
70+
}
71+
72+
override fun hashCode(): Int {
73+
var result = metadata.hashCode()
74+
result = 31 * result + spec.hashCode()
75+
return result
76+
}
77+
}
78+
79+
data class DevWorkspaceTemplateMetadata(
80+
val name: String,
81+
val namespace: String,
82+
val pluginRegistryUrl: String?,
83+
val ownerRefencesUids: List<String>
84+
) {
85+
companion object {
86+
fun from(map: Any) = object {
87+
val name = Utils.getValue(map, arrayOf("name"))
88+
val namespace = Utils.getValue(map, arrayOf("namespace"))
89+
val pluginRegistryUrl = Utils.getValue(map, arrayOf("annotations", "che.eclipse.org/plugin-registry-url"))
90+
91+
@Suppress("UNCHECKED_CAST")
92+
val ownerRefs = Utils.getValue(map, arrayOf("ownerReferences")) as? List<Map<String, Any>>
93+
val ownerRefUids: List<String> = ownerRefs
94+
?.filter {
95+
(it["apiVersion"] as? String)?.equals("workspace.devfile.io/v1alpha2", ignoreCase = true) == true &&
96+
(it["kind"] as? String)?.equals("DevWorkspace", ignoreCase = true) == true
97+
}
98+
?.mapNotNull { it["uid"] as? String }
99+
?: emptyList()
100+
101+
val data = DevWorkspaceTemplateMetadata(
102+
name as String,
103+
namespace as String,
104+
pluginRegistryUrl as String?,
105+
ownerRefUids
106+
)
107+
}.data
108+
}
109+
}
110+
111+
data class DevWorkspaceTemplateSpec(
112+
val components: List<Map<String, Any>>
113+
) {
114+
companion object {
115+
fun from(map: Any) = object {
116+
val rawComponents: Any? = Utils.getValue(map, arrayOf("components"))
117+
val components = if (rawComponents is List<*>) {
118+
rawComponents.filterIsInstance<Map<String, Any>>()
119+
} else {
120+
emptyList()
121+
}
122+
val data = DevWorkspaceTemplateSpec(components)
123+
}.data
124+
}
125+
126+
override fun equals(other: Any?): Boolean {
127+
if (this === other) return true
128+
if (javaClass != other?.javaClass) return false
129+
if (other !is DevWorkspaceTemplateSpec) return false
130+
131+
return components == other.components
132+
}
133+
134+
override fun hashCode(): Int {
135+
return components.hashCode()
136+
}
137+
}

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

Lines changed: 54 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,11 @@ import java.util.concurrent.Executors
2525
import java.util.concurrent.TimeUnit
2626

2727
class DevWorkspaces(private val client: ApiClient) {
28+
private val customApi = CustomObjectsApi(client)
29+
2830
companion object {
31+
private val CHE_EDITOR_ID_REGEX = Regex("che-.*-server", RegexOption.IGNORE_CASE)
32+
2933
val FAILED: String = "Failed"
3034
val RUNNING: String = "Running"
3135
val STOPPED: String = "Stopped"
@@ -35,25 +39,22 @@ class DevWorkspaces(private val client: ApiClient) {
3539

3640
@Throws(ApiException::class)
3741
fun list(namespace: String): List<DevWorkspace> {
38-
val customApi = CustomObjectsApi(client)
39-
try {
42+
try {
4043
val response = customApi.listNamespacedCustomObject(
4144
"workspace.devfile.io",
4245
"v1alpha2",
4346
namespace,
4447
"devworkspaces"
4548
).execute()
4649

50+
val devWorkspaceTemplateMap = getDevWorkspaceTemplateMap(namespace)
4751
val dwItems = Utils.getValue(response, arrayOf("items")) as List<*>
4852
return dwItems
4953
.stream()
5054
.map { dwItem -> DevWorkspace.from(dwItem) }
51-
.filter {
52-
val parts = it.editor.split("/")
53-
val segment = parts.getOrNull(1) ?: return@filter false
54-
Regex("che-.*-server").matches(segment)
55-
}
55+
.filter { isIdeaEditorBased(it, devWorkspaceTemplateMap) }
5656
.toList()
57+
5758
} catch (e: ApiException) {
5859
thisLogger().info(e.message)
5960

@@ -72,8 +73,29 @@ class DevWorkspaces(private val client: ApiClient) {
7273
}
7374
}
7475

76+
fun isIdeaEditorBased(devWorkspace: DevWorkspace, devWorkspaceTemplateMap: Map<String, List<DevWorkspaceTemplate>>): Boolean {
77+
// Quick editor ID check
78+
val segment = devWorkspace.cheEditor?.split("/")?.getOrNull(1)
79+
if (segment != null && CHE_EDITOR_ID_REGEX.matches(segment)) {
80+
return true
81+
}
82+
83+
// DevWorkspace Template check
84+
val templates = devWorkspaceTemplateMap[devWorkspace.uid] ?: return false
85+
return templates.any { template ->
86+
@Suppress("UNCHECKED_CAST")
87+
val components = template.components as? List<Any> ?: return@any false
88+
components.any { component: Any ->
89+
val map = component as? Map<*, *> ?: return@any false
90+
val volume = map["volume"] as? Map<*, *>
91+
// Check 'volume.name' first (v1alpha1), fallback to top-level 'name' (v1alpha2)
92+
val name = volume?.get("name") as? String ?: map["name"] as? String
93+
name.equals("idea-server", ignoreCase = true)
94+
}
95+
}
96+
}
97+
7598
fun get(namespace: String, name: String): DevWorkspace {
76-
val customApi = CustomObjectsApi(client)
7799
val dwObj = customApi.getNamespacedCustomObject(
78100
"workspace.devfile.io",
79101
"v1alpha2",
@@ -84,6 +106,29 @@ class DevWorkspaces(private val client: ApiClient) {
84106
return DevWorkspace.from(dwObj)
85107
}
86108

109+
// Returns a map of DW Owner UID tp list of DW Templates
110+
fun getDevWorkspaceTemplateMap(namespace: String): Map<String, List<DevWorkspaceTemplate>> {
111+
val dwTemplateList = customApi
112+
.listNamespacedCustomObject(
113+
"workspace.devfile.io",
114+
"v1alpha2",
115+
namespace,
116+
"devworkspacetemplates",
117+
)
118+
.execute()
119+
120+
val items = Utils.getValue(dwTemplateList, arrayOf("items")) as? List<*> ?: emptyList<Any>()
121+
return items
122+
.map { DevWorkspaceTemplate.from(it) }
123+
.flatMap { templ ->
124+
templ.ownerRefencesUids.map { uid -> uid to templ }
125+
}
126+
.groupBy(
127+
keySelector = { it.first }, // UID
128+
valueTransform = { it.second } // DevWorkspaceTemplate
129+
)
130+
}
131+
87132
@Throws(ApiException::class)
88133
fun start(namespace: String, name: String) {
89134
val patch = arrayOf(mapOf("op" to "replace", "path" to "/spec/started", "value" to true))
@@ -140,7 +185,6 @@ class DevWorkspaces(private val client: ApiClient) {
140185
// https://github.com/kubernetes-client/java/blob/master/examples/examples-release-20/src/main/java/io/kubernetes/client/examples/PatchExample.java
141186
@Throws(ApiException::class)
142187
private fun doPatch(namespace: String, name: String, body: Any) {
143-
val customApi = CustomObjectsApi(client)
144188
PatchUtils.patch(
145189
DevWorkspace.javaClass,
146190
{
@@ -161,10 +205,9 @@ class DevWorkspaces(private val client: ApiClient) {
161205
// Example:
162206
// https://github.com/kubernetes-client/java/blob/master/examples/examples-release-20/src/main/java/io/kubernetes/client/examples/WatchExample.java
163207
private fun createWatcher(namespace: String, fieldSelector: String = "", labelSelector: String = ""): Watch<Any> {
164-
val customObjectsApi = CustomObjectsApi(client)
165208
return Watch.createWatch(
166209
client,
167-
customObjectsApi.listNamespacedCustomObject(
210+
customApi.listNamespacedCustomObject(
168211
"workspace.devfile.io",
169212
"v1alpha2",
170213
namespace,

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

Lines changed: 2 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/
@@ -20,7 +20,7 @@ object Utils {
2020

2121
var value = obj
2222
for (s in path) {
23-
value = (value as Map<*, *>)[s]
23+
value = (value as Map<*, *>)[s] ?: return null
2424
}
2525

2626
return value

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

Lines changed: 2 additions & 1 deletion
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/
@@ -116,6 +116,7 @@ class DevSpacesWorkspacesStepView(
116116
doRefreshAllDevWorkspaces()
117117
enableButtons()
118118
} catch (e: Exception) {
119+
thisLogger().error("Refreshing workspaces failed.", e)
119120
Dialogs.error("Could not refresh workspaces: " + e.messageWithoutPrefix(), "Error Refreshing")
120121
}
121122
},

0 commit comments

Comments
 (0)