Browse Source

Support multiple Docker Compose files

Closes gh-41691
pull/41699/head
Moritz Halbritter 1 year ago
parent
commit
4eebb8e629
  1. 44
      spring-boot-project/spring-boot-docker-compose/src/dockerTest/java/org/springframework/boot/docker/compose/core/DockerCliIntegrationTests.java
  2. 5
      spring-boot-project/spring-boot-docker-compose/src/dockerTest/resources/org/springframework/boot/docker/compose/core/1.yaml
  3. 5
      spring-boot-project/spring-boot-docker-compose/src/dockerTest/resources/org/springframework/boot/docker/compose/core/2.yaml
  4. 8
      spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerCli.java
  5. 53
      spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerComposeFile.java
  6. 4
      spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerComposeOrigin.java
  7. 14
      spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/lifecycle/DockerComposeLifecycleManager.java
  8. 8
      spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/lifecycle/DockerComposeProperties.java
  9. 40
      spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/core/DockerComposeFileTests.java
  10. 24
      spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/core/DockerComposeOriginTests.java
  11. 4
      spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/lifecycle/DockerComposePropertiesTests.java

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

@ -67,7 +67,7 @@ class DockerCliIntegrationTests { @@ -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 { @@ -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 { @@ -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);
}
}

5
spring-boot-project/spring-boot-docker-compose/src/dockerTest/resources/org/springframework/boot/docker/compose/core/1.yaml

@ -0,0 +1,5 @@ @@ -0,0 +1,5 @@
services:
redis1:
image: '{imageName}'
ports:
- '6379'

5
spring-boot-project/spring-boot-docker-compose/src/dockerTest/resources/org/springframework/boot/docker/compose/core/2.yaml

@ -0,0 +1,5 @@ @@ -0,0 +1,5 @@
services:
redis2:
image: '{imageName}'
ports:
- '6379'

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

@ -1,5 +1,5 @@ @@ -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 { @@ -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");

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

@ -1,5 +1,5 @@ @@ -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; @@ -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; @@ -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 { @@ -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 { @@ -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 { @@ -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));
}
}

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

@ -1,5 +1,5 @@ @@ -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 @@ -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");
}

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

@ -39,6 +39,7 @@ import org.springframework.context.ApplicationListener; @@ -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 { @@ -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 { @@ -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;
}

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

@ -49,7 +49,7 @@ public class DockerComposeProperties { @@ -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 { @@ -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;
}

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

@ -1,5 +1,5 @@ @@ -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; @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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();
}
}

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

@ -1,5 +1,5 @@ @@ -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; @@ -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 { @@ -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 { @@ -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();
}
}

4
spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/lifecycle/DockerComposePropertiesTests.java

@ -47,7 +47,7 @@ class DockerComposePropertiesTests { @@ -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 { @@ -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);

Loading…
Cancel
Save