From da61d63db85c0cfe96fa2882cf915fdd9c67ef9f Mon Sep 17 00:00:00 2001 From: Dmytro Nosan Date: Thu, 24 Apr 2025 14:01:31 -0700 Subject: [PATCH] Add Docker configuration authentication to Maven and Gradle plugins Update the Maven and Gradle plugins to make use of the new Docker configuration authentication support. See gh-45269 Signed-off-by: Dmytro Nosan Co-authored-by: Phillip Webb --- .../pages/packaging-oci-image.adoc | 12 +++++++++ .../gradle/tasks/bundling/DockerSpec.java | 9 ++++--- .../tasks/bundling/DockerSpecTests.java | 12 ++++----- .../maven-plugin/pages/build-image.adoc | 12 +++++++++ .../boot/maven/BuildImageMojo.java | 6 ++--- .../springframework/boot/maven/Docker.java | 26 ++++++++++++------- .../boot/maven/DockerTests.java | 18 ++++++++----- 7 files changed, 66 insertions(+), 29 deletions(-) diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/antora/modules/gradle-plugin/pages/packaging-oci-image.adoc b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/antora/modules/gradle-plugin/pages/packaging-oci-image.adoc index 92dfb77043f..5f408d440ab 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/antora/modules/gradle-plugin/pages/packaging-oci-image.adoc +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/antora/modules/gradle-plugin/pages/packaging-oci-image.adoc @@ -100,6 +100,18 @@ The following table summarizes the available properties for `docker.builderRegis For more details, see also xref:packaging-oci-image.adoc#build-image.examples.docker[examples]. +[NOTE] +==== +If credentials are not provided, the plugin reads the user's existing Docker configuration file (typically located at `$HOME/.docker/config.json`) to determine authentication methods. +Using these methods, the plugin attempts to provide authentication credentials for the requested image. + +The plugin supports the following authentication methods: + +- *Credential Helpers*: External tools configured in the Docker configuration file to provide credentials for specific registries. For example, tools like `osxkeychain` or `ecr-login` handle authentication for certain registries. +- *Credential Store*: A default fallback mechanism that securely stores and retrieves credentials (e.g., `desktop` for Docker Desktop). +- *Static Credentials*: Credentials that are stored directly in the Docker configuration file under the `auths` section. +==== + [[build-image.customization]] diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/DockerSpec.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/DockerSpec.java index 62843a38152..c69e85ccc29 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/DockerSpec.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/DockerSpec.java @@ -145,13 +145,14 @@ public abstract class DockerSpec { } private BuilderDockerConfiguration customizeBuilderAuthentication(BuilderDockerConfiguration dockerConfiguration) { - return dockerConfiguration - .withBuilderRegistryAuthentication(getRegistryAuthentication("builder", this.builderRegistry, null)); + return dockerConfiguration.withBuilderRegistryAuthentication(getRegistryAuthentication("builder", + this.builderRegistry, DockerRegistryAuthentication.configuration(null))); } private BuilderDockerConfiguration customizePublishAuthentication(BuilderDockerConfiguration dockerConfiguration) { - return dockerConfiguration.withPublishRegistryAuthentication( - getRegistryAuthentication("publish", this.publishRegistry, DockerRegistryAuthentication.EMPTY_USER)); + return dockerConfiguration + .withPublishRegistryAuthentication(getRegistryAuthentication("publish", this.publishRegistry, + DockerRegistryAuthentication.configuration(DockerRegistryAuthentication.EMPTY_USER))); } private DockerRegistryAuthentication getRegistryAuthentication(String type, DockerRegistrySpec registry, diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/DockerSpecTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/DockerSpecTests.java index f968dee3f6f..7c71cf1c727 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/DockerSpecTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/DockerSpecTests.java @@ -54,7 +54,7 @@ class DockerSpecTests { void asDockerConfigurationWithDefaults() { BuilderDockerConfiguration dockerConfiguration = this.dockerSpec.asDockerConfiguration(); assertThat(dockerConfiguration.connection()).isNull(); - assertThat(dockerConfiguration.builderRegistryAuthentication()).isNull(); + assertThat(dockerConfiguration.builderRegistryAuthentication().getAuthHeader()).isNull(); assertThat(decoded(dockerConfiguration.publishRegistryAuthentication().getAuthHeader())) .contains("\"username\" : \"\"") .contains("\"password\" : \"\"") @@ -73,7 +73,7 @@ class DockerSpecTests { assertThat(host.secure()).isTrue(); assertThat(host.certificatePath()).isEqualTo("/tmp/ca-cert"); assertThat(dockerConfiguration.bindHostToBuilder()).isFalse(); - assertThat(dockerConfiguration.builderRegistryAuthentication()).isNull(); + assertThat(dockerConfiguration.builderRegistryAuthentication().getAuthHeader()).isNull(); assertThat(decoded(dockerConfiguration.publishRegistryAuthentication().getAuthHeader())) .contains("\"username\" : \"\"") .contains("\"password\" : \"\"") @@ -90,7 +90,7 @@ class DockerSpecTests { assertThat(host.secure()).isFalse(); assertThat(host.certificatePath()).isNull(); assertThat(dockerConfiguration.bindHostToBuilder()).isFalse(); - assertThat(dockerConfiguration.builderRegistryAuthentication()).isNull(); + assertThat(dockerConfiguration.builderRegistryAuthentication().getAuthHeader()).isNull(); assertThat(decoded(dockerConfiguration.publishRegistryAuthentication().getAuthHeader())) .contains("\"username\" : \"\"") .contains("\"password\" : \"\"") @@ -106,7 +106,7 @@ class DockerSpecTests { .connection(); assertThat(host.context()).isEqualTo("test-context"); assertThat(dockerConfiguration.bindHostToBuilder()).isFalse(); - assertThat(dockerConfiguration.builderRegistryAuthentication()).isNull(); + assertThat(dockerConfiguration.builderRegistryAuthentication().getAuthHeader()).isNull(); assertThat(decoded(dockerConfiguration.publishRegistryAuthentication().getAuthHeader())) .contains("\"username\" : \"\"") .contains("\"password\" : \"\"") @@ -132,7 +132,7 @@ class DockerSpecTests { assertThat(host.secure()).isFalse(); assertThat(host.certificatePath()).isNull(); assertThat(dockerConfiguration.bindHostToBuilder()).isTrue(); - assertThat(dockerConfiguration.builderRegistryAuthentication()).isNull(); + assertThat(dockerConfiguration.builderRegistryAuthentication().getAuthHeader()).isNull(); assertThat(decoded(dockerConfiguration.publishRegistryAuthentication().getAuthHeader())) .contains("\"username\" : \"\"") .contains("\"password\" : \"\"") @@ -213,7 +213,7 @@ class DockerSpecTests { } String decoded(String value) { - return new String(Base64.getDecoder().decode(value)); + return (value != null) ? new String(Base64.getDecoder().decode(value)) : value; } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/antora/modules/maven-plugin/pages/build-image.adoc b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/antora/modules/maven-plugin/pages/build-image.adoc index 7e37f8d36d5..f4e2642f931 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/antora/modules/maven-plugin/pages/build-image.adoc +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/antora/modules/maven-plugin/pages/build-image.adoc @@ -115,6 +115,18 @@ The following table summarizes the available parameters for `docker.builderRegis For more details, see also xref:build-image.adoc#build-image.examples.docker[examples]. +[NOTE] +==== +If credentials are not provided, the plugin reads the user's existing Docker configuration file (typically located at `$HOME/.docker/config.json`) to determine authentication methods. +Using these methods, the plugin attempts to provide authentication credentials for the requested image. + +The plugin supports the following authentication methods: + +- *Credential Helpers*: External tools configured in the Docker configuration file to provide credentials for specific registries. For example, tools like `osxkeychain` or `ecr-login` handle authentication for certain registries. +- *Credential Store*: A default fallback mechanism that securely stores and retrieves credentials (e.g., `desktop` for Docker Desktop). +- *Static Credentials*: Credentials that are stored directly in the Docker configuration file under the `auths` section. +==== + [[build-image.customization]] diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/BuildImageMojo.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/BuildImageMojo.java index 74f8aaec2c7..503b23e7472 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/BuildImageMojo.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/BuildImageMojo.java @@ -262,9 +262,9 @@ public abstract class BuildImageMojo extends AbstractPackagerMojo { Libraries libraries = getLibraries(Collections.emptySet()); try { BuildRequest request = getBuildRequest(libraries); - BuilderDockerConfiguration dockerConfiguration = (this.docker != null) - ? this.docker.asDockerConfiguration(request.isPublish()) - : new Docker().asDockerConfiguration(request.isPublish()); + Docker docker = (this.docker != null) ? this.docker : new Docker(); + BuilderDockerConfiguration dockerConfiguration = docker.asDockerConfiguration(getLog(), + request.isPublish()); Builder builder = new Builder(new MojoBuildLog(this::getLog), dockerConfiguration); builder.build(request); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Docker.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Docker.java index 1f8f7c0708c..92535afef95 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Docker.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Docker.java @@ -16,6 +16,8 @@ package org.springframework.boot.maven; +import org.apache.maven.plugin.logging.Log; + import org.springframework.boot.buildpack.platform.build.BuilderDockerConfiguration; import org.springframework.boot.buildpack.platform.docker.configuration.DockerRegistryAuthentication; @@ -141,15 +143,16 @@ public class Docker { * Returns this configuration as a {@link BuilderDockerConfiguration} instance. This * method should only be called when the configuration is complete and will no longer * be changed. + * @param log the output log * @param publish whether the image should be published * @return the Docker configuration */ - BuilderDockerConfiguration asDockerConfiguration(boolean publish) { + BuilderDockerConfiguration asDockerConfiguration(Log log, boolean publish) { BuilderDockerConfiguration dockerConfiguration = new BuilderDockerConfiguration(); dockerConfiguration = customizeHost(dockerConfiguration); dockerConfiguration = dockerConfiguration.withBindHostToBuilder(this.bindHostToBuilder); - dockerConfiguration = customizeBuilderAuthentication(dockerConfiguration); - dockerConfiguration = customizePublishAuthentication(dockerConfiguration, publish); + dockerConfiguration = customizeBuilderAuthentication(log, dockerConfiguration); + dockerConfiguration = customizePublishAuthentication(log, dockerConfiguration, publish); return dockerConfiguration; } @@ -167,18 +170,23 @@ public class Docker { return dockerConfiguration; } - private BuilderDockerConfiguration customizeBuilderAuthentication(BuilderDockerConfiguration dockerConfiguration) { - return dockerConfiguration - .withBuilderRegistryAuthentication(getRegistryAuthentication("builder", this.builderRegistry, null)); + private BuilderDockerConfiguration customizeBuilderAuthentication(Log log, + BuilderDockerConfiguration dockerConfiguration) { + DockerRegistryAuthentication authentication = DockerRegistryAuthentication.configuration(null, + (message, ex) -> log.warn(message)); + return dockerConfiguration.withBuilderRegistryAuthentication( + getRegistryAuthentication("builder", this.builderRegistry, authentication)); } - private BuilderDockerConfiguration customizePublishAuthentication(BuilderDockerConfiguration dockerConfiguration, - boolean publish) { + private BuilderDockerConfiguration customizePublishAuthentication(Log log, + BuilderDockerConfiguration dockerConfiguration, boolean publish) { if (!publish) { return dockerConfiguration; } + DockerRegistryAuthentication authentication = DockerRegistryAuthentication + .configuration(DockerRegistryAuthentication.EMPTY_USER, (message, ex) -> log.warn(message)); return dockerConfiguration.withPublishRegistryAuthentication( - getRegistryAuthentication("publish", this.publishRegistry, DockerRegistryAuthentication.EMPTY_USER)); + getRegistryAuthentication("publish", this.publishRegistry, authentication)); } private DockerRegistryAuthentication getRegistryAuthentication(String type, DockerRegistry registry, diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/DockerTests.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/DockerTests.java index 50342a7e567..770eb98edf3 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/DockerTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/DockerTests.java @@ -18,6 +18,8 @@ package org.springframework.boot.maven; import java.util.Base64; +import org.apache.maven.plugin.logging.Log; +import org.apache.maven.plugin.logging.SystemStreamLog; import org.junit.jupiter.api.Test; import org.springframework.boot.buildpack.platform.build.BuilderDockerConfiguration; @@ -34,12 +36,14 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException */ class DockerTests { + private final Log log = new SystemStreamLog(); + @Test void asDockerConfigurationWithDefaults() { Docker docker = new Docker(); BuilderDockerConfiguration dockerConfiguration = createDockerConfiguration(docker); assertThat(dockerConfiguration.connection()).isNull(); - assertThat(dockerConfiguration.builderRegistryAuthentication()).isNull(); + assertThat(dockerConfiguration.builderRegistryAuthentication().getAuthHeader()).isNull(); assertThat(decoded(dockerConfiguration.publishRegistryAuthentication().getAuthHeader())) .contains("\"username\" : \"\"") .contains("\"password\" : \"\"") @@ -59,7 +63,7 @@ class DockerTests { assertThat(host.secure()).isTrue(); assertThat(host.certificatePath()).isEqualTo("/tmp/ca-cert"); assertThat(dockerConfiguration.bindHostToBuilder()).isFalse(); - assertThat(createDockerConfiguration(docker).builderRegistryAuthentication()).isNull(); + assertThat(createDockerConfiguration(docker).builderRegistryAuthentication().getAuthHeader()).isNull(); assertThat(decoded(dockerConfiguration.publishRegistryAuthentication().getAuthHeader())) .contains("\"username\" : \"\"") .contains("\"password\" : \"\"") @@ -76,7 +80,7 @@ class DockerTests { .connection(); assertThat(context.context()).isEqualTo("test-context"); assertThat(dockerConfiguration.bindHostToBuilder()).isFalse(); - assertThat(createDockerConfiguration(docker).builderRegistryAuthentication()).isNull(); + assertThat(createDockerConfiguration(docker).builderRegistryAuthentication().getAuthHeader()).isNull(); assertThat(decoded(dockerConfiguration.publishRegistryAuthentication().getAuthHeader())) .contains("\"username\" : \"\"") .contains("\"password\" : \"\"") @@ -106,7 +110,7 @@ class DockerTests { assertThat(host.secure()).isTrue(); assertThat(host.certificatePath()).isEqualTo("/tmp/ca-cert"); assertThat(dockerConfiguration.bindHostToBuilder()).isTrue(); - assertThat(createDockerConfiguration(docker).builderRegistryAuthentication()).isNull(); + assertThat(createDockerConfiguration(docker).builderRegistryAuthentication().getAuthHeader()).isNull(); assertThat(decoded(dockerConfiguration.publishRegistryAuthentication().getAuthHeader())) .contains("\"username\" : \"\"") .contains("\"password\" : \"\"") @@ -157,7 +161,7 @@ class DockerTests { Docker docker = new Docker(); docker.setPublishRegistry( new Docker.DockerRegistry("user", null, "https://docker.example.com", "docker@example.com")); - BuilderDockerConfiguration dockerConfiguration = docker.asDockerConfiguration(false); + BuilderDockerConfiguration dockerConfiguration = docker.asDockerConfiguration(this.log, false); assertThat(dockerConfiguration.publishRegistryAuthentication()).isNull(); } @@ -193,12 +197,12 @@ class DockerTests { dockerRegistry.setToken("token"); Docker docker = new Docker(); docker.setPublishRegistry(dockerRegistry); - BuilderDockerConfiguration dockerConfiguration = docker.asDockerConfiguration(false); + BuilderDockerConfiguration dockerConfiguration = docker.asDockerConfiguration(this.log, false); assertThat(dockerConfiguration.publishRegistryAuthentication()).isNull(); } private BuilderDockerConfiguration createDockerConfiguration(Docker docker) { - return docker.asDockerConfiguration(true); + return docker.asDockerConfiguration(this.log, true); } String decoded(String value) {