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

Add http calls debugging #154

Merged
merged 3 commits 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
8 changes: 6 additions & 2 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ version.ref = "ktor"
module = "io.ktor:ktor-client-json"
version.ref = "ktor"

[libraries.ktor-client-logging]
module = "io.ktor:ktor-client-logging"
version.ref = "ktor"

[libraries.ktor-client-mock]
module = "io.ktor:ktor-client-mock"
version.ref = "ktor"
Expand Down Expand Up @@ -96,7 +100,7 @@ module = "org.jetbrains.kotlinx:kotlinx-io-core"
version.ref = "kotlinx-io"

[bundles]
ktor = ["ktor-client-core", "ktor-client-serialization", "ktor-client-json", "ktor-client-content-negotiation", "ktor-serialization-kotlinx-json", "ktor-network"]
ktor = ["ktor-client-core", "ktor-client-serialization", "ktor-client-json", "ktor-client-logging", "ktor-client-content-negotiation", "ktor-serialization-kotlinx-json", "ktor-network"]
ktx = ["ktx-coroutines-core", "ktx-serialization-core", "ktx-serialization-json"]

[plugins]
Expand All @@ -106,4 +110,4 @@ kotlinter = { id = "org.jmailen.kotlinter", version.ref = "plugin-kotlinter" }
publishOnCentral = { id = "org.danilopianini.publish-on-central", version.ref = "plugin-publishOnCentral" }
binaryCompatibilityValidator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version.ref = "plugin-binaryCompatibilityValidator" }
kover = { id = "org.jetbrains.kotlinx.kover", version.ref = "plugin-kover" }
detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "plugin-detekt" }
detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "plugin-detekt" }
23 changes: 21 additions & 2 deletions src/commonMain/kotlin/me/devnatan/yoki/YokiConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,11 @@ internal val DefaultYokiConfig = YokiConfig.builder().forCurrentPlatform().build
* @param apiVersion The version of the Docker API that will be used during communication.
* See more: [Versioned API and SDK](https://docs.docker.com/engine/api/#versioned-api-and-sdk).
*/
public class YokiConfig(public val socketPath: String, public val apiVersion: String) {
public class YokiConfig(
public val socketPath: String,
public val apiVersion: String,
public val debugHttpCalls: Boolean,
) {

init {
check(socketPath.isNotBlank()) { "Socket path must be provided and cannot be blank" }
Expand Down Expand Up @@ -49,6 +53,11 @@ public class YokiConfigBuilder {
prefix = null,
)

/**
* Whether to debug the HTTP calls to the Docker daemon.
*/
private var debugHttpCalls: Boolean = false

/**
* Sets the Docker socket path.
*
Expand All @@ -69,6 +78,16 @@ public class YokiConfigBuilder {
return this
}

/**
* Sets the debug logging of HTTP calls.
*
* @param debugHttpCalls whether to log the HTTP calls
*/
public fun debugHttpCalls(debugHttpCalls: Boolean = true): YokiConfigBuilder {
this.debugHttpCalls = debugHttpCalls
return this
}

/**
* Configures to use a Unix socket defaults common to the standard Docker configuration.
*
Expand Down Expand Up @@ -116,7 +135,7 @@ public class YokiConfigBuilder {
* Builds this class to a [YokiConfig].
*/
public fun build(): YokiConfig {
return YokiConfig(socketPath, apiVersion)
return YokiConfig(socketPath, apiVersion, debugHttpCalls)
}

/**
Expand Down
8 changes: 8 additions & 0 deletions src/commonMain/kotlin/me/devnatan/yoki/io/Http.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import io.ktor.client.plugins.ResponseException
import io.ktor.client.plugins.UserAgent
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.client.plugins.defaultRequest
import io.ktor.client.plugins.logging.*
import io.ktor.http.ContentType
import io.ktor.http.HttpStatusCode
import io.ktor.http.URLBuilder
Expand Down Expand Up @@ -37,6 +38,13 @@ internal fun createHttpClient(client: Yoki): HttpClient {
)
}

if (client.config.debugHttpCalls) {
install(Logging) {
logger = Logger.SIMPLE
level = LogLevel.ALL
}
}

install(UserAgent) { agent = "Yoki" }
configureHttpClient(client)

Expand Down
22 changes: 2 additions & 20 deletions src/commonMain/kotlin/me/devnatan/yoki/models/ExposedPort.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,12 @@ import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.SerializationException
import kotlinx.serialization.builtins.ListSerializer
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.JsonTransformingSerializer
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import me.devnatan.yoki.util.ListAsMapToEmptyObjectsSerializer

@Serializable
public data class ExposedPort internal constructor(
Expand Down Expand Up @@ -77,14 +69,4 @@ internal object ExposedPortSerializer : KSerializer<ExposedPort> {
}
}

internal object ExposedPortsSerializer :
JsonTransformingSerializer<List<ExposedPort>>(ListSerializer(ExposedPortSerializer)) {

override fun transformDeserialize(element: JsonElement): JsonElement {
return JsonArray(element.jsonObject.entries.map { JsonPrimitive(it.key) })
}

override fun transformSerialize(element: JsonElement): JsonElement {
return JsonObject(element.jsonArray.associate { it.jsonPrimitive.content to JsonObject(mapOf()) })
}
}
internal object ExposedPortsSerializer : ListAsMapToEmptyObjectsSerializer<ExposedPort>(ExposedPortSerializer)
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 @@ -2,9 +2,11 @@ package me.devnatan.yoki.models.container

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.builtins.serializer
import me.devnatan.yoki.models.ExposedPort
import me.devnatan.yoki.models.ExposedPortsSerializer
import me.devnatan.yoki.models.HealthConfig
import me.devnatan.yoki.util.ListAsMapToEmptyObjectsSerializer

@Serializable
public data class ContainerConfig(
Expand All @@ -23,7 +25,7 @@ public data class ContainerConfig(
@SerialName("Healthcheck") public val healthcheck: HealthConfig? = null,
@SerialName("ArgsEscaped") public val argsEscaped: Boolean? = null,
@SerialName("Image") public val image: String? = null,
@SerialName("Volumes") public val volumes: Map<String, String>? = emptyMap(),
@SerialName("Volumes") public val volumes: @Serializable(with = VolumesSerializer::class) List<String>? = emptyList(),
@SerialName("WorkingDir") public val workingDir: String? = null,
@SerialName("Entrypoint") public val entrypoint: List<String>? = emptyList(),
@SerialName("NetworkDisabled") public val networkDisabled: Boolean? = null,
Expand All @@ -34,3 +36,5 @@ public data class ContainerConfig(
@SerialName("StopTimeout") public val stopTimeout: Int? = null,
@SerialName("Shell") public val shell: List<String> = emptyList(),
)

internal object VolumesSerializer : ListAsMapToEmptyObjectsSerializer<String>(String.serializer())
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package me.devnatan.yoki.models.container

import kotlinx.serialization.Contextual
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import me.devnatan.yoki.models.ExposedPort
Expand All @@ -25,7 +24,7 @@ public data class ContainerCreateOptions(
@SerialName("Healthcheck") public var healthcheck: HealthConfig? = null,
@SerialName("ArgsEscaped") public var escapedArgs: Boolean? = null,
@SerialName("Image") public var image: String? = null,
@SerialName("Volumes") public var volumes: Map<String, @Contextual Any>? = null,
@SerialName("Volumes") public var volumes: @Serializable(with = VolumesSerializer::class) List<String>? = null,
@SerialName("WorkingDir") public var workingDirectory: String? = null,
@SerialName("Entrypoint") public var entrypoint: List<String>? = null,
@SerialName("NetworkDisabled") public var disabledNetwork: Boolean? = null,
Expand Down Expand Up @@ -57,7 +56,7 @@ public fun ContainerCreateOptions.hostConfig(block: HostConfig.() -> Unit) {
}

public fun ContainerCreateOptions.volume(string: String) {
this.volumes = mapOf(string to emptyMap<String, Any>()) + volumes.orEmpty()
this.volumes = this.volumes.orEmpty() + string
}

public fun ContainerCreateOptions.networkingConfig(block: NetworkingConfig.() -> Unit) {
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,25 @@
package me.devnatan.yoki.util

import kotlinx.serialization.KSerializer
import kotlinx.serialization.builtins.ListSerializer
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.JsonTransformingSerializer
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive

internal open class ListAsMapToEmptyObjectsSerializer<T : Any>(
private val tSerializer: KSerializer<T>,
) : JsonTransformingSerializer<List<T>>(ListSerializer(tSerializer)) {

override fun transformDeserialize(element: JsonElement): JsonElement {
return JsonArray(element.jsonObject.entries.map { JsonPrimitive(it.key) })
}

override fun transformSerialize(element: JsonElement): JsonElement {
return JsonObject(element.jsonArray.associate { it.jsonPrimitive.content to JsonObject(mapOf()) })
}
}
10 changes: 7 additions & 3 deletions src/commonTest/kotlin/me/devnatan/yoki/resource/ResourceIT.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,13 @@ package me.devnatan.yoki.resource
import me.devnatan.yoki.Yoki
import me.devnatan.yoki.createTestYoki

open class ResourceIT {
open class ResourceIT(
private val debugHttpCalls: Boolean = false,
) {

companion object {
val testClient: Yoki by lazy(::createTestYoki)
val testClient: Yoki by lazy {
createTestYoki {
debugHttpCalls([email protected])
}
}
}
Loading
Loading