Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix exec creation #152

Merged
merged 1 commit into from
May 19, 2024
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 @@ -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<String>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<Frame>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

/**
Expand All @@ -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<IdOnlyResponse>().id

/**
* Inspects an exec instance and returns low-level information about it.
* `docker exec inspect`
Expand Down
Original file line number Diff line number Diff line change
@@ -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))
}
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -86,19 +84,6 @@ public actual class ContainerResource(
public fun listAsync(options: ContainerListOptions = ContainerListOptions(all = true)): CompletableFuture<List<ContainerSummary>> =
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<String> =
coroutineScope.async { exec(container, options) }.asCompletableFuture()

/**
* Creates a new container.
*
Expand Down Expand Up @@ -438,33 +423,6 @@ public actual class ContainerResource(
): CompletableFuture<Unit> =
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<IdOnlyResponse>().id

@JvmSynthetic
public actual fun attach(container: String): Flow<Frame> = flow {
httpClient.preparePost("$CONTAINERS/$container/attach") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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<Frame> {
TODO("Not yet implemented")
}
Expand Down
Loading