Skip to content

Commit acd58ba

Browse files
committed
feat: allow to save cluster + token to kubeconf (#23649)
Signed-off-by: Andre Dietisheim <[email protected]> Assisted by: gemini-cli Assisted by: cursor Assisted by: qwen-code
1 parent 5c46dfa commit acd58ba

File tree

7 files changed

+548
-15
lines changed

7 files changed

+548
-15
lines changed

build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ dependencies {
4040
testImplementation("io.mockk:mockk:1.14.6")
4141
testImplementation("io.mockk:mockk-agent-jvm:1.14.6")
4242

43+
// Use version compatible with IntelliJ platform 2025.1.1 to avoid service loader conflicts
4344
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")
4445
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.2")
4546

src/main/kotlin/com/redhat/devtools/gateway/kubeconfig/KubeConfigUtils.kt

Lines changed: 62 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,13 @@ object KubeConfigUtils {
2121
fun getClusters(kubeconfigPaths: List<Path>): List<Cluster> {
2222
logger.info("Getting clusters from kubeconfig paths: $kubeconfigPaths")
2323
val kubeConfigs = toKubeConfigs(kubeconfigPaths)
24-
logger.info("Loaded ${kubeConfigs.size} kubeconfig files")
24+
logger.info("Loaded ${kubeConfigs.size} kubeconfig files from paths: $kubeconfigPaths")
25+
2526
val clusters = kubeConfigs
2627
.flatMap { kubeConfig ->
27-
val clusters = KubeConfigNamedCluster.fromKubeConfig(kubeConfig)
28-
clusters.map { clusterEntry ->
28+
val namedClusters = KubeConfigNamedCluster.fromKubeConfig(kubeConfig)
29+
logger.info("kubeConfig.clusters size for this config: ${kubeConfig.clusters?.size}")
30+
namedClusters.map { clusterEntry ->
2931
val cluster = toCluster(clusterEntry, kubeConfig)
3032
logger.info("Parsed cluster: ${cluster.name} at ${cluster.url}")
3133
cluster
@@ -99,21 +101,75 @@ object KubeConfigUtils {
99101

100102
fun getAllConfigsMerged(): String? {
101103
val kubeConfigPaths = getAllConfigs()
102-
103104
if (kubeConfigPaths.isEmpty()) {
104105
logger.debug("No kubeconfig files found.")
105106
return null
106107
}
107-
108108
val mergedKubeConfigs = mergeConfigs(kubeConfigPaths)
109109
if (mergedKubeConfigs.isEmpty()) {
110110
logger.debug("No valid kubeconfig content found.")
111111
return null
112112
}
113-
114113
return mergedKubeConfigs
115114
}
116115

116+
fun getCurrentUser(kubeConfig: KubeConfig): String {
117+
try {
118+
val currentContextName = kubeConfig.currentContext
119+
val currentContext = (kubeConfig.contexts as? List<*>)?.find {
120+
(it as? Map<*, *>)?.get("name") == currentContextName
121+
}
122+
val contextMap = currentContext as? Map<*, *>
123+
return (contextMap?.get("context") as? Map<*, *>)?.get("user") as? String ?: ""
124+
} catch (e: Exception) {
125+
logger.warn("Failed to get current user from kubeconfig: ${e.message}", e)
126+
return ""
127+
}
128+
}
129+
130+
fun getCurrentContext(kubeConfig: KubeConfig): KubeConfigNamedContext? {
131+
val currentContextName = kubeConfig.currentContext
132+
return (kubeConfig.contexts as? List<*>)
133+
?.mapNotNull { contextObject ->
134+
val contextMap = contextObject as? Map<*, *> ?: return@mapNotNull null
135+
val name = contextMap["name"] as? String ?: return@mapNotNull null
136+
val contextDetails = contextMap["context"] as? Map<*, *> ?: return@mapNotNull null
137+
if (name == currentContextName) {
138+
KubeConfigNamedContext(
139+
name = name,
140+
context = KubeConfigContext.fromMap(contextDetails) ?: return@mapNotNull null
141+
)
142+
} else {
143+
null
144+
}
145+
}?.firstOrNull()
146+
}
147+
148+
fun getCurrentContextClusterName(): String? {
149+
return try {
150+
getAllConfigs().firstNotNullOfOrNull { path ->
151+
if (!isValid(path)) {
152+
null
153+
} else {
154+
try {
155+
val kubeConfig = KubeConfig.loadKubeConfig(path.toFile().bufferedReader())
156+
if (!kubeConfig.currentContext.isNullOrBlank()) {
157+
getCurrentContext(kubeConfig)?.context?.cluster
158+
} else {
159+
null
160+
}
161+
} catch (e: Exception) {
162+
logger.warn("Error parsing kubeconfig file '$path' while determining current context: ${e.message}", e)
163+
null
164+
}
165+
}
166+
}
167+
} catch (e: Exception) {
168+
logger.warn("Failed to get current context cluster name from kubeconfig: ${e.message}", e)
169+
null
170+
}
171+
}
172+
117173
private fun mergeConfigs(kubeconfigs: List<Path>): String {
118174
return kubeconfigs
119175
.mapNotNull { path ->
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
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.kubeconfig
13+
14+
import io.kubernetes.client.util.KubeConfig
15+
import java.io.File
16+
import java.io.FileReader
17+
import kotlinx.coroutines.Dispatchers
18+
import kotlinx.coroutines.withContext
19+
import org.yaml.snakeyaml.Yaml
20+
import java.io.FileWriter
21+
22+
class KubeConfigWriter {
23+
companion object {
24+
/**
25+
* Finds the path to the kubeconfig file that contains a specific user.
26+
*
27+
* @param userName The name of the user to find.
28+
* @param kubeConfigEnv The value of the KUBECONFIG environment variable.
29+
* @return The absolute path to the kubeconfig file, or null if the user is not found.
30+
*/
31+
fun findKubeConfigFileForUser(userName: String, kubeConfigEnv: String?): String? {
32+
val files = if (kubeConfigEnv.isNullOrEmpty()) {
33+
listOf(File(System.getProperty("user.home"), ".kube/config"))
34+
} else {
35+
kubeConfigEnv.split(File.pathSeparator).map { File(it) }
36+
}
37+
for (file in files) {
38+
if (!file.exists() || !file.isFile) {
39+
continue
40+
}
41+
try {
42+
val kubeConfig = KubeConfig.loadKubeConfig(FileReader(file))
43+
val users = kubeConfig.users as? List<*>
44+
val userFound = users?.any { userObject ->
45+
val userMap = userObject as? Map<*, *>
46+
userMap?.get("name") as? String == userName
47+
}
48+
if (userFound == true) {
49+
return file.absolutePath
50+
}
51+
} catch (e: Exception) {
52+
// Log the exception, e.g., failed to parse
53+
}
54+
}
55+
return null
56+
}
57+
58+
/**
59+
* Applies changes to a kubeconfig file and saves it.
60+
*
61+
* @param filePath The path to the kubeconfig file to modify.
62+
* @param serverUrl The new server URL.
63+
* @param token The new token.
64+
*/
65+
suspend fun applyChangesAndSave(filePath: String, serverUrl: String, token: String) = withContext(Dispatchers.IO) {
66+
val yaml = Yaml()
67+
val file = File(filePath)
68+
val kubeConfigMap = yaml.load(file.inputStream()) as? MutableMap<String, Any> ?: mutableMapOf()
69+
70+
val clusters = kubeConfigMap["clusters"] as? MutableList<MutableMap<String, Any>> ?: mutableListOf()
71+
val existingCluster = clusters.find { (it["cluster"] as? Map<*, *>)?.get("server") == serverUrl }
72+
73+
if (existingCluster != null) {
74+
// Update existing
75+
val clusterName = existingCluster["name"] as? String
76+
val contexts = kubeConfigMap["contexts"] as? List<Map<String, Any>> ?: emptyList()
77+
val currentContext = contexts.find { it["name"] == kubeConfigMap["current-context"] }
78+
val userName = (currentContext?.get("context") as? Map<*, *>)?.get("user") as? String
79+
80+
if (userName != null) {
81+
val users = kubeConfigMap["users"] as? MutableList<MutableMap<String, Any>> ?: mutableListOf()
82+
val userToUpdate = users.find { it["name"] == userName }
83+
(userToUpdate?.get("user") as? MutableMap<String, Any>)?.set("token", token)
84+
}
85+
} else {
86+
// Create new
87+
val newName = serverUrl.substringAfter("://").replace(".", "-").replace(":", "-")
88+
val newClusterName = newName
89+
val newUserName = "$newName-user"
90+
val newContextName = "$newName-context"
91+
92+
val newCluster = mutableMapOf<String, Any>(
93+
"name" to newClusterName,
94+
"cluster" to mutableMapOf("server" to serverUrl)
95+
)
96+
clusters.add(newCluster)
97+
98+
val newUser = mutableMapOf<String, Any>(
99+
"name" to newUserName,
100+
"user" to mutableMapOf("token" to token)
101+
)
102+
val users = kubeConfigMap["users"] as? MutableList<MutableMap<String, Any>> ?: mutableListOf()
103+
users.add(newUser)
104+
105+
val newContext = mutableMapOf<String, Any>(
106+
"name" to newContextName,
107+
"context" to mutableMapOf(
108+
"cluster" to newClusterName,
109+
"user" to newUserName
110+
)
111+
)
112+
val contexts = kubeConfigMap["contexts"] as? MutableList<MutableMap<String, Any>> ?: mutableListOf()
113+
contexts.add(newContext)
114+
115+
kubeConfigMap["clusters"] = clusters
116+
kubeConfigMap["users"] = users
117+
kubeConfigMap["contexts"] = contexts
118+
kubeConfigMap["current-context"] = newContextName
119+
}
120+
121+
val writer = FileWriter(file)
122+
yaml.dump(kubeConfigMap, writer)
123+
writer.close()
124+
}
125+
}
126+
}

0 commit comments

Comments
 (0)