Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* Copyright (c) 2025 Red Hat, Inc.
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
*/
package com.redhat.devtools.gateway.kubeconfig

import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory
import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator
import io.kubernetes.client.persister.ConfigPersister
import java.io.File

class BlockStyleFilePersister(private val file: File) : ConfigPersister {

@Throws(java.io.IOException::class)
override fun save(
contexts: ArrayList<Any?>,
clusters: ArrayList<Any?>,
users: ArrayList<Any?>,
preferences: Any?,
currentContext: String?
) {
val config = mapOf(
"apiVersion" to "v1",
"kind" to "Config",
"current-context" to currentContext,
"preferences" to preferences,

"clusters" to clusters,
"contexts" to contexts,
"users" to users,
)

synchronized(file) {
val yamlFactory = YAMLFactory().apply {
configure(YAMLGenerator.Feature.MINIMIZE_QUOTES, true)
configure(YAMLGenerator.Feature.INDENT_ARRAYS_WITH_INDICATOR, true)
}
val mapper = ObjectMapper(yamlFactory)
mapper.writeValue(file, config)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import kotlin.io.path.exists
import kotlin.io.path.isRegularFile

class FileWatcher(
private val scope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default),
private val scope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO),
private val dispatcher: CoroutineDispatcher = Dispatchers.IO,
private val watchService: WatchService = FileSystems.getDefault().newWatchService()
) {
Expand All @@ -29,23 +29,31 @@ class FileWatcher(

fun start() {
this.watchJob = scope.launch(dispatcher) {
while (isActive) {
val key = watchService.poll(100, java.util.concurrent.TimeUnit.MILLISECONDS)
if (key == null) {
delay(100)
continue
try {
while (isActive) {
val key = watchService.poll(100, java.util.concurrent.TimeUnit.MILLISECONDS)
if (key == null) {
delay(100)
continue
}
val dir = registeredKeys[key] ?: continue
pollEvents(key, dir)
key.reset()
}
val dir = registeredKeys[key] ?: continue
pollEvents(key, dir)
key.reset()
} catch (e: ClosedWatchServiceException) {
// Watch service was closed, exit gracefully
}
}
}

fun stop() {
watchJob?.cancel()
watchJob = null
watchService.close()
try {
watchService.close()
} catch (e: Exception) {
// Ignore exceptions when closing
}
}

fun addFile(path: Path): FileWatcher {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,35 +11,42 @@
*/
package com.redhat.devtools.gateway.kubeconfig

import com.redhat.devtools.gateway.kubeconfig.KubeConfigUtils.sanitizeName
import com.redhat.devtools.gateway.kubeconfig.KubeConfigUtils.urlToName
import io.kubernetes.client.util.KubeConfig
import kotlin.collections.get


/**
* Domain classes representing the structure of a kubeconfig file.
*/
data class KubeConfigNamedCluster(
val name: String,
val cluster: KubeConfigCluster
val cluster: KubeConfigCluster,
val name: String = toName(cluster)
) {

companion object {
fun fromMap(name: String, clusterObject: Any?): KubeConfigNamedCluster? {
val clusterMap = clusterObject as? Map<*, *> ?: return null
val clusterDetails = clusterMap["cluster"] as? Map<*, *> ?: return null
fun fromMap(map: Map<*,*>): KubeConfigNamedCluster? {
val name = map["name"] as? String ?: return null
val clusterDetails = map["cluster"] as? Map<*, *> ?: return null

return KubeConfigNamedCluster(
name = name,
cluster = KubeConfigCluster.fromMap(clusterDetails) ?: return null
)
}

fun fromKubeConfig(kubeConfig: KubeConfig): List<KubeConfigNamedCluster> {
return (kubeConfig.clusters as? List<*>)
?.mapNotNull { clusterObject ->
val clusterMap = clusterObject as? Map<*, *> ?: return@mapNotNull null
val name = clusterMap["name"] as? String ?: return@mapNotNull null
fromMap(name, clusterObject)
} ?: emptyList()

private fun toName(cluster: KubeConfigCluster): String {
val url = cluster.server
return urlToName(url) ?: url
}

}

fun toMap(): MutableMap<String, Any> {
return mutableMapOf(
"name" to name,
"cluster" to cluster.toMap()
)
}
}

Expand All @@ -58,90 +65,157 @@ data class KubeConfigCluster(
)
}
}

fun toMap(): MutableMap<String, Any> {
val map = mutableMapOf<String, Any>()
map["server"] = server
certificateAuthorityData?.let { map["certificate-authority-data"] = it }
insecureSkipTlsVerify?.let { map["insecure-skip-tls-verify"] = it }
return map
}
}

data class KubeConfigNamedContext(
val name: String,
val context: KubeConfigContext
val context: KubeConfigContext,
val name: String = toName(context.user, context.cluster)
) {
companion object {
fun getByClusterName(clusterName: String, kubeConfig: KubeConfig): KubeConfigNamedContext? {
return (kubeConfig.contexts as? List<*>)?.firstNotNullOfOrNull { contextObject ->
val contextMap = contextObject as? Map<*, *> ?: return@firstNotNullOfOrNull null
val contextName = contextMap["name"] as? String ?: return@firstNotNullOfOrNull null
val contextEntry = getByName(contextName, contextObject)
if (contextEntry?.context?.cluster == clusterName) {
contextEntry
} else {
null
}

private fun toName(user: String, cluster: String): String {
val sanitizedUser = sanitizeName(user)
val sanitizedCluster = sanitizeName(cluster)

return when {
sanitizedUser.isEmpty() && sanitizedCluster.isEmpty() -> ""
sanitizedUser.isEmpty() -> sanitizedCluster
sanitizedCluster.isEmpty() -> sanitizedUser
else -> "$sanitizedUser/$sanitizedCluster"
}
}

private fun getByName(name: String, contextObject: Any?): KubeConfigNamedContext? {
val contextMap = contextObject as? Map<*, *> ?: return null
val contextDetails = contextMap["context"] as? Map<*, *> ?: return null
fun getByClusterName(name: String?, allConfigs: List<KubeConfig>): KubeConfigNamedContext? {
return allConfigs
.flatMap {
it.contexts ?: emptyList()
}
.mapNotNull {
fromMap(it as? Map<*,*>)
}
.firstOrNull { context ->
name == context.context.cluster
}
}

fun getByName(clusterName: String, kubeConfig: KubeConfig): KubeConfigNamedContext? {
return (kubeConfig.contexts as? List<*>)
?.firstNotNullOfOrNull { contextObject ->
val contextEntry = fromMap(contextObject as? Map<*, *>)
if (contextEntry?.context?.cluster == clusterName) {
contextEntry
} else {
null
}
}
}

fun fromMap(map: Map<*, *>?): KubeConfigNamedContext? {
if (map == null) return null
val name = map["name"] as? String ?: return null
val context = map["context"] as? Map<*, *> ?: return null
return KubeConfigNamedContext(
name = name,
context = KubeConfigContext.fromMap(contextDetails) ?: return null
context = KubeConfigContext.fromMap(context) ?: return null
)
}
}

fun toMap(): MutableMap<String, Any> {
return mutableMapOf(
"name" to name,
"context" to context.toMap()
)
}
}

data class KubeConfigContext(
val cluster: String,
val user: String,
val cluster: String,
val namespace: String? = null
) {
companion object {
fun fromMap(map: Map<*, *>): KubeConfigContext? {
val cluster = map["cluster"] as? String ?: return null
val user = map["user"] as? String ?: return null

return KubeConfigContext(
cluster = cluster,
user = user,
namespace = map["namespace"] as? String
)
}
}

fun toMap(): MutableMap<String, Any> {
val map = mutableMapOf<String, Any>()
map["cluster"] = cluster
map["user"] = user
namespace?.let { map["namespace"] = it }
return map
}
}

data class KubeConfigNamedUser(
val name: String,
val user: KubeConfigUser
val user: KubeConfigUser?,
val name: String
) {
companion object {
fun fromMap(name: String, userObject: Any?): KubeConfigNamedUser? {
val userMap = userObject as? Map<*, *> ?: return null
val userDetails = userMap["user"] as? Map<*, *> ?: return null


fun getByName(userName: String, config: KubeConfig?): KubeConfigNamedUser? {
return (config?.users ?: emptyList<Any>())
.mapNotNull {
it as? Map<String, Any>
}
.firstOrNull { user ->
userName == user["name"]
}
?.let { fromMap(it) }
}

fun fromMap(map: Map<*,*>): KubeConfigNamedUser? {
val name = map["name"] as? String ?: return null
val userDetails = map["user"] as? Map<*, *> ?: return null
val user = KubeConfigUser.fromMap(userDetails)
return KubeConfigNamedUser(
name = name,
user = KubeConfigUser.fromMap(userDetails)
user = user
)
}

fun getUserTokenForCluster(clusterName: String, kubeConfig: KubeConfig): String? {
val contextEntry = KubeConfigNamedContext.getByClusterName(clusterName, kubeConfig) ?: return null
val contextEntry = KubeConfigNamedContext.getByName(clusterName, kubeConfig) ?: return null
val userObject = (kubeConfig.users as? List<*>)?.firstOrNull { userObject ->
val userMap = userObject as? Map<*, *> ?: return@firstOrNull false
val userName = userMap["name"] as? String ?: return@firstOrNull false
userName == contextEntry.context.user
} ?: return null
return fromMap(contextEntry.context.user, userObject)?.user?.token
} as? Map<*,*> ?: return null
return fromMap(userObject)?.user?.token
}

fun isTokenAuth(kubeConfig: KubeConfig): Boolean {
return kubeConfig.credentials?.containsKey(KubeConfig.CRED_TOKEN_KEY) == true
}
}

fun toMap(): MutableMap<String, Any> {
return mutableMapOf(
"name" to name,
"user" to (user?.toMap() ?: mutableMapOf())
)
}
}

data class KubeConfigUser(
val token: String? = null,
var token: String? = null,
val clientCertificateData: String? = null,
val clientKeyData: String? = null,
val username: String? = null,
Expand All @@ -158,5 +232,17 @@ data class KubeConfigUser(
)
}
}

fun toMap(): MutableMap<String, Any> {
val map = mutableMapOf<String, Any>()
token?.let { map["token"] = it }
clientCertificateData?.let { map["client-certificate-data"] = it }
clientKeyData?.let { map["client-key-data"] = it }
username?.let { map["username"] = it }
password?.let { map["password"] = it }
return map
}
}



Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ class KubeConfigMonitor(

internal fun updateMonitoredPaths() {
val newPaths = mutableSetOf<Path>()
newPaths.addAll(kubeConfigUtils.getAllConfigs())
newPaths.addAll(kubeConfigUtils.getAllConfigFiles())
stopWatchingRemoved(newPaths)
startWatchingNew(newPaths)

Expand Down
Loading
Loading