diff --git a/spring-boot-project/spring-boot-docker-compose/src/dockerTest/java/org/springframework/boot/docker/compose/core/DockerCliIntegrationTests.java b/spring-boot-project/spring-boot-docker-compose/src/dockerTest/java/org/springframework/boot/docker/compose/core/DockerCliIntegrationTests.java index 2848836327..0ad06a64c6 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/dockerTest/java/org/springframework/boot/docker/compose/core/DockerCliIntegrationTests.java +++ b/spring-boot-project/spring-boot-docker-compose/src/dockerTest/java/org/springframework/boot/docker/compose/core/DockerCliIntegrationTests.java @@ -24,10 +24,13 @@ import java.nio.file.Path; import java.time.Duration; import java.util.Collections; import java.util.List; +import java.util.Set; +import java.util.UUID; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; +import org.springframework.boot.docker.compose.core.DockerCli.DockerComposeOptions; import org.springframework.boot.docker.compose.core.DockerCliCommand.ComposeConfig; import org.springframework.boot.docker.compose.core.DockerCliCommand.ComposeDown; import org.springframework.boot.docker.compose.core.DockerCliCommand.ComposePs; @@ -60,7 +63,7 @@ class DockerCliIntegrationTests { @Test void runBasicCommand() { - DockerCli cli = new DockerCli(null, null, Collections.emptySet()); + DockerCli cli = new DockerCli(null, null); List context = cli.run(new DockerCliCommand.Context()); assertThat(context).isNotEmpty(); } @@ -68,7 +71,9 @@ class DockerCliIntegrationTests { @Test void runLifecycle() throws IOException { File composeFile = createComposeFile("redis-compose.yaml"); - DockerCli cli = new DockerCli(null, DockerComposeFile.of(composeFile), Collections.emptySet()); + String projectName = UUID.randomUUID().toString(); + DockerCli cli = new DockerCli(null, new DockerComposeOptions(DockerComposeFile.of(composeFile), + Collections.emptySet(), List.of("--project-name=" + projectName))); try { // Verify that no services are running (this is a fresh compose project) List ps = cli.run(new ComposePs()); @@ -76,6 +81,7 @@ class DockerCliIntegrationTests { // List the config and verify that redis is there DockerCliComposeConfigResponse config = cli.run(new ComposeConfig()); assertThat(config.services()).containsOnlyKeys("redis"); + assertThat(config.name()).isEqualTo(projectName); // Run up cli.run(new ComposeUp(LogLevel.INFO, Collections.emptyList())); // Run ps and use id to run inspect on the id @@ -106,7 +112,8 @@ class DockerCliIntegrationTests { @Test void shouldWorkWithMultipleComposeFiles() throws IOException { List composeFiles = createComposeFiles(); - DockerCli cli = new DockerCli(null, DockerComposeFile.of(composeFiles), Collections.emptySet()); + DockerCli cli = new DockerCli(null, + new DockerComposeOptions(DockerComposeFile.of(composeFiles), Set.of("dev"), Collections.emptyList())); try { // List the config and verify that both redis are there DockerCliComposeConfigResponse config = cli.run(new ComposeConfig()); @@ -146,7 +153,8 @@ class DockerCliIntegrationTests { private static List createComposeFiles() throws IOException { File file1 = createComposeFile("1.yaml"); File file2 = createComposeFile("2.yaml"); - return List.of(file1, file2); + File file3 = createComposeFile("3.yaml"); + return List.of(file1, file2, file3); } } diff --git a/spring-boot-project/spring-boot-docker-compose/src/dockerTest/resources/org/springframework/boot/docker/compose/core/1.yaml b/spring-boot-project/spring-boot-docker-compose/src/dockerTest/resources/org/springframework/boot/docker/compose/core/1.yaml index e460afcf93..24ec799a8d 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/dockerTest/resources/org/springframework/boot/docker/compose/core/1.yaml +++ b/spring-boot-project/spring-boot-docker-compose/src/dockerTest/resources/org/springframework/boot/docker/compose/core/1.yaml @@ -1,5 +1,6 @@ services: redis1: + profiles: [ 'dev' ] image: '{imageName}' ports: - '6379' diff --git a/spring-boot-project/spring-boot-docker-compose/src/dockerTest/resources/org/springframework/boot/docker/compose/core/3.yaml b/spring-boot-project/spring-boot-docker-compose/src/dockerTest/resources/org/springframework/boot/docker/compose/core/3.yaml new file mode 100644 index 0000000000..32ef28fa5e --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/dockerTest/resources/org/springframework/boot/docker/compose/core/3.yaml @@ -0,0 +1,6 @@ +services: + redis3: + profiles: [ 'prod' ] + image: '{imageName}' + ports: + - '6379' diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerCli.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerCli.java index e0361bb0c1..3f92915ea5 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerCli.java +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerCli.java @@ -31,6 +31,7 @@ import org.apache.commons.logging.LogFactory; import org.springframework.boot.docker.compose.core.DockerCliCommand.Type; import org.springframework.boot.logging.LogLevel; import org.springframework.core.log.LogMessage; +import org.springframework.util.CollectionUtils; /** * Wrapper around {@code docker} and {@code docker-compose} command line tools. @@ -49,22 +50,18 @@ class DockerCli { private final DockerCommands dockerCommands; - private final DockerComposeFile composeFile; - - private final Set activeProfiles; + private final DockerComposeOptions dockerComposeOptions; /** * Create a new {@link DockerCli} instance. * @param workingDirectory the working directory or {@code null} - * @param composeFile the Docker Compose file to use - * @param activeProfiles the Docker Compose profiles to activate + * @param dockerComposeOptions the Docker Compose options to use or {@code null}. */ - DockerCli(File workingDirectory, DockerComposeFile composeFile, Set activeProfiles) { + DockerCli(File workingDirectory, DockerComposeOptions dockerComposeOptions) { this.processRunner = new ProcessRunner(workingDirectory); this.dockerCommands = dockerCommandsCache.computeIfAbsent(workingDirectory, (key) -> new DockerCommands(this.processRunner)); - this.composeFile = composeFile; - this.activeProfiles = (activeProfiles != null) ? activeProfiles : Collections.emptySet(); + this.dockerComposeOptions = (dockerComposeOptions != null) ? dockerComposeOptions : DockerComposeOptions.none(); } /** @@ -93,17 +90,25 @@ class DockerCli { case DOCKER -> new ArrayList<>(this.dockerCommands.get(type)); case DOCKER_COMPOSE -> { List result = new ArrayList<>(this.dockerCommands.get(type)); - if (this.composeFile != null) { - for (File file : this.composeFile.getFiles()) { + DockerComposeFile composeFile = this.dockerComposeOptions.composeFile(); + if (composeFile != null) { + for (File file : composeFile.getFiles()) { result.add("--file"); result.add(file.getPath()); } } result.add("--ansi"); result.add("never"); - for (String profile : this.activeProfiles) { - result.add("--profile"); - result.add(profile); + Set activeProfiles = this.dockerComposeOptions.activeProfiles(); + if (!CollectionUtils.isEmpty(activeProfiles)) { + for (String profile : activeProfiles) { + result.add("--profile"); + result.add(profile); + } + } + List arguments = this.dockerComposeOptions.arguments(); + if (!CollectionUtils.isEmpty(arguments)) { + result.addAll(arguments); } yield result; } @@ -115,7 +120,7 @@ class DockerCli { * @return the Docker Compose file */ DockerComposeFile getDockerComposeFile() { - return this.composeFile; + return this.dockerComposeOptions.composeFile(); } /** @@ -185,4 +190,22 @@ class DockerCli { } + /** + * Options for Docker Compose. + * + * @param composeFile the Docker Compose file to use + * @param activeProfiles the profiles to activate + * @param arguments the arguments to pass to Docker Compose + */ + record DockerComposeOptions(DockerComposeFile composeFile, Set activeProfiles, List arguments) { + DockerComposeOptions { + activeProfiles = (activeProfiles != null) ? activeProfiles : Collections.emptySet(); + arguments = (arguments != null) ? arguments : Collections.emptyList(); + } + + static DockerComposeOptions none() { + return new DockerComposeOptions(null, null, null); + } + } + } diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerCompose.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerCompose.java index fb45b8970d..129be08a07 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerCompose.java +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerCompose.java @@ -17,9 +17,11 @@ package org.springframework.boot.docker.compose.core; import java.time.Duration; +import java.util.Collections; import java.util.List; import java.util.Set; +import org.springframework.boot.docker.compose.core.DockerCli.DockerComposeOptions; import org.springframework.boot.logging.LogLevel; /** @@ -125,7 +127,22 @@ public interface DockerCompose { * @return a {@link DockerCompose} instance */ static DockerCompose get(DockerComposeFile file, String hostname, Set activeProfiles) { - DockerCli cli = new DockerCli(null, file, activeProfiles); + return get(file, hostname, activeProfiles, Collections.emptyList()); + } + + /** + * Factory method used to create a {@link DockerCompose} instance. + * @param file the Docker Compose file + * @param hostname the hostname used for services or {@code null} if the hostname + * should be deduced + * @param activeProfiles a set of the profiles that should be activated + * @param arguments the arguments to pass to Docker Compose + * @return a {@link DockerCompose} instance + * @since 3.4.0 + */ + static DockerCompose get(DockerComposeFile file, String hostname, Set activeProfiles, + List arguments) { + DockerCli cli = new DockerCli(null, new DockerComposeOptions(file, activeProfiles, arguments)); return new DefaultDockerCompose(cli, hostname); } diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/lifecycle/DockerComposeLifecycleManager.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/lifecycle/DockerComposeLifecycleManager.java index 6e9074216f..92259ad60d 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/lifecycle/DockerComposeLifecycleManager.java +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/lifecycle/DockerComposeLifecycleManager.java @@ -109,7 +109,8 @@ class DockerComposeLifecycleManager { } DockerComposeFile composeFile = getComposeFile(); Set activeProfiles = this.properties.getProfiles().getActive(); - DockerCompose dockerCompose = getDockerCompose(composeFile, activeProfiles); + List arguments = this.properties.getArguments(); + DockerCompose dockerCompose = getDockerCompose(composeFile, activeProfiles, arguments); if (!dockerCompose.hasDefinedServices()) { logger.warn(LogMessage.format("No services defined in Docker Compose file %s with active profiles %s", composeFile, activeProfiles)); @@ -159,8 +160,9 @@ class DockerComposeLifecycleManager { return composeFile; } - protected DockerCompose getDockerCompose(DockerComposeFile composeFile, Set activeProfiles) { - return DockerCompose.get(composeFile, this.properties.getHost(), activeProfiles); + protected DockerCompose getDockerCompose(DockerComposeFile composeFile, Set activeProfiles, + List arguments) { + return DockerCompose.get(composeFile, this.properties.getHost(), activeProfiles, arguments); } private boolean isIgnored(RunningService service) { diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/lifecycle/DockerComposeProperties.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/lifecycle/DockerComposeProperties.java index 25970e94f2..d6e98ec0f0 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/lifecycle/DockerComposeProperties.java +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/lifecycle/DockerComposeProperties.java @@ -46,6 +46,11 @@ public class DockerComposeProperties { */ private boolean enabled = true; + /** + * Arguments to pass to the Docker Compose command. + */ + private final List arguments = new ArrayList<>(); + /** * Paths to the Docker Compose configuration files. */ @@ -88,6 +93,10 @@ public class DockerComposeProperties { this.enabled = enabled; } + public List getArguments() { + return this.arguments; + } + public List getFile() { return this.file; } diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/lifecycle/DockerComposeLifecycleManagerTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/lifecycle/DockerComposeLifecycleManagerTests.java index 7e7a933e84..331b9dee36 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/lifecycle/DockerComposeLifecycleManagerTests.java +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/lifecycle/DockerComposeLifecycleManagerTests.java @@ -75,6 +75,8 @@ class DockerComposeLifecycleManagerTests { private Set activeProfiles; + private List arguments; + private GenericApplicationContext applicationContext; private TestSpringApplicationShutdownHandlers shutdownHandlers; @@ -358,6 +360,14 @@ class DockerComposeLifecycleManagerTests { assertThat(this.activeProfiles).containsExactly("my-profile"); } + @Test + void startGetsDockerComposeWithArguments() { + this.properties.getArguments().add("--project-name=test"); + setUpRunningServices(); + this.lifecycleManager.start(); + assertThat(this.arguments).containsExactly("--project-name=test"); + } + @Test void startPublishesEvent() { EventCapturingListener listener = new EventCapturingListener(); @@ -519,8 +529,10 @@ class DockerComposeLifecycleManagerTests { } @Override - protected DockerCompose getDockerCompose(DockerComposeFile composeFile, Set activeProfiles) { + protected DockerCompose getDockerCompose(DockerComposeFile composeFile, Set activeProfiles, + List arguments) { DockerComposeLifecycleManagerTests.this.activeProfiles = activeProfiles; + DockerComposeLifecycleManagerTests.this.arguments = arguments; return DockerComposeLifecycleManagerTests.this.dockerCompose; } diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/lifecycle/DockerComposePropertiesTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/lifecycle/DockerComposePropertiesTests.java index 5694de1937..777c08a103 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/lifecycle/DockerComposePropertiesTests.java +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/lifecycle/DockerComposePropertiesTests.java @@ -63,6 +63,7 @@ class DockerComposePropertiesTests { @Test void getWhenPropertiesReturnsBound() { Map source = new LinkedHashMap<>(); + source.put("spring.docker.compose.arguments", "--project-name=test,--progress=auto"); source.put("spring.docker.compose.file", "my-compose.yml"); source.put("spring.docker.compose.lifecycle-management", "start-only"); source.put("spring.docker.compose.host", "myhost"); @@ -76,6 +77,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.getArguments()).containsExactly("--project-name=test", "--progress=auto"); assertThat(properties.getFile()).containsExactly(new File("my-compose.yml")); assertThat(properties.getLifecycleManagement()).isEqualTo(LifecycleManagement.START_ONLY); assertThat(properties.getHost()).isEqualTo("myhost");