Skip to content

Commit c19fbc1

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 e159c5f commit c19fbc1

26 files changed

+3775
-1106
lines changed
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
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 com.fasterxml.jackson.databind.ObjectMapper
15+
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory
16+
import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator
17+
import io.kubernetes.client.persister.ConfigPersister
18+
import java.io.File
19+
20+
class BlockStyleFilePersister(private val file: File) : ConfigPersister {
21+
22+
@Throws(java.io.IOException::class)
23+
override fun save(
24+
contexts: ArrayList<Any?>,
25+
clusters: ArrayList<Any?>,
26+
users: ArrayList<Any?>,
27+
preferences: Any?,
28+
currentContext: String?
29+
) {
30+
val config = mapOf(
31+
"apiVersion" to "v1",
32+
"kind" to "Config",
33+
"current-context" to currentContext,
34+
"preferences" to preferences,
35+
36+
"clusters" to clusters,
37+
"contexts" to contexts,
38+
"users" to users,
39+
)
40+
41+
synchronized(file) {
42+
val yamlFactory = YAMLFactory().apply {
43+
configure(YAMLGenerator.Feature.MINIMIZE_QUOTES, true)
44+
configure(YAMLGenerator.Feature.INDENT_ARRAYS_WITH_INDICATOR, true)
45+
}
46+
val mapper = ObjectMapper(yamlFactory)
47+
mapper.writeValue(file, config)
48+
}
49+
}
50+
}

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

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import kotlin.io.path.exists
1818
import kotlin.io.path.isRegularFile
1919

2020
class FileWatcher(
21-
private val scope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default),
21+
private val scope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO),
2222
private val dispatcher: CoroutineDispatcher = Dispatchers.IO,
2323
private val watchService: WatchService = FileSystems.getDefault().newWatchService()
2424
) {
@@ -29,23 +29,31 @@ class FileWatcher(
2929

3030
fun start() {
3131
this.watchJob = scope.launch(dispatcher) {
32-
while (isActive) {
33-
val key = watchService.poll(100, java.util.concurrent.TimeUnit.MILLISECONDS)
34-
if (key == null) {
35-
delay(100)
36-
continue
32+
try {
33+
while (isActive) {
34+
val key = watchService.poll(100, java.util.concurrent.TimeUnit.MILLISECONDS)
35+
if (key == null) {
36+
delay(100)
37+
continue
38+
}
39+
val dir = registeredKeys[key] ?: continue
40+
pollEvents(key, dir)
41+
key.reset()
3742
}
38-
val dir = registeredKeys[key] ?: continue
39-
pollEvents(key, dir)
40-
key.reset()
43+
} catch (e: ClosedWatchServiceException) {
44+
// Watch service was closed, exit gracefully
4145
}
4246
}
4347
}
4448

4549
fun stop() {
4650
watchJob?.cancel()
4751
watchJob = null
48-
watchService.close()
52+
try {
53+
watchService.close()
54+
} catch (e: Exception) {
55+
// Ignore exceptions when closing
56+
}
4957
}
5058

5159
fun addFile(path: Path): FileWatcher {

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

Lines changed: 131 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -11,35 +11,42 @@
1111
*/
1212
package com.redhat.devtools.gateway.kubeconfig
1313

14+
import com.redhat.devtools.gateway.kubeconfig.KubeConfigUtils.sanitizeName
15+
import com.redhat.devtools.gateway.kubeconfig.KubeConfigUtils.urlToName
1416
import io.kubernetes.client.util.KubeConfig
15-
import kotlin.collections.get
17+
1618

1719
/**
1820
* Domain classes representing the structure of a kubeconfig file.
1921
*/
2022
data class KubeConfigNamedCluster(
21-
val name: String,
22-
val cluster: KubeConfigCluster
23+
val cluster: KubeConfigCluster,
24+
val name: String = toName(cluster)
2325
) {
26+
2427
companion object {
25-
fun fromMap(name: String, clusterObject: Any?): KubeConfigNamedCluster? {
26-
val clusterMap = clusterObject as? Map<*, *> ?: return null
27-
val clusterDetails = clusterMap["cluster"] as? Map<*, *> ?: return null
28-
28+
fun fromMap(map: Map<*,*>): KubeConfigNamedCluster? {
29+
val name = map["name"] as? String ?: return null
30+
val clusterDetails = map["cluster"] as? Map<*, *> ?: return null
31+
2932
return KubeConfigNamedCluster(
3033
name = name,
3134
cluster = KubeConfigCluster.fromMap(clusterDetails) ?: return null
3235
)
3336
}
34-
35-
fun fromKubeConfig(kubeConfig: KubeConfig): List<KubeConfigNamedCluster> {
36-
return (kubeConfig.clusters as? List<*>)
37-
?.mapNotNull { clusterObject ->
38-
val clusterMap = clusterObject as? Map<*, *> ?: return@mapNotNull null
39-
val name = clusterMap["name"] as? String ?: return@mapNotNull null
40-
fromMap(name, clusterObject)
41-
} ?: emptyList()
37+
38+
private fun toName(cluster: KubeConfigCluster): String {
39+
val url = cluster.server
40+
return urlToName(url) ?: url
4241
}
42+
43+
}
44+
45+
fun toMap(): MutableMap<String, Any> {
46+
return mutableMapOf(
47+
"name" to name,
48+
"cluster" to cluster.toMap()
49+
)
4350
}
4451
}
4552

@@ -58,90 +65,157 @@ data class KubeConfigCluster(
5865
)
5966
}
6067
}
68+
69+
fun toMap(): MutableMap<String, Any> {
70+
val map = mutableMapOf<String, Any>()
71+
map["server"] = server
72+
certificateAuthorityData?.let { map["certificate-authority-data"] = it }
73+
insecureSkipTlsVerify?.let { map["insecure-skip-tls-verify"] = it }
74+
return map
75+
}
6176
}
6277

6378
data class KubeConfigNamedContext(
64-
val name: String,
65-
val context: KubeConfigContext
79+
val context: KubeConfigContext,
80+
val name: String = toName(context.user, context.cluster)
6681
) {
6782
companion object {
68-
fun getByClusterName(clusterName: String, kubeConfig: KubeConfig): KubeConfigNamedContext? {
69-
return (kubeConfig.contexts as? List<*>)?.firstNotNullOfOrNull { contextObject ->
70-
val contextMap = contextObject as? Map<*, *> ?: return@firstNotNullOfOrNull null
71-
val contextName = contextMap["name"] as? String ?: return@firstNotNullOfOrNull null
72-
val contextEntry = getByName(contextName, contextObject)
73-
if (contextEntry?.context?.cluster == clusterName) {
74-
contextEntry
75-
} else {
76-
null
77-
}
83+
84+
private fun toName(user: String, cluster: String): String {
85+
val sanitizedUser = sanitizeName(user)
86+
val sanitizedCluster = sanitizeName(cluster)
87+
88+
return when {
89+
sanitizedUser.isEmpty() && sanitizedCluster.isEmpty() -> ""
90+
sanitizedUser.isEmpty() -> sanitizedCluster
91+
sanitizedCluster.isEmpty() -> sanitizedUser
92+
else -> "$sanitizedUser/$sanitizedCluster"
7893
}
7994
}
8095

81-
private fun getByName(name: String, contextObject: Any?): KubeConfigNamedContext? {
82-
val contextMap = contextObject as? Map<*, *> ?: return null
83-
val contextDetails = contextMap["context"] as? Map<*, *> ?: return null
96+
fun getByClusterName(name: String?, allConfigs: List<KubeConfig>): KubeConfigNamedContext? {
97+
return allConfigs
98+
.flatMap {
99+
it.contexts ?: emptyList()
100+
}
101+
.mapNotNull {
102+
fromMap(it as? Map<*,*>)
103+
}
104+
.firstOrNull { context ->
105+
name == context.context.cluster
106+
}
107+
}
108+
109+
fun getByName(clusterName: String, kubeConfig: KubeConfig): KubeConfigNamedContext? {
110+
return (kubeConfig.contexts as? List<*>)
111+
?.firstNotNullOfOrNull { contextObject ->
112+
val contextEntry = fromMap(contextObject as? Map<*, *>)
113+
if (contextEntry?.context?.cluster == clusterName) {
114+
contextEntry
115+
} else {
116+
null
117+
}
118+
}
119+
}
84120

121+
fun fromMap(map: Map<*, *>?): KubeConfigNamedContext? {
122+
if (map == null) return null
123+
val name = map["name"] as? String ?: return null
124+
val context = map["context"] as? Map<*, *> ?: return null
85125
return KubeConfigNamedContext(
86126
name = name,
87-
context = KubeConfigContext.fromMap(contextDetails) ?: return null
127+
context = KubeConfigContext.fromMap(context) ?: return null
88128
)
89129
}
90130
}
131+
132+
fun toMap(): MutableMap<String, Any> {
133+
return mutableMapOf(
134+
"name" to name,
135+
"context" to context.toMap()
136+
)
137+
}
91138
}
92139

93140
data class KubeConfigContext(
94-
val cluster: String,
95141
val user: String,
142+
val cluster: String,
96143
val namespace: String? = null
97144
) {
98145
companion object {
99146
fun fromMap(map: Map<*, *>): KubeConfigContext? {
100147
val cluster = map["cluster"] as? String ?: return null
101148
val user = map["user"] as? String ?: return null
102-
149+
103150
return KubeConfigContext(
104151
cluster = cluster,
105152
user = user,
106153
namespace = map["namespace"] as? String
107154
)
108155
}
109156
}
157+
158+
fun toMap(): MutableMap<String, Any> {
159+
val map = mutableMapOf<String, Any>()
160+
map["cluster"] = cluster
161+
map["user"] = user
162+
namespace?.let { map["namespace"] = it }
163+
return map
164+
}
110165
}
111166

112167
data class KubeConfigNamedUser(
113-
val name: String,
114-
val user: KubeConfigUser
168+
val user: KubeConfigUser?,
169+
val name: String
115170
) {
116171
companion object {
117-
fun fromMap(name: String, userObject: Any?): KubeConfigNamedUser? {
118-
val userMap = userObject as? Map<*, *> ?: return null
119-
val userDetails = userMap["user"] as? Map<*, *> ?: return null
120-
172+
173+
fun getByName(userName: String, config: KubeConfig?): KubeConfigNamedUser? {
174+
return (config?.users ?: emptyList<Any>())
175+
.mapNotNull {
176+
it as? Map<String, Any>
177+
}
178+
.firstOrNull { user ->
179+
userName == user["name"]
180+
}
181+
?.let { fromMap(it) }
182+
}
183+
184+
fun fromMap(map: Map<*,*>): KubeConfigNamedUser? {
185+
val name = map["name"] as? String ?: return null
186+
val userDetails = map["user"] as? Map<*, *> ?: return null
187+
val user = KubeConfigUser.fromMap(userDetails)
121188
return KubeConfigNamedUser(
122189
name = name,
123-
user = KubeConfigUser.fromMap(userDetails)
190+
user = user
124191
)
125192
}
126-
193+
127194
fun getUserTokenForCluster(clusterName: String, kubeConfig: KubeConfig): String? {
128-
val contextEntry = KubeConfigNamedContext.getByClusterName(clusterName, kubeConfig) ?: return null
195+
val contextEntry = KubeConfigNamedContext.getByName(clusterName, kubeConfig) ?: return null
129196
val userObject = (kubeConfig.users as? List<*>)?.firstOrNull { userObject ->
130197
val userMap = userObject as? Map<*, *> ?: return@firstOrNull false
131198
val userName = userMap["name"] as? String ?: return@firstOrNull false
132199
userName == contextEntry.context.user
133-
} ?: return null
134-
return fromMap(contextEntry.context.user, userObject)?.user?.token
200+
} as? Map<*,*> ?: return null
201+
return fromMap(userObject)?.user?.token
135202
}
136203

137204
fun isTokenAuth(kubeConfig: KubeConfig): Boolean {
138205
return kubeConfig.credentials?.containsKey(KubeConfig.CRED_TOKEN_KEY) == true
139206
}
140207
}
208+
209+
fun toMap(): MutableMap<String, Any> {
210+
return mutableMapOf(
211+
"name" to name,
212+
"user" to (user?.toMap() ?: mutableMapOf())
213+
)
214+
}
141215
}
142216

143217
data class KubeConfigUser(
144-
val token: String? = null,
218+
var token: String? = null,
145219
val clientCertificateData: String? = null,
146220
val clientKeyData: String? = null,
147221
val username: String? = null,
@@ -158,5 +232,17 @@ data class KubeConfigUser(
158232
)
159233
}
160234
}
235+
236+
fun toMap(): MutableMap<String, Any> {
237+
val map = mutableMapOf<String, Any>()
238+
token?.let { map["token"] = it }
239+
clientCertificateData?.let { map["client-certificate-data"] = it }
240+
clientKeyData?.let { map["client-key-data"] = it }
241+
username?.let { map["username"] = it }
242+
password?.let { map["password"] = it }
243+
return map
244+
}
161245
}
162246

247+
248+

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ class KubeConfigMonitor(
6565

6666
internal fun updateMonitoredPaths() {
6767
val newPaths = mutableSetOf<Path>()
68-
newPaths.addAll(kubeConfigUtils.getAllConfigs())
68+
newPaths.addAll(kubeConfigUtils.getAllConfigFiles())
6969
stopWatchingRemoved(newPaths)
7070
startWatchingNew(newPaths)
7171

0 commit comments

Comments
 (0)