Support multiple Docker Compose files
Closes gh-41691
This commit is contained in:
@@ -67,7 +67,7 @@ class DockerCliIntegrationTests {
|
||||
|
||||
@Test
|
||||
void runLifecycle() throws IOException {
|
||||
File composeFile = createComposeFile();
|
||||
File composeFile = createComposeFile("redis-compose.yaml");
|
||||
DockerCli cli = new DockerCli(null, DockerComposeFile.of(composeFile), Collections.emptySet());
|
||||
try {
|
||||
// Verify that no services are running (this is a fresh compose project)
|
||||
@@ -103,6 +103,26 @@ class DockerCliIntegrationTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldWorkWithMultipleComposeFiles() throws IOException {
|
||||
List<File> composeFiles = createComposeFiles();
|
||||
DockerCli cli = new DockerCli(null, DockerComposeFile.of(composeFiles), Collections.emptySet());
|
||||
try {
|
||||
// List the config and verify that both redis are there
|
||||
DockerCliComposeConfigResponse config = cli.run(new ComposeConfig());
|
||||
assertThat(config.services()).containsOnlyKeys("redis1", "redis2");
|
||||
// Run up
|
||||
cli.run(new ComposeUp(LogLevel.INFO, Collections.emptyList()));
|
||||
// Run ps and use id to run inspect on the id
|
||||
List<DockerCliComposePsResponse> ps = cli.run(new ComposePs());
|
||||
assertThat(ps).hasSize(2);
|
||||
}
|
||||
finally {
|
||||
// Clean up in any case
|
||||
quietComposeDown(cli);
|
||||
}
|
||||
}
|
||||
|
||||
private static void quietComposeDown(DockerCli cli) {
|
||||
try {
|
||||
cli.run(new ComposeDown(Duration.ZERO, Collections.emptyList()));
|
||||
@@ -112,13 +132,21 @@ class DockerCliIntegrationTests {
|
||||
}
|
||||
}
|
||||
|
||||
private static File createComposeFile() throws IOException {
|
||||
File composeFile = new ClassPathResource("redis-compose.yaml", DockerCliIntegrationTests.class).getFile();
|
||||
File tempComposeFile = Path.of(tempDir.toString(), composeFile.getName()).toFile();
|
||||
String composeFileContent = FileCopyUtils.copyToString(new FileReader(composeFile));
|
||||
composeFileContent = composeFileContent.replace("{imageName}", TestImage.REDIS.toString());
|
||||
FileCopyUtils.copy(composeFileContent, new FileWriter(tempComposeFile));
|
||||
return tempComposeFile;
|
||||
private static File createComposeFile(String resource) throws IOException {
|
||||
File source = new ClassPathResource(resource, DockerCliIntegrationTests.class).getFile();
|
||||
File target = Path.of(tempDir.toString(), source.getName()).toFile();
|
||||
String content = FileCopyUtils.copyToString(new FileReader(source));
|
||||
content = content.replace("{imageName}", TestImage.REDIS.toString());
|
||||
try (FileWriter writer = new FileWriter(target)) {
|
||||
FileCopyUtils.copy(content, writer);
|
||||
}
|
||||
return target;
|
||||
}
|
||||
|
||||
private static List<File> createComposeFiles() throws IOException {
|
||||
File file1 = createComposeFile("1.yaml");
|
||||
File file2 = createComposeFile("2.yaml");
|
||||
return List.of(file1, file2);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
services:
|
||||
redis1:
|
||||
image: '{imageName}'
|
||||
ports:
|
||||
- '6379'
|
||||
@@ -0,0 +1,5 @@
|
||||
services:
|
||||
redis2:
|
||||
image: '{imageName}'
|
||||
ports:
|
||||
- '6379'
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2023 the original author or authors.
|
||||
* Copyright 2012-2024 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.
|
||||
@@ -94,8 +94,10 @@ class DockerCli {
|
||||
case DOCKER_COMPOSE -> {
|
||||
List<String> result = new ArrayList<>(this.dockerCommands.get(type));
|
||||
if (this.composeFile != null) {
|
||||
result.add("--file");
|
||||
result.add(this.composeFile.toString());
|
||||
for (File file : this.composeFile.getFiles()) {
|
||||
result.add("--file");
|
||||
result.add(file.getPath());
|
||||
}
|
||||
}
|
||||
result.add("--ansi");
|
||||
result.add("never");
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2023 the original author or authors.
|
||||
* Copyright 2012-2024 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.
|
||||
@@ -21,7 +21,10 @@ import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
@@ -33,6 +36,7 @@ import org.springframework.util.Assert;
|
||||
* @author Phillip Webb
|
||||
* @since 3.1.0
|
||||
* @see #of(File)
|
||||
* @see #of(Collection)
|
||||
* @see #find(File)
|
||||
*/
|
||||
public final class DockerComposeFile {
|
||||
@@ -40,17 +44,31 @@ public final class DockerComposeFile {
|
||||
private static final List<String> SEARCH_ORDER = List.of("compose.yaml", "compose.yml", "docker-compose.yaml",
|
||||
"docker-compose.yml");
|
||||
|
||||
private final File file;
|
||||
private final List<File> files;
|
||||
|
||||
private DockerComposeFile(File file) {
|
||||
private DockerComposeFile(List<File> files) {
|
||||
Assert.state(!files.isEmpty(), "Files must not be empty");
|
||||
this.files = files.stream().map(DockerComposeFile::toCanonicalFile).toList();
|
||||
}
|
||||
|
||||
private static File toCanonicalFile(File file) {
|
||||
try {
|
||||
this.file = file.getCanonicalFile();
|
||||
return file.getCanonicalFile();
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new UncheckedIOException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the source docker compose files.
|
||||
* @return the source docker compose files
|
||||
* @since 3.4.0
|
||||
*/
|
||||
public List<File> getFiles() {
|
||||
return this.files;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
@@ -60,17 +78,20 @@ public final class DockerComposeFile {
|
||||
return false;
|
||||
}
|
||||
DockerComposeFile other = (DockerComposeFile) obj;
|
||||
return this.file.equals(other.file);
|
||||
return this.files.equals(other.files);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return this.file.hashCode();
|
||||
return this.files.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.file.toString();
|
||||
if (this.files.size() == 1) {
|
||||
return this.files.get(0).getPath();
|
||||
}
|
||||
return this.files.stream().map(File::toString).collect(Collectors.joining(", "));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -111,7 +132,23 @@ public final class DockerComposeFile {
|
||||
Assert.notNull(file, "File must not be null");
|
||||
Assert.isTrue(file.exists(), () -> "Docker Compose file '%s' does not exist".formatted(file));
|
||||
Assert.isTrue(file.isFile(), () -> "Docker compose file '%s' is not a file".formatted(file));
|
||||
return new DockerComposeFile(file);
|
||||
return new DockerComposeFile(Collections.singletonList(file));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link DockerComposeFile} for the given {@link File files}.
|
||||
* @param files the source files
|
||||
* @return the docker compose file
|
||||
* @since 3.4.0
|
||||
*/
|
||||
public static DockerComposeFile of(Collection<? extends File> files) {
|
||||
Assert.notNull(files, "Files must not be null");
|
||||
for (File file : files) {
|
||||
Assert.notNull(file, "File must not be null");
|
||||
Assert.isTrue(file.exists(), () -> "Docker Compose file '%s' does not exist".formatted(file));
|
||||
Assert.isTrue(file.isFile(), () -> "Docker compose file '%s' is not a file".formatted(file));
|
||||
}
|
||||
return new DockerComposeFile(List.copyOf(files));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2023 the original author or authors.
|
||||
* Copyright 2012-2024 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.
|
||||
@@ -31,7 +31,7 @@ public record DockerComposeOrigin(DockerComposeFile composeFile, String serviceN
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Docker compose service '%s' defined in '%s'".formatted(this.serviceName,
|
||||
return "Docker compose service '%s' defined in %s".formatted(this.serviceName,
|
||||
(this.composeFile != null) ? this.composeFile : "default compose file");
|
||||
}
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ import org.springframework.context.ApplicationListener;
|
||||
import org.springframework.context.event.SimpleApplicationEventMulticaster;
|
||||
import org.springframework.core.log.LogMessage;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
/**
|
||||
* Manages the lifecycle for docker compose services.
|
||||
@@ -110,7 +111,7 @@ class DockerComposeLifecycleManager {
|
||||
Set<String> activeProfiles = this.properties.getProfiles().getActive();
|
||||
DockerCompose dockerCompose = getDockerCompose(composeFile, activeProfiles);
|
||||
if (!dockerCompose.hasDefinedServices()) {
|
||||
logger.warn(LogMessage.format("No services defined in Docker Compose file '%s' with active profiles %s",
|
||||
logger.warn(LogMessage.format("No services defined in Docker Compose file %s with active profiles %s",
|
||||
composeFile, activeProfiles));
|
||||
return;
|
||||
}
|
||||
@@ -145,11 +146,16 @@ class DockerComposeLifecycleManager {
|
||||
}
|
||||
|
||||
protected DockerComposeFile getComposeFile() {
|
||||
DockerComposeFile composeFile = (this.properties.getFile() != null)
|
||||
? DockerComposeFile.of(this.properties.getFile()) : DockerComposeFile.find(this.workingDirectory);
|
||||
DockerComposeFile composeFile = (CollectionUtils.isEmpty(this.properties.getFile()))
|
||||
? DockerComposeFile.find(this.workingDirectory) : DockerComposeFile.of(this.properties.getFile());
|
||||
Assert.state(composeFile != null, () -> "No Docker Compose file found in directory '%s'".formatted(
|
||||
((this.workingDirectory != null) ? this.workingDirectory : new File(".")).toPath().toAbsolutePath()));
|
||||
logger.info(LogMessage.format("Using Docker Compose file '%s'", composeFile));
|
||||
if (composeFile.getFiles().size() == 1) {
|
||||
logger.info(LogMessage.format("Using Docker Compose file %s", composeFile.getFiles().get(0)));
|
||||
}
|
||||
else {
|
||||
logger.info(LogMessage.format("Using Docker Compose files %s", composeFile.toString()));
|
||||
}
|
||||
return composeFile;
|
||||
}
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ public class DockerComposeProperties {
|
||||
/**
|
||||
* Path to a specific docker compose configuration file.
|
||||
*/
|
||||
private File file;
|
||||
private final List<File> file = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* Docker compose lifecycle management.
|
||||
@@ -88,14 +88,10 @@ public class DockerComposeProperties {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
public File getFile() {
|
||||
public List<File> getFile() {
|
||||
return this.file;
|
||||
}
|
||||
|
||||
public void setFile(File file) {
|
||||
this.file = file;
|
||||
}
|
||||
|
||||
public LifecycleManagement getLifecycleManagement() {
|
||||
return this.lifecycleManagement;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2023 the original author or authors.
|
||||
* Copyright 2012-2024 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.
|
||||
@@ -18,6 +18,7 @@ package org.springframework.boot.docker.compose.core;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
@@ -59,12 +60,20 @@ class DockerComposeFileTests {
|
||||
assertThat(composeFile.toString()).endsWith(File.separator + "compose.yml");
|
||||
}
|
||||
|
||||
@Test
|
||||
void toStringReturnsFileNameList() throws Exception {
|
||||
File file1 = createTempFile("1.yml");
|
||||
File file2 = createTempFile("2.yml");
|
||||
DockerComposeFile composeFile = DockerComposeFile.of(List.of(file1, file2));
|
||||
assertThat(composeFile).hasToString(file1 + ", " + file2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void findFindsSingleFile() throws Exception {
|
||||
File file = new File(this.temp, "docker-compose.yml");
|
||||
FileCopyUtils.copy(new byte[0], file);
|
||||
DockerComposeFile composeFile = DockerComposeFile.find(file.getParentFile());
|
||||
assertThat(composeFile.toString()).endsWith(File.separator + "docker-compose.yml");
|
||||
assertThat(composeFile.getFiles()).containsExactly(file);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -74,7 +83,7 @@ class DockerComposeFileTests {
|
||||
File f2 = new File(this.temp, "compose.yml");
|
||||
FileCopyUtils.copy(new byte[0], f2);
|
||||
DockerComposeFile composeFile = DockerComposeFile.find(f1.getParentFile());
|
||||
assertThat(composeFile.toString()).endsWith(File.separator + "compose.yml");
|
||||
assertThat(composeFile.getFiles()).containsExactly(f2);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -94,24 +103,31 @@ class DockerComposeFileTests {
|
||||
|
||||
@Test
|
||||
void findWhenWorkingDirectoryIsNotDirectoryThrowsException() throws Exception {
|
||||
File file = new File(this.temp, "iamafile");
|
||||
FileCopyUtils.copy(new byte[0], file);
|
||||
File file = createTempFile("iamafile");
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> DockerComposeFile.find(file))
|
||||
.withMessageEndingWith("is not a directory");
|
||||
}
|
||||
|
||||
@Test
|
||||
void ofReturnsDockerComposeFile() throws Exception {
|
||||
File file = new File(this.temp, "anyfile.yml");
|
||||
FileCopyUtils.copy(new byte[0], file);
|
||||
File file = createTempFile("compose.yml");
|
||||
DockerComposeFile composeFile = DockerComposeFile.of(file);
|
||||
assertThat(composeFile).isNotNull();
|
||||
assertThat(composeFile).hasToString(file.getCanonicalPath());
|
||||
assertThat(composeFile.getFiles()).containsExactly(file);
|
||||
}
|
||||
|
||||
@Test
|
||||
void ofWithMultipleFilesReturnsDockerComposeFile() throws Exception {
|
||||
File file1 = createTempFile("1.yml");
|
||||
File file2 = createTempFile("2.yml");
|
||||
DockerComposeFile composeFile = DockerComposeFile.of(List.of(file1, file2));
|
||||
assertThat(composeFile).isNotNull();
|
||||
assertThat(composeFile.getFiles()).containsExactly(file1, file2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void ofWhenFileIsNullThrowsException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> DockerComposeFile.of(null))
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> DockerComposeFile.of((File) null))
|
||||
.withMessage("File must not be null");
|
||||
}
|
||||
|
||||
@@ -129,9 +145,13 @@ class DockerComposeFileTests {
|
||||
}
|
||||
|
||||
private DockerComposeFile createComposeFile(String name) throws IOException {
|
||||
return DockerComposeFile.of(createTempFile(name));
|
||||
}
|
||||
|
||||
private File createTempFile(String name) throws IOException {
|
||||
File file = new File(this.temp, name);
|
||||
FileCopyUtils.copy(new byte[0], file);
|
||||
return DockerComposeFile.of(file);
|
||||
return file.getCanonicalFile();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2023 the original author or authors.
|
||||
* Copyright 2012-2024 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.
|
||||
@@ -18,6 +18,7 @@ package org.springframework.boot.docker.compose.core;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
@@ -42,8 +43,17 @@ class DockerComposeOriginTests {
|
||||
void hasToString() throws Exception {
|
||||
DockerComposeFile composeFile = createTempComposeFile();
|
||||
DockerComposeOrigin origin = new DockerComposeOrigin(composeFile, "service-1");
|
||||
assertThat(origin.toString()).startsWith("Docker compose service 'service-1' defined in '")
|
||||
.endsWith("compose.yaml'");
|
||||
assertThat(origin.toString()).startsWith("Docker compose service 'service-1' defined in ")
|
||||
.endsWith("compose.yaml");
|
||||
}
|
||||
|
||||
@Test
|
||||
void hasToStringWithMultipleFiles() throws IOException {
|
||||
File file1 = createTempFile("1.yaml");
|
||||
File file2 = createTempFile("2.yaml");
|
||||
DockerComposeOrigin origin = new DockerComposeOrigin(DockerComposeFile.of(List.of(file1, file2)), "service-1");
|
||||
assertThat(origin.toString())
|
||||
.startsWith("Docker compose service 'service-1' defined in %s, %s".formatted(file1, file2));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -63,9 +73,13 @@ class DockerComposeOriginTests {
|
||||
}
|
||||
|
||||
private DockerComposeFile createTempComposeFile() throws IOException {
|
||||
File file = new File(this.temp, "compose.yaml");
|
||||
return DockerComposeFile.of(createTempFile("compose.yaml"));
|
||||
}
|
||||
|
||||
private File createTempFile(String filename) throws IOException {
|
||||
File file = new File(this.temp, filename);
|
||||
FileCopyUtils.copy(new byte[0], file);
|
||||
return DockerComposeFile.of(file);
|
||||
return file.getCanonicalFile();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ class DockerComposePropertiesTests {
|
||||
void getWhenNoPropertiesReturnsNew() {
|
||||
Binder binder = new Binder(new MapConfigurationPropertySource());
|
||||
DockerComposeProperties properties = DockerComposeProperties.get(binder);
|
||||
assertThat(properties.getFile()).isNull();
|
||||
assertThat(properties.getFile()).isEmpty();
|
||||
assertThat(properties.getLifecycleManagement()).isEqualTo(LifecycleManagement.START_AND_STOP);
|
||||
assertThat(properties.getHost()).isNull();
|
||||
assertThat(properties.getStart().getCommand()).isEqualTo(StartCommand.UP);
|
||||
@@ -76,7 +76,7 @@ class DockerComposePropertiesTests {
|
||||
source.put("spring.docker.compose.readiness.tcp.read-timeout", "500ms");
|
||||
Binder binder = new Binder(new MapConfigurationPropertySource(source));
|
||||
DockerComposeProperties properties = DockerComposeProperties.get(binder);
|
||||
assertThat(properties.getFile()).isEqualTo(new File("my-compose.yml"));
|
||||
assertThat(properties.getFile()).containsExactly(new File("my-compose.yml"));
|
||||
assertThat(properties.getLifecycleManagement()).isEqualTo(LifecycleManagement.START_ONLY);
|
||||
assertThat(properties.getHost()).isEqualTo("myhost");
|
||||
assertThat(properties.getStart().getCommand()).isEqualTo(StartCommand.START);
|
||||
|
||||
Reference in New Issue
Block a user