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
8 changes: 4 additions & 4 deletions SUPPORTED_ENDPOINTS.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Yoki supported Docker API endpoints

Supports 43 of 106 endpoints
Supports 46 of 106 endpoints

### Containers (15/25)
* [x] List containers - GET **/containers/json**
Expand All @@ -24,9 +24,9 @@ Supports 43 of 106 endpoints
* [ ] Attach to a container via a websocket - **POST /containers/:id/attach/ws**
* [x] Wait for a container - **POST /containers/:id/wait**
* [x] Remove a container - **DELETE /containers/:id**
* [ ] Get information about files in a container - **HEAD /containers/:id/archive**
* [ ] Get an archive of a filesystem resource in a container - **GET /containers/:id/archive**
* [ ] Extract an archive of files or folders to a directory in a container - **PUT /containers/:id/archive**
* [x] Get information about files in a container - **HEAD /containers/:id/archive**
* [x] Get an archive of a filesystem resource in a container - **GET /containers/:id/archive**
* [x] Extract an archive of files or folders to a directory in a container - **PUT /containers/:id/archive**
* [x] Delete stopped containers - **PUT /containers/prune**

### Images (3/15)
Expand Down
2 changes: 1 addition & 1 deletion THIRDPARTY.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ This document contains a list of third-party libraries that Yoki uses.
* [kotlinx-coroutines](https://github.com/Kotlin/kotlinx.coroutines)
* [kotlinx-serialization](https://github.com/Kotlin/kotlinx.serialization)
* [kotlinx-datetime](https://github.com/Kotlin/kotlinx-datetime)
* [kotlinx-io](https://github.com/Kotlin/kotlinx-io)
* [JetBrains Annotations](https://github.com/JetBrains/java-annotations)
* [Okio](https://github.com/square/okio)
* [publish-on-central](https://github.com/DanySK/publish-on-central)
* [kotlinter](https://github.com/jeremymailen/kotlinter-gradle)
* [detekt](https://github.com/detekt/detekt)
Expand Down
46 changes: 46 additions & 0 deletions api/yoki.api
Original file line number Diff line number Diff line change
Expand Up @@ -1283,6 +1283,48 @@ public final class me/devnatan/yoki/models/container/Container$Companion {
public final fun serializer ()Lkotlinx/serialization/KSerializer;
}

public final class me/devnatan/yoki/models/container/ContainerArchiveInfo {
public static final field Companion Lme/devnatan/yoki/models/container/ContainerArchiveInfo$Companion;
public synthetic fun <init> (ILjava/lang/String;JILjava/lang/String;Ljava/lang/String;Lkotlinx/serialization/internal/SerializationConstructorMarker;)V
public fun <init> (Ljava/lang/String;JILjava/lang/String;Ljava/lang/String;)V
public synthetic fun <init> (Ljava/lang/String;JILjava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1 ()Ljava/lang/String;
public final fun component2 ()J
public final fun component3 ()I
public final fun component4 ()Ljava/lang/String;
public final fun component5 ()Ljava/lang/String;
public final fun copy (Ljava/lang/String;JILjava/lang/String;Ljava/lang/String;)Lme/devnatan/yoki/models/container/ContainerArchiveInfo;
public static synthetic fun copy$default (Lme/devnatan/yoki/models/container/ContainerArchiveInfo;Ljava/lang/String;JILjava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lme/devnatan/yoki/models/container/ContainerArchiveInfo;
public fun equals (Ljava/lang/Object;)Z
public final fun getLinkTarget ()Ljava/lang/String;
public final fun getMode ()I
public final fun getModifiedAtRaw ()Ljava/lang/String;
public final fun getName ()Ljava/lang/String;
public final fun getSize ()J
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
public static final synthetic fun write$Self (Lme/devnatan/yoki/models/container/ContainerArchiveInfo;Lkotlinx/serialization/encoding/CompositeEncoder;Lkotlinx/serialization/descriptors/SerialDescriptor;)V
}

public final class me/devnatan/yoki/models/container/ContainerArchiveInfo$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
public static final field INSTANCE Lme/devnatan/yoki/models/container/ContainerArchiveInfo$$serializer;
public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lme/devnatan/yoki/models/container/ContainerArchiveInfo;
public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
public fun serialize (Lkotlinx/serialization/encoding/Encoder;Lme/devnatan/yoki/models/container/ContainerArchiveInfo;)V
public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer;
}

public final class me/devnatan/yoki/models/container/ContainerArchiveInfo$Companion {
public final fun serializer ()Lkotlinx/serialization/KSerializer;
}

public final class me/devnatan/yoki/models/container/ContainerArchiveInfoKt {
public static final fun getModifiedAt (Lme/devnatan/yoki/models/container/ContainerArchiveInfo;)Lkotlinx/datetime/Instant;
}

public final class me/devnatan/yoki/models/container/ContainerConfig {
public static final field Companion Lme/devnatan/yoki/models/container/ContainerConfig$Companion;
public fun <init> ()V
Expand Down Expand Up @@ -3667,9 +3709,12 @@ public final class me/devnatan/yoki/resource/container/ContainerRenameConflictEx

public final class me/devnatan/yoki/resource/container/ContainerResource {
public fun <init> (Lkotlinx/coroutines/CoroutineScope;Lkotlinx/serialization/json/Json;Lio/ktor/client/HttpClient;)V
public final fun archive (Ljava/lang/String;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public static synthetic fun archive$default (Lme/devnatan/yoki/resource/container/ContainerResource;Ljava/lang/String;Ljava/lang/String;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;
public final synthetic fun attach (Ljava/lang/String;)Lkotlinx/coroutines/flow/Flow;
public final synthetic fun create (Lme/devnatan/yoki/models/container/ContainerCreateOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public final fun createAsync (Lme/devnatan/yoki/models/container/ContainerCreateOptions;)Ljava/util/concurrent/CompletableFuture;
public final fun downloadArchive (Ljava/lang/String;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public final synthetic fun exec (Ljava/lang/String;Lme/devnatan/yoki/models/exec/ExecCreateOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public static synthetic fun exec$default (Lme/devnatan/yoki/resource/container/ContainerResource;Ljava/lang/String;Lme/devnatan/yoki/models/exec/ExecCreateOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;
public final fun execAsync (Ljava/lang/String;)Ljava/util/concurrent/CompletableFuture;
Expand Down Expand Up @@ -3724,6 +3769,7 @@ public final class me/devnatan/yoki/resource/container/ContainerResource {
public final fun stopAsync (Ljava/lang/String;I)Ljava/util/concurrent/CompletableFuture;
public final synthetic fun unpause (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public final fun unpauseAsync (Ljava/lang/String;)Ljava/util/concurrent/CompletableFuture;
public final fun uploadArchive (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public final synthetic fun wait (Ljava/lang/String;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public static synthetic fun wait$default (Lme/devnatan/yoki/resource/container/ContainerResource;Ljava/lang/String;Ljava/lang/String;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;
public final fun waitAsync (Ljava/lang/String;)Ljava/util/concurrent/CompletableFuture;
Expand Down
3 changes: 2 additions & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ kotlin {
implementation(libs.ktx.datetime)
implementation(libs.bundles.ktor)
implementation(libs.bundles.ktx)
implementation(libs.okio)
implementation(libs.kotlinx.io.core)
}
}

Expand All @@ -71,6 +71,7 @@ kotlin {
implementation(libs.junixsocket.common)
implementation(libs.ktor.client.engine.okhttp)
implementation(libs.slf4j.api)
implementation(libs.apache.compress)
}
}

Expand Down
15 changes: 10 additions & 5 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ ktor = "2.3.3"
junixsocket = "2.6.1"
ktx-datetime = "0.4.0"
junit = "5.10.0"
okio = "3.5.0"
slf4j = "2.0.7"
apache-compress = "1.23.0"
kotlinx-io = "0.2.1"
plugin-kotlinter = "3.16.0"
plugin-publishOnCentral = "5.0.11"
plugin-binaryCompatibilityValidator = "0.13.2"
Expand Down Expand Up @@ -82,14 +83,18 @@ version.ref = "ktx-datetime"
module = "org.junit.jupiter:junit-jupiter-engine"
version.ref = "junit"

[libraries.okio]
module = "com.squareup.okio:okio"
version.ref = "okio"

[libraries.slf4j-api]
module = "org.slf4j:slf4j-api"
version.ref = "slf4j"

[libraries.apache-compress]
module = "org.apache.commons:commons-compress"
version.ref = "apache-compress"

[libraries.kotlinx-io-core]
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"]
ktx = ["ktx-coroutines-core", "ktx-serialization-core", "ktx-serialization-json"]
Expand Down
11 changes: 7 additions & 4 deletions src/commonMain/kotlin/me/devnatan/yoki/io/Http.kt
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import kotlinx.serialization.json.Json
import me.devnatan.yoki.GenericDockerErrorResponse
import me.devnatan.yoki.Yoki
import me.devnatan.yoki.YokiResponseException
import okio.ByteString.Companion.encodeUtf8

internal expect fun <T : HttpClientEngineConfig> HttpClientConfig<out T>.configureHttpClient(client: Yoki)

Expand All @@ -45,11 +44,14 @@ internal fun createHttpClient(client: Yoki): HttpClient {
handleResponseExceptionWithRequest { exception, _ ->
val responseException = exception as? ResponseException ?: return@handleResponseExceptionWithRequest
val exceptionResponse = responseException.response
println("exceptionResponse = ${exceptionResponse.body<String>()}")

val error = exceptionResponse.body<GenericDockerErrorResponse>()
val errorMessage = runCatching {
exceptionResponse.body<GenericDockerErrorResponse>()
}.getOrNull()?.message
throw YokiResponseException(
cause = exception,
message = error.message,
message = errorMessage,
statusCode = exceptionResponse.status,
)
}
Expand All @@ -70,11 +72,12 @@ internal fun createHttpClient(client: Yoki): HttpClient {
}
}

@OptIn(ExperimentalStdlibApi::class)
private fun createUrlBuilder(socketPath: String): URLBuilder = if (isUnixSocket(socketPath)) {
URLBuilder(
protocol = URLProtocol.HTTP,
port = DOCKER_SOCKET_PORT,
host = socketPath.substringAfter(UNIX_SOCKET_PREFIX).encodeUtf8().hex() + ENCODED_HOSTNAME_SUFFIX,
host = socketPath.substringAfter(UNIX_SOCKET_PREFIX).toInt().toHexString() + ENCODED_HOSTNAME_SUFFIX,
)
} else {
val url = Url(socketPath)
Expand Down
7 changes: 7 additions & 0 deletions src/commonMain/kotlin/me/devnatan/yoki/io/TarFile.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package me.devnatan.yoki.io

import kotlinx.io.RawSource

internal expect fun readTarFile(input: RawSource): RawSource

internal expect fun writeTarFile(filePath: String): RawSource
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package me.devnatan.yoki.models.container

import kotlinx.datetime.Instant
import kotlinx.datetime.toInstant
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
public data class ContainerArchiveInfo(
val name: String,
val size: Long,
val mode: Int,
@SerialName("mtime") val modifiedAtRaw: String,
val linkTarget: String = "",
)

public val ContainerArchiveInfo.modifiedAt: Instant
get() = modifiedAtRaw.toInstant()
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package me.devnatan.yoki.resource.container

import kotlinx.coroutines.flow.Flow
import kotlinx.io.RawSource
import me.devnatan.yoki.YokiResponseException
import me.devnatan.yoki.models.Frame
import me.devnatan.yoki.models.ResizeTTYOptions
import me.devnatan.yoki.models.container.Container
import me.devnatan.yoki.models.container.ContainerArchiveInfo
import me.devnatan.yoki.models.container.ContainerCreateOptions
import me.devnatan.yoki.models.container.ContainerListOptions
import me.devnatan.yoki.models.container.ContainerPruneFilters
Expand All @@ -14,8 +16,11 @@ 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

internal const val FS_ROOT = "/"

public expect class ContainerResource {

/**
Expand Down Expand Up @@ -136,4 +141,30 @@ public expect class ContainerResource {

// TODO documentation
public suspend fun prune(filters: ContainerPruneFilters = ContainerPruneFilters()): ContainerPruneResult

/**
* Retrieves information about files of a container file system.
*
* @param container The container id.
* @param path The path to the file or directory inside the container file system.
*/
@JvmOverloads
public suspend fun archive(container: String, path: String = FS_ROOT): ContainerArchiveInfo

/**
* Downloads files from a container file system.
*
* @param container The container id.
* @param remotePath The path to the file or directory inside the container file system.
*/
public suspend fun downloadArchive(container: String, remotePath: String): RawSource

/**
* Uploads files into a container file system.
*
* @param container The container id.
* @param inputPath Path to the file that will be uploaded.
* @param remotePath Path to the file or directory inside the container file system.
*/
public suspend fun uploadArchive(container: String, inputPath: String, remotePath: String)
}
1 change: 1 addition & 0 deletions src/jvmMain/kotlin/me/devnatan/yoki/Yoki.jvm.kt
Original file line number Diff line number Diff line change
Expand Up @@ -47,5 +47,6 @@ public actual class Yoki public actual constructor(public actual val config: Yok

public actual fun close() {
cancel()
httpClient.close()
}
}
95 changes: 95 additions & 0 deletions src/jvmMain/kotlin/me/devnatan/yoki/io/CompressArchiveUtil.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package me.devnatan.yoki.io

import org.apache.commons.compress.archivers.tar.TarArchiveEntry
import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream
import org.apache.commons.compress.compressors.gzip.GzipCompressorOutputStream
import java.io.BufferedInputStream
import java.io.BufferedOutputStream
import java.io.IOException
import java.nio.file.FileVisitResult
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.SimpleFileVisitor
import java.nio.file.attribute.BasicFileAttributes
import kotlin.io.path.pathString
import kotlin.io.path.relativeTo

internal object CompressArchiveUtil {

@Throws(IOException::class)
fun tar(inputPath: Path, outputPath: Path, childrenOnly: Boolean) {
buildTarStream(outputPath).use { tarArchiveOutputStream ->
if (!Files.isDirectory(inputPath)) {
addFileToTar(tarArchiveOutputStream, inputPath, inputPath.fileName.toString())
} else {
var sourcePath: Path = inputPath
if (!childrenOnly) {
// In order to have the dossier as the root entry
sourcePath = inputPath.parent
}
Files.walkFileTree(inputPath, TarDirWalker(sourcePath, tarArchiveOutputStream))
}
tarArchiveOutputStream.flush()
}
}

@Throws(IOException::class)
fun addFileToTar(tarArchiveOutputStream: TarArchiveOutputStream, file: Path, entryName: String?) {
if (Files.isSymbolicLink(file)) {
tarArchiveOutputStream.putArchiveEntry(
TarArchiveEntry(entryName, TarArchiveEntry.LF_SYMLINK).apply {
linkName = Files.readSymbolicLink(file).toString()
},
)
} else {
val tarArchiveEntry = tarArchiveOutputStream.createArchiveEntry(
file.toFile(),
entryName,
) as TarArchiveEntry
if (file.toFile().canExecute()) {
tarArchiveEntry.mode = tarArchiveEntry.mode or 493
}
tarArchiveOutputStream.putArchiveEntry(tarArchiveEntry)
if (file.toFile().isFile()) {
BufferedInputStream(Files.newInputStream(file)).use { input ->
input.copyTo(tarArchiveOutputStream)
}
}
}
tarArchiveOutputStream.closeArchiveEntry()
}

@Throws(IOException::class)
private fun buildTarStream(outputPath: Path) =
TarArchiveOutputStream(GzipCompressorOutputStream(BufferedOutputStream(Files.newOutputStream(outputPath))))
.apply {
setLongFileMode(TarArchiveOutputStream.LONGFILE_POSIX)
setBigNumberMode(TarArchiveOutputStream.BIGNUMBER_POSIX)
}

class TarDirWalker(private val basePath: Path, private val tarArchiveOutputStream: TarArchiveOutputStream) :
SimpleFileVisitor<Path>() {
@Throws(IOException::class)
override fun preVisitDirectory(dir: Path, attrs: BasicFileAttributes): FileVisitResult {
if (dir != basePath) {
tarArchiveOutputStream.putArchiveEntry(
TarArchiveEntry(dir.relativeTo(basePath)),
)
tarArchiveOutputStream.closeArchiveEntry()
}
return FileVisitResult.CONTINUE
}

@Throws(IOException::class)
override fun visitFile(file: Path, attrs: BasicFileAttributes): FileVisitResult {
addFileToTar(tarArchiveOutputStream, file, file.relativize(basePath).pathString)
return FileVisitResult.CONTINUE
}

@Throws(IOException::class)
override fun visitFileFailed(file: Path, exc: IOException): FileVisitResult {
tarArchiveOutputStream.close()
throw exc
}
}
}
6 changes: 3 additions & 3 deletions src/jvmMain/kotlin/me/devnatan/yoki/io/Sockets.jvm.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package me.devnatan.yoki.io

import okhttp3.Dns
import okio.ByteString.Companion.decodeHex
import org.newsclub.net.unix.AFUNIXSocketAddress
import org.newsclub.net.unix.AFUNIXSocketFactory
import java.net.InetAddress
Expand All @@ -25,11 +24,12 @@ internal class SocketDns(private val isUnixSocket: Boolean) : Dns {
}

internal class UnixSocketFactory : AFUNIXSocketFactory() {
@OptIn(ExperimentalStdlibApi::class)
private fun decodeHostname(hostname: String): String {
return hostname
.substring(0, hostname.indexOf(ENCODED_HOSTNAME_SUFFIX))
.decodeHex()
.utf8()
.hexToByteArray()
.decodeToString()
}

override fun addressFromHost(host: String, port: Int): AFUNIXSocketAddress {
Expand Down
Loading