diff --git a/spring-boot-project/spring-boot-devtools/src/main/resources/org/springframework/boot/devtools/env/devtools-property-defaults.properties b/spring-boot-project/spring-boot-devtools/src/main/resources/org/springframework/boot/devtools/env/devtools-property-defaults.properties index 6d4f3416cc..1d86f50455 100644 --- a/spring-boot-project/spring-boot-devtools/src/main/resources/org/springframework/boot/devtools/env/devtools-property-defaults.properties +++ b/spring-boot-project/spring-boot-devtools/src/main/resources/org/springframework/boot/devtools/env/devtools-property-defaults.properties @@ -14,3 +14,4 @@ spring.template.provider.cache=false spring.thymeleaf.cache=false spring.web.resources.cache.period=0 spring.web.resources.chain.cache=false +spring.docker.compose.readiness.wait=only-if-started 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 0354867450..c969cf4405 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 @@ -69,11 +69,6 @@ class DefaultDockerCompose implements DockerCompose { return !this.cli.run(new DockerCliCommand.ComposeConfig()).services().isEmpty(); } - @Override - public boolean hasRunningServices() { - return runComposePs().stream().anyMatch(this::isRunning); - } - @Override public List getRunningServices() { List runningPsResponses = runComposePs().stream().filter(this::isRunning).toList(); 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 bd6e694248..160e156a24 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 @@ -19,7 +19,9 @@ package org.springframework.boot.docker.compose.core; import java.io.File; import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.function.Consumer; @@ -39,13 +41,13 @@ import org.springframework.core.log.LogMessage; */ class DockerCli { + private static final Map dockerCommandsCache = new HashMap<>(); + private static final Log logger = LogFactory.getLog(DockerCli.class); private final ProcessRunner processRunner; - private final List dockerCommand; - - private final List dockerComposeCommand; + private final DockerCommands dockerCommands; private final DockerComposeFile composeFile; @@ -59,56 +61,12 @@ class DockerCli { */ DockerCli(File workingDirectory, DockerComposeFile composeFile, Set activeProfiles) { this.processRunner = new ProcessRunner(workingDirectory); - this.dockerCommand = getDockerCommand(this.processRunner); - this.dockerComposeCommand = getDockerComposeCommand(this.processRunner); + this.dockerCommands = dockerCommandsCache.computeIfAbsent(workingDirectory, + (key) -> new DockerCommands(this.processRunner)); this.composeFile = composeFile; this.activeProfiles = (activeProfiles != null) ? activeProfiles : Collections.emptySet(); } - private List getDockerCommand(ProcessRunner processRunner) { - try { - String version = processRunner.run("docker", "version", "--format", "{{.Client.Version}}"); - logger.trace(LogMessage.format("Using docker %s", version)); - return List.of("docker"); - } - catch (ProcessStartException ex) { - throw new DockerProcessStartException("Unable to start docker process. Is docker correctly installed?", ex); - } - catch (ProcessExitException ex) { - if (ex.getStdErr().contains("docker daemon is not running") - || ex.getStdErr().contains("Cannot connect to the Docker daemon")) { - throw new DockerNotRunningException(ex.getStdErr(), ex); - } - throw ex; - - } - } - - private List getDockerComposeCommand(ProcessRunner processRunner) { - try { - DockerCliComposeVersionResponse response = DockerJson.deserialize( - processRunner.run("docker", "compose", "version", "--format", "json"), - DockerCliComposeVersionResponse.class); - logger.trace(LogMessage.format("Using docker compose %s", response.version())); - return List.of("docker", "compose"); - } - catch (ProcessExitException ex) { - // Ignore and try docker-compose - } - try { - DockerCliComposeVersionResponse response = DockerJson.deserialize( - processRunner.run("docker-compose", "version", "--format", "json"), - DockerCliComposeVersionResponse.class); - logger.trace(LogMessage.format("Using docker-compose %s", response.version())); - return List.of("docker-compose"); - } - catch (ProcessStartException ex) { - throw new DockerProcessStartException( - "Unable to start 'docker-compose' process or use 'docker compose'. Is docker correctly installed?", - ex); - } - } - /** * Run the given {@link DockerCli} command and return the response. * @param the response type @@ -132,9 +90,9 @@ class DockerCli { private List createCommand(Type type) { return switch (type) { - case DOCKER -> new ArrayList<>(this.dockerCommand); + case DOCKER -> new ArrayList<>(this.dockerCommands.get(type)); case DOCKER_COMPOSE -> { - List result = new ArrayList<>(this.dockerComposeCommand); + List result = new ArrayList<>(this.dockerCommands.get(type)); if (this.composeFile != null) { result.add("--file"); result.add(this.composeFile.toString()); @@ -158,4 +116,71 @@ class DockerCli { return this.composeFile; } + /** + * Holds details of the actual CLI commands to invoke. + */ + private static class DockerCommands { + + private final List dockerCommand; + + private final List dockerComposeCommand; + + DockerCommands(ProcessRunner processRunner) { + this.dockerCommand = getDockerCommand(processRunner); + this.dockerComposeCommand = getDockerComposeCommand(processRunner); + } + + private List getDockerCommand(ProcessRunner processRunner) { + try { + String version = processRunner.run("docker", "version", "--format", "{{.Client.Version}}"); + logger.trace(LogMessage.format("Using docker %s", version)); + return List.of("docker"); + } + catch (ProcessStartException ex) { + throw new DockerProcessStartException("Unable to start docker process. Is docker correctly installed?", + ex); + } + catch (ProcessExitException ex) { + if (ex.getStdErr().contains("docker daemon is not running") + || ex.getStdErr().contains("Cannot connect to the Docker daemon")) { + throw new DockerNotRunningException(ex.getStdErr(), ex); + } + throw ex; + } + } + + private List getDockerComposeCommand(ProcessRunner processRunner) { + try { + DockerCliComposeVersionResponse response = DockerJson.deserialize( + processRunner.run("docker", "compose", "version", "--format", "json"), + DockerCliComposeVersionResponse.class); + logger.trace(LogMessage.format("Using docker compose %s", response.version())); + return List.of("docker", "compose"); + } + catch (ProcessExitException ex) { + // Ignore and try docker-compose + } + try { + DockerCliComposeVersionResponse response = DockerJson.deserialize( + processRunner.run("docker-compose", "version", "--format", "json"), + DockerCliComposeVersionResponse.class); + logger.trace(LogMessage.format("Using docker-compose %s", response.version())); + return List.of("docker-compose"); + } + catch (ProcessStartException ex) { + throw new DockerProcessStartException( + "Unable to start 'docker-compose' process or use 'docker compose'. Is docker correctly installed?", + ex); + } + } + + List get(Type type) { + return switch (type) { + case DOCKER -> this.dockerCommand; + case DOCKER_COMPOSE -> this.dockerComposeCommand; + }; + } + + } + } 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 32767cccf6..de27b8298c 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 @@ -73,15 +73,6 @@ public interface DockerCompose { */ boolean hasDefinedServices(); - /** - * Return if services defined in the {@link DockerComposeFile} for the active profile - * are running. - * @return {@code true} if services are running - * @see #hasDefinedServices() - * @see #getRunningServices() - */ - boolean hasRunningServices(); - /** * Return the running services for the active profile, or an empty list if no services * are running. 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 e04ad34e4f..a5db683886 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 @@ -112,19 +112,21 @@ class DockerComposeLifecycleManager { Start start = this.properties.getStart(); Stop stop = this.properties.getStop(); Wait wait = this.properties.getReadiness().getWait(); - if (lifecycleManagement.shouldStart() && !dockerCompose.hasRunningServices()) { + List runningServices = dockerCompose.getRunningServices(); + if (lifecycleManagement.shouldStart() && runningServices.isEmpty()) { start.getCommand().applyTo(dockerCompose, start.getLogLevel()); + runningServices = dockerCompose.getRunningServices(); wait = (wait != Wait.ONLY_IF_STARTED) ? wait : Wait.ALWAYS; if (lifecycleManagement.shouldStop()) { this.shutdownHandlers.add(() -> stop.getCommand().applyTo(dockerCompose, stop.getTimeout())); } } - List runningServices = new ArrayList<>(dockerCompose.getRunningServices()); - runningServices.removeIf(this::isIgnored); + List relevantServices = new ArrayList<>(runningServices); + relevantServices.removeIf(this::isIgnored); if (wait == Wait.ALWAYS || wait == null) { - this.serviceReadinessChecks.waitUntilReady(runningServices); + this.serviceReadinessChecks.waitUntilReady(relevantServices); } - publishEvent(new DockerComposeServicesReadyEvent(this.applicationContext, runningServices)); + publishEvent(new DockerComposeServicesReadyEvent(this.applicationContext, relevantServices)); } protected DockerComposeFile getComposeFile() { 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 d048eeb962..c6c297d3a9 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 @@ -96,26 +96,6 @@ class DefaultDockerComposeTests { assertThat(compose.hasDefinedServices()).isTrue(); } - @Test - void hasRunningServicesWhenPsListsRunningServiceReturnsTrue() { - willReturn(List.of(new DockerCliComposePsResponse("id", "name", "image", "exited"), - new DockerCliComposePsResponse("id", "name", "image", "running"))) - .given(this.cli) - .run(new DockerCliCommand.ComposePs()); - DefaultDockerCompose compose = new DefaultDockerCompose(this.cli, HOST); - assertThat(compose.hasRunningServices()).isTrue(); - } - - @Test - void hasRunningServicesWhenPsListReturnsAllExitedReturnsFalse() { - willReturn(List.of(new DockerCliComposePsResponse("id", "name", "image", "exited"), - new DockerCliComposePsResponse("id", "name", "image", "running"))) - .given(this.cli) - .run(new DockerCliCommand.ComposePs()); - DefaultDockerCompose compose = new DefaultDockerCompose(this.cli, HOST); - assertThat(compose.hasRunningServices()).isTrue(); - } - @Test void getRunningServicesReturnsServices() { String id = "123"; 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 943e172e67..72d3dcbced 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 @@ -351,7 +351,6 @@ class DockerComposeLifecycleManagerTests { @SuppressWarnings("unchecked") private void setUpRunningServices(boolean started, Map labels) { given(this.dockerCompose.hasDefinedServices()).willReturn(true); - given(this.dockerCompose.hasRunningServices()).willReturn(true); RunningService runningService = mock(RunningService.class); given(runningService.labels()).willReturn(labels); this.runningServices = List.of(runningService);