Skip to content

Commit

Permalink
Add DockerLayerTask WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
SgtSilvio committed Dec 11, 2024
1 parent 1832047 commit 9ba9c18
Show file tree
Hide file tree
Showing 5 changed files with 167 additions and 48 deletions.
112 changes: 112 additions & 0 deletions src/main/kotlin/io/github/sgtsilvio/gradle/oci/DockerLayerTask.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package io.github.sgtsilvio.gradle.oci

import io.github.sgtsilvio.gradle.oci.internal.copyspec.DEFAULT_MODIFICATION_TIME
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream
import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.Optional
import org.gradle.kotlin.dsl.mapProperty
import org.gradle.kotlin.dsl.property
import org.gradle.process.ExecOperations
import java.io.ByteArrayInputStream
import java.io.FileInputStream
import java.nio.file.attribute.FileTime
import java.util.*

/**
* @author Silvio Giebl
*/
abstract class DockerLayerTask(private val execOperations: ExecOperations) : OciLayerTask() {

@get:Input
val from = project.objects.property<String>() // TODO replace from and platform with ImageInput

@get:Input
val platform = project.objects.property<String>()

@get:Input
val command = project.objects.property<String>()

@get:Input
@get:Optional
val shell = project.objects.property<String>()

@get:Input
@get:Optional
val user = project.objects.property<String>()

@get:Input
@get:Optional
val workingDirectory = project.objects.property<String>()

@get:Input
val environment = project.objects.mapProperty<String, String>()

override fun run(tos: TarArchiveOutputStream) {
val imageReference = UUID.randomUUID()
execOperations.exec {
commandLine("docker", "build", "-", "--platform", platform.get(), "-t", imageReference)
standardInput = ByteArrayInputStream(assembleDockerfile().toByteArray())
}
val tmpDir = temporaryDir
val savedImageTarFile = tmpDir.resolve("image.tar")
execOperations.exec {
commandLine("docker", "save", imageReference, "-o", savedImageTarFile)
}
execOperations.exec {
commandLine("docker", "rmi", imageReference)
}
val manifest = TarArchiveInputStream(FileInputStream(savedImageTarFile)).use { tis ->
while (tis.nextEntry != null) {
if (tis.currentEntry.name == "manifest.json") {
return@use tis.reader().readText()
}
}
throw IllegalStateException("manifest.json not found")
}
val lastLayerPath =
manifest.substringAfter("\"Layers\":[").substringBefore("]").split(",").last().removeSurrounding("\"")
TarArchiveInputStream(FileInputStream(savedImageTarFile)).use { tis ->
while (tis.nextEntry != null) {
if (tis.currentEntry.name == lastLayerPath) {
TarArchiveInputStream(tis).use { tis2 ->
while (tis2.nextEntry != null) {
val entry2 = tis2.currentEntry
entry2.lastModifiedTime = FileTime.from(DEFAULT_MODIFICATION_TIME)
tos.putArchiveEntry(entry2)
tis2.copyTo(tos)
tos.closeArchiveEntry()
}
}
return@use
}
}
throw IllegalStateException("$lastLayerPath not found")
}
tmpDir.deleteRecursively()
}

private fun assembleDockerfile() = buildString {
val from = from.get()
appendLine("FROM $from")
val shell = shell.orNull
if (shell != null) {
appendLine("SHELL $shell")
}
val user = user.orNull
if (user != null) {
appendLine("USER $user")
}
val workingDirectory = workingDirectory.orNull
if (workingDirectory != null) {
appendLine("WORKDIR $workingDirectory")
}
val environment = environment.get()
if (environment.isNotEmpty()) {
appendLine("ENV ").append(environment.map { "${it.key}=\"${it.value}\"" }.joinToString(" "))
}
appendLine("RUN echo \"Docker on MacOS creates a directory /root/.cache/rosetta in the first layer\"")
val command = command.get()
appendLine("RUN $command")
}
}
84 changes: 46 additions & 38 deletions src/main/kotlin/io/github/sgtsilvio/gradle/oci/OciLayerTask.kt
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,6 @@ import java.util.zip.GZIPOutputStream
*/
abstract class OciLayerTask : DefaultTask() {

private val _contents = project.objects.newOciCopySpec()

@get:Nested
protected val copySpecInput = _contents.asInput(project.providers)

@get:Input
val digestAlgorithm: Property<OciDigestAlgorithm> =
project.objects.property<OciDigestAlgorithm>().convention(OciDigestAlgorithm.SHA_256)
Expand Down Expand Up @@ -73,14 +68,8 @@ abstract class OciLayerTask : DefaultTask() {
diffId = properties.map { it.getProperty("diffId").toOciDigest() }
}

@get:Internal
val contents: OciCopySpec get() = _contents

fun contents(action: Action<in OciCopySpec>) = action.execute(_contents)

@TaskAction
protected fun run() {
val copySpecInput = copySpecInput.get()
val digestAlgorithm = digestAlgorithm.get()
val compression = compression.get()
val file = file.get().asFile
Expand All @@ -92,38 +81,57 @@ abstract class OciLayerTask : DefaultTask() {
TarArchiveOutputStream(dos, StandardCharsets.UTF_8.name()).use { tos ->
tos.setLongFileMode(TarArchiveOutputStream.LONGFILE_POSIX)
tos.setBigNumberMode(TarArchiveOutputStream.BIGNUMBER_POSIX)
copySpecInput.process(object : OciCopySpecVisitor {
override fun visitFile(fileMetadata: FileMetadata, fileSource: FileSource) {
tos.putArchiveEntry(TarArchiveEntry(fileMetadata.path).apply {
setPermissions(fileMetadata.permissions)
setUserId(fileMetadata.userId)
setGroupId(fileMetadata.groupId)
setModTime(fileMetadata.modificationTime.toEpochMilli())
size = fileMetadata.size
})
fileSource.copyTo(tos)
tos.closeArchiveEntry()
}

override fun visitDirectory(fileMetadata: FileMetadata) {
tos.putArchiveEntry(TarArchiveEntry(fileMetadata.path).apply {
setPermissions(fileMetadata.permissions)
setUserId(fileMetadata.userId)
setGroupId(fileMetadata.groupId)
setModTime(fileMetadata.modificationTime.toEpochMilli())
})
tos.closeArchiveEntry()
}

private fun TarArchiveEntry.setPermissions(permissions: Int) {
mode = (mode and 0b111_111_111.inv()) or (permissions and 0b111_111_111)
}
})
run(tos)
}
}
}
propertiesFile.writeText("digest=$digest\nsize=${file.length()}\ndiffId=$diffId")
}

protected abstract fun run(tos: TarArchiveOutputStream)
}

abstract class DefaultOciLayerTask : OciLayerTask() {

private val _contents = project.objects.newOciCopySpec()

@get:Nested
protected val copySpecInput = _contents.asInput(project.providers)

@get:Internal
val contents: OciCopySpec get() = _contents

fun contents(action: Action<in OciCopySpec>) = action.execute(_contents)

override fun run(tos: TarArchiveOutputStream) {
copySpecInput.get().process(object : OciCopySpecVisitor {
override fun visitFile(fileMetadata: FileMetadata, fileSource: FileSource) {
tos.putArchiveEntry(TarArchiveEntry(fileMetadata.path).apply {
setPermissions(fileMetadata.permissions)
setUserId(fileMetadata.userId)
setGroupId(fileMetadata.groupId)
setModTime(fileMetadata.modificationTime.toEpochMilli())
size = fileMetadata.size
})
fileSource.copyTo(tos)
tos.closeArchiveEntry()
}

override fun visitDirectory(fileMetadata: FileMetadata) {
tos.putArchiveEntry(TarArchiveEntry(fileMetadata.path).apply {
setPermissions(fileMetadata.permissions)
setUserId(fileMetadata.userId)
setGroupId(fileMetadata.groupId)
setModTime(fileMetadata.modificationTime.toEpochMilli())
})
tos.closeArchiveEntry()
}

private fun TarArchiveEntry.setPermissions(permissions: Int) {
mode = (mode and 0b111_111_111.inv()) or (permissions and 0b111_111_111)
}
})
}
}

enum class OciLayerCompression(internal val extension: String, internal val mediaType: String) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import org.gradle.api.tasks.testing.Test
*/
interface OciExtension : PlatformFactories, PlatformSelectorFactories {
val layerTaskClass get() = OciLayerTask::class
val defaultLayerTask get() = DefaultOciLayerTask::class
val dockerLayerTaskClass get() = DockerLayerTask::class
val imagesTaskClass get() = OciImagesTask::class
val pushTaskClass get() = OciPushTask::class
val pushSingleTaskClass get() = OciPushSingleTask::class
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ private const val DEFAULT_FILE_PERMISSIONS = 0b110_100_100
private const val DEFAULT_DIRECTORY_PERMISSIONS = 0b111_101_101
private const val DEFAULT_USER_ID = 0L
private const val DEFAULT_GROUP_ID = 0L
private val DEFAULT_MODIFICATION_TIME: Instant = Instant.ofEpochSecond(1)
internal val DEFAULT_MODIFICATION_TIME: Instant = Instant.ofEpochSecond(1)

internal fun OciCopySpecInput.process(visitor: OciCopySpecVisitor) {
val allFiles = HashMap<String, FileMetadata>()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
package io.github.sgtsilvio.gradle.oci.internal.dsl

import io.github.sgtsilvio.gradle.oci.OciCopySpec
import io.github.sgtsilvio.gradle.oci.OciLayerTask
import io.github.sgtsilvio.gradle.oci.OciMetadataTask
import io.github.sgtsilvio.gradle.oci.TASK_GROUP_NAME
import io.github.sgtsilvio.gradle.oci.*
import io.github.sgtsilvio.gradle.oci.attributes.*
import io.github.sgtsilvio.gradle.oci.dsl.Capability
import io.github.sgtsilvio.gradle.oci.dsl.OciImageDefinition
Expand Down Expand Up @@ -355,7 +352,7 @@ internal abstract class OciImageDefinitionImpl @Inject constructor(
createdBy.convention("gradle-oci: $name")
}

private var task: TaskProvider<OciLayerTask>? = null
private var task: TaskProvider<DefaultOciLayerTask>? = null
private var variantScopeConfigurations: LinkedList<Action<in OciCopySpec>>? = null
private var externalTask: TaskProvider<OciLayerTask>? = null

Expand Down Expand Up @@ -387,7 +384,7 @@ internal abstract class OciImageDefinitionImpl @Inject constructor(

fun contentsFromVariantScope(
variantScopeConfiguration: Action<in OciCopySpec>,
variantScopeTask: TaskProvider<OciLayerTask>,
variantScopeTask: TaskProvider<DefaultOciLayerTask>,
) {
if (externalTask != null) {
throw IllegalStateException("'contents {}' must not be called if 'contents(task)' was called")
Expand Down Expand Up @@ -488,7 +485,7 @@ internal abstract class OciImageDefinitionImpl @Inject constructor(
private val taskContainer: TaskContainer,
) : OciImageDefinition.VariantScope.Layer {

private var task: TaskProvider<OciLayerTask>? = null
private var task: TaskProvider<DefaultOciLayerTask>? = null
private var externalTask: TaskProvider<OciLayerTask>? = null

final override fun getName() = name
Expand Down Expand Up @@ -545,12 +542,12 @@ private fun TaskContainer.createLayerTask(
layerName: String,
platformPostfix: String,
projectLayout: ProjectLayout,
) = register<OciLayerTask>(createOciLayerClassifier(imageDefName, layerName).camelCase() + platformPostfix) {
) = register<DefaultOciLayerTask>(createOciLayerClassifier(imageDefName, layerName).camelCase() + platformPostfix) {
group = TASK_GROUP_NAME
description = "Assembles the '$layerName' layer of the '$imageDefName' OCI image."
destinationDirectory.set(projectLayout.buildDirectory.dir("oci/images/$imageDefName"))
classifier.set(createOciLayerClassifier(imageDefName, layerName) + platformPostfix)
}

private fun TaskProvider<OciLayerTask>.contents(configuration: Action<in OciCopySpec>) =
private fun TaskProvider<DefaultOciLayerTask>.contents(configuration: Action<in OciCopySpec>) =
configure { contents(configuration) }

0 comments on commit 9ba9c18

Please sign in to comment.