Skip to content

Commit e50da4f

Browse files
committed
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
2 parents dd49de0 + da61d63 commit e50da4f

File tree

23 files changed

+1235
-44
lines changed

23 files changed

+1235
-44
lines changed

spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Builder.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -246,13 +246,13 @@ private void pushImages(ImageReference name, List<ImageReference> tags) throws I
246246
private void pushImage(ImageReference reference) throws IOException {
247247
Consumer<TotalProgressEvent> progressConsumer = this.log.pushingImage(reference);
248248
TotalProgressPushListener listener = new TotalProgressPushListener(progressConsumer);
249-
String authHeader = authHeader(this.dockerConfiguration.publishRegistryAuthentication());
249+
String authHeader = authHeader(this.dockerConfiguration.publishRegistryAuthentication(), reference);
250250
this.docker.image().push(reference, listener, authHeader);
251251
this.log.pushedImage(reference);
252252
}
253253

254-
private static String authHeader(DockerRegistryAuthentication authentication) {
255-
return (authentication != null) ? authentication.getAuthHeader() : null;
254+
private static String authHeader(DockerRegistryAuthentication authentication, ImageReference reference) {
255+
return (authentication != null) ? authentication.getAuthHeader(reference) : null;
256256
}
257257

258258
/**
@@ -279,7 +279,7 @@ private class ImageFetcher {
279279
Image fetchImage(ImageType type, ImageReference reference) throws IOException {
280280
Assert.notNull(type, "'type' must not be null");
281281
Assert.notNull(reference, "'reference' must not be null");
282-
String authHeader = authHeader(this.registryAuthentication);
282+
String authHeader = authHeader(this.registryAuthentication, reference);
283283
Assert.state(authHeader == null || reference.getDomain().equals(this.domain),
284284
() -> String.format("%s '%s' must be pulled from the '%s' authenticated registry",
285285
StringUtils.capitalize(type.getDescription()), reference, this.domain));
@@ -300,7 +300,7 @@ Image fetchImage(ImageType type, ImageReference reference) throws IOException {
300300
private Image pullImage(ImageReference reference, ImageType imageType) throws IOException {
301301
TotalProgressPullListener listener = new TotalProgressPullListener(
302302
Builder.this.log.pullingImage(reference, this.defaultPlatform, imageType));
303-
String authHeader = authHeader(this.registryAuthentication);
303+
String authHeader = authHeader(this.registryAuthentication, reference);
304304
Image image = Builder.this.docker.image().pull(reference, this.defaultPlatform, listener, authHeader);
305305
Builder.this.log.pulledImage(image, imageType);
306306
if (this.defaultPlatform == null) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/*
2+
* Copyright 2012-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.buildpack.platform.docker.configuration;
18+
19+
import java.lang.invoke.MethodHandles;
20+
21+
import com.fasterxml.jackson.databind.JsonNode;
22+
23+
import org.springframework.boot.buildpack.platform.json.MappedObject;
24+
25+
/**
26+
* A class that represents credentials for a server as returned from a
27+
* {@link CredentialHelper}.
28+
*
29+
* @author Dmytro Nosan
30+
*/
31+
class Credential extends MappedObject {
32+
33+
/**
34+
* If the secret being stored is an identity token, the username should be set to
35+
* {@code <token>}.
36+
*/
37+
private static final String TOKEN_USERNAME = "<token>";
38+
39+
private final String username;
40+
41+
private final String secret;
42+
43+
private String serverUrl;
44+
45+
Credential(JsonNode node) {
46+
super(node, MethodHandles.lookup());
47+
this.username = valueAt("/Username", String.class);
48+
this.secret = valueAt("/Secret", String.class);
49+
this.serverUrl = valueAt("/ServerURL", String.class);
50+
}
51+
52+
String getUsername() {
53+
return this.username;
54+
}
55+
56+
String getSecret() {
57+
return this.secret;
58+
}
59+
60+
String getServerUrl() {
61+
return this.serverUrl;
62+
}
63+
64+
boolean isIdentityToken() {
65+
return TOKEN_USERNAME.equals(this.username);
66+
}
67+
68+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
/*
2+
* Copyright 2012-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.buildpack.platform.docker.configuration;
18+
19+
import java.io.IOException;
20+
import java.io.InputStream;
21+
import java.io.OutputStream;
22+
import java.nio.charset.StandardCharsets;
23+
import java.util.ArrayList;
24+
import java.util.List;
25+
import java.util.Set;
26+
27+
import com.sun.jna.Platform;
28+
29+
import org.springframework.boot.buildpack.platform.json.SharedObjectMapper;
30+
31+
/**
32+
* Invokes a Docker credential helper executable that can be used to get {@link Credential
33+
* credentials}.
34+
*
35+
* @author Dmytro Nosan
36+
* @author Phillip Webb
37+
*/
38+
class CredentialHelper {
39+
40+
private static final String USR_LOCAL_BIN = "/usr/local/bin/";
41+
42+
Set<String> CREDENTIAL_NOT_FOUND_MESSAGES = Set.of("credentials not found in native keychain",
43+
"no credentials server URL", "no credentials username");
44+
45+
private final String executable;
46+
47+
CredentialHelper(String executable) {
48+
this.executable = executable;
49+
}
50+
51+
Credential get(String serverUrl) throws IOException {
52+
ProcessBuilder processBuilder = processBuilder("get");
53+
Process process = start(processBuilder);
54+
try (OutputStream request = process.getOutputStream()) {
55+
request.write(serverUrl.getBytes(StandardCharsets.UTF_8));
56+
}
57+
try {
58+
int exitCode = process.waitFor();
59+
try (InputStream response = process.getInputStream()) {
60+
if (exitCode == 0) {
61+
return new Credential(SharedObjectMapper.get().readTree(response));
62+
}
63+
String errorMessage = new String(response.readAllBytes(), StandardCharsets.UTF_8);
64+
if (!isCredentialsNotFoundError(errorMessage)) {
65+
throw new IOException("%s' exited with code %d: %s".formatted(process, exitCode, errorMessage));
66+
}
67+
return null;
68+
}
69+
}
70+
catch (InterruptedException ex) {
71+
Thread.currentThread().interrupt();
72+
return null;
73+
}
74+
}
75+
76+
private ProcessBuilder processBuilder(String string) {
77+
ProcessBuilder processBuilder = new ProcessBuilder().redirectErrorStream(true);
78+
if (Platform.isWindows()) {
79+
processBuilder.command("cmd", "/c");
80+
}
81+
processBuilder.command(this.executable, string);
82+
return processBuilder;
83+
}
84+
85+
private Process start(ProcessBuilder processBuilder) throws IOException {
86+
try {
87+
return processBuilder.start();
88+
}
89+
catch (IOException ex) {
90+
if (!Platform.isMac()) {
91+
throw ex;
92+
}
93+
List<String> command = new ArrayList<>(processBuilder.command());
94+
command.set(0, USR_LOCAL_BIN + command.get(0));
95+
return processBuilder.command(command).start();
96+
}
97+
}
98+
99+
private boolean isCredentialsNotFoundError(String message) {
100+
return this.CREDENTIAL_NOT_FOUND_MESSAGES.contains(message.trim());
101+
}
102+
103+
}

spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerConfigurationMetadata.java

Lines changed: 83 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2023 the original author or authors.
2+
* Copyright 2012-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -23,7 +23,9 @@
2323
import java.nio.file.Path;
2424
import java.security.MessageDigest;
2525
import java.security.NoSuchAlgorithmException;
26+
import java.util.Base64;
2627
import java.util.HexFormat;
28+
import java.util.Map;
2729

2830
import com.fasterxml.jackson.core.JsonProcessingException;
2931
import com.fasterxml.jackson.databind.JsonNode;
@@ -32,11 +34,14 @@
3234
import org.springframework.boot.buildpack.platform.json.MappedObject;
3335
import org.springframework.boot.buildpack.platform.json.SharedObjectMapper;
3436
import org.springframework.boot.buildpack.platform.system.Environment;
37+
import org.springframework.util.Assert;
38+
import org.springframework.util.StringUtils;
3539

3640
/**
3741
* Docker configuration stored in metadata files managed by the Docker CLI.
3842
*
3943
* @author Scott Frederick
44+
* @author Dmytro Nosan
4045
*/
4146
final class DockerConfigurationMetadata {
4247

@@ -58,6 +63,8 @@ final class DockerConfigurationMetadata {
5863

5964
private static final String CONTEXT_FILE_NAME = "meta.json";
6065

66+
private static volatile DockerConfigurationMetadata systemEnvironmentConfigurationMetadata;
67+
6168
private final String configLocation;
6269

6370
private final DockerConfig config;
@@ -83,11 +90,24 @@ DockerContext forContext(String context) {
8390
}
8491

8592
static DockerConfigurationMetadata from(Environment environment) {
86-
String configLocation = (environment.get(DOCKER_CONFIG) != null) ? environment.get(DOCKER_CONFIG)
87-
: Path.of(System.getProperty("user.home"), CONFIG_DIR).toString();
93+
DockerConfigurationMetadata dockerConfigurationMetadata = (environment == Environment.SYSTEM)
94+
? DockerConfigurationMetadata.systemEnvironmentConfigurationMetadata : null;
95+
if (dockerConfigurationMetadata != null) {
96+
return dockerConfigurationMetadata;
97+
}
98+
String configLocation = environment.get(DOCKER_CONFIG);
99+
configLocation = (configLocation != null) ? configLocation : getUserHomeConfigLocation();
88100
DockerConfig dockerConfig = createDockerConfig(configLocation);
89101
DockerContext dockerContext = createDockerContext(configLocation, dockerConfig.getCurrentContext());
90-
return new DockerConfigurationMetadata(configLocation, dockerConfig, dockerContext);
102+
dockerConfigurationMetadata = new DockerConfigurationMetadata(configLocation, dockerConfig, dockerContext);
103+
if (environment == Environment.SYSTEM) {
104+
DockerConfigurationMetadata.systemEnvironmentConfigurationMetadata = dockerConfigurationMetadata;
105+
}
106+
return dockerConfigurationMetadata;
107+
}
108+
109+
private static String getUserHomeConfigLocation() {
110+
return Path.of(System.getProperty("user.home"), CONFIG_DIR).toString();
91111
}
92112

93113
private static DockerConfig createDockerConfig(String configLocation) {
@@ -148,15 +168,36 @@ static final class DockerConfig extends MappedObject {
148168

149169
private final String currentContext;
150170

171+
private final String credsStore;
172+
173+
private final Map<String, String> credHelpers;
174+
175+
private final Map<String, Auth> auths;
176+
151177
private DockerConfig(JsonNode node) {
152178
super(node, MethodHandles.lookup());
153179
this.currentContext = valueAt("/currentContext", String.class);
180+
this.credsStore = valueAt("/credsStore", String.class);
181+
this.credHelpers = mapAt("/credHelpers", JsonNode::textValue);
182+
this.auths = mapAt("/auths", Auth::new);
154183
}
155184

156185
String getCurrentContext() {
157186
return this.currentContext;
158187
}
159188

189+
String getCredsStore() {
190+
return this.credsStore;
191+
}
192+
193+
Map<String, String> getCredHelpers() {
194+
return this.credHelpers;
195+
}
196+
197+
Map<String, Auth> getAuths() {
198+
return this.auths;
199+
}
200+
160201
static DockerConfig fromJson(String json) throws JsonProcessingException {
161202
return new DockerConfig(SharedObjectMapper.get().readTree(json));
162203
}
@@ -167,6 +208,44 @@ static DockerConfig empty() {
167208

168209
}
169210

211+
static final class Auth extends MappedObject {
212+
213+
private final String username;
214+
215+
private final String password;
216+
217+
private final String email;
218+
219+
Auth(JsonNode node) {
220+
super(node, MethodHandles.lookup());
221+
String auth = valueAt("/auth", String.class);
222+
if (StringUtils.hasText(auth)) {
223+
String[] parts = new String(Base64.getDecoder().decode(auth)).split(":", 2);
224+
Assert.state(parts.length == 2, "Malformed auth in docker configuration metadata");
225+
this.username = parts[0];
226+
this.password = parts[1];
227+
}
228+
else {
229+
this.username = valueAt("/username", String.class);
230+
this.password = valueAt("/password", String.class);
231+
}
232+
this.email = valueAt("/email", String.class);
233+
}
234+
235+
String getUsername() {
236+
return this.username;
237+
}
238+
239+
String getPassword() {
240+
return this.password;
241+
}
242+
243+
String getEmail() {
244+
return this.email;
245+
}
246+
247+
}
248+
170249
static final class DockerContext extends MappedObject {
171250

172251
private final String dockerHost;

0 commit comments

Comments
 (0)