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
6 changes: 6 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import org.gradle.kotlin.dsl.dependencies
import org.jetbrains.changelog.Changelog
import org.jetbrains.changelog.markdownToHTML
import org.jetbrains.intellij.platform.gradle.TestFrameworkType
Expand Down Expand Up @@ -35,7 +36,12 @@ dependencies {
testImplementation("org.junit.platform:junit-platform-launcher:6.0.1")
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:6.0.1")
testImplementation("org.assertj:assertj-core:3.27.6")

testImplementation("io.mockk:mockk:1.14.6")
testImplementation("io.mockk:mockk-agent-jvm:1.14.6")

implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.0")

// IntelliJ Platform Gradle Plugin Dependencies Extension - read more: https://plugins.jetbrains.com/docs/intellij/tools-intellij-platform-gradle-plugin-dependencies-extension.html
intellijPlatform {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,14 @@ import com.jetbrains.gateway.api.GatewayConnectionHandle
import com.jetbrains.gateway.api.GatewayConnectionProvider
import com.redhat.devtools.gateway.openshift.DevWorkspaces
import com.redhat.devtools.gateway.openshift.OpenShiftClientFactory
import com.redhat.devtools.gateway.openshift.kube.KubeConfigBuilder
import com.redhat.devtools.gateway.openshift.kube.isNotFound
import com.redhat.devtools.gateway.openshift.kube.isUnauthorized
import com.redhat.devtools.gateway.kubeconfig.KubeConfigUtils
import com.redhat.devtools.gateway.openshift.isNotFound
import com.redhat.devtools.gateway.openshift.isUnauthorized
import com.redhat.devtools.gateway.util.messageWithoutPrefix
import com.redhat.devtools.gateway.view.ui.Dialogs
import io.kubernetes.client.openapi.ApiException
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.suspendCancellableCoroutine
import javax.swing.JComponent
import javax.swing.Timer
Expand All @@ -46,6 +45,8 @@ private const val DW_NAME = "dwName"
*/
class DevSpacesConnectionProvider : GatewayConnectionProvider {

private var clientFactory: OpenShiftClientFactory? = null

@OptIn(ExperimentalCoroutinesApi::class)
@Suppress("UnstableApiUsage")
override suspend fun connect(
Expand Down Expand Up @@ -183,7 +184,9 @@ class DevSpacesConnectionProvider : GatewayConnectionProvider {
val ctx = DevSpacesContext()

indicator.text2 = "Initializing Kubernetes connection…"
ctx.client = OpenShiftClientFactory().create()
val factory = OpenShiftClientFactory(KubeConfigUtils)
this.clientFactory = factory
ctx.client = factory.create()

indicator.text2 = "Fetching DevWorkspace “$dwName” from namespace “$dwNamespace”…"
ctx.devWorkspace = DevWorkspaces(ctx.client).get(dwNamespace, dwName)
Expand Down Expand Up @@ -225,7 +228,7 @@ class DevSpacesConnectionProvider : GatewayConnectionProvider {
private fun handleUnauthorizedError(err: ApiException): Boolean {
if (!err.isUnauthorized()) return false

val tokenNote = if (KubeConfigBuilder.isTokenAuthUsed())
val tokenNote = if (clientFactory?.isTokenAuth() == true)
"\n\nYou are using token-based authentication.\nUpdate your token in the kubeconfig file."
else ""

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/*
* 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 kotlinx.coroutines.*
import java.nio.file.*
import java.util.concurrent.ConcurrentHashMap
import kotlin.io.path.exists
import kotlin.io.path.isRegularFile

class FileWatcher(
private val scope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default),
private val dispatcher: CoroutineDispatcher = Dispatchers.IO,
private val watchService: WatchService = FileSystems.getDefault().newWatchService()
) {
private var onFileChanged: ((Path) -> Unit)? = null
private val registeredKeys = ConcurrentHashMap<WatchKey, Path>()
private val monitoredFiles = ConcurrentHashMap.newKeySet<Path>()
private var watchJob: Job? = null

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
}
val dir = registeredKeys[key] ?: continue
pollEvents(key, dir)
key.reset()
}
}
}

fun stop() {
watchJob?.cancel()
watchJob = null
watchService.close()
}

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
monitoredFiles.add(path)
onFileChanged?.invoke(path)
}
return this
}

fun removeFile(path: Path): FileWatcher {
monitoredFiles.remove(path)
return this
}

fun onFileChanged(action: ((Path) -> Unit)?) {
this.onFileChanged = action
}

private fun pollEvents(key: WatchKey, dir: Path) {
for (event in key.pollEvents()) {
val relativePath = event.context() as? Path ?: continue
val changedFile = dir.resolve(relativePath)

if (monitoredFiles.contains(changedFile)
&& event.kind() != StandardWatchEventKinds.OVERFLOW
) {
onFileChanged?.invoke(changedFile)
}
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
/*
* 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 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
) {
companion object {
fun fromMap(name: String, clusterObject: Any?): KubeConfigNamedCluster? {
val clusterMap = clusterObject as? Map<*, *> ?: return null
val clusterDetails = clusterMap["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()
}
}
}

data class KubeConfigCluster(
val server: String,
val certificateAuthorityData: String? = null,
val insecureSkipTlsVerify: Boolean? = null
) {
companion object {
fun fromMap(map: Map<*, *>): KubeConfigCluster? {
val server = map["server"] as? String ?: return null
return KubeConfigCluster(
server = server,
certificateAuthorityData = map["certificate-authority-data"] as? String,
insecureSkipTlsVerify = map["insecure-skip-tls-verify"] as? Boolean
)
}
}
}

data class KubeConfigNamedContext(
val name: String,
val context: KubeConfigContext
) {
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 getByName(name: String, contextObject: Any?): KubeConfigNamedContext? {
val contextMap = contextObject as? Map<*, *> ?: return null
val contextDetails = contextMap["context"] as? Map<*, *> ?: return null

return KubeConfigNamedContext(
name = name,
context = KubeConfigContext.fromMap(contextDetails) ?: return null
)
}
}
}

data class KubeConfigContext(
val cluster: String,
val user: 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
)
}
}
}

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

return KubeConfigNamedUser(
name = name,
user = KubeConfigUser.fromMap(userDetails)
)
}

fun getUserTokenForCluster(clusterName: String, kubeConfig: KubeConfig): String? {
val contextEntry = KubeConfigNamedContext.getByClusterName(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
}

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

data class KubeConfigUser(
val token: String? = null,
val clientCertificateData: String? = null,
val clientKeyData: String? = null,
val username: String? = null,
val password: String? = null
) {
companion object {
fun fromMap(map: Map<*, *>): KubeConfigUser {
return KubeConfigUser(
token = map["token"] as? String,
clientCertificateData = map["client-certificate-data"] as? String,
clientKeyData = map["client-key-data"] as? String,
username = map["username"] as? String,
password = map["password"] as? String
)
}
}
}

Loading
Loading