Skip to content
Merged
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 @@ -14,11 +14,12 @@ package com.github.devspaces.gateway
import com.github.devspaces.gateway.openshift.DevWorkspaces
import com.github.devspaces.gateway.openshift.Pods
import com.github.devspaces.gateway.openshift.Utils
import com.github.devspaces.gateway.server.RemoteServer
import com.jetbrains.gateway.thinClientLink.LinkedClientManager
import com.jetbrains.gateway.thinClientLink.ThinClientHandle
import com.jetbrains.rd.util.lifetime.Lifetime
import io.kubernetes.client.openapi.ApiException
import io.kubernetes.client.openapi.models.V1Pod
import org.bouncycastle.util.Arrays
import java.io.Closeable
import java.io.IOException
import java.net.URI

Expand All @@ -33,68 +34,33 @@ class DevSpacesConnection(private val devSpacesContext: DevSpacesContext) {
if (!isStarted) {
DevWorkspaces(devSpacesContext.client).start(dwNamespace, dwName)
}

DevWorkspaces(devSpacesContext.client).waitRunning(dwNamespace, dwName)

val dwPods = Pods(devSpacesContext.client)
.list(
dwNamespace,
String.format("controller.devfile.io/devworkspace_name=%s", dwName)
)
val remoteServer = RemoteServer(devSpacesContext)
remoteServer.waitProjects()
val projectStatus = remoteServer.getProjectStatus()

if (dwPods.items.size != 1) throw IOException(
if (projectStatus.joinLink.isEmpty()) throw IOException(
String.format(
"Expected 1 pod, but found %d",
dwPods.items.size
"Connection link to the remote server not found in the DevWorkspace: %s",
dwName
)
)
val dwPod = dwPods.items[0]

val tcpLink = getConnectionLink(dwPod)
if (tcpLink == "") throw IOException(
String.format(
"Connection link to the remote server not found in the Pod: %s",
dwPod.metadata?.name
val client = LinkedClientManager.getInstance().startNewClient(Lifetime.Eternal, URI(projectStatus.joinLink), "")
val forwarder = Pods(devSpacesContext.client).forward(remoteServer.pod, 5990, 5990)
client.run {
lifetime.onTermination(forwarder)
lifetime.onTermination(
Closeable {
val projectStatus = remoteServer.getProjectStatus()
if (Arrays.isNullOrEmpty(projectStatus.projects)) {
DevWorkspaces(devSpacesContext.client).stop(dwNamespace, dwName)
}
}
)
)

val client = LinkedClientManager.getInstance().startNewClient(Lifetime.Eternal, URI(tcpLink), "")

val forwarder = Pods(devSpacesContext.client).forward(dwPod, 5990, 5990)
client.run { lifetime.onTermination(forwarder) }

return client
}

@Throws(IOException::class, ApiException::class)
private fun getConnectionLink(pod: V1Pod): String {
val container =
pod.spec?.containers!!.find { container -> container.ports!!.any { port -> port.name == "idea-server" } }
if (container == null) throw IOException(
String.format(
"Remote server container not found in the Pod: %s",
pod.metadata?.name
)
)

// 1 minute to grab connection link
for (i in 1..12) {
val result = Pods(devSpacesContext.client).exec(
pod,
arrayOf("/bin/sh", "-c", "/idea-server/bin/remote-dev-server.sh status \$PROJECT_SOURCE | grep -Eo -m1 'tcp://[^\"]+'"),
container.name
).trim()
if (result.startsWith("tcp://")) return result

// wait a bit, maybe remote server hasn't been started yet
Thread.sleep(5 * 1000)
}

throw IOException(
String.format(
"Connection link to the remote server not found in the Pod: %s",
pod.metadata?.name
)
)
return client
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit

class DevWorkspaces(private val client: ApiClient) {

private var devWorkspaceStartingTimeout: Long = 300

@Throws(ApiException::class)
fun list(namespace: String): Any {
val customApi = CustomObjectsApi(client)
Expand Down Expand Up @@ -53,50 +56,68 @@ class DevWorkspaces(private val client: ApiClient) {

@Throws(ApiException::class)
fun patch(namespace: String, name: String, body: Any) {
val customApi = CustomObjectsApi(client)
customApi.patchNamespacedCustomObject(
"workspace.devfile.io",
"v1alpha2",
namespace,
"devworkspaces",
name,
body,
null,
null,
null
)
doPatch(namespace, name, body)
}

@Throws(ApiException::class)
fun start(namespace: String, name: String) {
val patch = arrayOf(mapOf("op" to "replace", "path" to "/spec/started", "value" to true))
patch(namespace, name, patch)
doPatch(namespace, name, patch)
}

@Throws(ApiException::class)
fun stop(namespace: String, name: String) {
val patch = arrayOf(mapOf("op" to "replace", "path" to "/spec/started", "value" to false))
doPatch(namespace, name, patch)
}

@Throws(ApiException::class, IOException::class)
fun waitRunning(namespace: String, name: String) {
val lock = Object()
val dwPhase = java.util.concurrent.atomic.AtomicReference<String>()

val executor = Executors.newScheduledThreadPool(1)
val executor = Executors.newSingleThreadScheduledExecutor()
executor.scheduleAtFixedRate(
{
val devWorkspace = get(namespace, name)
dwPhase.set(Utils.getValue(devWorkspace, arrayOf("status", "phase")) as String)

if (dwPhase.get() == "Running" || dwPhase.get() == "Failed") {
synchronized(lock) {
lock.notify()
}
executor.shutdown()
}
},
0, 5, TimeUnit.SECONDS
}, 0, 5, TimeUnit.SECONDS
)

synchronized(lock) {
lock.wait()
try {
executor.awaitTermination(devWorkspaceStartingTimeout, TimeUnit.SECONDS)
} finally {
executor.shutdown()
}

if (dwPhase.get() != "Running") throw IOException("Failed to start Dev Workspace")
if (dwPhase.get() == "Failed") throw IOException(
String.format("DevWorkspace '%s' failed to start", name)
)

if (dwPhase.get() != "Running") throw IOException(
String.format(
"DevWorkspace '%s' is not running after %d seconds",
name,
devWorkspaceStartingTimeout
)
)
}

@Throws(ApiException::class)
private fun doPatch(namespace: String, name: String, body: Any) {
val customApi = CustomObjectsApi(client)
customApi.patchNamespacedCustomObject(
"workspace.devfile.io",
"v1alpha2",
namespace,
"devworkspaces",
name,
body,
null,
null,
null
)
}
}
49 changes: 32 additions & 17 deletions src/main/kotlin/com/github/devspaces/gateway/openshift/Pods.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import io.ktor.utils.io.jvm.nio.*
import io.kubernetes.client.Exec
import io.kubernetes.client.PortForward
import io.kubernetes.client.openapi.ApiClient
import io.kubernetes.client.openapi.ApiException
import io.kubernetes.client.openapi.apis.CoreV1Api
import io.kubernetes.client.openapi.models.V1Pod
import io.kubernetes.client.openapi.models.V1PodList
Expand All @@ -29,22 +30,9 @@ import java.nio.channels.*


class Pods(private val client: ApiClient) {
@Throws(Exception::class)
fun list(namespace: String, labelSelector: String = ""): V1PodList {
val customApi = CoreV1Api(client)
return customApi.listNamespacedPod(
namespace,
"false",
false,
"",
"",
labelSelector,
-1,
"",
"",
-1,
false
)
@Throws(ApiException::class)
fun list(namespace: String): V1PodList {
return doList(namespace)
}

// Sample:
Expand Down Expand Up @@ -102,7 +90,11 @@ class Pods(private val client: ApiClient) {
}

@Throws(IOException::class)
private suspend fun copyStreams(clientSocket: Socket, forwardResult: PortForward.PortForwardResult, remotePort: Int) {
private suspend fun copyStreams(
clientSocket: Socket,
forwardResult: PortForward.PortForwardResult,
remotePort: Int
) {
coroutineScope {
ensureActive()
launch {
Expand All @@ -124,4 +116,27 @@ class Pods(private val client: ApiClient) {
}
destination.run { flush() }
}

@Throws(ApiException::class)
fun findFirst(namespace: String, labelSelector: String): V1Pod? {
val pods = doList(namespace, labelSelector)
return pods.items[0]
}

@Throws(ApiException::class)
private fun doList(namespace: String, labelSelector: String = ""): V1PodList {
return CoreV1Api(client).listNamespacedPod(
namespace,
"false",
false,
"",
"",
labelSelector,
-1,
"",
"",
-1,
false
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* Copyright (c) 2024 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.github.devspaces.gateway.server

data class ProjectInfo(
val projectName: String,
val projectPath: String,
val joinLink: String,
val httpLink: String,
val gatewayLink: String,
)

Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* Copyright (c) 2024 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.github.devspaces.gateway.server

data class ProjectStatus(
val joinLink: String,
val httpLink: String,
val gatewayLink: String,
val appVersion: String,
val runtimeVersion: String,
val projects: Array<ProjectInfo>
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false

other as ProjectStatus

if (joinLink != other.joinLink) return false
if (httpLink != other.httpLink) return false
if (gatewayLink != other.gatewayLink) return false
if (appVersion != other.appVersion) return false
if (runtimeVersion != other.runtimeVersion) return false
if (!projects.contentEquals(other.projects)) return false

return true
}

override fun hashCode(): Int {
var result = joinLink.hashCode()
result = 31 * result + httpLink.hashCode()
result = 31 * result + gatewayLink.hashCode()
result = 31 * result + appVersion.hashCode()
result = 31 * result + runtimeVersion.hashCode()
result = 31 * result + projects.contentHashCode()
return result
}

}

Loading