Skip to content

Commit 29459eb

Browse files
committed
Add dockerPull and dockerRemove
Also, dockerRun is no longer expected to auto pull images. Authored-by: Leonhardt Koepsell <[email protected]>
1 parent 76d5882 commit 29459eb

File tree

22 files changed

+303
-3
lines changed

22 files changed

+303
-3
lines changed

CHANGELOG.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,15 @@ All notable changes to this project will be documented in this file.
99
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
1010
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
1111

12-
## [0.1.3] - 2025-04-22
12+
## [Unreleased]
13+
14+
### Added
15+
16+
- dockerPull and dockerRemove
1317

1418
### Fixed
1519

16-
- Remove the unreliable dependency on Docker CLI
20+
- Remove the unreliable dependency on Docker CLI. With this change, dockerRun no longer automatically pulls the image.
1721

1822
### Chore
1923

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import dev.codebandits.container.gradle.tasks.ContainerRunTask
2828

2929
tasks {
3030
register<ContainerRunTask>("writeHello") {
31+
dockerPull { image = "alpine:latest" }
3132
dockerRun {
3233
image = "alpine:latest"
3334
entrypoint = "sh"

examples/basic-groovy/build.gradle

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,19 @@ plugins {
55
}
66

77
tasks.register('sayHello', ContainerRunTask) {
8+
dockerPull {
9+
it.image.set('alpine:latest')
10+
}
811
dockerRun {
912
it.image.set('alpine:latest')
1013
it.args.set(['echo', 'Hello from a container!'] as String[])
1114
}
1215
}
1316

1417
tasks.register('writeHello', ContainerRunTask) {
18+
dockerPull {
19+
it.image.set('alpine:latest')
20+
}
1521
dockerRun {
1622
it.image.set('alpine:latest')
1723
it.entrypoint.set('sh')

examples/basic-kotlin/build.gradle.kts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,19 @@ plugins {
66

77
tasks {
88
register<ContainerRunTask>("sayHello") {
9+
dockerPull {
10+
image = "alpine:latest"
11+
}
912
dockerRun {
1013
image = "alpine:latest"
1114
args = arrayOf("echo", "Hello from a container!")
1215
}
1316
}
1417

1518
register<ContainerRunTask>("writeHello") {
19+
dockerPull {
20+
image = "alpine:latest"
21+
}
1622
dockerRun {
1723
image = "alpine:latest"
1824
entrypoint = "sh"

examples/buildpacks/build.gradle.kts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ tasks {
1010
inputs.file("index.html")
1111
inputs.file("project.toml")
1212
outputImages.dockerLocal("my-image:latest")
13+
dockerPull {
14+
image = "buildpacksio/pack:latest"
15+
}
1316
dockerRun {
1417
image = "buildpacksio/pack:latest"
1518
args = arrayOf(

examples/dind-build/build.gradle.kts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ tasks {
99
register<ContainerRunTask>("buildImage") {
1010
inputs.file("Dockerfile")
1111
outputImages.dockerLocal("my-image:latest")
12+
dockerPull {
13+
image = "docker:dind"
14+
}
1215
dockerRun {
1316
image = "docker:dind"
1417
entrypoint = "docker"

examples/input-output-chaining/build.gradle.kts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ tasks {
2121
register<ContainerRunTask>("buildImage") {
2222
inputs.file("Dockerfile")
2323
outputImages.dockerLocal("my-image:latest")
24+
dockerPull {
25+
image = "docker:dind"
26+
}
2427
dockerRun {
2528
image = "docker:dind"
2629
entrypoint = "docker"

src/main/kotlin/dev/codebandits/container/gradle/tasks/ContainerRunTask.kt

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package dev.codebandits.container.gradle.tasks
22

33
import com.github.dockerjava.api.async.ResultCallback
4+
import com.github.dockerjava.api.command.PullImageResultCallback
45
import com.github.dockerjava.api.command.WaitContainerResultCallback
56
import com.github.dockerjava.api.model.Bind
67
import com.github.dockerjava.api.model.Frame
@@ -12,6 +13,17 @@ import org.gradle.api.model.ObjectFactory
1213
import org.gradle.api.provider.Property
1314

1415
public abstract class ContainerRunTask : ContainerExecTask() {
16+
17+
public open class DockerPullSpec(objects: ObjectFactory) {
18+
public val image: Property<String> = objects.property(String::class.java)
19+
public val dockerHost: Property<String> = objects.property(String::class.java)
20+
}
21+
22+
public open class DockerRemoveSpec(objects: ObjectFactory) {
23+
public val image: Property<String> = objects.property(String::class.java)
24+
public val dockerHost: Property<String> = objects.property(String::class.java)
25+
}
26+
1527
public open class DockerRunSpec(objects: ObjectFactory) {
1628
public val image: Property<String> = objects.property(String::class.java)
1729
public val volumes: Property<Array<String>> = objects.property(Array<String>::class.java).convention(emptyArray())
@@ -21,10 +33,42 @@ public abstract class ContainerRunTask : ContainerExecTask() {
2133
public val user: Property<String> = objects.property(String::class.java)
2234
public val privileged: Property<Boolean> = objects.property(Boolean::class.java).convention(false)
2335
public val autoRemove: Property<Boolean> = objects.property(Boolean::class.java).convention(true)
24-
public val alwaysPull: Property<Boolean> = objects.property(Boolean::class.java).convention(true)
2536
public val dockerHost: Property<String> = objects.property(String::class.java)
2637
}
2738

39+
public fun dockerPull(configure: DockerPullSpec.() -> Unit) {
40+
steps.add(
41+
ExecutionStep(
42+
action = {
43+
val spec = DockerPullSpec(project.objects).apply(configure)
44+
val dockerHost = spec.dockerHost.orNull
45+
val dockerClient = createDockerClient(dockerHost)
46+
47+
dockerClient
48+
.pullImageCmd(spec.image.get())
49+
.exec(PullImageResultCallback())
50+
.awaitCompletion()
51+
},
52+
)
53+
)
54+
}
55+
56+
public fun dockerRemove(configure: DockerRemoveSpec.() -> Unit) {
57+
steps.add(
58+
ExecutionStep(
59+
action = {
60+
val spec = DockerRemoveSpec(project.objects).apply(configure)
61+
val dockerHost = spec.dockerHost.orNull
62+
val dockerClient = createDockerClient(dockerHost)
63+
64+
dockerClient
65+
.removeImageCmd(spec.image.get())
66+
.exec()
67+
},
68+
)
69+
)
70+
}
71+
2872
public fun dockerRun(configure: DockerRunSpec.() -> Unit) {
2973
steps.add(
3074
ExecutionStep(
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
package dev.codebandits.container.gradle
2+
3+
import dev.codebandits.container.gradle.helpers.appendLine
4+
import org.gradle.testkit.runner.GradleRunner
5+
import org.gradle.testkit.runner.TaskOutcome
6+
import org.junit.jupiter.api.Test
7+
import strikt.api.expectThat
8+
import strikt.assertions.contains
9+
import strikt.assertions.isEqualTo
10+
import strikt.assertions.isNotNull
11+
import strikt.assertions.isTrue
12+
import java.util.UUID
13+
14+
class DockerPullTest : GradleProjectTest() {
15+
16+
@Test
17+
fun `dockerPull pulls the specified image`() {
18+
removeImage("alpine:3.18.9")
19+
20+
buildGradleKtsFile.appendLine(
21+
"""
22+
import dev.codebandits.container.gradle.tasks.ContainerRunTask
23+
24+
plugins {
25+
id("dev.codebandits.container")
26+
}
27+
28+
tasks {
29+
register<ContainerRunTask>("pullAlpineImage") {
30+
dockerPull {
31+
image = "alpine:3.18.9"
32+
}
33+
}
34+
}
35+
""".trimIndent()
36+
)
37+
38+
val result = GradleRunner.create()
39+
.withPluginClasspath()
40+
.withProjectDir(projectDirectory.toFile())
41+
.withArguments("pullAlpineImage")
42+
.build()
43+
44+
expectThat(result).and {
45+
get { task(":pullAlpineImage") }.isNotNull().get { outcome }.isEqualTo(TaskOutcome.SUCCESS)
46+
}
47+
48+
expectThat(imageExists("alpine:3.18.9")).isTrue()
49+
}
50+
51+
@Test
52+
fun `dockerPull fails when pulling an image that does not exist`() {
53+
buildGradleKtsFile.appendLine(
54+
"""
55+
import dev.codebandits.container.gradle.tasks.ContainerRunTask
56+
57+
plugins {
58+
id("dev.codebandits.container")
59+
}
60+
61+
tasks {
62+
register<ContainerRunTask>("pullImageNotExist") {
63+
dockerPull {
64+
image = "alpine:${UUID.randomUUID()}"
65+
}
66+
}
67+
}
68+
""".trimIndent()
69+
)
70+
71+
val result = GradleRunner.create()
72+
.withPluginClasspath()
73+
.withProjectDir(projectDirectory.toFile())
74+
.withArguments("pullImageNotExist")
75+
.buildAndFail()
76+
77+
expectThat(result).and {
78+
get { task(":pullImageNotExist") }.isNotNull().get { outcome }.isEqualTo(TaskOutcome.FAILED)
79+
get { output }.contains("manifest unknown")
80+
}
81+
}
82+
83+
private fun removeImage(imageReference: String) {
84+
ProcessBuilder("docker", "rmi", "-f", imageReference)
85+
.inheritIO()
86+
.start()
87+
.waitFor()
88+
}
89+
90+
private fun imageExists(imageReference: String): Boolean {
91+
val process = ProcessBuilder("docker", "image", "inspect", imageReference)
92+
.redirectErrorStream(true)
93+
.start()
94+
val exitCode = process.waitFor()
95+
return exitCode == 0
96+
}
97+
}
98+
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package dev.codebandits.container.gradle
2+
3+
import dev.codebandits.container.gradle.helpers.appendLine
4+
import org.gradle.testkit.runner.GradleRunner
5+
import org.gradle.testkit.runner.TaskOutcome
6+
import org.junit.jupiter.api.Test
7+
import strikt.api.expectThat
8+
import strikt.assertions.contains
9+
import strikt.assertions.isEqualTo
10+
import strikt.assertions.isFalse
11+
import strikt.assertions.isNotNull
12+
import java.util.*
13+
14+
class DockerRemoveTest : GradleProjectTest() {
15+
16+
@Test
17+
fun `dockerRemove removes the specified image`() {
18+
pullImage("alpine:3.18.9")
19+
20+
buildGradleKtsFile.appendLine(
21+
"""
22+
import dev.codebandits.container.gradle.tasks.ContainerRunTask
23+
24+
plugins {
25+
id("dev.codebandits.container")
26+
}
27+
28+
tasks {
29+
register<ContainerRunTask>("removeAlpineImage") {
30+
dockerRemove {
31+
image = "alpine:3.18.9"
32+
}
33+
}
34+
}
35+
""".trimIndent()
36+
)
37+
38+
val result = GradleRunner.create()
39+
.withPluginClasspath()
40+
.withProjectDir(projectDirectory.toFile())
41+
.withArguments("removeAlpineImage")
42+
.build()
43+
44+
expectThat(result).and {
45+
get { task(":removeAlpineImage") }.isNotNull().get { outcome }.isEqualTo(TaskOutcome.SUCCESS)
46+
}
47+
48+
expectThat(imageExists("alpine:3.18.9")).isFalse()
49+
}
50+
51+
@Test
52+
fun `dockerRemove fails when removing an image that does not exist`() {
53+
buildGradleKtsFile.appendLine(
54+
"""
55+
import dev.codebandits.container.gradle.tasks.ContainerRunTask
56+
57+
plugins {
58+
id("dev.codebandits.container")
59+
}
60+
61+
tasks {
62+
register<ContainerRunTask>("removeImageNotExist") {
63+
dockerRemove {
64+
image = "alpine:${UUID.randomUUID()}"
65+
}
66+
}
67+
}
68+
""".trimIndent()
69+
)
70+
71+
val result = GradleRunner.create()
72+
.withPluginClasspath()
73+
.withProjectDir(projectDirectory.toFile())
74+
.withArguments("removeImageNotExist")
75+
.buildAndFail()
76+
77+
expectThat(result).and {
78+
get { task(":removeImageNotExist") }.isNotNull().get { outcome }.isEqualTo(TaskOutcome.FAILED)
79+
get { output }.contains("No such image")
80+
}
81+
}
82+
83+
private fun pullImage(imageReference: String) {
84+
ProcessBuilder("docker", "pull", imageReference)
85+
.inheritIO()
86+
.start()
87+
.waitFor()
88+
}
89+
90+
private fun imageExists(imageReference: String): Boolean {
91+
val process = ProcessBuilder("docker", "image", "inspect", imageReference)
92+
.redirectErrorStream(true)
93+
.start()
94+
val exitCode = process.waitFor()
95+
return exitCode == 0
96+
}
97+
}

src/testFeatures/kotlin/dev/codebandits/container/gradle/DockerRunAutoRemoveTest.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ class DockerRunAutoRemoveTest : GradleProjectTest() {
2424
2525
tasks {
2626
register<ContainerRunTask>("printID") {
27+
dockerPull { image = "alpine:latest" }
2728
dockerRun {
2829
image = "alpine:latest"
2930
entrypoint = "echo"
@@ -62,6 +63,7 @@ class DockerRunAutoRemoveTest : GradleProjectTest() {
6263
6364
tasks {
6465
register<ContainerRunTask>("printID") {
66+
dockerPull { image = "alpine:latest" }
6567
dockerRun {
6668
image = "alpine:latest"
6769
entrypoint = "echo"
@@ -101,6 +103,7 @@ class DockerRunAutoRemoveTest : GradleProjectTest() {
101103
102104
tasks {
103105
register<ContainerRunTask>("printID") {
106+
dockerPull { image = "alpine:latest" }
104107
dockerRun {
105108
image = "alpine:latest"
106109
entrypoint = "echo"

0 commit comments

Comments
 (0)