Add property to control Docker Compose start command execution

If the property 'spring.docker.compose.start.skip' is set to 'never',
the start command is always executed. The default value of 'if-running'
only executes the start command if there are no services running
already, which is the old behavior.

Closes gh-39749
This commit is contained in:
Moritz Halbritter 2024-04-05 08:36:21 +02:00
parent 2ab0e024c8
commit 6d192e62fd
5 changed files with 116 additions and 8 deletions

View File

@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -32,6 +32,7 @@ import org.springframework.boot.docker.compose.core.DockerComposeFile;
import org.springframework.boot.docker.compose.core.RunningService; import org.springframework.boot.docker.compose.core.RunningService;
import org.springframework.boot.docker.compose.lifecycle.DockerComposeProperties.Readiness.Wait; import org.springframework.boot.docker.compose.lifecycle.DockerComposeProperties.Readiness.Wait;
import org.springframework.boot.docker.compose.lifecycle.DockerComposeProperties.Start; import org.springframework.boot.docker.compose.lifecycle.DockerComposeProperties.Start;
import org.springframework.boot.docker.compose.lifecycle.DockerComposeProperties.Start.Skip;
import org.springframework.boot.docker.compose.lifecycle.DockerComposeProperties.Stop; import org.springframework.boot.docker.compose.lifecycle.DockerComposeProperties.Stop;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationListener; import org.springframework.context.ApplicationListener;
@ -119,7 +120,11 @@ class DockerComposeLifecycleManager {
Wait wait = this.properties.getReadiness().getWait(); Wait wait = this.properties.getReadiness().getWait();
List<RunningService> runningServices = dockerCompose.getRunningServices(); List<RunningService> runningServices = dockerCompose.getRunningServices();
if (lifecycleManagement.shouldStart()) { if (lifecycleManagement.shouldStart()) {
if (runningServices.isEmpty()) { Skip skip = this.properties.getStart().getSkip();
if (skip.shouldSkip(runningServices)) {
logger.info(skip.getLogMessage());
}
else {
start.getCommand().applyTo(dockerCompose, start.getLogLevel()); start.getCommand().applyTo(dockerCompose, start.getLogLevel());
runningServices = dockerCompose.getRunningServices(); runningServices = dockerCompose.getRunningServices();
if (wait == Wait.ONLY_IF_STARTED) { if (wait == Wait.ONLY_IF_STARTED) {
@ -129,9 +134,6 @@ class DockerComposeLifecycleManager {
this.shutdownHandlers.add(() -> stop.getCommand().applyTo(dockerCompose, stop.getTimeout())); this.shutdownHandlers.add(() -> stop.getCommand().applyTo(dockerCompose, stop.getTimeout()));
} }
} }
else {
logger.info("There are already Docker Compose services running, skipping startup");
}
} }
List<RunningService> relevantServices = new ArrayList<>(runningServices); List<RunningService> relevantServices = new ArrayList<>(runningServices);
relevantServices.removeIf(this::isIgnored); relevantServices.removeIf(this::isIgnored);

View File

@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -19,10 +19,12 @@ package org.springframework.boot.docker.compose.lifecycle;
import java.io.File; import java.io.File;
import java.time.Duration; import java.time.Duration;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set; import java.util.Set;
import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.bind.Binder; import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.boot.docker.compose.core.RunningService;
import org.springframework.boot.logging.LogLevel; import org.springframework.boot.logging.LogLevel;
/** /**
@ -148,6 +150,11 @@ public class DockerComposeProperties {
*/ */
private LogLevel logLevel = LogLevel.INFO; private LogLevel logLevel = LogLevel.INFO;
/**
* Whether to skip executing the start command.
*/
private Skip skip = Skip.IF_RUNNING;
public StartCommand getCommand() { public StartCommand getCommand() {
return this.command; return this.command;
} }
@ -164,6 +171,51 @@ public class DockerComposeProperties {
this.logLevel = logLevel; this.logLevel = logLevel;
} }
public Skip getSkip() {
return this.skip;
}
public void setSkip(Skip skip) {
this.skip = skip;
}
/**
* Start command skip mode.
*/
public enum Skip {
/**
* Never skip start.
*/
NEVER {
@Override
boolean shouldSkip(List<RunningService> runningServices) {
return false;
}
},
/**
* Skip start if there are already services running.
*/
IF_RUNNING {
@Override
boolean shouldSkip(List<RunningService> runningServices) {
return !runningServices.isEmpty();
}
@Override
String getLogMessage() {
return "There are already Docker Compose services running, skipping startup";
}
};
abstract boolean shouldSkip(List<RunningService> runningServices);
String getLogMessage() {
return "";
}
}
} }
/** /**

View File

@ -18,6 +18,10 @@
"name": "spring.docker.compose.start.log-level", "name": "spring.docker.compose.start.log-level",
"defaultValue": "info" "defaultValue": "info"
}, },
{
"name": "spring.docker.compose.start.skip",
"defaultValue": "if-running"
},
{ {
"name": "spring.docker.compose.stop.command", "name": "spring.docker.compose.stop.command",
"defaultValue": "stop" "defaultValue": "stop"

View File

@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -39,6 +39,7 @@ import org.springframework.boot.docker.compose.core.DockerCompose;
import org.springframework.boot.docker.compose.core.DockerComposeFile; import org.springframework.boot.docker.compose.core.DockerComposeFile;
import org.springframework.boot.docker.compose.core.RunningService; import org.springframework.boot.docker.compose.core.RunningService;
import org.springframework.boot.docker.compose.lifecycle.DockerComposeProperties.Readiness.Wait; import org.springframework.boot.docker.compose.lifecycle.DockerComposeProperties.Readiness.Wait;
import org.springframework.boot.docker.compose.lifecycle.DockerComposeProperties.Start.Skip;
import org.springframework.boot.test.system.CapturedOutput; import org.springframework.boot.test.system.CapturedOutput;
import org.springframework.boot.test.system.OutputCaptureExtension; import org.springframework.boot.test.system.OutputCaptureExtension;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
@ -384,6 +385,38 @@ class DockerComposeLifecycleManagerTests {
assertThat(output).doesNotContain("There are already Docker Compose services running, skipping startup"); assertThat(output).doesNotContain("There are already Docker Compose services running, skipping startup");
} }
@Test
void shouldStartIfSkipModeIsIfRunningAndNoServicesAreRunning() {
given(this.dockerCompose.hasDefinedServices()).willReturn(true);
this.properties.getStart().setSkip(Skip.IF_RUNNING);
this.lifecycleManager.start();
then(this.dockerCompose).should().up(any());
}
@Test
void shouldNotStartIfSkipModeIsIfRunningAndServicesAreAlreadyRunning() {
setUpRunningServices();
this.properties.getStart().setSkip(Skip.IF_RUNNING);
this.lifecycleManager.start();
then(this.dockerCompose).should(never()).up(any());
}
@Test
void shouldStartIfSkipModeIsNeverAndNoServicesAreRunning() {
given(this.dockerCompose.hasDefinedServices()).willReturn(true);
this.properties.getStart().setSkip(Skip.NEVER);
this.lifecycleManager.start();
then(this.dockerCompose).should().up(any());
}
@Test
void shouldStartIfSkipModeIsNeverAndServicesAreAlreadyRunning() {
setUpRunningServices();
this.properties.getStart().setSkip(Skip.NEVER);
this.lifecycleManager.start();
then(this.dockerCompose).should().up(any());
}
private void setUpRunningServices() { private void setUpRunningServices() {
setUpRunningServices(true); setUpRunningServices(true);
} }

View File

@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -18,16 +18,21 @@ package org.springframework.boot.docker.compose.lifecycle;
import java.io.File; import java.io.File;
import java.time.Duration; import java.time.Duration;
import java.util.Collections;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.boot.context.properties.bind.Binder; import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.boot.context.properties.source.MapConfigurationPropertySource; import org.springframework.boot.context.properties.source.MapConfigurationPropertySource;
import org.springframework.boot.docker.compose.core.RunningService;
import org.springframework.boot.docker.compose.lifecycle.DockerComposeProperties.Readiness.Wait; import org.springframework.boot.docker.compose.lifecycle.DockerComposeProperties.Readiness.Wait;
import org.springframework.boot.docker.compose.lifecycle.DockerComposeProperties.Start.Skip;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
/** /**
* Tests for {@link DockerComposeProperties}. * Tests for {@link DockerComposeProperties}.
@ -84,4 +89,16 @@ class DockerComposePropertiesTests {
assertThat(properties.getReadiness().getTcp().getReadTimeout()).isEqualTo(Duration.ofMillis(500)); assertThat(properties.getReadiness().getTcp().getReadTimeout()).isEqualTo(Duration.ofMillis(500));
} }
@Test
void skipModeNeverShouldNeverSkip() {
assertThat(Skip.NEVER.shouldSkip(Collections.emptyList())).isFalse();
assertThat(Skip.NEVER.shouldSkip(List.of(mock(RunningService.class)))).isFalse();
}
@Test
void skipModeIfRunningShouldSkipWhenServicesAreRunning() {
assertThat(Skip.IF_RUNNING.shouldSkip(Collections.emptyList())).isFalse();
assertThat(Skip.IF_RUNNING.shouldSkip(List.of(mock(RunningService.class)))).isTrue();
}
} }