Skip to content

Commit

Permalink
Support multiple Docker Compose files
Browse files Browse the repository at this point in the history
Closes gh-41691
  • Loading branch information
mhalbritter committed Aug 5, 2024
1 parent c7e29b7 commit 4eebb8e
Show file tree
Hide file tree
Showing 11 changed files with 161 additions and 48 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ void runBasicCommand() {

@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)
Expand Down Expand Up @@ -103,6 +103,26 @@ void runLifecycle() throws IOException {
}
}

@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()));
Expand All @@ -112,13 +132,21 @@ private static void quietComposeDown(DockerCli cli) {
}
}

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);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
services:
redis1:
image: '{imageName}'
ports:
- '6379'
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
services:
redis2:
image: '{imageName}'
ports:
- '6379'
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -94,8 +94,10 @@ private List<String> createCommand(Type type) {
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");
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -21,7 +21,10 @@
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;

Expand All @@ -33,24 +36,39 @@
* @author Phillip Webb
* @since 3.1.0
* @see #of(File)
* @see #of(Collection)
* @see #find(File)
*/
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) {
Expand All @@ -60,17 +78,20 @@ public boolean equals(Object obj) {
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(", "));
}

/**
Expand Down Expand Up @@ -111,7 +132,23 @@ public static DockerComposeFile of(File file) {
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));
}

}
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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");
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
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.
Expand Down Expand Up @@ -110,7 +111,7 @@ void start() {
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;
}
Expand Down Expand Up @@ -145,11 +146,16 @@ void start() {
}

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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -88,14 +88,10 @@ public void setEnabled(boolean enabled) {
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;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -18,6 +18,7 @@

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;
Expand Down Expand Up @@ -59,12 +60,20 @@ void toStringReturnsFileName() throws Exception {
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
Expand All @@ -74,7 +83,7 @@ void findWhenMultipleFilesPicksBest() throws Exception {
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
Expand All @@ -94,24 +103,31 @@ void findWhenWorkingDirectoryDoesNotExistReturnsNull() {

@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");
}

Expand All @@ -129,9 +145,13 @@ void ofWhenFileIsNotFileThrowsException() {
}

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();
}

}
Loading

0 comments on commit 4eebb8e

Please sign in to comment.