Skip to content

fix: removed duplicate code in KubeConfigUpdate. Added tests#310

Open
adietish wants to merge 1 commit into
redhat-developer:mainfrom
adietish:dedupe_kubeconfigupdate
Open

fix: removed duplicate code in KubeConfigUpdate. Added tests#310
adietish wants to merge 1 commit into
redhat-developer:mainfrom
adietish:dedupe_kubeconfigupdate

Conversation

@adietish
Copy link
Copy Markdown
Collaborator

No description provided.

Signed-off-by: Andre Dietisheim <adietish@redhat.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 26, 2026

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

Release Notes

  • Improvements

    • Enhanced client certificate authentication support with improved context creation workflows
    • Strengthened kubeconfig configuration persistence and state management across multiple files
  • Tests

    • Expanded test coverage for client certificate authentication scenarios
    • Added validation for configuration updates and context management edge cases

Walkthrough

This PR refactors the kubeconfig update pipeline to centralize persistence logic in the base class via an injected persisterFactory, exposes toName() as a Java-static method, and adds comprehensive test coverage for client-certificate context creation and token updates using a mocked persister.

Changes

Kubeconfig persistence and context refactoring

Layer / File(s) Summary
Expose toName() as Java-callable static method
src/main/kotlin/.../KubeConfigEntries.kt
toName() in KubeConfigNamedContext companion object is changed from private to @JvmStatic, making it callable from Java as a static method.
Centralize persistence infrastructure in base class
src/main/kotlin/.../KubeConfigUpdate.kt
KubeConfigUpdate constructor accepts a persisterFactory parameter. New saveConfigs and saveConfig helper methods centralize kubeconfig write decisions based on path and context changes. Persister instantiation switches to use the injected factory.
Move context creation and name uniqueness logic to base class
src/main/kotlin/.../KubeConfigUpdate.kt
Context entry creation, unique user/cluster/context name generation, and existing-name collection across all configs are moved from subclasses to a shared base-class ContextEntries data structure and helper methods.
Refactor UpdateClientCert and CreateContextWithClientCert subclasses
src/main/kotlin/.../KubeConfigUpdate.kt
UpdateClientCert and CreateContextWithClientCert now accept persisterFactory injection and delegate context entry creation and persistence to base-class helpers. Subclass-local save current-context logic is removed.
Test infrastructure refactoring and helper setup
src/test/kotlin/.../KubeConfigUpdateTest.kt
Test class uses an injected mocked ConfigPersister (via testPersisterFactory) instead of constructing BlockStyleFilePersister and temp files. New test data classes support client-cert flows; helper methods verify client certificate fields and set up mocks for existing-context updates.
Refactored CreateContext test suite with mocked persister
src/test/kotlin/.../KubeConfigUpdateTest.kt
All CreateContext tests (unique name generation, duplicate handling, null fields, current-context setting) are updated to use mocked persister and verify save() calls.
New and refactored UpdateToken, UpdateClientCert, CreateContextWithClientCert tests
src/test/kotlin/.../KubeConfigUpdateTest.kt
New test coverage for CreateContextWithClientCert context creation and unique user generation. UpdateToken tests expanded to validate token persistence and current-context updates across single and multi-config scenarios. New UpdateClientCert tests for certificate updates. Null preferences handling expanded.

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 2.13% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The pull request has no description provided by the author, making it impossible to assess its relevance to the changeset. Add a description explaining the refactoring objectives, benefits of deduplication, and test coverage improvements for better context.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: deduplication of code in KubeConfigUpdate and addition of comprehensive tests.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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 and usage tips.

@adietish adietish self-assigned this May 26, 2026
@adietish adietish moved this to Ready for Review in Eclipse Che Team C Backlog May 26, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
src/main/kotlin/com/redhat/devtools/gateway/kubeconfig/KubeConfigUpdate.kt (1)

74-75: ⚡ Quick win

Avoid positional saveConfig overloads with identical collection types.

These overloads take users, clusters, and contexts as separate ArrayList<Any?> parameters, and the private delegate reorders them. A future call-site swap will still compile and can write a malformed kubeconfig. Passing ContextEntries directly would make this helper much safer.

Refactor direction
-    protected fun saveConfig(
-        config: KubeConfig,
-        users: ArrayList<Any?>,
-        clusters: ArrayList<Any?>,
-        contexts: ArrayList<Any?>,
-        currentContext: String
-    ) {
-        saveConfig(contexts, clusters, users, config.preferences, currentContext, config.path)
-    }
+    protected fun saveConfig(config: KubeConfig, entries: ContextEntries) {
+        saveConfig(
+            entries.contexts,
+            entries.clusters,
+            entries.users,
+            config.preferences,
+            entries.currentContextName,
+            config.path
+        )
+    }

Then update the two callers to pass entries / contextEntries directly.

Also applies to: 85-95

🤖 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/KubeConfigUpdate.kt`
around lines 74 - 75, The saveConfig overloads currently accept separate
ArrayList<Any?> for users, clusters, and contexts which is error-prone; change
the private helper signature (the delegated saveConfig) to accept the
strongly-typed ContextEntries (or the appropriate ContextEntries wrapper type)
instead of positional ArrayList<Any?> parameters, update the two callers that
currently pass entries / contextEntries to pass the ContextEntries object
directly, and remove any manual reordering logic inside saveConfig so the
original entries are preserved by type; reference the saveConfig overloads and
the callers that pass entries/contextEntries to locate the changes.
🤖 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 `@src/main/kotlin/com/redhat/devtools/gateway/kubeconfig/KubeConfigUpdate.kt`:
- Around line 209-214: When setting a token in setTokenFor (and in the
UpdateToken flow), also clear any existing client cert fields to avoid
presenting both credentials: after Utils.setValue(this, token,
arrayOf("user","token")) remove or null out "user.client-certificate-data" and
"user.client-key-data" on the same user object (use the existing Utils helpers —
e.g., Utils.setValue(this, null, arrayOf("user","client-certificate-data")) /
Utils.setValue(this, null, arrayOf("user","client-key-data")) or the
Utils.removeValue equivalent) so UpdateToken mirrors UpdateClientCert's behavior
of removing the opposite auth field.

In
`@src/test/kotlin/com/redhat/devtools/gateway/kubeconfig/KubeConfigUpdateTest.kt`:
- Around line 29-32: The tests currently use a single mock (mockPersister)
returned by testPersisterFactory so they only assert multiple save() calls
rather than per-file routing; change testPersisterFactory to create and return a
distinct mock ConfigPersister for each File (store them in a map keyed by
File/path) and update assertions to verify each file's dedicated mock was
invoked exactly once (e.g., verify(exactly = 1) {
persistersByFile[userFile].save(...) } and verify for the other file including
the context name matcher) so the test validates that each config file is
persisted by its own factory-produced persister.

---

Nitpick comments:
In `@src/main/kotlin/com/redhat/devtools/gateway/kubeconfig/KubeConfigUpdate.kt`:
- Around line 74-75: The saveConfig overloads currently accept separate
ArrayList<Any?> for users, clusters, and contexts which is error-prone; change
the private helper signature (the delegated saveConfig) to accept the
strongly-typed ContextEntries (or the appropriate ContextEntries wrapper type)
instead of positional ArrayList<Any?> parameters, update the two callers that
currently pass entries / contextEntries to pass the ContextEntries object
directly, and remove any manual reordering logic inside saveConfig so the
original entries are preserved by type; reference the saveConfig overloads and
the callers that pass entries/contextEntries to locate the changes.
🪄 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: d0d178b7-c346-4934-b30f-3011c0683346

📥 Commits

Reviewing files that changed from the base of the PR and between 9f7b425 and c40fe65.

📒 Files selected for processing (3)
  • src/main/kotlin/com/redhat/devtools/gateway/kubeconfig/KubeConfigEntries.kt
  • src/main/kotlin/com/redhat/devtools/gateway/kubeconfig/KubeConfigUpdate.kt
  • src/test/kotlin/com/redhat/devtools/gateway/kubeconfig/KubeConfigUpdateTest.kt

Comment on lines +209 to 214
private fun setTokenFor(username: String, config: KubeConfig) {
config.users?.find { user ->
username == Utils.getValue(user, arrayOf("name"))
}?.apply {
Utils.setValue(this, token, arrayOf("user", "token"))
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="src/main/kotlin/com/redhat/devtools/gateway/kubeconfig/KubeConfigUpdate.kt"

echo "== File existence =="
ls -l "$FILE"

echo
echo "== setTokenFor block (around lines 190-240) =="
nl -ba "$FILE" | sed -n '180,240p'

echo
echo "== UpdateToken / UpdateClientCert (best-effort ranges) =="
rg -n "class UpdateToken|class UpdateClientCert|fun setTokenFor|setClientCert" "$FILE" -n || true

# Print larger region around each match line
for ln in $(rg -n "class UpdateToken|class UpdateClientCert|fun setTokenFor|setClientCert" "$FILE" | cut -d: -f1 | sort -n | uniq); do
  echo
  echo "== Context around $ln =="
  nl -ba "$FILE" | sed -n "$((ln-30)),$((ln+40))p"
done

Repository: redhat-developer/devspaces-gateway-plugin

Length of output: 341


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="src/main/kotlin/com/redhat/devtools/gateway/kubeconfig/KubeConfigUpdate.kt"

echo "== setTokenFor block (approx 180-240) =="
awk 'NR>=180 && NR<=240 {printf "%6d  %s\n", NR, $0}' "$FILE"

echo
echo "== Auth-mode updater classes/logic near matches =="
rg -n "class UpdateToken|class UpdateClientCert|fun setTokenFor|setClientCert|removeValue|client-certificate-data|client-key-data|arrayOf\\(\"user\", \"token\"\\)" "$FILE" || true

Repository: redhat-developer/devspaces-gateway-plugin

Length of output: 3576


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="src/main/kotlin/com/redhat/devtools/gateway/kubeconfig/KubeConfigUpdate.kt"

echo "== UpdateClientCert.setClientCert block =="
awk 'NR>=232 && NR<=285 {printf "%6d  %s\n", NR, $0}' "$FILE"

echo
echo "== Utils.removeValue usage in this file =="
rg -n "Utils\\.removeValue" "$FILE" || true

echo
echo "== Find Utils.removeValue definition (search repo) =="
rg -n "fun removeValue\\(" -S . || true

echo
echo "== Search for token vs cert precedence in repo comments/usages =="
rg -n "client-certificate-data|client-key-data|auth.*token|precedence" -S src/main/kotlin || true

Repository: redhat-developer/devspaces-gateway-plugin

Length of output: 11712


🌐 Web query:

Kubernetes client-go kubeconfig precedence when both token and client-certificate-data are set

💡 Result:

In client-go, when both a bearer token (token or token-file) and a client certificate (client-certificate-data and client-key-data) are present in the kubeconfig, client-go treats this as a mutual exclusivity error: it will not construct a request with both token auth and certificate auth. Specifically, transport/round_trippers.go checks for “basic auth or token auth” and errors if both username/password or bearer token are set together (it refuses combining certain auth types) [1]. In the rest.Config structures, the client TLS material has its own precedence rules (CertData overrides CertFile, KeyData overrides KeyFile) [2] [3], and the bearer token file has precedence over bearer token string (the last successfully read value from the token file takes precedence over the bearer token string) [2] [3]. But precedence between token auth vs client-certificate-data is effectively: “no precedence because it’s invalid to have both”; client-go will fail configuration/transport setup rather than choose one auth method. Practical implication: ensure your kubeconfig “user” section includes either: - token (or tokenFile / bearerTokenFile) and not client-certificate-data/client-key-data, or - client-certificate-data/client-key-data (and not a bearer token). (Separate from client-go’s behavior, the Kubernetes API server supports X.509 client certificate authentication when presented and validated against its client trust CA) [4].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="src/main/kotlin/com/redhat/devtools/gateway/kubeconfig/KubeConfigUpdate.kt"
UTILS="src/main/kotlin/com/redhat/devtools/gateway/openshift/Utils.kt"

echo "== All occurrences of client-certificate-data/client-key-data in KubeConfigUpdate.kt =="
rg -n "client-certificate-data|client-key-data|removeValue\\(this, arrayOf\\(\"user\", \"token\"\\)" "$FILE" || true

echo
echo "== UpdateToken setTokenFor body (exact) =="
awk 'NR>=200 && NR<=220 {printf "%6d  %s\n", NR, $0}' "$FILE"

echo
echo "== UpdateClientCert setClientCert body (exact) =="
awk 'NR>=256 && NR<=275 {printf "%6d  %s\n", NR, $0}' "$FILE"

echo
echo "== Utils.removeValue implementation (exact) =="
awk 'NR>=1 && NR<=120 {printf "%6d  %s\n", NR, $0}' "$UTILS"

Repository: redhat-developer/devspaces-gateway-plugin

Length of output: 5117


🌐 Web query:

In Kubernetes/client-go, what happens if a kubeconfig user contains both bearer token (user.token) and client-certificate-data/client-key-data? Is it rejected, ignored, or does it authenticate via both?

💡 Result:

In Kubernetes client-go, if a kubeconfig user definition contains both a bearer token (user.token) and client-certificate-data/client-key-data, the resulting rest.Config object will contain both sets of credentials [1][2]. When client-go builds the REST client configuration from the kubeconfig file, it populates the rest.Config struct with the values present in the user section [1]. Specifically: - The Token is stored in the BearerToken field [1][3]. - The certificate and key data are stored in the TLSClientConfig.CertData and TLSClientConfig.KeyData fields [1][3][2]. Because these fields are independent within the rest.Config object, the client library does not reject or ignore one in favor of the other during the loading process [1][3]. When a request is eventually made, the underlying transport used by client-go will generally prioritize the authentication methods it supports. In practice, when both are provided, the Kubernetes API server or the client transport logic may behave differently based on the specific implementation, but client-go itself does not "choose" one over the other at the configuration loading stage [1][2]. If you are using these credentials, it is important to note that the API server's authentication layer will evaluate them according to its own configured authentication strategies [4]. If the server receives a request with both a client certificate and an Authorization header (bearer token), the API server's authentication chain will typically attempt to authenticate the user using the provided methods sequentially [4]. Most Kubernetes API server configurations will successfully authenticate the user if either method is valid, though this can vary depending on the specific enabled authentication modules [4].

Citations:


Clear client-cert fields when switching back to token auth.

UpdateClientCert removes user.token, but UpdateToken only sets user.token and leaves user.client-certificate-data / user.client-key-data in the same kubeconfig user. Since client-go can load token + client cert together into the same config, the resulting requests may present both credentials, letting the API server authenticate via either method and causing inconsistent auth behavior.

Suggested fix
         private fun setTokenFor(username: String, config: KubeConfig) {
             config.users?.find { user ->
                 username == Utils.getValue(user, arrayOf("name"))
             }?.apply {
                 Utils.setValue(this, token, arrayOf("user", "token"))
+                Utils.removeValue(this, arrayOf("user", "client-certificate-data"))
+                Utils.removeValue(this, arrayOf("user", "client-key-data"))
             }
         }
📝 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
private fun setTokenFor(username: String, config: KubeConfig) {
config.users?.find { user ->
username == Utils.getValue(user, arrayOf("name"))
}?.apply {
Utils.setValue(this, token, arrayOf("user", "token"))
}
private fun setTokenFor(username: String, config: KubeConfig) {
config.users?.find { user ->
username == Utils.getValue(user, arrayOf("name"))
}?.apply {
Utils.setValue(this, token, arrayOf("user", "token"))
Utils.removeValue(this, arrayOf("user", "client-certificate-data"))
Utils.removeValue(this, arrayOf("user", "client-key-data"))
}
}
🤖 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/KubeConfigUpdate.kt`
around lines 209 - 214, When setting a token in setTokenFor (and in the
UpdateToken flow), also clear any existing client cert fields to avoid
presenting both credentials: after Utils.setValue(this, token,
arrayOf("user","token")) remove or null out "user.client-certificate-data" and
"user.client-key-data" on the same user object (use the existing Utils helpers —
e.g., Utils.setValue(this, null, arrayOf("user","client-certificate-data")) /
Utils.setValue(this, null, arrayOf("user","client-key-data")) or the
Utils.removeValue equivalent) so UpdateToken mirrors UpdateClientCert's behavior
of removing the opposite auth field.

Comment on lines +29 to 32
private val kubeConfigPath = Path.of("/test/kubeconfig-primary")
private val mockPersister = mockk<ConfigPersister>(relaxed = true)
private val testPersisterFactory: (File) -> ConfigPersister = { mockPersister }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Per-file persistence routing is not actually validated by these tests.

testPersisterFactory always returns the same mockPersister, so “different files” tests only prove multiple save() calls happened, not that each config file was persisted through its own factory-produced persister. A file-routing regression can still pass.

Suggested test hardening
- private val mockPersister = mockk<ConfigPersister>(relaxed = true)
- private val testPersisterFactory: (File) -> ConfigPersister = { mockPersister }
+ private val persistersByFile = mutableMapOf<File, ConfigPersister>()
+ private val testPersisterFactory: (File) -> ConfigPersister = { file ->
+     persistersByFile.getOrPut(file) { mockk(relaxed = true) }
+ }
// in split-file tests
val userPersister = persistersByFile[userConfigFile.toFile()]!!
val currentContextPersister = persistersByFile[currentContextConfigFile.toFile()]!!

verify(exactly = 1) { userPersister.save(any(), any(), any(), any(), any()) }
verify(exactly = 1) { currentContextPersister.save(any(), any(), any(), any(), match { it == data.contextName }) }

As per coding guidelines, "Focus on major issues impacting performance, readability, maintainability and security. Avoid nitpicks and avoid verbosity."

Also applies to: 249-320, 365-450

🤖 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/test/kotlin/com/redhat/devtools/gateway/kubeconfig/KubeConfigUpdateTest.kt`
around lines 29 - 32, The tests currently use a single mock (mockPersister)
returned by testPersisterFactory so they only assert multiple save() calls
rather than per-file routing; change testPersisterFactory to create and return a
distinct mock ConfigPersister for each File (store them in a map keyed by
File/path) and update assertions to verify each file's dedicated mock was
invoked exactly once (e.g., verify(exactly = 1) {
persistersByFile[userFile].save(...) } and verify for the other file including
the context name matcher) so the test validates that each config file is
persisted by its own factory-produced persister.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant