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 2848836327e7..cbe2f8b42359 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,6 +24,8 @@ 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; @@ -35,6 +37,7 @@ import org.springframework.boot.docker.compose.core.DockerCliCommand.ComposeStop; import org.springframework.boot.docker.compose.core.DockerCliCommand.ComposeUp; import org.springframework.boot.docker.compose.core.DockerCliCommand.Inspect; +import org.springframework.boot.docker.compose.core.DockerCompose.Options; import org.springframework.boot.logging.LogLevel; import org.springframework.boot.testsupport.container.DisabledIfDockerUnavailable; import org.springframework.boot.testsupport.container.TestImage; @@ -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,10 @@ void runBasicCommand() { @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(); + Options options = Options.get(DockerComposeFile.of(composeFile), Collections.emptySet(), + List.of("--project-name=" + projectName)); + DockerCli cli = new DockerCli(null, options); try { // Verify that no services are running (this is a fresh compose project) List ps = cli.run(new ComposePs()); @@ -76,6 +82,7 @@ void runLifecycle() throws IOException { // 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 +113,8 @@ void runLifecycle() throws IOException { @Test void shouldWorkWithMultipleComposeFiles() throws IOException { List composeFiles = createComposeFiles(); - DockerCli cli = new DockerCli(null, DockerComposeFile.of(composeFiles), Collections.emptySet()); + Options options = Options.get(DockerComposeFile.of(composeFiles), Set.of("dev"), Collections.emptyList()); + DockerCli cli = new DockerCli(null, options); try { // List the config and verify that both redis are there DockerCliComposeConfigResponse config = cli.run(new ComposeConfig()); @@ -146,7 +154,8 @@ private static File createComposeFile(String resource) throws IOException { 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 e460afcf939d..d03a48e4aafc 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 000000000000..d9ba4a51e55c --- /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/DefaultDockerCompose.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DefaultDockerCompose.java index e50340605b14..e457db2bd534 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DefaultDockerCompose.java +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DefaultDockerCompose.java @@ -97,7 +97,8 @@ public List getRunningServices() { if (runningPsResponses.isEmpty()) { return Collections.emptyList(); } - DockerComposeFile dockerComposeFile = this.cli.getDockerComposeFile(); + DockerCompose.Options options = this.cli.getDockerComposeOptions(); + DockerComposeFile dockerComposeFile = options.getComposeFile(); List result = new ArrayList<>(); Map inspected = inspect(runningPsResponses); for (DockerCliComposePsResponse psResponse : runningPsResponses) { 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 e0361bb0c15b..12c30145c697 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 @@ -18,7 +18,6 @@ import java.io.File; import java.util.ArrayList; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -31,6 +30,7 @@ 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 +49,18 @@ class DockerCli { private final DockerCommands dockerCommands; - private final DockerComposeFile composeFile; - - private final Set activeProfiles; + private final DockerCompose.Options 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, DockerCompose.Options 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 : DockerCompose.Options.NONE; } /** @@ -93,17 +89,26 @@ private List createCommand(Type type) { 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()) { + DockerCompose.Options options = this.dockerComposeOptions; + DockerComposeFile composeFile = options.getComposeFile(); + 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 = options.getActiveProfiles(); + if (!CollectionUtils.isEmpty(activeProfiles)) { + for (String profile : activeProfiles) { + result.add("--profile"); + result.add(profile); + } + } + List arguments = options.getArguments(); + if (!CollectionUtils.isEmpty(arguments)) { + result.addAll(arguments); } yield result; } @@ -111,11 +116,11 @@ private List createCommand(Type type) { } /** - * Return the {@link DockerComposeFile} being used by this CLI instance. - * @return the Docker Compose file + * Return the {@link DockerCompose.Options} being used by this CLI instance. + * @return the Docker Compose options */ - DockerComposeFile getDockerComposeFile() { - return this.composeFile; + DockerCompose.Options getDockerComposeOptions() { + return this.dockerComposeOptions; } /** 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 fb45b8970d07..af1256baddbd 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,6 +17,7 @@ package org.springframework.boot.docker.compose.core; import java.time.Duration; +import java.util.Collections; import java.util.List; import java.util.Set; @@ -125,8 +126,61 @@ public interface DockerCompose { * @return a {@link DockerCompose} instance */ static DockerCompose get(DockerComposeFile file, String hostname, Set activeProfiles) { - DockerCli cli = new DockerCli(null, file, activeProfiles); + DockerCli cli = new DockerCli(null, Options.get(file, activeProfiles, Collections.emptyList())); return new DefaultDockerCompose(cli, hostname); } + /** + * Factory method used to create a {@link DockerCompose} instance. + * @param hostname the hostname used for services or {@code null} if the hostname + * @param options the Docker Compose options or {@code null} + * @return a {@link DockerCompose} instance + * @since 3.4.0 + */ + static DockerCompose get(String hostname, Options options) { + DockerCli cli = new DockerCli(null, options); + return new DefaultDockerCompose(cli, hostname); + } + + /** + * Docker Compose options that should be applied before any subcommand. + */ + interface Options { + + /** + * No options. + */ + Options NONE = get(null, Collections.emptySet(), Collections.emptyList()); + + /** + * Factory method used to create a {@link DockerCompose.Options} instance. + * @param file the Docker Compose file to use + * @param activeProfiles the Docker Compose profiles to activate + * @param arguments the additional Docker Compose arguments + * @return the Docker Compose options + */ + static Options get(DockerComposeFile file, Set activeProfiles, List arguments) { + return new DockerComposeOptions(file, activeProfiles, arguments); + } + + /** + * the Docker Compose a file to use. + * @return compose a file to use + */ + DockerComposeFile getComposeFile(); + + /** + * the Docker Compose profiles to activate. + * @return profiles to activate + */ + Set getActiveProfiles(); + + /** + * the additional Docker Compose arguments. + * @return additional arguments + */ + List getArguments(); + + } + } diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerComposeOptions.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerComposeOptions.java new file mode 100644 index 000000000000..fc3dd3a749c3 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerComposeOptions.java @@ -0,0 +1,94 @@ +/* + * 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.core; + +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +import org.springframework.core.style.ToStringCreator; + +/** + * Default {@link DockerCompose.Options} implementation. + * + * @author Dmytro Nosan + */ +final class DockerComposeOptions implements DockerCompose.Options { + + private final DockerComposeFile composeFile; + + private final Set activeProfiles; + + private final List arguments; + + /** + * Create a new {@link DockerComposeOptions} instance. + * @param composeFile the Docker Compose file to use + * @param activeProfiles the Docker Compose profiles to activate + * @param arguments the additional Docker Compose arguments (e.g. --project-name=...) + */ + DockerComposeOptions(DockerComposeFile composeFile, Set activeProfiles, List arguments) { + this.composeFile = composeFile; + this.activeProfiles = (activeProfiles != null) ? activeProfiles : Collections.emptySet(); + this.arguments = (arguments != null) ? arguments : Collections.emptyList(); + } + + @Override + public DockerComposeFile getComposeFile() { + return this.composeFile; + } + + @Override + public Set getActiveProfiles() { + return this.activeProfiles; + } + + @Override + public List getArguments() { + return this.arguments; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + DockerComposeOptions that = (DockerComposeOptions) obj; + return Objects.equals(this.composeFile, that.composeFile) + && Objects.equals(this.activeProfiles, that.activeProfiles) + && Objects.equals(this.arguments, that.arguments); + } + + @Override + public int hashCode() { + return Objects.hash(this.composeFile, this.activeProfiles, this.arguments); + } + + @Override + public String toString() { + ToStringCreator creator = new ToStringCreator(this); + creator.append("composeFile", this.composeFile); + creator.append("activeProfiles", this.activeProfiles); + creator.append("arguments", this.arguments); + return creator.toString(); + } + +} 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 6e9074216ffb..2bc2cd49bca6 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 @@ void start() { } 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,10 @@ protected DockerComposeFile getComposeFile() { 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) { + DockerCompose.Options options = DockerCompose.Options.get(composeFile, activeProfiles, arguments); + return DockerCompose.get(this.properties.getHost(), options); } 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 25970e94f241..4d840feadb66 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 void setEnabled(boolean enabled) { 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/core/DefaultDockerComposeTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/core/DefaultDockerComposeTests.java index a03ff984dc1f..68dded1db978 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/core/DefaultDockerComposeTests.java +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/core/DefaultDockerComposeTests.java @@ -106,6 +106,7 @@ void getRunningServicesReturnsServices() { HostConfig hostConfig = null; DockerCliInspectResponse inspectResponse = new DockerCliInspectResponse(id, config, networkSettings, hostConfig); + willReturn(mock(DockerCompose.Options.class)).given(this.cli).getDockerComposeOptions(); willReturn(List.of(psResponse)).given(this.cli).run(new DockerCliCommand.ComposePs()); willReturn(List.of(inspectResponse)).given(this.cli).run(new DockerCliCommand.Inspect(List.of(id))); DefaultDockerCompose compose = new DefaultDockerCompose(this.cli, HOST); @@ -132,6 +133,7 @@ void getRunningServicesWhenNoHostUsesHostFromContext() { hostConfig); willReturn(List.of(new DockerCliContextResponse("test", true, "https://192.168.1.1"))).given(this.cli) .run(new DockerCliCommand.Context()); + willReturn(mock(DockerCompose.Options.class)).given(this.cli).getDockerComposeOptions(); willReturn(List.of(psResponse)).given(this.cli).run(new DockerCliCommand.ComposePs()); willReturn(List.of(inspectResponse)).given(this.cli).run(new DockerCliCommand.Inspect(List.of(id))); DefaultDockerCompose compose = new DefaultDockerCompose(this.cli, null); @@ -148,6 +150,7 @@ void worksWithTruncatedIds() { DockerCliComposePsResponse psResponse = new DockerCliComposePsResponse(shortId, "name", "redis", "running"); Config config = new Config("redis", Collections.emptyMap(), Collections.emptyMap(), Collections.emptyList()); DockerCliInspectResponse inspectResponse = new DockerCliInspectResponse(longId, config, null, null); + willReturn(mock(DockerCompose.Options.class)).given(this.cli).getDockerComposeOptions(); willReturn(List.of(new DockerCliContextResponse("test", true, "https://192.168.1.1"))).given(this.cli) .run(new DockerCliCommand.Context()); willReturn(List.of(psResponse)).given(this.cli).run(new DockerCliCommand.ComposePs()); 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 7e7a933e84b2..331b9dee36b2 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 @@ void startGetsDockerComposeWithActiveProfiles() { 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 @@ protected DockerComposeFile getComposeFile() { } @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 5694de1937ce..777c08a1033c 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 @@ void getWhenNoPropertiesReturnsNew() { @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 @@ void getWhenPropertiesReturnsBound() { 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");