Merge pull request #42571 from nosan

* pr/42571:
  Polish "Add property to specify Docker Compose flags"
  Add property to specify Docker Compose flags

Closes gh-42571
This commit is contained in:
Moritz Halbritter
2024-10-10 08:56:21 +02:00
9 changed files with 103 additions and 23 deletions

View File

@@ -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<DockerCliContextResponse> 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<DockerCliComposePsResponse> 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<File> 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<File> 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);
}
}

View File

@@ -1,5 +1,6 @@
services:
redis1:
profiles: [ 'dev' ]
image: '{imageName}'
ports:
- '6379'

View File

@@ -0,0 +1,6 @@
services:
redis3:
profiles: [ 'prod' ]
image: '{imageName}'
ports:
- '6379'

View File

@@ -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<String> 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<String> 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<String> 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<String> activeProfiles = this.dockerComposeOptions.activeProfiles();
if (!CollectionUtils.isEmpty(activeProfiles)) {
for (String profile : activeProfiles) {
result.add("--profile");
result.add(profile);
}
}
List<String> 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<String> activeProfiles, List<String> arguments) {
DockerComposeOptions {
activeProfiles = (activeProfiles != null) ? activeProfiles : Collections.emptySet();
arguments = (arguments != null) ? arguments : Collections.emptyList();
}
static DockerComposeOptions none() {
return new DockerComposeOptions(null, null, null);
}
}
}

View File

@@ -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<String> 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<String> activeProfiles,
List<String> arguments) {
DockerCli cli = new DockerCli(null, new DockerComposeOptions(file, activeProfiles, arguments));
return new DefaultDockerCompose(cli, hostname);
}

View File

@@ -109,7 +109,8 @@ class DockerComposeLifecycleManager {
}
DockerComposeFile composeFile = getComposeFile();
Set<String> activeProfiles = this.properties.getProfiles().getActive();
DockerCompose dockerCompose = getDockerCompose(composeFile, activeProfiles);
List<String> 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<String> activeProfiles) {
return DockerCompose.get(composeFile, this.properties.getHost(), activeProfiles);
protected DockerCompose getDockerCompose(DockerComposeFile composeFile, Set<String> activeProfiles,
List<String> arguments) {
return DockerCompose.get(composeFile, this.properties.getHost(), activeProfiles, arguments);
}
private boolean isIgnored(RunningService service) {

View File

@@ -46,6 +46,11 @@ public class DockerComposeProperties {
*/
private boolean enabled = true;
/**
* Arguments to pass to the Docker Compose command.
*/
private final List<String> arguments = new ArrayList<>();
/**
* Paths to the Docker Compose configuration files.
*/
@@ -88,6 +93,10 @@ public class DockerComposeProperties {
this.enabled = enabled;
}
public List<String> getArguments() {
return this.arguments;
}
public List<File> getFile() {
return this.file;
}

View File

@@ -75,6 +75,8 @@ class DockerComposeLifecycleManagerTests {
private Set<String> activeProfiles;
private List<String> 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<String> activeProfiles) {
protected DockerCompose getDockerCompose(DockerComposeFile composeFile, Set<String> activeProfiles,
List<String> arguments) {
DockerComposeLifecycleManagerTests.this.activeProfiles = activeProfiles;
DockerComposeLifecycleManagerTests.this.arguments = arguments;
return DockerComposeLifecycleManagerTests.this.dockerCompose;
}

View File

@@ -63,6 +63,7 @@ class DockerComposePropertiesTests {
@Test
void getWhenPropertiesReturnsBound() {
Map<String, String> 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");