Skip to content
Draft
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
Expand Up @@ -204,7 +204,7 @@ class DevSpacesConnection(private val devSpacesContext: DevSpacesContext) {
private fun onRestartAnnotated(namespace: String, workspaceName: String, thinClient: ThinClientHandle): () -> Job {
return {
CoroutineScope(Dispatchers.IO).launch {
val restartHandler = DevWorkspaceRestart(namespace, workspaceName, devSpacesContext.client)
val restartHandler = DevWorkspaceRestart(devSpacesContext)
restartHandler.execute(thinClient)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,13 @@
*/
package com.redhat.devtools.gateway.devworkspace

import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.diagnostic.thisLogger
import com.jetbrains.gateway.thinClientLink.ThinClientHandle
import com.redhat.devtools.gateway.DevSpacesContext
import com.redhat.devtools.gateway.openshift.DevWorkspacePods
import io.kubernetes.client.openapi.ApiClient
import com.redhat.devtools.gateway.server.RemoteIDEServer
import com.redhat.devtools.gateway.service.DevSpacesConnectionService
import kotlinx.coroutines.delay
import kotlin.time.Duration.Companion.seconds

Expand All @@ -29,20 +32,23 @@
* 5. Clean up the restart annotation
*/
class DevWorkspaceRestart(
private val namespace: String,
private val workspaceName: String,
private val client: ApiClient,
private val workspaces: DevWorkspaces = DevWorkspaces(client),
private val pods: DevWorkspacePods = DevWorkspacePods(client)
private val devSpacesContext: DevSpacesContext,
private val workspaces: DevWorkspaces = DevWorkspaces(devSpacesContext.client),
private val pods: DevWorkspacePods = DevWorkspacePods(devSpacesContext.client)
) {

private val namespace get() = devSpacesContext.devWorkspace.namespace
private val workspaceName get() = devSpacesContext.devWorkspace.name

@Suppress("UnstableApiUsage")
suspend fun execute(thinClient: ThinClientHandle) {
try {
close(thinClient)
stopWorkspace()
stopWorkspaceAndWait()
waitForPodsDeleted()
startWorkspace()
startWorkspaceAndWait()
waitForIDEReady()
triggerIDEConnect()
removeAnnotation()
} catch (e: Exception) {
thisLogger().error("Workspace restart failed for $namespace/$workspaceName", e)
Expand All @@ -51,34 +57,108 @@
}
}

private suspend fun waitForIDEReady() {
thisLogger().debug("Waiting for IDE server to be ready...")

try {
RemoteIDEServer(devSpacesContext).waitServerReady()
thisLogger().debug("IDE server is ready")
} catch (e: Exception) {
thisLogger().error("IDE server did not become ready", e)
throw e
}
}

/**
* Triggers IDE reconnect safely on UI thread.
*/
private fun triggerIDEConnect() {
try {
ApplicationManager.getApplication().invokeLater {
try {
DevSpacesConnectionService
.getInstance()
.connect(devSpacesContext)

thisLogger().info("IDE reconnect triggered successfully")

} catch (e: Exception) {
thisLogger().error("Failed to execute IDE reconnect", e)
}
}
} catch (e: Exception) {
thisLogger().error("Failed to schedule IDE reconnect", e)
}
}

@Suppress("UnstableApiUsage")
private suspend fun close(thinClient: ThinClientHandle) {
thisLogger().debug("Closing thin client for $namespace/$workspaceName")
thinClient.close()
delay(1.seconds) // Give time for port forwarder cleanup
}

private fun stopWorkspace() {
workspaces.stop(namespace, workspaceName)
thisLogger().debug("workspace $namespace/$workspaceName stop requested.")
private fun stopWorkspaceAndWait() {
thisLogger().debug("Stopping workspace and waiting...")

workspaces.stopAndWait(
namespace,
workspaceName,
DevWorkspaces.RUNNING_TIMEOUT
)

thisLogger().debug("Workspace stopped")
}

private suspend fun waitForPodsDeleted() {
val podsDeleted = pods.waitForPodsDeleted(
private fun startWorkspaceAndWait() {
thisLogger().debug("Starting workspace and waiting...")

workspaces.startAndWait(
namespace,
workspaceName,
20
DevWorkspaces.RUNNING_TIMEOUT
)
if (podsDeleted) {
thisLogger().debug("All pods for $namespace/$workspaceName have been deleted.")
} else {
thisLogger().warn("Pods for $namespace/$workspaceName were not deleted within timeout, proceeding anyway.")
}

thisLogger().debug("Workspace started and running")
}

private fun startWorkspace() {
workspaces.start(namespace, workspaceName)
thisLogger().debug("workspace $namespace/$workspaceName start requested.")
private suspend fun waitForPodsDeleted() {
thisLogger().debug("Waiting until all pods are deleted for $namespace/$workspaceName")

val labelSelector = "controller.devfile.io/devworkspace_name=$workspaceName"

val timeoutSeconds = 120L
val startTime = System.currentTimeMillis()

while (true) {
val podList = try {
pods.doList(namespace, labelSelector)
} catch (e: Exception) {
thisLogger().warn("Failed to list pods, retrying...", e)
delay(1000)

Check warning on line 138 in src/main/kotlin/com/redhat/devtools/gateway/devworkspace/DevWorkspaceRestart.kt

View workflow job for this annotation

GitHub Actions / Inspect code

Long overload to Duration conversion

Legacy Long overload can be converted to Duration
continue
}

val activePods = podList.items.filter { it.metadata?.deletionTimestamp == null }
val podsCount = activePods.size

if (podsCount == 0) {
thisLogger().debug("All pods deleted for $namespace/$workspaceName")
return
}

thisLogger().debug("Still waiting... $podsCount pods remaining")

val elapsed = (System.currentTimeMillis() - startTime) / 1000
if (elapsed >= timeoutSeconds) {
val podNames = activePods.map { it.metadata?.name }
throw IllegalStateException(
"Timeout waiting for pods deletion. Remaining pods: $podNames"
)
}

delay(1000)

Check warning on line 160 in src/main/kotlin/com/redhat/devtools/gateway/devworkspace/DevWorkspaceRestart.kt

View workflow job for this annotation

GitHub Actions / Inspect code

Long overload to Duration conversion

Legacy Long overload can be converted to Duration
}
}

private fun removeAnnotation() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -394,7 +394,7 @@ class DevWorkspacePods(private val client: ApiClient) {
}

@Throws(ApiException::class)
private fun doList(namespace: String, labelSelector: String = ""): V1PodList {
fun doList(namespace: String, labelSelector: String = ""): V1PodList {
return CoreV1Api(client)
.listNamespacedPod(namespace)
.labelSelector(labelSelector)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* Copyright (c) 2026 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.service

import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.components.Service
import com.intellij.openapi.diagnostic.thisLogger
import com.intellij.openapi.progress.ProgressManager
import com.redhat.devtools.gateway.DevSpacesBundle
import com.redhat.devtools.gateway.DevSpacesConnection
import com.redhat.devtools.gateway.DevSpacesContext
import com.redhat.devtools.gateway.util.messageWithoutPrefix
import com.redhat.devtools.gateway.view.ui.Dialogs

@Service
class DevSpacesConnectionService {

fun connect(devSpacesContext: DevSpacesContext) {
ProgressManager.getInstance().runProcessWithProgressSynchronously(
{
try {
DevSpacesConnection(devSpacesContext).connect(
{
thisLogger().debug("IDE connected successfully")
},
{
thisLogger().warn("IDE connection failed")
},
{
thisLogger().debug("Workspace stopped after connection")
}
)
} catch (e: Exception) {
thisLogger().error("Workspace IDE connection failed.", e)
Dialogs.error(
e.messageWithoutPrefix() ?: "Could not connect to workspace IDE",
"Connection Error"
)
}
},
DevSpacesBundle.message("connector.loader.devspaces.connecting.text"),
true,
null
)
}

companion object {
fun getInstance(): DevSpacesConnectionService =
ApplicationManager.getApplication().getService(DevSpacesConnectionService::class.java)
}
}
Loading