From 9ed78dd0b00e8a3a64b5da8d81bc950402140986 Mon Sep 17 00:00:00 2001 From: Tobias Gesellchen Date: Sat, 13 Nov 2021 11:08:45 +0100 Subject: [PATCH] Allow custom callbacks at imageBuild() --- .../docker/remote/api/client/ImageApi.kt | 50 +++++++++++++---- .../api/client/ImageApiIntegrationTest.java | 48 +++++++++++++++- .../remote/api/client/ImageApiTest.java | 55 +++++++++++++++++++ 3 files changed, 141 insertions(+), 12 deletions(-) create mode 100644 api-client/src/test/java/de/gesellix/docker/remote/api/client/ImageApiTest.java diff --git a/api-client/src/main/kotlin/de/gesellix/docker/remote/api/client/ImageApi.kt b/api-client/src/main/kotlin/de/gesellix/docker/remote/api/client/ImageApi.kt index 74a70423..14633259 100644 --- a/api-client/src/main/kotlin/de/gesellix/docker/remote/api/client/ImageApi.kt +++ b/api-client/src/main/kotlin/de/gesellix/docker/remote/api/client/ImageApi.kt @@ -15,12 +15,14 @@ import de.gesellix.docker.engine.DockerClientConfig import de.gesellix.docker.engine.RequestMethod.DELETE import de.gesellix.docker.engine.RequestMethod.GET import de.gesellix.docker.engine.RequestMethod.POST +import de.gesellix.docker.remote.api.BuildInfo import de.gesellix.docker.remote.api.BuildPruneResponse import de.gesellix.docker.remote.api.ContainerConfig import de.gesellix.docker.remote.api.HistoryResponseItem import de.gesellix.docker.remote.api.IdResponse import de.gesellix.docker.remote.api.Image import de.gesellix.docker.remote.api.ImageDeleteResponseItem +import de.gesellix.docker.remote.api.ImageID import de.gesellix.docker.remote.api.ImagePruneResponse import de.gesellix.docker.remote.api.ImageSearchResponseItem import de.gesellix.docker.remote.api.ImageSummary @@ -169,6 +171,7 @@ class ImageApi(dockerClientConfig: DockerClientConfig = defaultClientConfig, pro // with Dockerfiles relying on `buildx`. Maybe we can simply shell out to a locally installed docker cli, // but that's something the user might solve themselves. @Throws(UnsupportedOperationException::class, ClientException::class, ServerException::class) + @JvmOverloads fun imageBuild( dockerfile: String?, t: String?, @@ -196,7 +199,8 @@ class ImageApi(dockerClientConfig: DockerClientConfig = defaultClientConfig, pro platform: String?, target: String?, outputs: String?, - inputStream: InputStream + inputStream: InputStream, + callback: StreamCallback? = null, timeoutMillis: Long? = null /*= 24.hours.toLongMilliseconds()*/ ) { val localVariableConfig = imageBuildRequestConfig( dockerfile = dockerfile, @@ -228,25 +232,25 @@ class ImageApi(dockerClientConfig: DockerClientConfig = defaultClientConfig, pro inputStream = inputStream ) - val localVarResponse = requestStream( + val localVarResponse = requestStream( localVariableConfig ) - val timeout = Duration.of(1, ChronoUnit.MINUTES) - // TODO collect events and/or extract the image id - // from either `aux ID=sha:.*`, - // or `stream Successfully built .*`, - // or `stream Successfully tagged .*` messages. - val callback = LoggingCallback() + val timeout = if (timeoutMillis == null) { + Duration.of(10, ChronoUnit.MINUTES) + } else { + Duration.of(timeoutMillis, ChronoUnit.MILLIS) + } + val actualCallback = callback ?: LoggingCallback() return when (localVarResponse.responseType) { ResponseType.Success -> { runBlocking { launch { withTimeout(timeout.toMillis()) { - callback.onStarting(this@launch::cancel) - (localVarResponse as SuccessStream<*>).data.collect { callback.onNext(it) } - callback.onFinished() + actualCallback.onStarting(this@launch::cancel) + ((localVarResponse as SuccessStream<*>).data as Flow).collect { actualCallback.onNext(it) } + actualCallback.onFinished() } } } @@ -265,6 +269,30 @@ class ImageApi(dockerClientConfig: DockerClientConfig = defaultClientConfig, pro } } + fun getImageId(buildInfos: List): ImageID? { + val firstAux = buildInfos.stream() + .filter { (_, _, _, _, _, _, _, aux): BuildInfo -> aux != null } + .findFirst() + if (firstAux.isPresent) { + return firstAux.get().aux + } else { + val idFromStream = buildInfos.stream() + .filter { (_, stream): BuildInfo -> stream?.contains("Successfully built ")!! } + .findFirst() + return if (idFromStream.isPresent) { + ImageID(idFromStream.get().stream!!.removePrefix("Successfully built ").replaceAfter('\n', "").trim()) + } else { + val tagFromStream = buildInfos.stream() + .filter { (_, stream): BuildInfo -> stream?.contains("Successfully tagged ")!! } + .findFirst() + tagFromStream.map { (_, stream): BuildInfo -> + ImageID(stream!!.removePrefix("Successfully tagged ").replaceAfter('\n', "").trim()) + } + .orElse(null) + } + } + } + /** * To obtain the request config of the operation imageBuild * diff --git a/api-client/src/test/java/de/gesellix/docker/remote/api/client/ImageApiIntegrationTest.java b/api-client/src/test/java/de/gesellix/docker/remote/api/client/ImageApiIntegrationTest.java index 6fc1a204..d4f3a20a 100644 --- a/api-client/src/test/java/de/gesellix/docker/remote/api/client/ImageApiIntegrationTest.java +++ b/api-client/src/test/java/de/gesellix/docker/remote/api/client/ImageApiIntegrationTest.java @@ -2,6 +2,7 @@ import com.squareup.moshi.Moshi; import de.gesellix.docker.builder.BuildContextBuilder; +import de.gesellix.docker.remote.api.BuildInfo; import de.gesellix.docker.remote.api.BuildPruneResponse; import de.gesellix.docker.remote.api.ContainerCreateRequest; import de.gesellix.docker.remote.api.ContainerCreateResponse; @@ -10,8 +11,10 @@ import de.gesellix.docker.remote.api.IdResponse; import de.gesellix.docker.remote.api.Image; import de.gesellix.docker.remote.api.ImageDeleteResponseItem; +import de.gesellix.docker.remote.api.ImageID; import de.gesellix.docker.remote.api.ImageSearchResponseItem; import de.gesellix.docker.remote.api.ImageSummary; +import de.gesellix.docker.remote.api.core.StreamCallback; import de.gesellix.docker.remote.api.testutil.DockerEngineAvailable; import de.gesellix.docker.remote.api.testutil.DockerRegistry; import de.gesellix.docker.remote.api.testutil.HttpTestServer; @@ -23,6 +26,8 @@ import de.gesellix.testutil.ResourceReader; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @@ -33,10 +38,15 @@ import java.net.InetSocketAddress; import java.net.URL; import java.nio.file.Paths; +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; import static de.gesellix.docker.remote.api.testutil.Constants.LABEL_KEY; import static de.gesellix.docker.remote.api.testutil.Constants.LABEL_VALUE; @@ -48,11 +58,14 @@ import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; @DockerEngineAvailable class ImageApiIntegrationTest { + private static Logger log = LoggerFactory.getLogger(ImageApiIntegrationTest.class); + @InjectDockerClient private EngineApiClient engineApiClient; @@ -81,10 +94,43 @@ public void imageBuildAndPrune() throws IOException { File inputDirectory = ResourceReader.getClasspathResourceAsFile(dockerfile, ImageApi.class).getParentFile(); InputStream buildContext = newBuildContext(inputDirectory); + List infos = new ArrayList<>(); + Duration timeout = Duration.of(1, ChronoUnit.MINUTES); + CountDownLatch latch = new CountDownLatch(1); + StreamCallback callback = new StreamCallback() { + @Override + public void onNext(BuildInfo element) { + log.info(element.toString()); + infos.add(element); + } + + @Override + public void onFailed(Exception e) { + log.error("Build failed", e); + latch.countDown(); + } + + @Override + public void onFinished() { + latch.countDown(); + } + }; assertDoesNotThrow(() -> imageApi.imageBuild(Paths.get(dockerfile).getFileName().toString(), "test:build", null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, buildContext)); + null, null, null, null, buildContext, + callback, timeout.toMillis())); + try { + latch.await(2, TimeUnit.MINUTES); + } + catch (InterruptedException e) { + log.error("Wait interrupted", e); + } + + ImageID imageId = imageApi.getImageId(infos); + assertNotNull(imageId); + assertNotNull(imageId.getID()); + assertTrue(imageId.getID().matches(".+")); Map> filter = new HashMap<>(); filter.put("label", singletonList(LABEL_KEY)); diff --git a/api-client/src/test/java/de/gesellix/docker/remote/api/client/ImageApiTest.java b/api-client/src/test/java/de/gesellix/docker/remote/api/client/ImageApiTest.java new file mode 100644 index 00000000..32ff0a41 --- /dev/null +++ b/api-client/src/test/java/de/gesellix/docker/remote/api/client/ImageApiTest.java @@ -0,0 +1,55 @@ +package de.gesellix.docker.remote.api.client; + +import de.gesellix.docker.engine.DockerClientConfig; +import de.gesellix.docker.remote.api.BuildInfo; +import de.gesellix.docker.remote.api.ImageID; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class ImageApiTest { + + private ImageApi imageApi; + + @BeforeEach + public void setup() { + imageApi = new ImageApi(new DockerClientConfig()); + } + + @Test + public void getImageIdFromAux() { + List infos = new ArrayList<>(); + infos.add(new BuildInfo(null, null, null, null, null, null, null, new ImageID("sha256:expected-id"))); + infos.add(new BuildInfo(null, "Successfully built f9d5f290d048\nfoo bar", null, null, null, null, null, null)); + infos.add(new BuildInfo(null, "Successfully tagged image:tag\nbar baz", null, null, null, null, null, null)); + + ImageID imageId = imageApi.getImageId(infos); + + assertEquals("sha256:expected-id", imageId.getID()); + } + + @Test + public void getImageIdFromStreamWithBuildMessage() { + List infos = new ArrayList<>(); + infos.add(new BuildInfo(null, "Successfully built f9d5f290d048\nfoo bar", null, null, null, null, null, null)); + infos.add(new BuildInfo(null, "Successfully tagged image:tag\nbar baz", null, null, null, null, null, null)); + + ImageID imageId = imageApi.getImageId(infos); + + assertEquals("f9d5f290d048", imageId.getID()); + } + + @Test + public void getImageIdFromStreamWithTagMessage() { + List infos = new ArrayList<>(); + infos.add(new BuildInfo(null, "Successfully tagged image:tag\nbar baz", null, null, null, null, null, null)); + + ImageID imageId = imageApi.getImageId(infos); + + assertEquals("image:tag", imageId.getID()); + } +}