@@ -14,8 +14,8 @@ package com.redhat.devtools.gateway.devworkspace
1414import com.google.gson.reflect.TypeToken
1515import com.intellij.openapi.diagnostic.thisLogger
1616import com.redhat.devtools.gateway.openshift.Utils
17+ import com.redhat.devtools.gateway.openshift.isDevWorkspaceCrdMissing
1718import com.redhat.devtools.gateway.openshift.isRetryable
18- import com.redhat.devtools.gateway.openshift.shouldBeIgnored
1919import io.kubernetes.client.openapi.ApiClient
2020import io.kubernetes.client.openapi.ApiException
2121import io.kubernetes.client.openapi.apis.CustomObjectsApi
@@ -26,9 +26,23 @@ import kotlinx.coroutines.withTimeoutOrNull
2626import java.io.IOException
2727import java.util.concurrent.CancellationException
2828
29+ data class DevWorkspaceListItem (
30+ val workspace : DevWorkspace ,
31+ val editor : WorkspaceEditorInfo ,
32+ )
33+
2934data class DevWorkspaceListResult (
30- val items : List <DevWorkspace >,
31- val resourceVersion : String?
35+ val items : List <DevWorkspaceListItem >,
36+ val resourceVersion : String? ,
37+ val templates : Map <String , List <DevWorkspaceTemplate >> = emptyMap(),
38+ // True when the template list request failed with 401/403/404 (error ignored), so templates are unavailable.
39+ // An empty map alone does not imply this.
40+ val templatesUnavailable : Boolean = false
41+ )
42+
43+ data class Templates (
44+ val map : Map <String , List <DevWorkspaceTemplate >>,
45+ val unavailable : Boolean // true when 401/403/404
3246)
3347
3448val DevWorkspace .cheEditor: String
@@ -40,8 +54,6 @@ class DevWorkspaces(private val client: ApiClient) {
4054 private val customApi = CustomObjectsApi (client)
4155
4256 companion object {
43- private val CHE_EDITOR_ID_REGEX = Regex (" che-.*-server" , RegexOption .IGNORE_CASE )
44-
4557 const val FAILED : String = " Failed"
4658 const val RUNNING : String = " Running"
4759 const val STOPPED : String = " Stopped"
@@ -53,77 +65,41 @@ class DevWorkspaces(private val client: ApiClient) {
5365
5466 @Throws(ApiException ::class )
5567 fun listWithResult (namespace : String ): DevWorkspaceListResult {
56- try {
57- val response = customApi.listNamespacedCustomObject(
68+ val response = try {
69+ customApi.listNamespacedCustomObject(
5870 " workspace.devfile.io" ,
5971 " v1alpha2" ,
6072 namespace,
6173 " devworkspaces"
6274 ).execute()
63-
64- val devWorkspaceTemplateMap = getTemplateMap(namespace)
65- val dwItems = Utils .getValue(response, arrayOf(" items" )) as List <* >
66- val dwList = dwItems
67- .map { dwItem -> DevWorkspace .from(dwItem) }
68- .filter { isIdeaEditorBased(it, devWorkspaceTemplateMap) }
69- val lastResourceVersion = (Utils .getValue(response, arrayOf(" metadata" , " resourceVersion" )) as String? )
70-
71- return DevWorkspaceListResult (dwList, lastResourceVersion)
7275 } catch (e: ApiException ) {
73- thisLogger().info(e.message)
74-
75- return when (e.code) {
76- 403 , 404 -> {
77- // There might be some namespaces (OpenShift projects) in which the user cannot list resource "devworkspaces"
78- // e.g. "openshift-virtualization-os-images" on Red Hat Dev Sandbox, or the given cluster doesn't have
79- // the RedHat DevSpaces operator installed on it, etc.
80- //
81- // It doesn't make sense to show an error to the user in such cases,
82- // so let's skip it silently.
83- DevWorkspaceListResult (emptyList(), null )
84- }
85- else -> {
86- thisLogger().error(" Kubernetes API error ${e.code} " , e)
87- throw e
88- }
76+ if (e.isSkippableNamespaceForDevWorkspaceListing(namespace)) {
77+ thisLogger().info(" Ignored: ${e.message} " )
78+ return DevWorkspaceListResult (emptyList(), null )
79+ } else {
80+ thisLogger().error(" Kubernetes API error ${e.code} " , e)
81+ throw e
8982 }
9083 }
84+
85+ val templates = loadTemplates(namespace)
86+ val dwItems = Utils .getValue(response, arrayOf(" items" )) as List <* >
87+ val dwList = dwItems
88+ .map { dwItem -> DevWorkspace .from(dwItem) }
89+ .map { dw -> DevWorkspaceListItem (dw, WorkspaceEditorInfoProvider .create(dw, templates.map)) }
90+ val lastResourceVersion = (Utils .getValue(response, arrayOf(" metadata" , " resourceVersion" )) as String? )
91+
92+ return DevWorkspaceListResult (
93+ dwList,
94+ lastResourceVersion,
95+ templates.map,
96+ templatesUnavailable = templates.unavailable
97+ )
9198 }
9299
93100 @Throws(ApiException ::class )
94101 fun list (namespace : String ): List <DevWorkspace > {
95- return listWithResult(namespace).items
96- }
97-
98- fun isIdeaEditorBased (devWorkspace : DevWorkspace , devWorkspaceTemplateMap : Map <String , List <DevWorkspaceTemplate >>): Boolean {
99- // Quick editor ID check
100- if (devWorkspace.cheEditor.split(" /" ).any { CHE_EDITOR_ID_REGEX .matches(it) }) {
101- return true
102- }
103-
104- // DevWorkspace Template check
105- val templates = devWorkspaceTemplateMap[devWorkspace.uid] ? : return false
106- return templates.any { template ->
107- @Suppress(" UNCHECKED_CAST" )
108- val components = template.components as ? List <Any > ? : return @any false
109- components.any { component: Any ->
110- val map = component as ? Map <* , * > ? : return @any false
111- val volume = map[" volume" ] as ? Map <* , * >
112- // Check 'volume.name' first (v1alpha1), fallback to top-level 'name' (v1alpha2)
113- val name = volume?.get(" name" ) as ? String ? : map[" name" ] as ? String
114- name.equals(" idea-server" , ignoreCase = true )
115- }
116- }
117- }
118-
119- // Creates a filter for the Idea-based DevWorkspaces
120- fun createIdeaEditorFilter (
121- namespace : String
122- ): (DevWorkspace ) -> Boolean {
123- val templateMap = getTemplateMap(namespace)
124- return { dw: DevWorkspace ->
125- isIdeaEditorBased(dw, templateMap)
126- }
102+ return listWithResult(namespace).items.map { it.workspace }
127103 }
128104
129105 fun get (namespace : String , name : String ): DevWorkspace {
@@ -137,8 +113,18 @@ class DevWorkspaces(private val client: ApiClient) {
137113 return DevWorkspace .from(dwObj)
138114 }
139115
140- // Returns a map of DW Owner UID tp list of DW Templates
141- private fun getTemplateMap (namespace : String ): Map <String , List <DevWorkspaceTemplate >> {
116+ /* *
117+ * Loads all DevWorkspaceTemplates for the given namespace and groups them by their owner reference UID.
118+ *
119+ * Queries the Kubernetes API for `devworkspacetemplates` resources in the specified namespace,
120+ * parses each template, and builds a map from owner UID to the list of templates owned by that UID.
121+ *
122+ * If the API returns a 401/403/404, returns an empty map with `unavailable = true`.
123+ *
124+ * @param namespace the Kubernetes namespace to list templates from
125+ * @return a [Templates] containing the UID-to-templates map and an availability flag
126+ */
127+ fun loadTemplates (namespace : String ): Templates {
142128 try {
143129 val dwTemplateList = customApi
144130 .listNamespacedCustomObject(
@@ -150,7 +136,7 @@ class DevWorkspaces(private val client: ApiClient) {
150136 .execute()
151137
152138 val items = Utils .getValue(dwTemplateList, arrayOf(" items" )) as ? List <* > ? : emptyList<Any >()
153- return items
139+ val map = items
154140 .map { DevWorkspaceTemplate .from(it) }
155141 .flatMap { templ ->
156142 templ.ownerRefencesUids.map { uid -> uid to templ }
@@ -159,9 +145,10 @@ class DevWorkspaces(private val client: ApiClient) {
159145 keySelector = { it.first }, // UID
160146 valueTransform = { it.second } // DevWorkspaceTemplate
161147 )
148+ return Templates (map, unavailable = false )
162149 } catch (e: ApiException ) {
163- if (e.shouldBeIgnored ()) {
164- return emptyMap()
150+ if (e.isIgnorableTemplateListError ()) {
151+ return Templates ( emptyMap(), unavailable = true )
165152 }
166153 thisLogger().info(e.message)
167154 throw e
@@ -324,4 +311,33 @@ class DevWorkspaces(private val client: ApiClient) {
324311 object : TypeToken <Watch .Response <Any >>() {}.type
325312 )
326313 }
314+
315+ /* * Returns `true` if the given exception is ignorable when listing templates.
316+ * Returns `false` otherwise.
317+ * Template list failures with 401, 403, or 404 are silently degraded to an empty
318+ * map with [Templates.unavailable] set to true.
319+ *
320+ * Note: 401 is ignorable for templates because templates are optional metadata
321+ * (editor detection falls back to annotation). However, 401 for devworkspaces
322+ * listing is NOT ignorable and rethrows — see [isSkippableNamespaceForDevWorkspaceListing].
323+ */
324+ private fun ApiException.isIgnorableTemplateListError (): Boolean =
325+ code == 401 || code == 403 || code == 404
326+
327+ /* * Returns `true` if the given exception is skippable when listing devworkspaces
328+ * for a specific namespace during multi-namespace scanning.
329+ * Returns `false` otherwise.
330+ * Skippable errors: CRD missing (404 with CRD-not-found response body),
331+ * 403 (Forbidden), or plain 404 — the namespace has no DevSpaces/DevWorkspaces
332+ * resources or access is denied for system namespaces in multi-namespace scans.
333+ * Non-skippable: 401 (Unauthorized) propagates/rethrows, and other errors
334+ * propagate normally.
335+ */
336+ private fun ApiException.isSkippableNamespaceForDevWorkspaceListing (namespace : String ): Boolean = when {
337+ isDevWorkspaceCrdMissing() -> true
338+ code == 403 -> true
339+ code == 404 -> true
340+ else -> false
341+ }
342+
327343}
0 commit comments