Merge pull request #45269 from nosan
* pr/45269: Add Docker configuration authentication to Maven and Gradle plugins Support Docker configuration authentication including helper support Polish 'Update `DockerConfigurationMetadata` to support credentials' Update `DockerConfigurationMetadata` to support credentials Closes gh-45269
This commit is contained in:
@@ -246,13 +246,13 @@ public class Builder {
|
||||
private void pushImage(ImageReference reference) throws IOException {
|
||||
Consumer<TotalProgressEvent> progressConsumer = this.log.pushingImage(reference);
|
||||
TotalProgressPushListener listener = new TotalProgressPushListener(progressConsumer);
|
||||
String authHeader = authHeader(this.dockerConfiguration.publishRegistryAuthentication());
|
||||
String authHeader = authHeader(this.dockerConfiguration.publishRegistryAuthentication(), reference);
|
||||
this.docker.image().push(reference, listener, authHeader);
|
||||
this.log.pushedImage(reference);
|
||||
}
|
||||
|
||||
private static String authHeader(DockerRegistryAuthentication authentication) {
|
||||
return (authentication != null) ? authentication.getAuthHeader() : null;
|
||||
private static String authHeader(DockerRegistryAuthentication authentication, ImageReference reference) {
|
||||
return (authentication != null) ? authentication.getAuthHeader(reference) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -279,7 +279,7 @@ public class Builder {
|
||||
Image fetchImage(ImageType type, ImageReference reference) throws IOException {
|
||||
Assert.notNull(type, "'type' must not be null");
|
||||
Assert.notNull(reference, "'reference' must not be null");
|
||||
String authHeader = authHeader(this.registryAuthentication);
|
||||
String authHeader = authHeader(this.registryAuthentication, reference);
|
||||
Assert.state(authHeader == null || reference.getDomain().equals(this.domain),
|
||||
() -> String.format("%s '%s' must be pulled from the '%s' authenticated registry",
|
||||
StringUtils.capitalize(type.getDescription()), reference, this.domain));
|
||||
@@ -300,7 +300,7 @@ public class Builder {
|
||||
private Image pullImage(ImageReference reference, ImageType imageType) throws IOException {
|
||||
TotalProgressPullListener listener = new TotalProgressPullListener(
|
||||
Builder.this.log.pullingImage(reference, this.defaultPlatform, imageType));
|
||||
String authHeader = authHeader(this.registryAuthentication);
|
||||
String authHeader = authHeader(this.registryAuthentication, reference);
|
||||
Image image = Builder.this.docker.image().pull(reference, this.defaultPlatform, listener, authHeader);
|
||||
Builder.this.log.pulledImage(image, imageType);
|
||||
if (this.defaultPlatform == null) {
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* 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.configuration;
|
||||
|
||||
import java.lang.invoke.MethodHandles;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
|
||||
import org.springframework.boot.buildpack.platform.json.MappedObject;
|
||||
|
||||
/**
|
||||
* A class that represents credentials for a server as returned from a
|
||||
* {@link CredentialHelper}.
|
||||
*
|
||||
* @author Dmytro Nosan
|
||||
*/
|
||||
class Credential extends MappedObject {
|
||||
|
||||
/**
|
||||
* If the secret being stored is an identity token, the username should be set to
|
||||
* {@code <token>}.
|
||||
*/
|
||||
private static final String TOKEN_USERNAME = "<token>";
|
||||
|
||||
private final String username;
|
||||
|
||||
private final String secret;
|
||||
|
||||
private String serverUrl;
|
||||
|
||||
Credential(JsonNode node) {
|
||||
super(node, MethodHandles.lookup());
|
||||
this.username = valueAt("/Username", String.class);
|
||||
this.secret = valueAt("/Secret", String.class);
|
||||
this.serverUrl = valueAt("/ServerURL", String.class);
|
||||
}
|
||||
|
||||
String getUsername() {
|
||||
return this.username;
|
||||
}
|
||||
|
||||
String getSecret() {
|
||||
return this.secret;
|
||||
}
|
||||
|
||||
String getServerUrl() {
|
||||
return this.serverUrl;
|
||||
}
|
||||
|
||||
boolean isIdentityToken() {
|
||||
return TOKEN_USERNAME.equals(this.username);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* 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.configuration;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import com.sun.jna.Platform;
|
||||
|
||||
import org.springframework.boot.buildpack.platform.json.SharedObjectMapper;
|
||||
|
||||
/**
|
||||
* Invokes a Docker credential helper executable that can be used to get {@link Credential
|
||||
* credentials}.
|
||||
*
|
||||
* @author Dmytro Nosan
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class CredentialHelper {
|
||||
|
||||
private static final String USR_LOCAL_BIN = "/usr/local/bin/";
|
||||
|
||||
Set<String> CREDENTIAL_NOT_FOUND_MESSAGES = Set.of("credentials not found in native keychain",
|
||||
"no credentials server URL", "no credentials username");
|
||||
|
||||
private final String executable;
|
||||
|
||||
CredentialHelper(String executable) {
|
||||
this.executable = executable;
|
||||
}
|
||||
|
||||
Credential get(String serverUrl) throws IOException {
|
||||
ProcessBuilder processBuilder = processBuilder("get");
|
||||
Process process = start(processBuilder);
|
||||
try (OutputStream request = process.getOutputStream()) {
|
||||
request.write(serverUrl.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
try {
|
||||
int exitCode = process.waitFor();
|
||||
try (InputStream response = process.getInputStream()) {
|
||||
if (exitCode == 0) {
|
||||
return new Credential(SharedObjectMapper.get().readTree(response));
|
||||
}
|
||||
String errorMessage = new String(response.readAllBytes(), StandardCharsets.UTF_8);
|
||||
if (!isCredentialsNotFoundError(errorMessage)) {
|
||||
throw new IOException("%s' exited with code %d: %s".formatted(process, exitCode, errorMessage));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
catch (InterruptedException ex) {
|
||||
Thread.currentThread().interrupt();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private ProcessBuilder processBuilder(String string) {
|
||||
ProcessBuilder processBuilder = new ProcessBuilder().redirectErrorStream(true);
|
||||
if (Platform.isWindows()) {
|
||||
processBuilder.command("cmd", "/c");
|
||||
}
|
||||
processBuilder.command(this.executable, string);
|
||||
return processBuilder;
|
||||
}
|
||||
|
||||
private Process start(ProcessBuilder processBuilder) throws IOException {
|
||||
try {
|
||||
return processBuilder.start();
|
||||
}
|
||||
catch (IOException ex) {
|
||||
if (!Platform.isMac()) {
|
||||
throw ex;
|
||||
}
|
||||
List<String> command = new ArrayList<>(processBuilder.command());
|
||||
command.set(0, USR_LOCAL_BIN + command.get(0));
|
||||
return processBuilder.command(command).start();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isCredentialsNotFoundError(String message) {
|
||||
return this.CREDENTIAL_NOT_FOUND_MESSAGES.contains(message.trim());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2023 the original author or authors.
|
||||
* 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.
|
||||
@@ -23,7 +23,9 @@ import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Base64;
|
||||
import java.util.HexFormat;
|
||||
import java.util.Map;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
@@ -32,11 +34,14 @@ import com.fasterxml.jackson.databind.node.NullNode;
|
||||
import org.springframework.boot.buildpack.platform.json.MappedObject;
|
||||
import org.springframework.boot.buildpack.platform.json.SharedObjectMapper;
|
||||
import org.springframework.boot.buildpack.platform.system.Environment;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Docker configuration stored in metadata files managed by the Docker CLI.
|
||||
*
|
||||
* @author Scott Frederick
|
||||
* @author Dmytro Nosan
|
||||
*/
|
||||
final class DockerConfigurationMetadata {
|
||||
|
||||
@@ -58,6 +63,8 @@ final class DockerConfigurationMetadata {
|
||||
|
||||
private static final String CONTEXT_FILE_NAME = "meta.json";
|
||||
|
||||
private static volatile DockerConfigurationMetadata systemEnvironmentConfigurationMetadata;
|
||||
|
||||
private final String configLocation;
|
||||
|
||||
private final DockerConfig config;
|
||||
@@ -83,11 +90,24 @@ final class DockerConfigurationMetadata {
|
||||
}
|
||||
|
||||
static DockerConfigurationMetadata from(Environment environment) {
|
||||
String configLocation = (environment.get(DOCKER_CONFIG) != null) ? environment.get(DOCKER_CONFIG)
|
||||
: Path.of(System.getProperty("user.home"), CONFIG_DIR).toString();
|
||||
DockerConfigurationMetadata dockerConfigurationMetadata = (environment == Environment.SYSTEM)
|
||||
? DockerConfigurationMetadata.systemEnvironmentConfigurationMetadata : null;
|
||||
if (dockerConfigurationMetadata != null) {
|
||||
return dockerConfigurationMetadata;
|
||||
}
|
||||
String configLocation = environment.get(DOCKER_CONFIG);
|
||||
configLocation = (configLocation != null) ? configLocation : getUserHomeConfigLocation();
|
||||
DockerConfig dockerConfig = createDockerConfig(configLocation);
|
||||
DockerContext dockerContext = createDockerContext(configLocation, dockerConfig.getCurrentContext());
|
||||
return new DockerConfigurationMetadata(configLocation, dockerConfig, dockerContext);
|
||||
dockerConfigurationMetadata = new DockerConfigurationMetadata(configLocation, dockerConfig, dockerContext);
|
||||
if (environment == Environment.SYSTEM) {
|
||||
DockerConfigurationMetadata.systemEnvironmentConfigurationMetadata = dockerConfigurationMetadata;
|
||||
}
|
||||
return dockerConfigurationMetadata;
|
||||
}
|
||||
|
||||
private static String getUserHomeConfigLocation() {
|
||||
return Path.of(System.getProperty("user.home"), CONFIG_DIR).toString();
|
||||
}
|
||||
|
||||
private static DockerConfig createDockerConfig(String configLocation) {
|
||||
@@ -148,15 +168,36 @@ final class DockerConfigurationMetadata {
|
||||
|
||||
private final String currentContext;
|
||||
|
||||
private final String credsStore;
|
||||
|
||||
private final Map<String, String> credHelpers;
|
||||
|
||||
private final Map<String, Auth> auths;
|
||||
|
||||
private DockerConfig(JsonNode node) {
|
||||
super(node, MethodHandles.lookup());
|
||||
this.currentContext = valueAt("/currentContext", String.class);
|
||||
this.credsStore = valueAt("/credsStore", String.class);
|
||||
this.credHelpers = mapAt("/credHelpers", JsonNode::textValue);
|
||||
this.auths = mapAt("/auths", Auth::new);
|
||||
}
|
||||
|
||||
String getCurrentContext() {
|
||||
return this.currentContext;
|
||||
}
|
||||
|
||||
String getCredsStore() {
|
||||
return this.credsStore;
|
||||
}
|
||||
|
||||
Map<String, String> getCredHelpers() {
|
||||
return this.credHelpers;
|
||||
}
|
||||
|
||||
Map<String, Auth> getAuths() {
|
||||
return this.auths;
|
||||
}
|
||||
|
||||
static DockerConfig fromJson(String json) throws JsonProcessingException {
|
||||
return new DockerConfig(SharedObjectMapper.get().readTree(json));
|
||||
}
|
||||
@@ -167,6 +208,44 @@ final class DockerConfigurationMetadata {
|
||||
|
||||
}
|
||||
|
||||
static final class Auth extends MappedObject {
|
||||
|
||||
private final String username;
|
||||
|
||||
private final String password;
|
||||
|
||||
private final String email;
|
||||
|
||||
Auth(JsonNode node) {
|
||||
super(node, MethodHandles.lookup());
|
||||
String auth = valueAt("/auth", String.class);
|
||||
if (StringUtils.hasText(auth)) {
|
||||
String[] parts = new String(Base64.getDecoder().decode(auth)).split(":", 2);
|
||||
Assert.state(parts.length == 2, "Malformed auth in docker configuration metadata");
|
||||
this.username = parts[0];
|
||||
this.password = parts[1];
|
||||
}
|
||||
else {
|
||||
this.username = valueAt("/username", String.class);
|
||||
this.password = valueAt("/password", String.class);
|
||||
}
|
||||
this.email = valueAt("/email", String.class);
|
||||
}
|
||||
|
||||
String getUsername() {
|
||||
return this.username;
|
||||
}
|
||||
|
||||
String getPassword() {
|
||||
return this.password;
|
||||
}
|
||||
|
||||
String getEmail() {
|
||||
return this.email;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static final class DockerContext extends MappedObject {
|
||||
|
||||
private final String dockerHost;
|
||||
|
||||
@@ -16,12 +16,17 @@
|
||||
|
||||
package org.springframework.boot.buildpack.platform.docker.configuration;
|
||||
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
import org.springframework.boot.buildpack.platform.docker.type.ImageReference;
|
||||
|
||||
/**
|
||||
* Docker registry authentication configuration.
|
||||
*
|
||||
* @author Scott Frederick
|
||||
* @since 2.4.0
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface DockerRegistryAuthentication {
|
||||
|
||||
/**
|
||||
@@ -30,6 +35,17 @@ public interface DockerRegistryAuthentication {
|
||||
*/
|
||||
DockerRegistryAuthentication EMPTY_USER = DockerRegistryAuthentication.user("", "", "", "");
|
||||
|
||||
/**
|
||||
* Returns the auth header that should be used for docker authentication for the given
|
||||
* image reference.
|
||||
* @param imageReference the image reference or {@code null}
|
||||
* @return the auth header
|
||||
* @since 3.5.0
|
||||
*/
|
||||
default String getAuthHeader(ImageReference imageReference) {
|
||||
return getAuthHeader();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the auth header that should be used for docker authentication.
|
||||
* @return the auth header
|
||||
@@ -63,4 +79,33 @@ public interface DockerRegistryAuthentication {
|
||||
return new DockerRegistryUserAuthentication(username, password, serverAddress, email);
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method that returns a new {@link DockerRegistryAuthentication} instance
|
||||
* that uses the standard docker JSON config (including support for credential
|
||||
* helpers) to generate auth headers.
|
||||
* @param fallback the fallback authentication to use if no suitable config is found
|
||||
* @return a new {@link DockerRegistryAuthentication} instance
|
||||
* @since 3.5.0
|
||||
* @see #configuration(DockerRegistryAuthentication, BiConsumer)
|
||||
*/
|
||||
static DockerRegistryAuthentication configuration(DockerRegistryAuthentication fallback) {
|
||||
return configuration(fallback, (message, ex) -> System.out.println(message));
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method that returns a new {@link DockerRegistryAuthentication} instance
|
||||
* that uses the standard docker JSON config (including support for credential
|
||||
* helpers) to generate auth headers.
|
||||
* @param fallback the fallback authentication to use if no suitable config is found
|
||||
* @param credentialHelperExceptionHandler callback that should handle credential
|
||||
* helper exceptions
|
||||
* @return a new {@link DockerRegistryAuthentication} instance
|
||||
* @since 3.5.0
|
||||
* @see #configuration(DockerRegistryAuthentication, BiConsumer)
|
||||
*/
|
||||
static DockerRegistryAuthentication configuration(DockerRegistryAuthentication fallback,
|
||||
BiConsumer<String, Exception> credentialHelperExceptionHandler) {
|
||||
return new DockerRegistryConfigAuthentication(fallback, credentialHelperExceptionHandler);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
/*
|
||||
* 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.configuration;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfigurationMetadata.Auth;
|
||||
import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfigurationMetadata.DockerConfig;
|
||||
import org.springframework.boot.buildpack.platform.docker.type.ImageReference;
|
||||
import org.springframework.boot.buildpack.platform.system.Environment;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* {@link DockerRegistryAuthentication} for
|
||||
* {@link DockerRegistryAuthentication#configuration(DockerRegistryAuthentication, BiConsumer)}.
|
||||
*
|
||||
* @author Dmytro Nosan
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class DockerRegistryConfigAuthentication implements DockerRegistryAuthentication {
|
||||
|
||||
private static final String DEFAULT_DOMAIN = "docker.io";
|
||||
|
||||
private static final String INDEX_URL = "https://index.docker.io/v1/";
|
||||
|
||||
private static Map<String, Credential> credentialFromHelperCache = new ConcurrentHashMap<>();
|
||||
|
||||
private final DockerRegistryAuthentication fallback;
|
||||
|
||||
private final BiConsumer<String, Exception> credentialHelperExceptionHandler;
|
||||
|
||||
private final Function<String, CredentialHelper> credentialHelperFactory;
|
||||
|
||||
private final DockerConfig dockerConfig;
|
||||
|
||||
DockerRegistryConfigAuthentication(DockerRegistryAuthentication fallback,
|
||||
BiConsumer<String, Exception> credentialHelperExceptionHandler) {
|
||||
this(fallback, credentialHelperExceptionHandler, Environment.SYSTEM,
|
||||
(helper) -> new CredentialHelper("docker-credential-" + helper.trim()));
|
||||
}
|
||||
|
||||
DockerRegistryConfigAuthentication(DockerRegistryAuthentication fallback,
|
||||
BiConsumer<String, Exception> credentialHelperExceptionHandler, Environment environment,
|
||||
Function<String, CredentialHelper> credentialHelperFactory) {
|
||||
this.fallback = fallback;
|
||||
this.credentialHelperExceptionHandler = credentialHelperExceptionHandler;
|
||||
this.dockerConfig = DockerConfigurationMetadata.from(environment).getConfiguration();
|
||||
this.credentialHelperFactory = credentialHelperFactory;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAuthHeader() {
|
||||
return getAuthHeader(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAuthHeader(ImageReference imageReference) {
|
||||
String serverUrl = getServerUrl(imageReference);
|
||||
DockerRegistryAuthentication authentication = getAuthentication(serverUrl);
|
||||
return (authentication != null) ? authentication.getAuthHeader(imageReference) : null;
|
||||
}
|
||||
|
||||
private String getServerUrl(ImageReference imageReference) {
|
||||
String domain = (imageReference != null) ? imageReference.getDomain() : null;
|
||||
return (!DEFAULT_DOMAIN.equals(domain)) ? domain : INDEX_URL;
|
||||
}
|
||||
|
||||
private DockerRegistryAuthentication getAuthentication(String serverUrl) {
|
||||
Credential credentialsFromHelper = getCredentialsFromHelper(serverUrl);
|
||||
Map.Entry<String, Auth> authConfigEntry = getAuthConfigEntry(serverUrl);
|
||||
serverUrl = (authConfigEntry != null) ? authConfigEntry.getKey() : serverUrl;
|
||||
Auth authConfig = (authConfigEntry != null) ? authConfigEntry.getValue() : null;
|
||||
if (credentialsFromHelper != null) {
|
||||
return getAuthentication(credentialsFromHelper, authConfig, serverUrl);
|
||||
}
|
||||
if (authConfigEntry != null) {
|
||||
return DockerRegistryAuthentication.user(authConfig.getUsername(), authConfig.getPassword(), serverUrl,
|
||||
authConfig.getEmail());
|
||||
}
|
||||
return this.fallback;
|
||||
}
|
||||
|
||||
private DockerRegistryAuthentication getAuthentication(Credential credentialsFromHelper, Auth authConfig,
|
||||
String serverUrl) {
|
||||
if (credentialsFromHelper.isIdentityToken()) {
|
||||
return DockerRegistryAuthentication.token(credentialsFromHelper.getSecret());
|
||||
}
|
||||
String username = credentialsFromHelper.getUsername();
|
||||
String password = credentialsFromHelper.getSecret();
|
||||
String serverAddress = (credentialsFromHelper.getServerUrl() != null
|
||||
&& !credentialsFromHelper.getServerUrl().isEmpty()) ? credentialsFromHelper.getServerUrl() : serverUrl;
|
||||
String email = (authConfig != null) ? authConfig.getEmail() : null;
|
||||
return DockerRegistryAuthentication.user(username, password, serverAddress, email);
|
||||
}
|
||||
|
||||
private Credential getCredentialsFromHelper(String serverUrl) {
|
||||
return (StringUtils.hasText(serverUrl))
|
||||
? credentialFromHelperCache.computeIfAbsent(serverUrl, this::computeCredentialsFromHelper) : null;
|
||||
}
|
||||
|
||||
private Credential computeCredentialsFromHelper(String serverUrl) {
|
||||
CredentialHelper credentialHelper = getCredentialHelper(serverUrl);
|
||||
if (credentialHelper != null) {
|
||||
try {
|
||||
return credentialHelper.get(serverUrl);
|
||||
}
|
||||
catch (IOException ex) {
|
||||
String message = "Error retrieving credentials for '%s' due to: %s".formatted(serverUrl,
|
||||
ex.getMessage());
|
||||
this.credentialHelperExceptionHandler.accept(message, ex);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private CredentialHelper getCredentialHelper(String serverUrl) {
|
||||
String name = this.dockerConfig.getCredHelpers().get(serverUrl);
|
||||
name = (StringUtils.hasText(name)) ? name : this.dockerConfig.getCredsStore();
|
||||
return (name != null) ? this.credentialHelperFactory.apply(name.trim()) : null;
|
||||
}
|
||||
|
||||
private Entry<String, Auth> getAuthConfigEntry(String serverUrl) {
|
||||
for (Map.Entry<String, Auth> candidate : this.dockerConfig.getAuths().entrySet()) {
|
||||
if (candidate.getKey().equals(serverUrl) || candidate.getKey().endsWith("://" + serverUrl)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
* 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.
|
||||
@@ -19,7 +19,8 @@ package org.springframework.boot.buildpack.platform.docker.configuration;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
/**
|
||||
* Docker registry authentication configuration using a token.
|
||||
* {@link DockerRegistryAuthentication} for
|
||||
* {@link DockerRegistryAuthentication#user(String, String, String, String)}.
|
||||
*
|
||||
* @author Scott Frederick
|
||||
*/
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
* 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.
|
||||
@@ -19,7 +19,8 @@ package org.springframework.boot.buildpack.platform.docker.configuration;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
/**
|
||||
* Docker registry authentication configuration using user credentials.
|
||||
* {@link DockerRegistryAuthentication} for
|
||||
* {@link DockerRegistryAuthentication#token(String)}.
|
||||
*
|
||||
* @author Scott Frederick
|
||||
*/
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2024 the original author or authors.
|
||||
* 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.
|
||||
@@ -25,7 +25,9 @@ import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
@@ -38,6 +40,7 @@ import org.springframework.util.StreamUtils;
|
||||
* Base class for mapped JSON objects.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Dmytro Nosan
|
||||
* @since 2.3.0
|
||||
*/
|
||||
public class MappedObject {
|
||||
@@ -75,6 +78,23 @@ public class MappedObject {
|
||||
return valueAt(this, this.node, this.lookup, expression, type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a {@link Map} at the given JSON path expression with a value mapped from a
|
||||
* related {@link JsonNode}.
|
||||
* @param <V> the value type
|
||||
* @param expression the JSON path expression
|
||||
* @param valueMapper function to map the value from the {@link JsonNode}
|
||||
* @return the map
|
||||
* @since 3.5.0
|
||||
*/
|
||||
protected <V> Map<String, V> mapAt(String expression, Function<JsonNode, V> valueMapper) {
|
||||
Map<String, V> map = new LinkedHashMap<>();
|
||||
getNode().at(expression)
|
||||
.fields()
|
||||
.forEachRemaining((entry) -> map.put(entry.getKey(), valueMapper.apply(entry.getValue())));
|
||||
return Collections.unmodifiableMap(map);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get children at the given JSON path expression by constructing them using the given
|
||||
* factory.
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* 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.configuration;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import com.sun.jna.Platform;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIOException;
|
||||
|
||||
/**
|
||||
* Tests for {@link CredentialHelper}.
|
||||
*
|
||||
* @author Dmytro Nosan
|
||||
*/
|
||||
class CredentialHelperTests {
|
||||
|
||||
private static CredentialHelper helper;
|
||||
|
||||
@BeforeAll
|
||||
static void setUp() throws Exception {
|
||||
String executableName = "docker-credential-test" + ((Platform.isWindows()) ? ".bat" : ".sh");
|
||||
String executable = new ClassPathResource(executableName, CredentialHelperTests.class).getFile()
|
||||
.getAbsolutePath();
|
||||
helper = new CredentialHelper(executable);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getWhenKnowUser() throws Exception {
|
||||
Credential credentials = helper.get("user.example.com");
|
||||
assertThat(credentials).isNotNull();
|
||||
assertThat(credentials.isIdentityToken()).isFalse();
|
||||
assertThat(credentials.getServerUrl()).isEqualTo("user.example.com");
|
||||
assertThat(credentials.getUsername()).isEqualTo("username");
|
||||
assertThat(credentials.getSecret()).isEqualTo("secret");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getWhenKnowToken() throws Exception {
|
||||
Credential credentials = helper.get("token.example.com");
|
||||
assertThat(credentials).isNotNull();
|
||||
assertThat(credentials.isIdentityToken()).isTrue();
|
||||
assertThat(credentials.getServerUrl()).isEqualTo("token.example.com");
|
||||
assertThat(credentials.getUsername()).isEqualTo("<token>");
|
||||
assertThat(credentials.getSecret()).isEqualTo("secret");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getWhenCredentialsMissingMessageReturnsNull() throws Exception {
|
||||
Credential credentials = helper.get("credentials.missing.example.com");
|
||||
assertThat(credentials).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getWhenUsernameMissingMessageReturnsNull() throws Exception {
|
||||
Credential credentials = helper.get("username.missing.example.com");
|
||||
assertThat(credentials).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getWhenUrlMissingMessageReturnsNull() throws Exception {
|
||||
Credential credentials = helper.get("url.missing.example.com");
|
||||
assertThat(credentials).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getWhenUnknownErrorThrowsException() {
|
||||
assertThatIOException().isThrownBy(() -> helper.get("invalid.example.com"))
|
||||
.withMessageContaining("Unknown error");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getWhenCommandDoesNotExistErrorThrowsException() {
|
||||
String name = "docker-credential-%s".formatted(UUID.randomUUID().toString());
|
||||
assertThatIOException().isThrownBy(() -> new CredentialHelper(name).get("invalid.example.com"))
|
||||
.withMessageContaining(name);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* 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.configuration;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.buildpack.platform.json.SharedObjectMapper;
|
||||
import org.springframework.boot.testsupport.classpath.resources.WithResource;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link Credential}.
|
||||
*
|
||||
* @author Dmytro Nosan
|
||||
*/
|
||||
class CredentialsTests {
|
||||
|
||||
@Test
|
||||
@WithResource(name = "credentials.json", content = """
|
||||
{
|
||||
"ServerURL": "https://index.docker.io/v1/",
|
||||
"Username": "user",
|
||||
"Secret": "secret"
|
||||
}
|
||||
""")
|
||||
void createWhenUserCredentials() throws Exception {
|
||||
Credential credentials = getCredentials("credentials.json");
|
||||
assertThat(credentials.getUsername()).isEqualTo("user");
|
||||
assertThat(credentials.getSecret()).isEqualTo("secret");
|
||||
assertThat(credentials.getServerUrl()).isEqualTo("https://index.docker.io/v1/");
|
||||
assertThat(credentials.isIdentityToken()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithResource(name = "credentials.json", content = """
|
||||
{
|
||||
"ServerURL": "https://index.docker.io/v1/",
|
||||
"Username": "<token>",
|
||||
"Secret": "secret"
|
||||
}
|
||||
""")
|
||||
void createWhenTokenCredentials() throws Exception {
|
||||
Credential credentials = getCredentials("credentials.json");
|
||||
assertThat(credentials.getUsername()).isEqualTo("<token>");
|
||||
assertThat(credentials.getSecret()).isEqualTo("secret");
|
||||
assertThat(credentials.getServerUrl()).isEqualTo("https://index.docker.io/v1/");
|
||||
assertThat(credentials.isIdentityToken()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithResource(name = "credentials.json", content = """
|
||||
{
|
||||
"Username": "user",
|
||||
"Secret": "secret"
|
||||
}
|
||||
""")
|
||||
void createWhenNoServerUrl() throws Exception {
|
||||
Credential credentials = getCredentials("credentials.json");
|
||||
assertThat(credentials.getUsername()).isEqualTo("user");
|
||||
assertThat(credentials.getSecret()).isEqualTo("secret");
|
||||
assertThat(credentials.getServerUrl()).isNull();
|
||||
assertThat(credentials.isIdentityToken()).isFalse();
|
||||
}
|
||||
|
||||
private Credential getCredentials(String name) throws IOException {
|
||||
try (InputStream inputStream = new ClassPathResource(name).getInputStream()) {
|
||||
return new Credential(SharedObjectMapper.get().readTree(inputStream));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2023 the original author or authors.
|
||||
* 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.
|
||||
@@ -26,6 +26,7 @@ import java.util.regex.Pattern;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfigurationMetadata.DockerConfig;
|
||||
import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfigurationMetadata.DockerContext;
|
||||
import org.springframework.boot.buildpack.platform.json.AbstractJsonTests;
|
||||
|
||||
@@ -36,6 +37,7 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException
|
||||
* Tests for {@link DockerConfigurationMetadata}.
|
||||
*
|
||||
* @author Scott Frederick
|
||||
* @author Dmytro Nosan
|
||||
*/
|
||||
class DockerConfigurationMetadataTests extends AbstractJsonTests {
|
||||
|
||||
@@ -46,6 +48,9 @@ class DockerConfigurationMetadataTests extends AbstractJsonTests {
|
||||
this.environment.put("DOCKER_CONFIG", pathToResource("with-context/config.json"));
|
||||
DockerConfigurationMetadata config = DockerConfigurationMetadata.from(this.environment::get);
|
||||
assertThat(config.getConfiguration().getCurrentContext()).isEqualTo("test-context");
|
||||
assertThat(config.getConfiguration().getAuths()).isEmpty();
|
||||
assertThat(config.getConfiguration().getCredHelpers()).isEmpty();
|
||||
assertThat(config.getConfiguration().getCredsStore()).isNull();
|
||||
assertThat(config.getContext().getDockerHost()).isEqualTo("unix:///home/user/.docker/docker.sock");
|
||||
assertThat(config.getContext().isTlsVerify()).isFalse();
|
||||
assertThat(config.getContext().getTlsPath()).isNull();
|
||||
@@ -56,6 +61,9 @@ class DockerConfigurationMetadataTests extends AbstractJsonTests {
|
||||
this.environment.put("DOCKER_CONFIG", pathToResource("without-context/config.json"));
|
||||
DockerConfigurationMetadata config = DockerConfigurationMetadata.from(this.environment::get);
|
||||
assertThat(config.getConfiguration().getCurrentContext()).isNull();
|
||||
assertThat(config.getConfiguration().getAuths()).isEmpty();
|
||||
assertThat(config.getConfiguration().getCredHelpers()).isEmpty();
|
||||
assertThat(config.getConfiguration().getCredsStore()).isNull();
|
||||
assertThat(config.getContext().getDockerHost()).isNull();
|
||||
assertThat(config.getContext().isTlsVerify()).isFalse();
|
||||
assertThat(config.getContext().getTlsPath()).isNull();
|
||||
@@ -66,6 +74,9 @@ class DockerConfigurationMetadataTests extends AbstractJsonTests {
|
||||
this.environment.put("DOCKER_CONFIG", pathToResource("with-default-context/config.json"));
|
||||
DockerConfigurationMetadata config = DockerConfigurationMetadata.from(this.environment::get);
|
||||
assertThat(config.getConfiguration().getCurrentContext()).isEqualTo("default");
|
||||
assertThat(config.getConfiguration().getAuths()).isEmpty();
|
||||
assertThat(config.getConfiguration().getCredHelpers()).isEmpty();
|
||||
assertThat(config.getConfiguration().getCredsStore()).isNull();
|
||||
assertThat(config.getContext().getDockerHost()).isNull();
|
||||
assertThat(config.getContext().isTlsVerify()).isFalse();
|
||||
assertThat(config.getContext().getTlsPath()).isNull();
|
||||
@@ -95,10 +106,38 @@ class DockerConfigurationMetadataTests extends AbstractJsonTests {
|
||||
this.environment.put("DOCKER_CONFIG", "docker-config-dummy-path");
|
||||
DockerConfigurationMetadata config = DockerConfigurationMetadata.from(this.environment::get);
|
||||
assertThat(config.getConfiguration().getCurrentContext()).isNull();
|
||||
assertThat(config.getConfiguration().getAuths()).isEmpty();
|
||||
assertThat(config.getConfiguration().getCredHelpers()).isEmpty();
|
||||
assertThat(config.getConfiguration().getCredsStore()).isNull();
|
||||
assertThat(config.getContext().getDockerHost()).isNull();
|
||||
assertThat(config.getContext().isTlsVerify()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void configWithAuthIsRead() throws Exception {
|
||||
this.environment.put("DOCKER_CONFIG", pathToResource("with-auth/config.json"));
|
||||
DockerConfigurationMetadata metadata = DockerConfigurationMetadata.from(this.environment::get);
|
||||
DockerConfig configuration = metadata.getConfiguration();
|
||||
assertThat(configuration.getCredsStore()).isEqualTo("desktop");
|
||||
assertThat(configuration.getCredHelpers()).hasSize(3)
|
||||
.containsEntry("azurecr.io", "acr-env")
|
||||
.containsEntry("ecr.us-east-1.amazonaws.com", "ecr-login")
|
||||
.containsEntry("gcr.io", "gcr");
|
||||
assertThat(configuration.getAuths()).hasSize(3).hasEntrySatisfying("https://index.docker.io/v1/", (auth) -> {
|
||||
assertThat(auth.getUsername()).isEqualTo("username");
|
||||
assertThat(auth.getPassword()).isEqualTo("password");
|
||||
assertThat(auth.getEmail()).isEqualTo("test@gmail.com");
|
||||
}).hasEntrySatisfying("custom-registry.example.com", (auth) -> {
|
||||
assertThat(auth.getUsername()).isEqualTo("customUser");
|
||||
assertThat(auth.getPassword()).isEqualTo("customPass");
|
||||
assertThat(auth.getEmail()).isNull();
|
||||
}).hasEntrySatisfying("my-registry.example.com", (auth) -> {
|
||||
assertThat(auth.getUsername()).isEqualTo("user");
|
||||
assertThat(auth.getPassword()).isEqualTo("password");
|
||||
assertThat(auth.getEmail()).isNull();
|
||||
});
|
||||
}
|
||||
|
||||
private String pathToResource(String resource) throws URISyntaxException {
|
||||
URL url = getClass().getResource(resource);
|
||||
return Paths.get(url.toURI()).getParent().toAbsolutePath().toString();
|
||||
|
||||
@@ -0,0 +1,357 @@
|
||||
/*
|
||||
* 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.configuration;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Base64;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
|
||||
import org.springframework.boot.buildpack.platform.docker.type.ImageReference;
|
||||
import org.springframework.boot.buildpack.platform.json.SharedObjectMapper;
|
||||
import org.springframework.boot.testsupport.classpath.resources.ResourcesRoot;
|
||||
import org.springframework.boot.testsupport.classpath.resources.WithResource;
|
||||
import org.springframework.boot.testsupport.system.OutputCaptureExtension;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Tests for {@link DockerRegistryConfigAuthentication}.
|
||||
*
|
||||
* @author Dmytro Nosan
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
@ExtendWith(OutputCaptureExtension.class)
|
||||
class DockerRegistryConfigAuthenticationTests {
|
||||
|
||||
private final Map<String, String> environment = new LinkedHashMap<>();
|
||||
|
||||
private final Map<String, Exception> helperExceptions = new LinkedHashMap<>();
|
||||
|
||||
private Map<String, CredentialHelper> credentialHelpers = new HashMap<>();
|
||||
|
||||
@WithResource(name = "config.json", content = """
|
||||
{
|
||||
"auths": {
|
||||
"https://index.docker.io/v1/": {
|
||||
"auth": "dXNlcm5hbWU6cGFzc3dvcmQ=",
|
||||
"email": "test@gmail.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
""")
|
||||
@Test
|
||||
void getAuthHeaderWhenAuthForDockerDomain(@ResourcesRoot Path directory) throws Exception {
|
||||
this.environment.put("DOCKER_CONFIG", directory.toString());
|
||||
ImageReference imageReference = ImageReference.of("docker.io/ubuntu:latest");
|
||||
String authHeader = getAuthHeader(imageReference);
|
||||
assertThat(decode(authHeader)).hasSize(4)
|
||||
.containsEntry("serveraddress", "https://index.docker.io/v1/")
|
||||
.containsEntry("username", "username")
|
||||
.containsEntry("password", "password")
|
||||
.containsEntry("email", "test@gmail.com");
|
||||
}
|
||||
|
||||
@WithResource(name = "config.json", content = """
|
||||
{
|
||||
"auths": {
|
||||
"https://index.docker.io/v1/": {
|
||||
"auth": "dXNlcm5hbWU6cGFzc3dvcmQ=",
|
||||
"email": "test@gmail.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
""")
|
||||
@Test
|
||||
void getAuthHeaderWhenAuthForLegacyDockerDomain(@ResourcesRoot Path directory) throws Exception {
|
||||
this.environment.put("DOCKER_CONFIG", directory.toString());
|
||||
ImageReference imageReference = ImageReference.of("index.docker.io/ubuntu:latest");
|
||||
String authHeader = getAuthHeader(imageReference);
|
||||
assertThat(decode(authHeader)).hasSize(4)
|
||||
.containsEntry("serveraddress", "https://index.docker.io/v1/")
|
||||
.containsEntry("username", "username")
|
||||
.containsEntry("password", "password")
|
||||
.containsEntry("email", "test@gmail.com");
|
||||
}
|
||||
|
||||
@WithResource(name = "config.json", content = """
|
||||
{
|
||||
"auths": {
|
||||
"my-registry.example.com": {
|
||||
"auth": "Y3VzdG9tVXNlcjpjdXN0b21QYXNz"
|
||||
}
|
||||
}
|
||||
}
|
||||
""")
|
||||
@Test
|
||||
void getAuthHeaderWhenAuthForCustomDomain(@ResourcesRoot Path directory) throws Exception {
|
||||
this.environment.put("DOCKER_CONFIG", directory.toString());
|
||||
ImageReference imageReference = ImageReference.of("my-registry.example.com/ubuntu:latest");
|
||||
String authHeader = getAuthHeader(imageReference);
|
||||
assertThat(decode(authHeader)).hasSize(4)
|
||||
.containsEntry("serveraddress", "my-registry.example.com")
|
||||
.containsEntry("username", "customUser")
|
||||
.containsEntry("password", "customPass")
|
||||
.containsEntry("email", null);
|
||||
}
|
||||
|
||||
@WithResource(name = "config.json", content = """
|
||||
{
|
||||
"auths": {
|
||||
"https://my-registry.example.com": {
|
||||
"auth": "Y3VzdG9tVXNlcjpjdXN0b21QYXNz"
|
||||
}
|
||||
}
|
||||
}
|
||||
""")
|
||||
@Test
|
||||
void getAuthHeaderWhenAuthForCustomDomainWithLegacyFormat(@ResourcesRoot Path directory) throws Exception {
|
||||
this.environment.put("DOCKER_CONFIG", directory.toString());
|
||||
ImageReference imageReference = ImageReference.of("my-registry.example.com/ubuntu:latest");
|
||||
String authHeader = getAuthHeader(imageReference);
|
||||
assertThat(decode(authHeader)).hasSize(4)
|
||||
.containsEntry("serveraddress", "https://my-registry.example.com")
|
||||
.containsEntry("username", "customUser")
|
||||
.containsEntry("password", "customPass")
|
||||
.containsEntry("email", null);
|
||||
}
|
||||
|
||||
@WithResource(name = "config.json", content = """
|
||||
{
|
||||
}
|
||||
""")
|
||||
@Test
|
||||
void getAuthHeaderWhenEmptyConfigDirectoryReturnsFallback(@ResourcesRoot Path directory) throws Exception {
|
||||
this.environment.put("DOCKER_CONFIG", directory.toString());
|
||||
ImageReference imageReference = ImageReference.of("docker.io/ubuntu:latest");
|
||||
String authHeader = getAuthHeader(imageReference, DockerRegistryAuthentication.EMPTY_USER);
|
||||
assertThat(decode(authHeader)).hasSize(4)
|
||||
.containsEntry("serveraddress", "")
|
||||
.containsEntry("username", "")
|
||||
.containsEntry("password", "")
|
||||
.containsEntry("email", "");
|
||||
}
|
||||
|
||||
@WithResource(name = "config.json", content = """
|
||||
{
|
||||
"credsStore": "desktop"
|
||||
}
|
||||
""")
|
||||
@WithResource(name = "credentials.json", content = """
|
||||
{
|
||||
"ServerURL": "https://index.docker.io/v1/",
|
||||
"Username": "<token>",
|
||||
"Secret": "secret"
|
||||
}
|
||||
""")
|
||||
@Test
|
||||
void getAuthHeaderWhenUsingHelperFromCredsStore(@ResourcesRoot Path directory) throws Exception {
|
||||
this.environment.put("DOCKER_CONFIG", directory.toString());
|
||||
ImageReference imageReference = ImageReference.of("docker.io/ubuntu:latest");
|
||||
mockHelper("desktop", "https://index.docker.io/v1/", "credentials.json");
|
||||
String authHeader = getAuthHeader(imageReference);
|
||||
assertThat(decode(authHeader)).hasSize(1).containsEntry("identitytoken", "secret");
|
||||
}
|
||||
|
||||
@WithResource(name = "config.json", content = """
|
||||
{
|
||||
"auths": {
|
||||
"gcr.io": {
|
||||
"email": "test@gmail.com"
|
||||
}
|
||||
},
|
||||
"credsStore": "desktop",
|
||||
"credHelpers": {
|
||||
"gcr.io": "gcr"
|
||||
}
|
||||
}
|
||||
""")
|
||||
@WithResource(name = "credentials.json", content = """
|
||||
{
|
||||
"ServerURL": "https://my-gcr.io",
|
||||
"Username": "username",
|
||||
"Secret": "secret"
|
||||
}
|
||||
""")
|
||||
@Test
|
||||
void getAuthHeaderWhenUsingHelperFromCredsStoreAndUseEmailFromAuth(@ResourcesRoot Path directory) throws Exception {
|
||||
this.environment.put("DOCKER_CONFIG", directory.toString());
|
||||
ImageReference imageReference = ImageReference.of("gcr.io/ubuntu:latest");
|
||||
mockHelper("gcr", "gcr.io", "credentials.json");
|
||||
String authHeader = getAuthHeader(imageReference);
|
||||
assertThat(decode(authHeader)).hasSize(4)
|
||||
.containsEntry("serveraddress", "https://my-gcr.io")
|
||||
.containsEntry("username", "username")
|
||||
.containsEntry("password", "secret")
|
||||
.containsEntry("email", "test@gmail.com");
|
||||
}
|
||||
|
||||
@WithResource(name = "config.json", content = """
|
||||
{
|
||||
"credsStore": "desktop",
|
||||
"credHelpers": {
|
||||
"gcr.io": "gcr"
|
||||
}
|
||||
}
|
||||
""")
|
||||
@WithResource(name = "credentials.json", content = """
|
||||
{
|
||||
"Username": "username",
|
||||
"Secret": "secret"
|
||||
}
|
||||
""")
|
||||
@Test
|
||||
void getAuthHeaderWhenUsingHelperFromCredHelpersUsesProvidedServerUrl(@ResourcesRoot Path directory)
|
||||
throws Exception {
|
||||
this.environment.put("DOCKER_CONFIG", directory.toString());
|
||||
ImageReference imageReference = ImageReference.of("gcr.io/ubuntu:latest");
|
||||
mockHelper("gcr", "gcr.io", "credentials.json");
|
||||
String authHeader = getAuthHeader(imageReference);
|
||||
assertThat(decode(authHeader)).hasSize(4)
|
||||
.containsEntry("serveraddress", "gcr.io")
|
||||
.containsEntry("username", "username")
|
||||
.containsEntry("password", "secret")
|
||||
.containsEntry("email", null);
|
||||
}
|
||||
|
||||
@WithResource(name = "config.json", content = """
|
||||
{
|
||||
"auths": {
|
||||
"gcr.io": {
|
||||
"auth": "dXNlcm5hbWU6cGFzc3dvcmQ=",
|
||||
"email": "test@gmail.com"
|
||||
}
|
||||
},
|
||||
"credsStore": "desktop",
|
||||
"credHelpers": {
|
||||
"gcr.io": "gcr"
|
||||
}
|
||||
}
|
||||
""")
|
||||
@Test
|
||||
void getAuthHeaderWhenUsingHelperThatFailsLogsErrorAndReturnsFromAuths(@ResourcesRoot Path directory)
|
||||
throws Exception {
|
||||
this.environment.put("DOCKER_CONFIG", directory.toString());
|
||||
ImageReference imageReference = ImageReference.of("gcr.io/ubuntu:latest");
|
||||
CredentialHelper helper = mockHelper("gcr");
|
||||
given(helper.get("gcr.io")).willThrow(new IOException("Failed to obtain credentials for registry"));
|
||||
String authHeader = getAuthHeader(imageReference);
|
||||
assertThat(decode(authHeader)).hasSize(4)
|
||||
.containsEntry("serveraddress", "gcr.io")
|
||||
.containsEntry("username", "username")
|
||||
.containsEntry("password", "password")
|
||||
.containsEntry("email", "test@gmail.com");
|
||||
assertThat(this.helperExceptions).hasSize(1);
|
||||
assertThat(this.helperExceptions.keySet().iterator().next())
|
||||
.contains("Error retrieving credentials for 'gcr.io' due to: Failed to obtain credentials for registry");
|
||||
}
|
||||
|
||||
@WithResource(name = "config.json", content = """
|
||||
{
|
||||
"credsStore": "desktop",
|
||||
"credHelpers": {
|
||||
"gcr.io": "gcr"
|
||||
}
|
||||
}
|
||||
""")
|
||||
@Test
|
||||
void getAuthHeaderWhenUsingHelperThatFailsAndNoAuthLogsErrorAndReturnsFallback(@ResourcesRoot Path directory)
|
||||
throws Exception {
|
||||
this.environment.put("DOCKER_CONFIG", directory.toString());
|
||||
ImageReference imageReference = ImageReference.of("gcr.io/ubuntu:latest");
|
||||
CredentialHelper helper = mockHelper("gcr");
|
||||
given(helper.get("gcr.io")).willThrow(new IOException("Failed to obtain credentials for registry"));
|
||||
String authHeader = getAuthHeader(imageReference, DockerRegistryAuthentication.EMPTY_USER);
|
||||
assertThat(this.helperExceptions).hasSize(1);
|
||||
assertThat(this.helperExceptions.keySet().iterator().next())
|
||||
.contains("Error retrieving credentials for 'gcr.io' due to: Failed to obtain credentials for registry");
|
||||
assertThat(decode(authHeader)).hasSize(4)
|
||||
.containsEntry("serveraddress", "")
|
||||
.containsEntry("username", "")
|
||||
.containsEntry("password", "")
|
||||
.containsEntry("email", "");
|
||||
}
|
||||
|
||||
@WithResource(name = "config.json", content = """
|
||||
{
|
||||
"credsStore": "desktop",
|
||||
"credHelpers": {
|
||||
"gcr.io": ""
|
||||
}
|
||||
}
|
||||
""")
|
||||
@Test
|
||||
void getAuthHeaderWhenEmptyCredHelperReturnsFallbackAndDoesNotUseCredStore(@ResourcesRoot Path directory)
|
||||
throws Exception {
|
||||
this.environment.put("DOCKER_CONFIG", directory.toString());
|
||||
ImageReference imageReference = ImageReference.of("gcr.io/ubuntu:latest");
|
||||
String authHeader = getAuthHeader(imageReference, DockerRegistryAuthentication.EMPTY_USER);
|
||||
assertThat(decode(authHeader)).hasSize(4)
|
||||
.containsEntry("serveraddress", "")
|
||||
.containsEntry("username", "")
|
||||
.containsEntry("password", "")
|
||||
.containsEntry("email", "");
|
||||
}
|
||||
|
||||
private String getAuthHeader(ImageReference imageReference) {
|
||||
return getAuthHeader(imageReference, null);
|
||||
}
|
||||
|
||||
private String getAuthHeader(ImageReference imageReference, DockerRegistryAuthentication fallback) {
|
||||
DockerRegistryConfigAuthentication authentication = getAuthentication(fallback);
|
||||
return authentication.getAuthHeader(imageReference);
|
||||
}
|
||||
|
||||
private DockerRegistryConfigAuthentication getAuthentication(DockerRegistryAuthentication fallback) {
|
||||
return new DockerRegistryConfigAuthentication(fallback, this.helperExceptions::put, this.environment::get,
|
||||
this.credentialHelpers::get);
|
||||
}
|
||||
|
||||
private void mockHelper(String name, String serverUrl, String credentialsResourceName) throws Exception {
|
||||
CredentialHelper helper = mockHelper(name);
|
||||
given(helper.get(serverUrl)).willReturn(getCredentials(credentialsResourceName));
|
||||
}
|
||||
|
||||
private CredentialHelper mockHelper(String name) {
|
||||
CredentialHelper helper = mock(CredentialHelper.class);
|
||||
this.credentialHelpers.put(name, helper);
|
||||
return helper;
|
||||
}
|
||||
|
||||
private Credential getCredentials(String resourceName) throws Exception {
|
||||
try (InputStream inputStream = new ClassPathResource(resourceName).getInputStream()) {
|
||||
return new Credential(SharedObjectMapper.get().readTree(inputStream));
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, String> decode(String authHeader) throws Exception {
|
||||
assertThat(authHeader).isNotNull();
|
||||
return SharedObjectMapper.get().readValue(Base64.getDecoder().decode(authHeader), new TypeReference<>() {
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
@echo off
|
||||
|
||||
set /p registryUrl=
|
||||
|
||||
if "%registryUrl%" == "user.example.com" (
|
||||
echo {
|
||||
echo "ServerURL": "%registryUrl%",
|
||||
echo "Username": "username",
|
||||
echo "Secret": "secret"
|
||||
echo }
|
||||
exit /b 0
|
||||
)
|
||||
|
||||
if "%registryUrl%" == "token.example.com" (
|
||||
echo {
|
||||
echo "ServerURL": "%registryUrl%",
|
||||
echo "Username": "<token>",
|
||||
echo "Secret": "secret"
|
||||
echo }
|
||||
exit /b 0
|
||||
)
|
||||
|
||||
if "%registryUrl%" == "url.missing.example.com" (
|
||||
echo no credentials server URL >&2
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
if "%registryUrl%" == "username.missing.example.com" (
|
||||
echo no credentials username >&2
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
if "%registryUrl%" == "credentials.missing.example.com" (
|
||||
echo credentials not found in native keychain >&2
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo Unknown error >&2
|
||||
exit /b 1
|
||||
@@ -0,0 +1,43 @@
|
||||
#!/bin/sh
|
||||
|
||||
read -r registryUrl
|
||||
|
||||
if [ "$registryUrl" = "user.example.com" ]; then
|
||||
cat <<EOF
|
||||
{
|
||||
"ServerURL": "${registryUrl}",
|
||||
"Username": "username",
|
||||
"Secret": "secret"
|
||||
}
|
||||
EOF
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "$registryUrl" = "token.example.com" ]; then
|
||||
cat <<EOF
|
||||
{
|
||||
"ServerURL": "${registryUrl}",
|
||||
"Username": "<token>",
|
||||
"Secret": "secret"
|
||||
}
|
||||
EOF
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "$registryUrl" = "url.missing.example.com" ]; then
|
||||
echo "no credentials server URL" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$registryUrl" = "username.missing.example.com" ]; then
|
||||
echo "no credentials username" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$registryUrl" = "credentials.missing.example.com" ]; then
|
||||
echo "credentials not found in native keychain" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Unknown error" >&2
|
||||
exit 1
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"auths": {
|
||||
"https://index.docker.io/v1/": {
|
||||
"auth": "dXNlcm5hbWU6cGFzc3dvcmQ=",
|
||||
"email": "test@gmail.com"
|
||||
},
|
||||
"custom-registry.example.com": {
|
||||
"auth": "Y3VzdG9tVXNlcjpjdXN0b21QYXNz"
|
||||
},
|
||||
"my-registry.example.com": {
|
||||
"username": "user",
|
||||
"password": "password"
|
||||
}
|
||||
},
|
||||
"credsStore": "desktop",
|
||||
"credHelpers": {
|
||||
"gcr.io": "gcr",
|
||||
"ecr.us-east-1.amazonaws.com": "ecr-login",
|
||||
"azurecr.io": "acr-env"
|
||||
}
|
||||
}
|
||||
@@ -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]]
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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]]
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user