From a2291b30d74f773ca6b7b24e4380a1358ab69212 Mon Sep 17 00:00:00 2001 From: Didier Villevalois Date: Sun, 19 May 2024 20:45:50 +0200 Subject: [PATCH] Fix exec creation (#152) --- .../me/devnatan/yoki/models/ProcessConfig.kt | 2 +- .../resource/container/ContainerResource.kt | 9 --- .../container/ContainerResourceExt.kt | 10 ---- .../yoki/resource/exec/ExecResource.kt | 34 +++++++++++ .../yoki/resource/exec/ExecResourceExt.kt | 33 +++++++++++ .../yoki/resource/exec/ExecContainerIT.kt | 56 ++++++++++++++++++ .../container/ContainerResource.jvm.kt | 58 +++---------------- .../container/ContainerResource.native.kt | 14 ----- 8 files changed, 132 insertions(+), 84 deletions(-) create mode 100644 src/commonMain/kotlin/me/devnatan/yoki/resource/exec/ExecResourceExt.kt create mode 100644 src/commonTest/kotlin/me/devnatan/yoki/resource/exec/ExecContainerIT.kt diff --git a/src/commonMain/kotlin/me/devnatan/yoki/models/ProcessConfig.kt b/src/commonMain/kotlin/me/devnatan/yoki/models/ProcessConfig.kt index 3dc218bd..43e164db 100644 --- a/src/commonMain/kotlin/me/devnatan/yoki/models/ProcessConfig.kt +++ b/src/commonMain/kotlin/me/devnatan/yoki/models/ProcessConfig.kt @@ -5,7 +5,7 @@ import kotlinx.serialization.Serializable @Serializable public data class ProcessConfig internal constructor( val privileged: Boolean, - val user: String, + val user: String? = null, val tty: Boolean, val entrypoint: String, val arguments: List, diff --git a/src/commonMain/kotlin/me/devnatan/yoki/resource/container/ContainerResource.kt b/src/commonMain/kotlin/me/devnatan/yoki/resource/container/ContainerResource.kt index 15971fb4..ba08fb2d 100644 --- a/src/commonMain/kotlin/me/devnatan/yoki/resource/container/ContainerResource.kt +++ b/src/commonMain/kotlin/me/devnatan/yoki/resource/container/ContainerResource.kt @@ -14,7 +14,6 @@ import me.devnatan.yoki.models.container.ContainerPruneResult import me.devnatan.yoki.models.container.ContainerRemoveOptions import me.devnatan.yoki.models.container.ContainerSummary import me.devnatan.yoki.models.container.ContainerWaitResult -import me.devnatan.yoki.models.exec.ExecCreateOptions import me.devnatan.yoki.resource.image.ImageNotFoundException import kotlin.jvm.JvmOverloads import kotlin.time.Duration @@ -125,14 +124,6 @@ public expect class ContainerResource { */ public suspend fun resizeTTY(container: String, options: ResizeTTYOptions = ResizeTTYOptions()) - /** - * Runs a command inside a running container. - * - * @param container The container id to execute the command. - * @param options Exec instance command options. - */ - public suspend fun exec(container: String, options: ExecCreateOptions = ExecCreateOptions()): String - // TODO documentation public fun attach(container: String): Flow diff --git a/src/commonMain/kotlin/me/devnatan/yoki/resource/container/ContainerResourceExt.kt b/src/commonMain/kotlin/me/devnatan/yoki/resource/container/ContainerResourceExt.kt index 795c5aec..3e48e784 100644 --- a/src/commonMain/kotlin/me/devnatan/yoki/resource/container/ContainerResourceExt.kt +++ b/src/commonMain/kotlin/me/devnatan/yoki/resource/container/ContainerResourceExt.kt @@ -59,16 +59,6 @@ public suspend inline fun ContainerResource.resizeTTY(container: String, options resizeTTY(container, ResizeTTYOptions().apply(options)) } -/** - * Runs a command inside a running container. - * - * @param container The container id to execute the command. - * @param options Exec instance command options. - */ -public suspend inline fun ContainerResource.exec(container: String, options: ExecCreateOptions.() -> Unit) { - exec(container, ExecCreateOptions().apply(options)) -} - // public inline fun ContainerResource.logs( // id: String, // block: ContainerLogsOptions.() -> Unit, diff --git a/src/commonMain/kotlin/me/devnatan/yoki/resource/exec/ExecResource.kt b/src/commonMain/kotlin/me/devnatan/yoki/resource/exec/ExecResource.kt index a4c8d35d..05fc8116 100644 --- a/src/commonMain/kotlin/me/devnatan/yoki/resource/exec/ExecResource.kt +++ b/src/commonMain/kotlin/me/devnatan/yoki/resource/exec/ExecResource.kt @@ -6,10 +6,15 @@ import io.ktor.client.request.get import io.ktor.client.request.post import io.ktor.client.request.setBody import io.ktor.http.HttpStatusCode +import kotlin.jvm.JvmSynthetic import me.devnatan.yoki.io.requestCatching +import me.devnatan.yoki.models.IdOnlyResponse import me.devnatan.yoki.models.ResizeTTYOptions +import me.devnatan.yoki.models.exec.ExecCreateOptions import me.devnatan.yoki.models.exec.ExecInspectResponse import me.devnatan.yoki.models.exec.ExecStartOptions +import me.devnatan.yoki.resource.ResourcePaths.CONTAINERS +import me.devnatan.yoki.resource.container.ContainerNotFoundException import me.devnatan.yoki.resource.container.ContainerNotRunningException /** @@ -27,6 +32,35 @@ public class ExecResource internal constructor( const val BASE_PATH = "/exec" } + /** + * Runs a command inside a running container. + * + * @param id The container id to execute the command. + * @param options Exec instance command options. + * @throws ContainerNotFoundException If container instance is not found. + * @throws ContainerNotRunningException If the container is not running. + */ + @JvmSynthetic + public suspend fun create(id: String, options: ExecCreateOptions): String = + requestCatching( + HttpStatusCode.NotFound to { cause -> + ContainerNotFoundException( + cause, + id, + ) + }, + HttpStatusCode.Conflict to { cause -> + ContainerNotRunningException( + cause, + id, + ) + }, + ) { + httpClient.post("$CONTAINERS/$id/exec") { + setBody(options) + } + }.body().id + /** * Inspects an exec instance and returns low-level information about it. * `docker exec inspect` diff --git a/src/commonMain/kotlin/me/devnatan/yoki/resource/exec/ExecResourceExt.kt b/src/commonMain/kotlin/me/devnatan/yoki/resource/exec/ExecResourceExt.kt new file mode 100644 index 00000000..8fbb5dc1 --- /dev/null +++ b/src/commonMain/kotlin/me/devnatan/yoki/resource/exec/ExecResourceExt.kt @@ -0,0 +1,33 @@ +package me.devnatan.yoki.resource.exec + +import me.devnatan.yoki.models.exec.ExecCreateOptions +import me.devnatan.yoki.models.exec.ExecStartOptions +import me.devnatan.yoki.resource.container.ContainerNotFoundException +import me.devnatan.yoki.resource.container.ContainerNotRunningException + +/** + * Creates a new container. + * + * @param id The container id to execute the command. + * @param options Options to customize the container creation. + * @throws ContainerNotFoundException If container instance is not found. + * @throws ContainerNotRunningException If the container is not running. + */ +public suspend inline fun ExecResource.create(id: String, options: ExecCreateOptions.() -> Unit): String { + return create(id, ExecCreateOptions().apply(options)) +} + +/** + * Starts a previously set up exec instance. + * + * If detach is true, this endpoint returns immediately after starting the command. + * Otherwise, it sets up an interactive session with the command. + * + * @param id The exec instance id to be started. + * @param options Options to customize the exec start. + * @throws ExecNotFoundException If exec instance is not found. + * @throws ContainerNotRunningException If the container in which the exec instance was created is not running. + */ +public suspend inline fun ExecResource.start(id: String, options: ExecStartOptions.() -> Unit = {}) { + start(id, ExecStartOptions().apply(options)) +} diff --git a/src/commonTest/kotlin/me/devnatan/yoki/resource/exec/ExecContainerIT.kt b/src/commonTest/kotlin/me/devnatan/yoki/resource/exec/ExecContainerIT.kt new file mode 100644 index 00000000..10ecea07 --- /dev/null +++ b/src/commonTest/kotlin/me/devnatan/yoki/resource/exec/ExecContainerIT.kt @@ -0,0 +1,56 @@ +package me.devnatan.yoki.resource.exec + +import kotlinx.coroutines.test.runTest +import me.devnatan.yoki.resource.ResourceIT +import me.devnatan.yoki.withContainer +import kotlin.test.Test +import kotlin.test.assertEquals + +class ExecContainerIT : ResourceIT() { + + @Test + fun `exec a command in a container`() = runTest { + testClient.withContainer( + "busybox:latest", + { + command = listOf("sleep", "infinity") + }, + ) { id -> + testClient.containers.start(id) + + val execId = testClient.exec.create(id) { + command = listOf("true") + } + + testClient.exec.start(execId) + + val exec = testClient.exec.inspect(execId) + assertEquals(exec.exitCode, 0) + + testClient.containers.stop(id) + } + } + + @Test + fun `exec a failing command in a container`() = runTest { + testClient.withContainer( + "busybox:latest", + { + command = listOf("sleep", "infinity") + }, + ) { id -> + testClient.containers.start(id) + + val execId = testClient.exec.create(id) { + command = listOf("false") + } + + testClient.exec.start(execId) + + val exec = testClient.exec.inspect(execId) + assertEquals(exec.exitCode, 1) + + testClient.containers.stop(id) + } + } +} diff --git a/src/jvmMain/kotlin/me/devnatan/yoki/resource/container/ContainerResource.jvm.kt b/src/jvmMain/kotlin/me/devnatan/yoki/resource/container/ContainerResource.jvm.kt index d2d67e58..2aeb77f3 100644 --- a/src/jvmMain/kotlin/me/devnatan/yoki/resource/container/ContainerResource.jvm.kt +++ b/src/jvmMain/kotlin/me/devnatan/yoki/resource/container/ContainerResource.jvm.kt @@ -13,9 +13,16 @@ import io.ktor.client.request.put import io.ktor.client.request.setBody import io.ktor.http.ContentType import io.ktor.http.HttpStatusCode +import io.ktor.util.cio.toByteReadChannel import io.ktor.utils.io.ByteReadChannel -import io.ktor.utils.io.jvm.javaio.toByteReadChannel import io.ktor.utils.io.readUTF8Line +import java.io.InputStream +import java.util.concurrent.CompletableFuture +import kotlin.io.encoding.Base64 +import kotlin.io.encoding.ExperimentalEncodingApi +import kotlin.time.Duration +import kotlin.time.DurationUnit +import kotlin.time.toDuration import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.async import kotlinx.coroutines.flow.Flow @@ -32,7 +39,6 @@ import me.devnatan.yoki.io.readTarFile import me.devnatan.yoki.io.requestCatching import me.devnatan.yoki.io.writeTarFile import me.devnatan.yoki.models.Frame -import me.devnatan.yoki.models.IdOnlyResponse import me.devnatan.yoki.models.ResizeTTYOptions import me.devnatan.yoki.models.Stream import me.devnatan.yoki.models.container.Container @@ -45,16 +51,8 @@ import me.devnatan.yoki.models.container.ContainerPruneResult import me.devnatan.yoki.models.container.ContainerRemoveOptions import me.devnatan.yoki.models.container.ContainerSummary import me.devnatan.yoki.models.container.ContainerWaitResult -import me.devnatan.yoki.models.exec.ExecCreateOptions import me.devnatan.yoki.resource.ResourcePaths.CONTAINERS import me.devnatan.yoki.resource.image.ImageNotFoundException -import java.io.InputStream -import java.util.concurrent.CompletableFuture -import kotlin.io.encoding.Base64 -import kotlin.io.encoding.ExperimentalEncodingApi -import kotlin.time.Duration -import kotlin.time.DurationUnit -import kotlin.time.toDuration public actual class ContainerResource( private val coroutineScope: CoroutineScope, @@ -86,19 +84,6 @@ public actual class ContainerResource( public fun listAsync(options: ContainerListOptions = ContainerListOptions(all = true)): CompletableFuture> = coroutineScope.async { list(options) }.asCompletableFuture() - /** - * Runs a command inside a running container. - * - * @param container Unique identifier or name of the container. - * @param options Exec instance command options. - */ - @JvmOverloads - public fun execAsync( - container: String, - options: ExecCreateOptions = ExecCreateOptions(), - ): CompletableFuture = - coroutineScope.async { exec(container, options) }.asCompletableFuture() - /** * Creates a new container. * @@ -438,33 +423,6 @@ public actual class ContainerResource( ): CompletableFuture = coroutineScope.async { resizeTTY(container, options) }.asCompletableFuture() - /** - * Runs a command inside a running container. - * - * @param container The container id to execute the command. - * @param options Exec instance command options. - */ - @JvmSynthetic - public actual suspend fun exec(container: String, options: ExecCreateOptions): String = - requestCatching( - HttpStatusCode.NotFound to { cause -> - ContainerNotFoundException( - cause, - container, - ) - }, - HttpStatusCode.Conflict to { cause -> - ContainerNotRunningException( - cause, - container, - ) - }, - ) { - httpClient.post("$CONTAINERS/$container/exec") { - setBody(options) - } - }.body().id - @JvmSynthetic public actual fun attach(container: String): Flow = flow { httpClient.preparePost("$CONTAINERS/$container/attach") { diff --git a/src/nativeMain/kotlin/me/devnatan/yoki/resource/container/ContainerResource.native.kt b/src/nativeMain/kotlin/me/devnatan/yoki/resource/container/ContainerResource.native.kt index 390211ec..bdea2616 100644 --- a/src/nativeMain/kotlin/me/devnatan/yoki/resource/container/ContainerResource.native.kt +++ b/src/nativeMain/kotlin/me/devnatan/yoki/resource/container/ContainerResource.native.kt @@ -13,7 +13,6 @@ import me.devnatan.yoki.models.container.ContainerPruneResult import me.devnatan.yoki.models.container.ContainerRemoveOptions import me.devnatan.yoki.models.container.ContainerSummary import me.devnatan.yoki.models.container.ContainerWaitResult -import me.devnatan.yoki.models.exec.ExecCreateOptions import kotlin.time.Duration public actual class ContainerResource { @@ -141,19 +140,6 @@ public actual class ContainerResource { public actual suspend fun resizeTTY(container: String, options: ResizeTTYOptions) { } - /** - * Runs a command inside a running container. - * - * @param container The container id to execute the command. - * @param options Exec instance command options. - */ - public actual suspend fun exec( - container: String, - options: ExecCreateOptions, - ): String { - TODO("Not yet implemented") - } - public actual fun attach(container: String): Flow { TODO("Not yet implemented") }