Skip to content

Commit 9625146

Browse files
committed
Restore support for docker compose versions earlier than 2.24
Fixes gh-43710
1 parent 9dea1e1 commit 9625146

File tree

3 files changed

+104
-27
lines changed

3 files changed

+104
-27
lines changed

spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerCli.java

+20-12
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-2024 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.
@@ -28,6 +28,7 @@
2828
import org.apache.commons.logging.Log;
2929
import org.apache.commons.logging.LogFactory;
3030

31+
import org.springframework.boot.docker.compose.core.DockerCliCommand.ComposeVersion;
3132
import org.springframework.boot.docker.compose.core.DockerCliCommand.Type;
3233
import org.springframework.boot.logging.LogLevel;
3334
import org.springframework.core.log.LogMessage;
@@ -53,6 +54,8 @@ class DockerCli {
5354

5455
private final Set<String> activeProfiles;
5556

57+
private final ComposeVersion composeVersion;
58+
5659
/**
5760
* Create a new {@link DockerCli} instance.
5861
* @param workingDirectory the working directory or {@code null}
@@ -65,6 +68,7 @@ class DockerCli {
6568
(key) -> new DockerCommands(this.processRunner));
6669
this.composeFile = composeFile;
6770
this.activeProfiles = (activeProfiles != null) ? activeProfiles : Collections.emptySet();
71+
this.composeVersion = ComposeVersion.of(this.dockerCommands.get(Type.DOCKER_COMPOSE).version());
6872
}
6973

7074
/**
@@ -75,7 +79,7 @@ class DockerCli {
7579
*/
7680
<R> R run(DockerCliCommand<R> dockerCommand) {
7781
List<String> command = createCommand(dockerCommand.getType());
78-
command.addAll(dockerCommand.getCommand());
82+
command.addAll(dockerCommand.getCommand(this.composeVersion));
7983
Consumer<String> outputConsumer = createOutputConsumer(dockerCommand.getLogLevel());
8084
String json = this.processRunner.run(outputConsumer, command.toArray(new String[0]));
8185
return dockerCommand.deserialize(json);
@@ -90,9 +94,9 @@ private Consumer<String> createOutputConsumer(LogLevel logLevel) {
9094

9195
private List<String> createCommand(Type type) {
9296
return switch (type) {
93-
case DOCKER -> new ArrayList<>(this.dockerCommands.get(type));
97+
case DOCKER -> new ArrayList<>(this.dockerCommands.get(type).command());
9498
case DOCKER_COMPOSE -> {
95-
List<String> result = new ArrayList<>(this.dockerCommands.get(type));
99+
List<String> result = new ArrayList<>(this.dockerCommands.get(type).command());
96100
if (this.composeFile != null) {
97101
result.add("--file");
98102
result.add(this.composeFile.toString());
@@ -121,20 +125,20 @@ DockerComposeFile getDockerComposeFile() {
121125
*/
122126
private static class DockerCommands {
123127

124-
private final List<String> dockerCommand;
128+
private final DockerCommand dockerCommand;
125129

126-
private final List<String> dockerComposeCommand;
130+
private final DockerCommand dockerComposeCommand;
127131

128132
DockerCommands(ProcessRunner processRunner) {
129133
this.dockerCommand = getDockerCommand(processRunner);
130134
this.dockerComposeCommand = getDockerComposeCommand(processRunner);
131135
}
132136

133-
private List<String> getDockerCommand(ProcessRunner processRunner) {
137+
private DockerCommand getDockerCommand(ProcessRunner processRunner) {
134138
try {
135139
String version = processRunner.run("docker", "version", "--format", "{{.Client.Version}}");
136140
logger.trace(LogMessage.format("Using docker %s", version));
137-
return List.of("docker");
141+
return new DockerCommand(version, List.of("docker"));
138142
}
139143
catch (ProcessStartException ex) {
140144
throw new DockerProcessStartException("Unable to start docker process. Is docker correctly installed?",
@@ -149,13 +153,13 @@ private List<String> getDockerCommand(ProcessRunner processRunner) {
149153
}
150154
}
151155

152-
private List<String> getDockerComposeCommand(ProcessRunner processRunner) {
156+
private DockerCommand getDockerComposeCommand(ProcessRunner processRunner) {
153157
try {
154158
DockerCliComposeVersionResponse response = DockerJson.deserialize(
155159
processRunner.run("docker", "compose", "version", "--format", "json"),
156160
DockerCliComposeVersionResponse.class);
157161
logger.trace(LogMessage.format("Using docker compose %s", response.version()));
158-
return List.of("docker", "compose");
162+
return new DockerCommand(response.version(), List.of("docker", "compose"));
159163
}
160164
catch (ProcessExitException ex) {
161165
// Ignore and try docker-compose
@@ -165,7 +169,7 @@ private List<String> getDockerComposeCommand(ProcessRunner processRunner) {
165169
processRunner.run("docker-compose", "version", "--format", "json"),
166170
DockerCliComposeVersionResponse.class);
167171
logger.trace(LogMessage.format("Using docker-compose %s", response.version()));
168-
return List.of("docker-compose");
172+
return new DockerCommand(response.version(), List.of("docker-compose"));
169173
}
170174
catch (ProcessStartException ex) {
171175
throw new DockerProcessStartException(
@@ -174,7 +178,7 @@ private List<String> getDockerComposeCommand(ProcessRunner processRunner) {
174178
}
175179
}
176180

177-
List<String> get(Type type) {
181+
DockerCommand get(Type type) {
178182
return switch (type) {
179183
case DOCKER -> this.dockerCommand;
180184
case DOCKER_COMPOSE -> this.dockerComposeCommand;
@@ -183,4 +187,8 @@ List<String> get(Type type) {
183187

184188
}
185189

190+
private record DockerCommand(String version, List<String> command) {
191+
192+
}
193+
186194
}

spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerCliCommand.java

+49-7
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@
2020
import java.util.ArrayList;
2121
import java.util.Collection;
2222
import java.util.List;
23+
import java.util.Locale;
2324
import java.util.Objects;
25+
import java.util.function.Function;
2426

2527
import org.springframework.boot.logging.LogLevel;
2628

@@ -42,19 +44,24 @@ abstract sealed class DockerCliCommand<R> {
4244

4345
private final boolean listResponse;
4446

45-
private final List<String> command;
47+
private final Function<ComposeVersion, List<String>> command;
4648

4749
private DockerCliCommand(Type type, Class<?> responseType, boolean listResponse, String... command) {
4850
this(type, LogLevel.OFF, responseType, listResponse, command);
4951
}
5052

5153
private DockerCliCommand(Type type, LogLevel logLevel, Class<?> responseType, boolean listResponse,
5254
String... command) {
55+
this(type, logLevel, responseType, listResponse, (version) -> List.of(command));
56+
}
57+
58+
private DockerCliCommand(Type type, LogLevel logLevel, Class<?> responseType, boolean listResponse,
59+
Function<ComposeVersion, List<String>> command) {
5360
this.type = type;
5461
this.logLevel = logLevel;
5562
this.responseType = responseType;
5663
this.listResponse = listResponse;
57-
this.command = List.of(command);
64+
this.command = command;
5865
}
5966

6067
Type getType() {
@@ -65,8 +72,8 @@ LogLevel getLogLevel() {
6572
return this.logLevel;
6673
}
6774

68-
List<String> getCommand() {
69-
return this.command;
75+
List<String> getCommand(ComposeVersion composeVersion) {
76+
return this.command.apply(composeVersion);
7077
}
7178

7279
@SuppressWarnings("unchecked")
@@ -90,7 +97,8 @@ public boolean equals(Object obj) {
9097
boolean result = this.type == other.type;
9198
result = result && this.responseType == other.responseType;
9299
result = result && this.listResponse == other.listResponse;
93-
result = result && this.command.equals(other.command);
100+
result = result
101+
&& this.command.apply(ComposeVersion.UNKNOWN).equals(other.command.apply(ComposeVersion.UNKNOWN));
94102
return result;
95103
}
96104

@@ -150,9 +158,16 @@ static final class ComposeConfig extends DockerCliCommand<DockerCliComposeConfig
150158
*/
151159
static final class ComposePs extends DockerCliCommand<List<DockerCliComposePsResponse>> {
152160

161+
private static final List<String> WITHOUT_ORPHANS = List.of("ps", "--format=json");
162+
163+
private static final List<String> WITH_ORPHANS = List.of("ps", "--orphans=false", "--format=json");
164+
153165
ComposePs() {
154-
super(Type.DOCKER_COMPOSE, DockerCliComposePsResponse.class, true, "ps", "--orphans=false",
155-
"--format=json");
166+
super(Type.DOCKER_COMPOSE, LogLevel.OFF, DockerCliComposePsResponse.class, true, ComposePs::getPsCommand);
167+
}
168+
169+
private static List<String> getPsCommand(ComposeVersion composeVersion) {
170+
return (composeVersion.isLessThan(2, 24)) ? WITHOUT_ORPHANS : WITH_ORPHANS;
156171
}
157172

158173
}
@@ -218,4 +233,31 @@ enum Type {
218233

219234
}
220235

236+
/**
237+
* Docker compose version.
238+
*
239+
* @param major the major component
240+
* @param minor the minor component
241+
*/
242+
record ComposeVersion(int major, int minor) {
243+
244+
public static final ComposeVersion UNKNOWN = new ComposeVersion(0, 0);
245+
246+
boolean isLessThan(int major, int minor) {
247+
return major() < major || major() == major && minor() < minor;
248+
}
249+
250+
static ComposeVersion of(String value) {
251+
try {
252+
value = (!value.toLowerCase(Locale.ROOT).startsWith("v")) ? value : value.substring(1);
253+
String[] parts = value.split("\\.");
254+
return new ComposeVersion(Integer.parseInt(parts[0]), Integer.parseInt(parts[1]));
255+
}
256+
catch (Exception ex) {
257+
return UNKNOWN;
258+
}
259+
}
260+
261+
}
262+
221263
}

spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/core/DockerCliCommandTests.java

+35-8
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
import org.junit.jupiter.api.Test;
2323

24+
import org.springframework.boot.docker.compose.core.DockerCliCommand.ComposeVersion;
2425
import org.springframework.boot.logging.LogLevel;
2526

2627
import static org.assertj.core.api.Assertions.assertThat;
@@ -34,35 +35,46 @@
3435
*/
3536
class DockerCliCommandTests {
3637

38+
private static final ComposeVersion COMPOSE_VERSION = ComposeVersion.of("2.31.0");
39+
3740
@Test
3841
void context() {
3942
DockerCliCommand<?> command = new DockerCliCommand.Context();
4043
assertThat(command.getType()).isEqualTo(DockerCliCommand.Type.DOCKER);
41-
assertThat(command.getCommand()).containsExactly("context", "ls", "--format={{ json . }}");
44+
assertThat(command.getCommand(COMPOSE_VERSION)).containsExactly("context", "ls", "--format={{ json . }}");
4245
assertThat(command.deserialize("[]")).isInstanceOf(List.class);
4346
}
4447

4548
@Test
4649
void inspect() {
4750
DockerCliCommand<?> command = new DockerCliCommand.Inspect(List.of("123", "345"));
4851
assertThat(command.getType()).isEqualTo(DockerCliCommand.Type.DOCKER);
49-
assertThat(command.getCommand()).containsExactly("inspect", "--format={{ json . }}", "123", "345");
52+
assertThat(command.getCommand(COMPOSE_VERSION)).containsExactly("inspect", "--format={{ json . }}", "123",
53+
"345");
5054
assertThat(command.deserialize("[]")).isInstanceOf(List.class);
5155
}
5256

5357
@Test
5458
void composeConfig() {
5559
DockerCliCommand<?> command = new DockerCliCommand.ComposeConfig();
5660
assertThat(command.getType()).isEqualTo(DockerCliCommand.Type.DOCKER_COMPOSE);
57-
assertThat(command.getCommand()).containsExactly("config", "--format=json");
61+
assertThat(command.getCommand(COMPOSE_VERSION)).containsExactly("config", "--format=json");
5862
assertThat(command.deserialize("{}")).isInstanceOf(DockerCliComposeConfigResponse.class);
5963
}
6064

6165
@Test
6266
void composePs() {
6367
DockerCliCommand<?> command = new DockerCliCommand.ComposePs();
6468
assertThat(command.getType()).isEqualTo(DockerCliCommand.Type.DOCKER_COMPOSE);
65-
assertThat(command.getCommand()).containsExactly("ps", "--orphans=false", "--format=json");
69+
assertThat(command.getCommand(COMPOSE_VERSION)).containsExactly("ps", "--orphans=false", "--format=json");
70+
assertThat(command.deserialize("[]")).isInstanceOf(List.class);
71+
}
72+
73+
@Test
74+
void composePsWhenLessThanV224() {
75+
DockerCliCommand<?> command = new DockerCliCommand.ComposePs();
76+
assertThat(command.getType()).isEqualTo(DockerCliCommand.Type.DOCKER_COMPOSE);
77+
assertThat(command.getCommand(ComposeVersion.of("2.23"))).containsExactly("ps", "--format=json");
6678
assertThat(command.deserialize("[]")).isInstanceOf(List.class);
6779
}
6880

@@ -71,15 +83,15 @@ void composeUp() {
7183
DockerCliCommand<?> command = new DockerCliCommand.ComposeUp(LogLevel.INFO);
7284
assertThat(command.getType()).isEqualTo(DockerCliCommand.Type.DOCKER_COMPOSE);
7385
assertThat(command.getLogLevel()).isEqualTo(LogLevel.INFO);
74-
assertThat(command.getCommand()).containsExactly("up", "--no-color", "--detach", "--wait");
86+
assertThat(command.getCommand(COMPOSE_VERSION)).containsExactly("up", "--no-color", "--detach", "--wait");
7587
assertThat(command.deserialize("[]")).isNull();
7688
}
7789

7890
@Test
7991
void composeDown() {
8092
DockerCliCommand<?> command = new DockerCliCommand.ComposeDown(Duration.ofSeconds(1));
8193
assertThat(command.getType()).isEqualTo(DockerCliCommand.Type.DOCKER_COMPOSE);
82-
assertThat(command.getCommand()).containsExactly("down", "--timeout", "1");
94+
assertThat(command.getCommand(COMPOSE_VERSION)).containsExactly("down", "--timeout", "1");
8395
assertThat(command.deserialize("[]")).isNull();
8496
}
8597

@@ -88,16 +100,31 @@ void composeStart() {
88100
DockerCliCommand<?> command = new DockerCliCommand.ComposeStart(LogLevel.INFO);
89101
assertThat(command.getType()).isEqualTo(DockerCliCommand.Type.DOCKER_COMPOSE);
90102
assertThat(command.getLogLevel()).isEqualTo(LogLevel.INFO);
91-
assertThat(command.getCommand()).containsExactly("start");
103+
assertThat(command.getCommand(COMPOSE_VERSION)).containsExactly("start");
92104
assertThat(command.deserialize("[]")).isNull();
93105
}
94106

95107
@Test
96108
void composeStop() {
97109
DockerCliCommand<?> command = new DockerCliCommand.ComposeStop(Duration.ofSeconds(1));
98110
assertThat(command.getType()).isEqualTo(DockerCliCommand.Type.DOCKER_COMPOSE);
99-
assertThat(command.getCommand()).containsExactly("stop", "--timeout", "1");
111+
assertThat(command.getCommand(COMPOSE_VERSION)).containsExactly("stop", "--timeout", "1");
100112
assertThat(command.deserialize("[]")).isNull();
101113
}
102114

115+
@Test
116+
void composeVersionTests() {
117+
ComposeVersion version = ComposeVersion.of("2.31.0-desktop");
118+
assertThat(version.major()).isEqualTo(2);
119+
assertThat(version.minor()).isEqualTo(31);
120+
assertThat(version.isLessThan(1, 0)).isFalse();
121+
assertThat(version.isLessThan(2, 0)).isFalse();
122+
assertThat(version.isLessThan(2, 31)).isFalse();
123+
assertThat(version.isLessThan(2, 32)).isTrue();
124+
assertThat(version.isLessThan(3, 0)).isTrue();
125+
ComposeVersion versionWithPrefix = ComposeVersion.of("v2.31.0-desktop");
126+
assertThat(versionWithPrefix.major()).isEqualTo(2);
127+
assertThat(versionWithPrefix.minor()).isEqualTo(31);
128+
}
129+
103130
}

0 commit comments

Comments
 (0)