Skip to content

[DO NOT MERGE] fix: bypass JVM proxy for Kubernetes API client connections#341

Draft
TheChosenMok wants to merge 3 commits into
redhat-developer:mainfrom
TheChosenMok:proxy-support-oidc
Draft

[DO NOT MERGE] fix: bypass JVM proxy for Kubernetes API client connections#341
TheChosenMok wants to merge 3 commits into
redhat-developer:mainfrom
TheChosenMok:proxy-support-oidc

Conversation

@TheChosenMok

Copy link
Copy Markdown

Summary

  • Set Proxy.NO_PROXY on all ApiClient OkHttp instances in BaseClientBuilder and LinkClientBuilder
  • OkHttp inherits the JVM default ProxySelector, which in corporate environments routes cluster API calls through an HTTP forward proxy. This may cause silent failures (403, proxy auth errors, PKIX)
  • The Kubernetes API server is a direct-access endpoint and should never be routed through a forward proxy

Note: This branch is based on PR #340 (crw-11741). Merge #340 first, then this.

Test plan

  • Verify workspace listing works in environments with corporate HTTP proxy configured in IntelliJ
  • Verify workspace listing still works in environments without proxy
  • Verify deep-link connections work behind proxy

Made with Cursor

adietish and others added 3 commits July 18, 2026 21:58
Signed-off-by: Andre Dietisheim <adietish@redhat.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…partial (crw-11741)

Signed-off-by: Andre Dietisheim <adietish@redhat.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
OkHttp inherits the JVM default ProxySelector, which in corporate environments routes cluster API calls through an HTTP forward proxy. This may cause silent failures (403, proxy auth errors, PKIX)

Set Proxy.NO_PROXY on all ApiClient OkHttp instances.

Co-authored-by: Cursor <cursoragent@cursor.com>
@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

The PR updates IntelliJ IDEA/Gateway Gradle routing, refactors kubeconfig file and cluster monitoring for concurrency and state-based updates, and configures OpenShift API clients to bypass JVM proxy settings.

IDE build and run routing

Layer / File(s) Summary
Platform selection and IDE run configuration
.run/Run Plugin (IDEA).run.xml, build.gradle.kts, gradle.properties
Gradle properties and resolved platform values select IDEA or Gateway settings, while the run configuration and task use runIdeIdea with IntelliJ IDEA Ultimate.

Kubeconfig file and cluster monitoring

Layer / File(s) Summary
File watching lifecycle and event handling
src/main/kotlin/com/redhat/devtools/gateway/kubeconfig/FileWatcher.kt, src/test/kotlin/com/redhat/devtools/gateway/kubeconfig/FileWatcherTest.kt
File and directory tracking, callback synchronization, shutdown handling, non-existent file registration, and delete-event notifications are implemented and tested.
Cluster state and refresh reconciliation
src/main/kotlin/com/redhat/devtools/gateway/kubeconfig/KubeConfigMonitor.kt, src/test/kotlin/com/redhat/devtools/gateway/kubeconfig/KubeConfigMonitorTest.kt
Cluster publication uses StateFlow, monitored paths are reconciled from discovered config files, and refresh generations coalesce parsing updates.

OpenShift API proxy bypass

Layer / File(s) Summary
HTTP client proxy configuration
src/main/kotlin/com/redhat/devtools/gateway/openshift/apiclient/*ClientBuilder.kt
HTTP clients created by base, kubeconfig, default, and fallback paths use Proxy.NO_PROXY.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant FileWatcher
  participant KubeConfigMonitor
  participant KubeConfigUtils
  FileWatcher->>KubeConfigMonitor: notify file change
  KubeConfigMonitor->>KubeConfigUtils: getAllConfigFiles and getClusters
  KubeConfigUtils-->>KubeConfigMonitor: cluster list
  KubeConfigMonitor-->>KubeConfigMonitor: publish current StateFlow value
Loading

Suggested reviewers: azatsarynnyy, vrubezhny, adietish

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 8.11% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the main change: bypassing the JVM proxy for Kubernetes API client connections.
Description check ✅ Passed The description matches the changeset and explains the proxy bypass behavior and test intent.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@TheChosenMok TheChosenMok changed the title fix: bypass JVM proxy for Kubernetes API client connections [DO NOT MERGE] fix: bypass JVM proxy for Kubernetes API client connections Jul 21, 2026
@TheChosenMok
TheChosenMok marked this pull request as draft July 21, 2026 21:34

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@build.gradle.kts`:
- Around line 244-250: Preserve the runIdeForUiTests task compatibility while
adding runIdeIdea, or update every invocation in
.github/workflows/run-ui-tests.yml atomically to use the replacement task.
Ensure Linux, Windows, and macOS UI-test jobs can still launch the IDE
successfully.

In `@src/main/kotlin/com/redhat/devtools/gateway/kubeconfig/FileWatcher.kt`:
- Around line 80-89: Update addFile and the registerDirectory flow in
FileWatcher so a missing parent directory does not let IOException or
NoSuchFileException escape when tracking a not-yet-existing file. Handle the
failed directory registration consistently with the watcher’s existing
lifecycle, while still retaining the file for later monitoring once its parent
becomes available; preserve normal registration behavior for existing
directories.

In `@src/main/kotlin/com/redhat/devtools/gateway/kubeconfig/KubeConfigMonitor.kt`:
- Around line 94-126: Handle exceptions from kubeConfigUtils.getClusters()
within refreshClusters’ launched coroutine, logging a structured error and
surfacing the refresh failure through the existing user-visible error mechanism.
Keep _clusters unchanged when parsing fails, while preserving mutex handling and
generation checks for successful refreshes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Enterprise

Run ID: 72d108e3-cfc8-4a30-8e9e-a3a72205dc65

📥 Commits

Reviewing files that changed from the base of the PR and between 07ca022 and 8f54463.

📒 Files selected for processing (9)
  • .run/Run Plugin (IDEA).run.xml
  • build.gradle.kts
  • gradle.properties
  • src/main/kotlin/com/redhat/devtools/gateway/kubeconfig/FileWatcher.kt
  • src/main/kotlin/com/redhat/devtools/gateway/kubeconfig/KubeConfigMonitor.kt
  • src/main/kotlin/com/redhat/devtools/gateway/openshift/apiclient/BaseClientBuilder.kt
  • src/main/kotlin/com/redhat/devtools/gateway/openshift/apiclient/LinkClientBuilder.kt
  • src/test/kotlin/com/redhat/devtools/gateway/kubeconfig/FileWatcherTest.kt
  • src/test/kotlin/com/redhat/devtools/gateway/kubeconfig/KubeConfigMonitorTest.kt
💤 Files with no reviewable changes (1)
  • gradle.properties

Comment thread build.gradle.kts
Comment on lines +244 to +250
// Visible next to runIde under "intellij platform" in the Gradle tool window
register("runIdeIdea") {
type = IntelliJPlatformType.IntellijIdeaUltimate
version = providers.gradleProperty("platformVersion")

plugins {
robotServerPlugin()
bundledPlugin("com.jetbrains.gateway")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Preserve or migrate the UI-test Gradle task.

.github/workflows/run-ui-tests.yml still invokes runIdeForUiTests on Linux, Windows, and macOS. Removing that task causes every UI-test job to fail before the IDE starts. Update the workflow atomically or retain a compatibility task.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@build.gradle.kts` around lines 244 - 250, Preserve the runIdeForUiTests task
compatibility while adding runIdeIdea, or update every invocation in
.github/workflows/run-ui-tests.yml atomically to use the replacement task.
Ensure Linux, Windows, and macOS UI-test jobs can still launch the IDE
successfully.

Comment on lines 80 to 89
fun addFile(path: Path): FileWatcher {
if (!path.exists()
|| !path.isRegularFile()) {
return this
}
val parentDir = path.parent
if (parentDir != null
&& !monitoredFiles.contains(path)) {
val watchKey = parentDir.register(watchService,
StandardWatchEventKinds.ENTRY_CREATE,
StandardWatchEventKinds.ENTRY_MODIFY,
StandardWatchEventKinds.ENTRY_DELETE
)
registeredKeys[watchKey] = parentDir
registerDirectory(parentDir)
monitoredFiles.add(path)
onFileChanged?.invoke(path)
invokeOnFileChanged(path)
}
return this
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Unhandled IOException when a monitored file's parent directory doesn't exist yet.

addFile() was changed to support tracking non-existent files (per the docstring), but registerDirectory() still calls directory.register(...) unconditionally. If the parent directory itself doesn't exist (a plausible case for a custom KUBECONFIG entry or a not-yet-mounted config dir — exactly the scenario this feature targets), Path.register() throws IOException/NoSuchFileException, which propagates uncaught through addFile()KubeConfigMonitor.startWatchingNew()/updateMonitoredPaths()/start(), which per the linked snippet in DevSpacesServerStepView has no surrounding try/catch.

🛡️ Proposed fix
     private fun registerDirectory(directory: Path) {
         if (registeredDirectories.values.any { it == directory }) {
             return
         }
-        val watchKey = directory.register(
-            watchService,
-            StandardWatchEventKinds.ENTRY_CREATE,
-            StandardWatchEventKinds.ENTRY_MODIFY,
-            StandardWatchEventKinds.ENTRY_DELETE
-        )
-        registeredDirectories[watchKey] = directory
+        try {
+            val watchKey = directory.register(
+                watchService,
+                StandardWatchEventKinds.ENTRY_CREATE,
+                StandardWatchEventKinds.ENTRY_MODIFY,
+                StandardWatchEventKinds.ENTRY_DELETE
+            )
+            registeredDirectories[watchKey] = directory
+        } catch (e: IOException) {
+            // Parent directory doesn't exist yet; the file stays in monitoredFiles
+            // but won't be watched until a later addFile()/updateMonitoredPaths() retries.
+        }
     }

Also applies to: 116-127

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/kotlin/com/redhat/devtools/gateway/kubeconfig/FileWatcher.kt` around
lines 80 - 89, Update addFile and the registerDirectory flow in FileWatcher so a
missing parent directory does not let IOException or NoSuchFileException escape
when tracking a not-yet-existing file. Handle the failed directory registration
consistently with the watcher’s existing lifecycle, while still retaining the
file for later monitoring once its parent becomes available; preserve normal
registration behavior for existing directories.

Comment on lines +94 to 126
/**
* Schedules a cluster refresh. Multiple rapid calls (e.g. per-file [FileWatcher.addFile]
* notifies during bootstrap) coalesce: only the latest generation parses and emits.
* Paths are snapshotted when the work runs, not when it is scheduled, so a burst of
* sync registrations yields one full-list emit.
*
* [MutableStateFlow] skips collector notifications when the new list equals the previous.
*/
internal fun refreshClusters() {
logger.info("Reparsing kubeconfig files. Monitored paths: $monitoredPaths")
val allClusters = kubeConfigUtils.getClusters(monitoredPaths.toList())
val gen = refreshGeneration.incrementAndGet()
scope.launch {
logger.info("Emitting ${allClusters.size} clusters to SharedFlow")
_clusters.emit(allClusters)
refreshMutex.withLock {
if (gen != refreshGeneration.get()) {
return@withLock
}
val monitored = fileWatcher.getMonitoredFiles().toList()
logger.info("Reparsing kubeconfig files. Monitored paths: $monitored")
val allClusters = kubeConfigUtils.getClusters(monitored)
if (gen != refreshGeneration.get()) {
return@withLock
}
val previous = _clusters.value
_clusters.value = allClusters
if (previous == allClusters) {
logger.info("Skipping notify; clusters unchanged (${allClusters.size})")
} else {
logger.info(
"Updated clusters (${allClusters.size}): " +
allClusters.map { "${it.name}@${it.url}" }
)
}
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Unhandled parsing errors from kubeConfigUtils.getClusters().

The call reads/parses external kubeconfig files but isn't guarded. An exception (malformed YAML, permission error, etc.) propagates out of the launched coroutine uncaught — the mutex is released fine, but the refresh silently fails with no structured error handling or user-visible feedback, and _clusters is left stale without any signal that a refresh failed.

🛡️ Proposed fix
                 val monitored = fileWatcher.getMonitoredFiles().toList()
                 logger.info("Reparsing kubeconfig files. Monitored paths: $monitored")
-                val allClusters = kubeConfigUtils.getClusters(monitored)
+                val allClusters = try {
+                    kubeConfigUtils.getClusters(monitored)
+                } catch (e: Exception) {
+                    logger.warn("Failed to parse kubeconfig files: $monitored", e)
+                    return@withLock
+                }
                 if (gen != refreshGeneration.get()) {
                     return@withLock
                 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/**
* Schedules a cluster refresh. Multiple rapid calls (e.g. per-file [FileWatcher.addFile]
* notifies during bootstrap) coalesce: only the latest generation parses and emits.
* Paths are snapshotted when the work runs, not when it is scheduled, so a burst of
* sync registrations yields one full-list emit.
*
* [MutableStateFlow] skips collector notifications when the new list equals the previous.
*/
internal fun refreshClusters() {
logger.info("Reparsing kubeconfig files. Monitored paths: $monitoredPaths")
val allClusters = kubeConfigUtils.getClusters(monitoredPaths.toList())
val gen = refreshGeneration.incrementAndGet()
scope.launch {
logger.info("Emitting ${allClusters.size} clusters to SharedFlow")
_clusters.emit(allClusters)
refreshMutex.withLock {
if (gen != refreshGeneration.get()) {
return@withLock
}
val monitored = fileWatcher.getMonitoredFiles().toList()
logger.info("Reparsing kubeconfig files. Monitored paths: $monitored")
val allClusters = kubeConfigUtils.getClusters(monitored)
if (gen != refreshGeneration.get()) {
return@withLock
}
val previous = _clusters.value
_clusters.value = allClusters
if (previous == allClusters) {
logger.info("Skipping notify; clusters unchanged (${allClusters.size})")
} else {
logger.info(
"Updated clusters (${allClusters.size}): " +
allClusters.map { "${it.name}@${it.url}" }
)
}
}
}
/**
* Schedules a cluster refresh. Multiple rapid calls (e.g. per-file [FileWatcher.addFile]
* notifies during bootstrap) coalesce: only the latest generation parses and emits.
* Paths are snapshotted when the work runs, not when it is scheduled, so a burst of
* sync registrations yields one full-list emit.
*
* [MutableStateFlow] skips collector notifications when the new list equals the previous.
*/
internal fun refreshClusters() {
val gen = refreshGeneration.incrementAndGet()
scope.launch {
refreshMutex.withLock {
if (gen != refreshGeneration.get()) {
return@withLock
}
val monitored = fileWatcher.getMonitoredFiles().toList()
logger.info("Reparsing kubeconfig files. Monitored paths: $monitored")
val allClusters = try {
kubeConfigUtils.getClusters(monitored)
} catch (e: Exception) {
logger.warn("Failed to parse kubeconfig files: $monitored", e)
return@withLock
}
if (gen != refreshGeneration.get()) {
return@withLock
}
val previous = _clusters.value
_clusters.value = allClusters
if (previous == allClusters) {
logger.info("Skipping notify; clusters unchanged (${allClusters.size})")
} else {
logger.info(
"Updated clusters (${allClusters.size}): " +
allClusters.map { "${it.name}@${it.url}" }
)
}
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/kotlin/com/redhat/devtools/gateway/kubeconfig/KubeConfigMonitor.kt`
around lines 94 - 126, Handle exceptions from kubeConfigUtils.getClusters()
within refreshClusters’ launched coroutine, logging a structured error and
surfacing the refresh failure through the existing user-visible error mechanism.
Keep _clusters unchanged when parsing fails, while preserving mutex handling and
generation checks for successful refreshes.

@adietish adietish left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I dont understand why in this PR (that Cursor created) the code is actually bypassing the proxy rather than set it up as configured in the IntellIJ settings

*/
protected fun createHttpClient(sslContext: SSLContext, trustManager: X509TrustManager): OkHttpClient =
OkHttpClient.Builder()
.proxy(Proxy.NO_PROXY)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

odd, why would it bypass the proxy? Dont we want to the exact opposite which is to set up the proxy that is possibly configured in the Gateway/JetBrains IDEA settings

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants