Skip to content

Commit 4eebb8e

Browse files
committed
Support multiple Docker Compose files
Closes gh-41691
1 parent c7e29b7 commit 4eebb8e

File tree

11 files changed

+161
-48
lines changed

11 files changed

+161
-48
lines changed

spring-boot-project/spring-boot-docker-compose/src/dockerTest/java/org/springframework/boot/docker/compose/core/DockerCliIntegrationTests.java

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ void runBasicCommand() {
6767

6868
@Test
6969
void runLifecycle() throws IOException {
70-
File composeFile = createComposeFile();
70+
File composeFile = createComposeFile("redis-compose.yaml");
7171
DockerCli cli = new DockerCli(null, DockerComposeFile.of(composeFile), Collections.emptySet());
7272
try {
7373
// Verify that no services are running (this is a fresh compose project)
@@ -103,6 +103,26 @@ void runLifecycle() throws IOException {
103103
}
104104
}
105105

106+
@Test
107+
void shouldWorkWithMultipleComposeFiles() throws IOException {
108+
List<File> composeFiles = createComposeFiles();
109+
DockerCli cli = new DockerCli(null, DockerComposeFile.of(composeFiles), Collections.emptySet());
110+
try {
111+
// List the config and verify that both redis are there
112+
DockerCliComposeConfigResponse config = cli.run(new ComposeConfig());
113+
assertThat(config.services()).containsOnlyKeys("redis1", "redis2");
114+
// Run up
115+
cli.run(new ComposeUp(LogLevel.INFO, Collections.emptyList()));
116+
// Run ps and use id to run inspect on the id
117+
List<DockerCliComposePsResponse> ps = cli.run(new ComposePs());
118+
assertThat(ps).hasSize(2);
119+
}
120+
finally {
121+
// Clean up in any case
122+
quietComposeDown(cli);
123+
}
124+
}
125+
106126
private static void quietComposeDown(DockerCli cli) {
107127
try {
108128
cli.run(new ComposeDown(Duration.ZERO, Collections.emptyList()));
@@ -112,13 +132,21 @@ private static void quietComposeDown(DockerCli cli) {
112132
}
113133
}
114134

115-
private static File createComposeFile() throws IOException {
116-
File composeFile = new ClassPathResource("redis-compose.yaml", DockerCliIntegrationTests.class).getFile();
117-
File tempComposeFile = Path.of(tempDir.toString(), composeFile.getName()).toFile();
118-
String composeFileContent = FileCopyUtils.copyToString(new FileReader(composeFile));
119-
composeFileContent = composeFileContent.replace("{imageName}", TestImage.REDIS.toString());
120-
FileCopyUtils.copy(composeFileContent, new FileWriter(tempComposeFile));
121-
return tempComposeFile;
135+
private static File createComposeFile(String resource) throws IOException {
136+
File source = new ClassPathResource(resource, DockerCliIntegrationTests.class).getFile();
137+
File target = Path.of(tempDir.toString(), source.getName()).toFile();
138+
String content = FileCopyUtils.copyToString(new FileReader(source));
139+
content = content.replace("{imageName}", TestImage.REDIS.toString());
140+
try (FileWriter writer = new FileWriter(target)) {
141+
FileCopyUtils.copy(content, writer);
142+
}
143+
return target;
144+
}
145+
146+
private static List<File> createComposeFiles() throws IOException {
147+
File file1 = createComposeFile("1.yaml");
148+
File file2 = createComposeFile("2.yaml");
149+
return List.of(file1, file2);
122150
}
123151

124152
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
services:
2+
redis1:
3+
image: '{imageName}'
4+
ports:
5+
- '6379'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
services:
2+
redis2:
3+
image: '{imageName}'
4+
ports:
5+
- '6379'

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

Lines changed: 5 additions & 3 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-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.
@@ -94,8 +94,10 @@ private List<String> createCommand(Type type) {
9494
case DOCKER_COMPOSE -> {
9595
List<String> result = new ArrayList<>(this.dockerCommands.get(type));
9696
if (this.composeFile != null) {
97-
result.add("--file");
98-
result.add(this.composeFile.toString());
97+
for (File file : this.composeFile.getFiles()) {
98+
result.add("--file");
99+
result.add(file.getPath());
100+
}
99101
}
100102
result.add("--ansi");
101103
result.add("never");

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

Lines changed: 45 additions & 8 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-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.
@@ -21,7 +21,10 @@
2121
import java.io.UncheckedIOException;
2222
import java.nio.file.Files;
2323
import java.nio.file.Path;
24+
import java.util.Collection;
25+
import java.util.Collections;
2426
import java.util.List;
27+
import java.util.stream.Collectors;
2528

2629
import org.springframework.util.Assert;
2730

@@ -33,24 +36,39 @@
3336
* @author Phillip Webb
3437
* @since 3.1.0
3538
* @see #of(File)
39+
* @see #of(Collection)
3640
* @see #find(File)
3741
*/
3842
public final class DockerComposeFile {
3943

4044
private static final List<String> SEARCH_ORDER = List.of("compose.yaml", "compose.yml", "docker-compose.yaml",
4145
"docker-compose.yml");
4246

43-
private final File file;
47+
private final List<File> files;
4448

45-
private DockerComposeFile(File file) {
49+
private DockerComposeFile(List<File> files) {
50+
Assert.state(!files.isEmpty(), "Files must not be empty");
51+
this.files = files.stream().map(DockerComposeFile::toCanonicalFile).toList();
52+
}
53+
54+
private static File toCanonicalFile(File file) {
4655
try {
47-
this.file = file.getCanonicalFile();
56+
return file.getCanonicalFile();
4857
}
4958
catch (IOException ex) {
5059
throw new UncheckedIOException(ex);
5160
}
5261
}
5362

63+
/**
64+
* Returns the source docker compose files.
65+
* @return the source docker compose files
66+
* @since 3.4.0
67+
*/
68+
public List<File> getFiles() {
69+
return this.files;
70+
}
71+
5472
@Override
5573
public boolean equals(Object obj) {
5674
if (this == obj) {
@@ -60,17 +78,20 @@ public boolean equals(Object obj) {
6078
return false;
6179
}
6280
DockerComposeFile other = (DockerComposeFile) obj;
63-
return this.file.equals(other.file);
81+
return this.files.equals(other.files);
6482
}
6583

6684
@Override
6785
public int hashCode() {
68-
return this.file.hashCode();
86+
return this.files.hashCode();
6987
}
7088

7189
@Override
7290
public String toString() {
73-
return this.file.toString();
91+
if (this.files.size() == 1) {
92+
return this.files.get(0).getPath();
93+
}
94+
return this.files.stream().map(File::toString).collect(Collectors.joining(", "));
7495
}
7596

7697
/**
@@ -111,7 +132,23 @@ public static DockerComposeFile of(File file) {
111132
Assert.notNull(file, "File must not be null");
112133
Assert.isTrue(file.exists(), () -> "Docker Compose file '%s' does not exist".formatted(file));
113134
Assert.isTrue(file.isFile(), () -> "Docker compose file '%s' is not a file".formatted(file));
114-
return new DockerComposeFile(file);
135+
return new DockerComposeFile(Collections.singletonList(file));
136+
}
137+
138+
/**
139+
* Creates a new {@link DockerComposeFile} for the given {@link File files}.
140+
* @param files the source files
141+
* @return the docker compose file
142+
* @since 3.4.0
143+
*/
144+
public static DockerComposeFile of(Collection<? extends File> files) {
145+
Assert.notNull(files, "Files must not be null");
146+
for (File file : files) {
147+
Assert.notNull(file, "File must not be null");
148+
Assert.isTrue(file.exists(), () -> "Docker Compose file '%s' does not exist".formatted(file));
149+
Assert.isTrue(file.isFile(), () -> "Docker compose file '%s' is not a file".formatted(file));
150+
}
151+
return new DockerComposeFile(List.copyOf(files));
115152
}
116153

117154
}

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

Lines changed: 2 additions & 2 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-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.
@@ -31,7 +31,7 @@ public record DockerComposeOrigin(DockerComposeFile composeFile, String serviceN
3131

3232
@Override
3333
public String toString() {
34-
return "Docker compose service '%s' defined in '%s'".formatted(this.serviceName,
34+
return "Docker compose service '%s' defined in %s".formatted(this.serviceName,
3535
(this.composeFile != null) ? this.composeFile : "default compose file");
3636
}
3737

spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/lifecycle/DockerComposeLifecycleManager.java

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
import org.springframework.context.event.SimpleApplicationEventMulticaster;
4040
import org.springframework.core.log.LogMessage;
4141
import org.springframework.util.Assert;
42+
import org.springframework.util.CollectionUtils;
4243

4344
/**
4445
* Manages the lifecycle for docker compose services.
@@ -110,7 +111,7 @@ void start() {
110111
Set<String> activeProfiles = this.properties.getProfiles().getActive();
111112
DockerCompose dockerCompose = getDockerCompose(composeFile, activeProfiles);
112113
if (!dockerCompose.hasDefinedServices()) {
113-
logger.warn(LogMessage.format("No services defined in Docker Compose file '%s' with active profiles %s",
114+
logger.warn(LogMessage.format("No services defined in Docker Compose file %s with active profiles %s",
114115
composeFile, activeProfiles));
115116
return;
116117
}
@@ -145,11 +146,16 @@ void start() {
145146
}
146147

147148
protected DockerComposeFile getComposeFile() {
148-
DockerComposeFile composeFile = (this.properties.getFile() != null)
149-
? DockerComposeFile.of(this.properties.getFile()) : DockerComposeFile.find(this.workingDirectory);
149+
DockerComposeFile composeFile = (CollectionUtils.isEmpty(this.properties.getFile()))
150+
? DockerComposeFile.find(this.workingDirectory) : DockerComposeFile.of(this.properties.getFile());
150151
Assert.state(composeFile != null, () -> "No Docker Compose file found in directory '%s'".formatted(
151152
((this.workingDirectory != null) ? this.workingDirectory : new File(".")).toPath().toAbsolutePath()));
152-
logger.info(LogMessage.format("Using Docker Compose file '%s'", composeFile));
153+
if (composeFile.getFiles().size() == 1) {
154+
logger.info(LogMessage.format("Using Docker Compose file %s", composeFile.getFiles().get(0)));
155+
}
156+
else {
157+
logger.info(LogMessage.format("Using Docker Compose files %s", composeFile.toString()));
158+
}
153159
return composeFile;
154160
}
155161

spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/lifecycle/DockerComposeProperties.java

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ public class DockerComposeProperties {
4949
/**
5050
* Path to a specific docker compose configuration file.
5151
*/
52-
private File file;
52+
private final List<File> file = new ArrayList<>();
5353

5454
/**
5555
* Docker compose lifecycle management.
@@ -88,14 +88,10 @@ public void setEnabled(boolean enabled) {
8888
this.enabled = enabled;
8989
}
9090

91-
public File getFile() {
91+
public List<File> getFile() {
9292
return this.file;
9393
}
9494

95-
public void setFile(File file) {
96-
this.file = file;
97-
}
98-
9995
public LifecycleManagement getLifecycleManagement() {
10096
return this.lifecycleManagement;
10197
}

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

Lines changed: 30 additions & 10 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-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.
@@ -18,6 +18,7 @@
1818

1919
import java.io.File;
2020
import java.io.IOException;
21+
import java.util.List;
2122

2223
import org.junit.jupiter.api.Test;
2324
import org.junit.jupiter.api.io.TempDir;
@@ -59,12 +60,20 @@ void toStringReturnsFileName() throws Exception {
5960
assertThat(composeFile.toString()).endsWith(File.separator + "compose.yml");
6061
}
6162

63+
@Test
64+
void toStringReturnsFileNameList() throws Exception {
65+
File file1 = createTempFile("1.yml");
66+
File file2 = createTempFile("2.yml");
67+
DockerComposeFile composeFile = DockerComposeFile.of(List.of(file1, file2));
68+
assertThat(composeFile).hasToString(file1 + ", " + file2);
69+
}
70+
6271
@Test
6372
void findFindsSingleFile() throws Exception {
6473
File file = new File(this.temp, "docker-compose.yml");
6574
FileCopyUtils.copy(new byte[0], file);
6675
DockerComposeFile composeFile = DockerComposeFile.find(file.getParentFile());
67-
assertThat(composeFile.toString()).endsWith(File.separator + "docker-compose.yml");
76+
assertThat(composeFile.getFiles()).containsExactly(file);
6877
}
6978

7079
@Test
@@ -74,7 +83,7 @@ void findWhenMultipleFilesPicksBest() throws Exception {
7483
File f2 = new File(this.temp, "compose.yml");
7584
FileCopyUtils.copy(new byte[0], f2);
7685
DockerComposeFile composeFile = DockerComposeFile.find(f1.getParentFile());
77-
assertThat(composeFile.toString()).endsWith(File.separator + "compose.yml");
86+
assertThat(composeFile.getFiles()).containsExactly(f2);
7887
}
7988

8089
@Test
@@ -94,24 +103,31 @@ void findWhenWorkingDirectoryDoesNotExistReturnsNull() {
94103

95104
@Test
96105
void findWhenWorkingDirectoryIsNotDirectoryThrowsException() throws Exception {
97-
File file = new File(this.temp, "iamafile");
98-
FileCopyUtils.copy(new byte[0], file);
106+
File file = createTempFile("iamafile");
99107
assertThatIllegalArgumentException().isThrownBy(() -> DockerComposeFile.find(file))
100108
.withMessageEndingWith("is not a directory");
101109
}
102110

103111
@Test
104112
void ofReturnsDockerComposeFile() throws Exception {
105-
File file = new File(this.temp, "anyfile.yml");
106-
FileCopyUtils.copy(new byte[0], file);
113+
File file = createTempFile("compose.yml");
107114
DockerComposeFile composeFile = DockerComposeFile.of(file);
108115
assertThat(composeFile).isNotNull();
109-
assertThat(composeFile).hasToString(file.getCanonicalPath());
116+
assertThat(composeFile.getFiles()).containsExactly(file);
117+
}
118+
119+
@Test
120+
void ofWithMultipleFilesReturnsDockerComposeFile() throws Exception {
121+
File file1 = createTempFile("1.yml");
122+
File file2 = createTempFile("2.yml");
123+
DockerComposeFile composeFile = DockerComposeFile.of(List.of(file1, file2));
124+
assertThat(composeFile).isNotNull();
125+
assertThat(composeFile.getFiles()).containsExactly(file1, file2);
110126
}
111127

112128
@Test
113129
void ofWhenFileIsNullThrowsException() {
114-
assertThatIllegalArgumentException().isThrownBy(() -> DockerComposeFile.of(null))
130+
assertThatIllegalArgumentException().isThrownBy(() -> DockerComposeFile.of((File) null))
115131
.withMessage("File must not be null");
116132
}
117133

@@ -129,9 +145,13 @@ void ofWhenFileIsNotFileThrowsException() {
129145
}
130146

131147
private DockerComposeFile createComposeFile(String name) throws IOException {
148+
return DockerComposeFile.of(createTempFile(name));
149+
}
150+
151+
private File createTempFile(String name) throws IOException {
132152
File file = new File(this.temp, name);
133153
FileCopyUtils.copy(new byte[0], file);
134-
return DockerComposeFile.of(file);
154+
return file.getCanonicalFile();
135155
}
136156

137157
}

0 commit comments

Comments
 (0)