diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Builder.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Builder.java index 72234aa5a1c2..bd71b38912b5 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Builder.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Builder.java @@ -21,6 +21,7 @@ import java.util.function.Consumer; import org.springframework.boot.buildpack.platform.docker.DockerApi; +import org.springframework.boot.buildpack.platform.docker.DockerLog; import org.springframework.boot.buildpack.platform.docker.TotalProgressEvent; import org.springframework.boot.buildpack.platform.docker.TotalProgressPullListener; import org.springframework.boot.buildpack.platform.docker.TotalProgressPushListener; @@ -75,7 +76,7 @@ public Builder(DockerConfiguration dockerConfiguration) { * @param log a logger used to record output */ public Builder(BuildLog log) { - this(log, new DockerApi(), null); + this(log, new DockerApi(null, BuildLogDockerLogDelegate.get(log)), null); } /** @@ -85,8 +86,8 @@ public Builder(BuildLog log) { * @since 2.4.0 */ public Builder(BuildLog log, DockerConfiguration dockerConfiguration) { - this(log, new DockerApi((dockerConfiguration != null) ? dockerConfiguration.getHost() : null), - dockerConfiguration); + this(log, new DockerApi((dockerConfiguration != null) ? dockerConfiguration.getHost() : null, + BuildLogDockerLogDelegate.get(log)), dockerConfiguration); } Builder(BuildLog log, DockerApi docker, DockerConfiguration dockerConfiguration) { @@ -262,6 +263,41 @@ private Image pullImage(ImageReference reference, ImageType imageType) throws IO } + /** + * A {@link DockerLog} implementation that delegates logging to a provided + * {@link AbstractBuildLog}. + */ + static final class BuildLogDockerLogDelegate implements DockerLog { + + private final AbstractBuildLog log; + + private BuildLogDockerLogDelegate(AbstractBuildLog log) { + this.log = log; + } + + @Override + public void log(String message) { + this.log.log(message); + } + + /** + * Creates{@link DockerLog} instance based on the provided {@link BuildLog}. + *

+ * If the provided {@link BuildLog} instance is an {@link AbstractBuildLog}, the + * method returns a {@link BuildLogDockerLogDelegate}, otherwise it returns a + * default {@link DockerLog#toSystemOut()}. + * @param log the {@link BuildLog} instance to delegate + * @return a {@link DockerLog} instance for logging + */ + static DockerLog get(BuildLog log) { + if (log instanceof AbstractBuildLog) { + return new BuildLogDockerLogDelegate(((AbstractBuildLog) log)); + } + return DockerLog.toSystemOut(); + } + + } + /** * {@link BuildpackResolverContext} implementation for the {@link Builder}. */ diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/DockerApi.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/DockerApi.java index 0f0d7f1d3c9a..c20664fc14d9 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/DockerApi.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/DockerApi.java @@ -87,7 +87,7 @@ public class DockerApi { * Create a new {@link DockerApi} instance. */ public DockerApi() { - this(HttpTransport.create(null)); + this(HttpTransport.create(null), DockerLog.toSystemOut()); } /** @@ -96,21 +96,34 @@ public DockerApi() { * @since 2.4.0 */ public DockerApi(DockerHostConfiguration dockerHost) { - this(HttpTransport.create(dockerHost)); + this(HttpTransport.create(dockerHost), DockerLog.toSystemOut()); + } + + /** + * Create a new {@link DockerApi} instance. + * @param dockerHost the Docker daemon host information + * @param log a logger used to record output + * @since 3.5.0 + */ + public DockerApi(DockerHostConfiguration dockerHost, DockerLog log) { + this(HttpTransport.create(dockerHost), log); } /** * Create a new {@link DockerApi} instance backed by a specific {@link HttpTransport} * implementation. * @param http the http implementation + * @param log a logger used to record output */ - DockerApi(HttpTransport http) { + DockerApi(HttpTransport http, DockerLog log) { + Assert.notNull(http, "'http' must not be null"); + Assert.notNull(log, "'log' must not be null"); this.http = http; this.jsonStream = new JsonStream(SharedObjectMapper.get()); this.image = new ImageApi(); this.container = new ContainerApi(); this.volume = new VolumeApi(); - this.system = new SystemApi(); + this.system = new SystemApi(log); } private HttpTransport http() { @@ -485,7 +498,10 @@ public void delete(VolumeName name, boolean force) throws IOException { */ class SystemApi { - SystemApi() { + private final DockerLog log; + + SystemApi(DockerLog log) { + this.log = log; } /** @@ -502,6 +518,7 @@ ApiVersion getApiVersion() { } } catch (Exception ex) { + this.log.log("Warning: Failed to determine Docker API version: " + ex.getMessage()); // fall through to return default value } return UNKNOWN_API_VERSION; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/DockerLog.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/DockerLog.java new file mode 100644 index 000000000000..40dd367c6e37 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/DockerLog.java @@ -0,0 +1,54 @@ +/* + * Copyright 2012-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker; + +import java.io.PrintStream; + +/** + * Callback interface used to provide {@link DockerApi} output logging. + * + * @author Dmytro Nosan + * @since 3.5.0 + * @see #toSystemOut() + */ +public interface DockerLog { + + /** + * Logs a given message. + * @param message the message to log + */ + void log(String message); + + /** + * Factory method that returns a {@link DockerLog} the outputs to {@link System#out}. + * @return {@link DockerLog} instance that logs to system out + */ + static DockerLog toSystemOut() { + return to(System.out); + } + + /** + * Factory method that returns a {@link DockerLog} the outputs to a given + * {@link PrintStream}. + * @param out the print stream used to output the log + * @return {@link DockerLog} instance that logs to the given print stream + */ + static DockerLog to(PrintStream out) { + return new PrintStreamDockerLog(out); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/PrintStreamDockerLog.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/PrintStreamDockerLog.java new file mode 100644 index 000000000000..d90da67d2495 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/PrintStreamDockerLog.java @@ -0,0 +1,42 @@ +/* + * Copyright 2012-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker; + +import java.io.PrintStream; + +import org.springframework.util.Assert; + +/** + * {@link DockerLog} implementation that prints output to a {@link PrintStream}. + * + * @author Dmytro Nosan + */ +class PrintStreamDockerLog implements DockerLog { + + private final PrintStream stream; + + PrintStreamDockerLog(PrintStream stream) { + Assert.notNull(stream, "'stream' must not be null"); + this.stream = stream; + } + + @Override + public void log(String message) { + this.stream.println(message); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuilderTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuilderTests.java index ba440ade8c4b..0d1541b7b0e4 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuilderTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuilderTests.java @@ -25,10 +25,12 @@ import org.mockito.ArgumentCaptor; import org.mockito.stubbing.Answer; +import org.springframework.boot.buildpack.platform.build.Builder.BuildLogDockerLogDelegate; import org.springframework.boot.buildpack.platform.docker.DockerApi; import org.springframework.boot.buildpack.platform.docker.DockerApi.ContainerApi; import org.springframework.boot.buildpack.platform.docker.DockerApi.ImageApi; import org.springframework.boot.buildpack.platform.docker.DockerApi.VolumeApi; +import org.springframework.boot.buildpack.platform.docker.DockerLog; import org.springframework.boot.buildpack.platform.docker.TotalProgressPullListener; import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration; import org.springframework.boot.buildpack.platform.docker.transport.DockerEngineException; @@ -75,6 +77,26 @@ void createWithDockerConfiguration() { assertThat(builder).isNotNull(); } + @Test + void createDockerApiWithLogDockerLogDelegate() { + Builder builder = new Builder(BuildLog.toSystemOut()); + assertThat(builder).isNotNull(); + assertThat(builder).extracting("docker") + .extracting("system") + .extracting("log") + .isInstanceOf(BuildLogDockerLogDelegate.class); + } + + @Test + void createDockerApiWithLogDockerSystemOutDelegate() { + Builder builder = new Builder(mock(BuildLog.class)); + assertThat(builder).isNotNull(); + assertThat(builder).extracting("docker") + .extracting("system") + .extracting("log") + .isInstanceOf(DockerLog.toSystemOut().getClass()); + } + @Test void buildWhenRequestIsNullThrowsException() { Builder builder = new Builder(); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/DockerApiTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/DockerApiTests.java index f6edf8f9114f..4a17fcc30b60 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/DockerApiTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/DockerApiTests.java @@ -59,6 +59,8 @@ import org.springframework.boot.buildpack.platform.io.IOConsumer; import org.springframework.boot.buildpack.platform.io.Owner; import org.springframework.boot.buildpack.platform.io.TarArchive; +import org.springframework.boot.testsupport.system.CapturedOutput; +import org.springframework.boot.testsupport.system.OutputCaptureExtension; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; @@ -82,7 +84,7 @@ * @author Rafael Ceccone * @author Moritz Halbritter */ -@ExtendWith(MockitoExtension.class) +@ExtendWith({ MockitoExtension.class, OutputCaptureExtension.class }) class DockerApiTests { private static final String API_URL = "/v" + DockerApi.API_VERSION; @@ -108,7 +110,7 @@ class DockerApiTests { @BeforeEach void setup() { - this.dockerApi = new DockerApi(this.http); + this.dockerApi = new DockerApi(this.http, DockerLog.toSystemOut()); } private HttpTransport http() { @@ -732,9 +734,10 @@ void getApiVersionWithNoVersionHeaderReturnsUnknownVersion() throws Exception { } @Test - void getApiVersionWithExceptionReturnsUnknownVersion() throws Exception { + void getApiVersionWithExceptionReturnsUnknownVersion(CapturedOutput output) throws Exception { given(http().head(eq(new URI(PING_URL)))).willThrow(new IOException("simulated error")); assertThat(this.api.getApiVersion()).isEqualTo(DockerApi.UNKNOWN_API_VERSION); + assertThat(output).contains("Warning: Failed to determine Docker API version: simulated error"); } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/DockerLogTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/DockerLogTests.java new file mode 100644 index 000000000000..6e9ec2afe42f --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/DockerLogTests.java @@ -0,0 +1,51 @@ +/* + * Copyright 2012-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.boot.testsupport.system.CapturedOutput; +import org.springframework.boot.testsupport.system.OutputCaptureExtension; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link DockerLog}. + * + * @author Dmytro nosan + */ +@ExtendWith(OutputCaptureExtension.class) +class DockerLogTests { + + @Test + void toSystemOutPrintsToSystemOut(CapturedOutput output) { + DockerLog logger = DockerLog.toSystemOut(); + logger.log("Hello world"); + assertThat(output.getErr()).isEmpty(); + assertThat(output.getOut()).contains("Hello world"); + } + + @Test + void toPrintsToOutput(CapturedOutput output) { + DockerLog logger = DockerLog.to(System.err); + logger.log("Hello world"); + assertThat(output.getOut()).isEmpty(); + assertThat(output.getErr()).contains("Hello world"); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/PrintStreamDockerLogTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/PrintStreamDockerLogTests.java new file mode 100644 index 000000000000..c07c5b62587c --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/PrintStreamDockerLogTests.java @@ -0,0 +1,55 @@ +/* + * Copyright 2012-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link PrintStreamDockerLog}. + * + * @author Dmytro Nosan + */ +class PrintStreamDockerLogTests { + + @Test + void printsExpectedOutput() { + TestPrintStream stream = new TestPrintStream(); + PrintStreamDockerLog logger = new PrintStreamDockerLog(stream); + logger.log("Some message"); + logger.log("Some message1"); + assertThat(stream.toString()).isEqualTo(String.format("Some message%nSome message1%n")); + } + + static class TestPrintStream extends PrintStream { + + TestPrintStream() { + super(new ByteArrayOutputStream()); + } + + @Override + public String toString() { + return this.out.toString(); + } + + } + +}