From 9b46fa78ab48017c6d0eb563ed24e5f7335834c4 Mon Sep 17 00:00:00 2001 From: stanislav-shymov Date: Thu, 26 Sep 2024 16:23:15 +0300 Subject: [PATCH 1/9] healthcheck suppor in docker-compose configuration --- doc/changelog.md | 2 + .../io/fabric8/maven/docker/StartMojo.java | 12 +++ .../docker/access/ContainerCreateConfig.java | 46 ++++++++-- .../access/util/ComposeDurationUtil.java | 52 +++++++++++ .../docker/config/RunImageConfiguration.java | 13 +++ .../compose/DockerComposeConfigHandler.java | 1 + .../compose/DockerComposeServiceWrapper.java | 77 +++++++++++++--- .../maven/docker/service/RunService.java | 3 +- .../maven/docker/service/WaitService.java | 5 + .../access/ContainerCreateConfigTest.java | 91 ++++++++++++++++--- .../access/util/ComposeDurationUtilTest.java | 27 ++++++ .../maven/docker/service/RunServiceTest.java | 22 +++-- src/test/resources/compose/docker-compose.yml | 8 +- .../docker/containerCreateConfigAll.json | 10 ++ 14 files changed, 324 insertions(+), 45 deletions(-) create mode 100644 src/main/java/io/fabric8/maven/docker/access/util/ComposeDurationUtil.java create mode 100644 src/test/java/io/fabric8/maven/docker/access/util/ComposeDurationUtilTest.java diff --git a/doc/changelog.md b/doc/changelog.md index 40a966e68..434483745 100644 --- a/doc/changelog.md +++ b/doc/changelog.md @@ -4,6 +4,8 @@ * **0.45.1 (2024-09-29)**: - Make copy docker-buildx binary to temporary config directory work on windows too ([1819](https://github.com/fabric8io/docker-maven-plugin/pull/1819)) - Pull FROM images in relative path Dockerfiles ([1823](https://github.com/fabric8io/docker-maven-plugin/issues/1823)) + - Docker-compose healthcheck configuration support + - Docker container wait timeout default value made configurable using defaultContainerWaitTimeout configuration option * **0.45.0 (2024-07-27)**: - Automatically create parent directories of portPropertyFile path ([1761](https://github.com/fabric8io/docker-maven-plugin/pull/1761)) diff --git a/src/main/java/io/fabric8/maven/docker/StartMojo.java b/src/main/java/io/fabric8/maven/docker/StartMojo.java index e19249e18..0e175c562 100644 --- a/src/main/java/io/fabric8/maven/docker/StartMojo.java +++ b/src/main/java/io/fabric8/maven/docker/StartMojo.java @@ -55,6 +55,7 @@ @Mojo(name = "start", defaultPhase = LifecyclePhase.PRE_INTEGRATION_TEST) public class StartMojo extends AbstractDockerMojo { + public static final String DOCKER_DEFAULT_CONTAINER_WAIT_TIMEOUT = "docker.defaultContainerWaitTimeout"; @Parameter(property = "docker.showLogs") private String showLogs; @@ -98,6 +99,14 @@ public class StartMojo extends AbstractDockerMojo { @Parameter(property = "docker.autoCreateCustomNetworks", defaultValue = "false") protected boolean autoCreateCustomNetworks; + /** + * Global across all the containers default wait time is milliseconds. + * Overriding that property might become particularly useful when docker-compose config defines + * the healthchecks, but some containers require more time to become healthy. + */ + @Parameter(property = DOCKER_DEFAULT_CONTAINER_WAIT_TIMEOUT, defaultValue = "10000") + protected int defaultContainerWaitTimeout = 10000; + // property file to write out with port mappings @Parameter protected String portPropertyFile; @@ -274,6 +283,9 @@ private void startImage(final ImageConfiguration imageConfig, final Properties projProperties = project.getProperties(); final RunImageConfiguration runConfig = imageConfig.getRunConfiguration(); final PortMapping portMapping = runService.createPortMapping(runConfig, projProperties); + if (!projProperties.containsKey(DOCKER_DEFAULT_CONTAINER_WAIT_TIMEOUT)) { + projProperties.put(DOCKER_DEFAULT_CONTAINER_WAIT_TIMEOUT, defaultContainerWaitTimeout); + } final LogDispatcher dispatcher = getLogDispatcher(hub); StartContainerExecutor startExecutor = new StartContainerExecutor.Builder() diff --git a/src/main/java/io/fabric8/maven/docker/access/ContainerCreateConfig.java b/src/main/java/io/fabric8/maven/docker/access/ContainerCreateConfig.java index 1b8545b8f..ab059bd12 100644 --- a/src/main/java/io/fabric8/maven/docker/access/ContainerCreateConfig.java +++ b/src/main/java/io/fabric8/maven/docker/access/ContainerCreateConfig.java @@ -3,20 +3,21 @@ import com.google.gson.JsonArray; import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; +import io.fabric8.maven.docker.config.HealthCheckConfiguration; +import io.fabric8.maven.docker.config.HealthCheckMode; import org.apache.commons.text.StrSubstitutor; import java.io.FileNotFoundException; import java.io.FileReader; import java.io.IOException; -import java.util.Enumeration; -import java.util.List; -import java.util.Map; -import java.util.Properties; -import java.util.Set; +import java.util.*; import io.fabric8.maven.docker.config.Arguments; import io.fabric8.maven.docker.util.JsonFactory; +import static io.fabric8.maven.docker.access.util.ComposeDurationUtil.goDurationToNanoseconds; + public class ContainerCreateConfig { private final JsonObject createConfig = new JsonObject(); @@ -72,7 +73,7 @@ public ContainerCreateConfig environment(String envPropsFile, Map labels) { + public ContainerCreateConfig labels(Map labels) { if (labels != null && labels.size() > 0) { createConfig.add("Labels", JsonFactory.newJsonObject(labels)); } @@ -111,6 +112,37 @@ public ContainerCreateConfig exposedPorts(Set portSpecs) { return this; } + public ContainerCreateConfig healthcheck(HealthCheckConfiguration healthCheckConfiguration) { + if (healthCheckConfiguration != null) { + JsonObject healthcheck = new JsonObject(); + if (healthCheckConfiguration.getCmd() != null) { + healthcheck.add("Test", JsonFactory.newJsonArray(healthCheckConfiguration.getCmd().asStrings())); + } + if (healthCheckConfiguration.getMode() != HealthCheckMode.none) { + if (healthCheckConfiguration.getRetries() != null) { + healthcheck.add("Retries", new JsonPrimitive(healthCheckConfiguration.getRetries())); + } + if (healthCheckConfiguration.getInterval() != null) { + String intervalValue = healthCheckConfiguration.getInterval(); + String field = "Interval"; + healthcheck.add(field, new JsonPrimitive(goDurationToNanoseconds(intervalValue, field))); + } + if (healthCheckConfiguration.getStartPeriod() != null) { + String field = "StartPeriod"; + String intervalValue = healthCheckConfiguration.getStartPeriod(); + healthcheck.add(field, new JsonPrimitive(goDurationToNanoseconds(intervalValue, field))); + } + if (healthCheckConfiguration.getTimeout() != null) { + String field = "Timeout"; + String intervalValue = healthCheckConfiguration.getTimeout(); + healthcheck.add(field, new JsonPrimitive(goDurationToNanoseconds(intervalValue, field))); + } + } + createConfig.add("Healthcheck", healthcheck); + } + return this; + } + public String getImageName() { return imageName; } diff --git a/src/main/java/io/fabric8/maven/docker/access/util/ComposeDurationUtil.java b/src/main/java/io/fabric8/maven/docker/access/util/ComposeDurationUtil.java new file mode 100644 index 000000000..e1f221ea8 --- /dev/null +++ b/src/main/java/io/fabric8/maven/docker/access/util/ComposeDurationUtil.java @@ -0,0 +1,52 @@ +package io.fabric8.maven.docker.access.util; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static java.util.Objects.requireNonNull; + +/** + * Partial implementation of the patterns from https://pkg.go.dev/maze.io/x/duration + * This implementation doesn't support combinations of timeunits. + */ +public class ComposeDurationUtil { + private static Pattern SIMPLE_GO_DURATION_FORMAT = Pattern.compile("^([\\d]+)(ns|us|ms|s|m|h|d|w|y)?$"); + private static Map goTypesToJava = new HashMap() {{ + put("ns", TimeUnit.NANOSECONDS); + put("us", TimeUnit.MICROSECONDS); + put("ms", TimeUnit.MILLISECONDS); + put("s", TimeUnit.SECONDS); + put("m", TimeUnit.MINUTES); + put("h", TimeUnit.HOURS); + put("d", TimeUnit.DAYS); + }}; + + public static long goDurationToNanoseconds(String goDuration, String field) { + requireNonNull(goDuration); + + Matcher matcher = SIMPLE_GO_DURATION_FORMAT.matcher(goDuration); + if (!matcher.matches()) { + String message = String.format("Unsupported duration value \"%s\" for the field \"%s\"", goDuration, field); + throw new IllegalArgumentException(message); + } + long duration = Long.valueOf(matcher.group(1)); + if (matcher.groupCount() == 2 && matcher.group(2) != null) { + String type = matcher.group(2); + + if (goTypesToJava.containsKey(type)) { + duration = TimeUnit.NANOSECONDS.convert(duration, goTypesToJava.get(type)); + } else if ("w".equals(type)) { + duration = 7 * TimeUnit.NANOSECONDS.convert(duration, TimeUnit.DAYS); + } else if ("y".equals(type)) { + duration = 365 * TimeUnit.NANOSECONDS.convert(duration, TimeUnit.DAYS); + } else { + throw new IllegalArgumentException("Unsupported time unit: " + type); + } + } + + return duration; + } +} diff --git a/src/main/java/io/fabric8/maven/docker/config/RunImageConfiguration.java b/src/main/java/io/fabric8/maven/docker/config/RunImageConfiguration.java index cfed39a6c..ec1688389 100644 --- a/src/main/java/io/fabric8/maven/docker/config/RunImageConfiguration.java +++ b/src/main/java/io/fabric8/maven/docker/config/RunImageConfiguration.java @@ -43,6 +43,10 @@ public class RunImageConfiguration implements Serializable { @Parameter private List dependsOn; + // healthcheck + @Parameter + private HealthCheckConfiguration healthCheckConfiguration; + /** * container entry point * @@ -264,6 +268,10 @@ public List getDependsOn() { return EnvUtil.splitAtCommasAndTrim(dependsOn); } + public HealthCheckConfiguration getHealthCheckConfiguration() { + return healthCheckConfiguration; + } + public String getUser() { return user; } @@ -582,6 +590,11 @@ public Builder dependsOn(List dependsOn) { return this; } + public Builder healthcheck(HealthCheckConfiguration healthCheckConfiguration) { + config.healthCheckConfiguration = healthCheckConfiguration; + return this; + } + public Builder dns(List dns) { config.dns = dns; return this; diff --git a/src/main/java/io/fabric8/maven/docker/config/handler/compose/DockerComposeConfigHandler.java b/src/main/java/io/fabric8/maven/docker/config/handler/compose/DockerComposeConfigHandler.java index 573b2d068..0f4d25017 100644 --- a/src/main/java/io/fabric8/maven/docker/config/handler/compose/DockerComposeConfigHandler.java +++ b/src/main/java/io/fabric8/maven/docker/config/handler/compose/DockerComposeConfigHandler.java @@ -198,6 +198,7 @@ private RunImageConfiguration createRunConfiguration(DockerComposeServiceWrapper // container_name is taken as an alias and ignored here for run config // devices not supported .dependsOn(wrapper.getDependsOn()) // depends_on relies that no container_name is set + .healthcheck(wrapper.getHealthCheckConfiguration()) .wait(wrapper.getWaitConfiguration()) .dns(wrapper.getDns()) .dnsSearch(wrapper.getDnsSearch()) diff --git a/src/main/java/io/fabric8/maven/docker/config/handler/compose/DockerComposeServiceWrapper.java b/src/main/java/io/fabric8/maven/docker/config/handler/compose/DockerComposeServiceWrapper.java index 253b9e61b..bdc6b3c90 100644 --- a/src/main/java/io/fabric8/maven/docker/config/handler/compose/DockerComposeServiceWrapper.java +++ b/src/main/java/io/fabric8/maven/docker/config/handler/compose/DockerComposeServiceWrapper.java @@ -10,6 +10,8 @@ import java.util.Objects; import io.fabric8.maven.docker.config.Arguments; +import io.fabric8.maven.docker.config.HealthCheckConfiguration; +import io.fabric8.maven.docker.config.HealthCheckMode; import io.fabric8.maven.docker.config.ImageConfiguration; import io.fabric8.maven.docker.config.LogConfiguration; import io.fabric8.maven.docker.config.NetworkConfig; @@ -124,7 +126,7 @@ List getDependsOn() { return new ArrayList<>(asMap("depends_on").keySet()); } } - + boolean usesLongSyntaxDependsOn() { return asObject("depends_on") instanceof Map; } @@ -133,6 +135,55 @@ public String getPlatform() { return asString("platform"); } + public HealthCheckConfiguration getHealthCheckConfiguration() { + if (!configuration.containsKey("healthcheck")) { + return null; + } + Map healthCheckAsMap = (Map) configuration.get("healthcheck"); + HealthCheckConfiguration.Builder builder = new HealthCheckConfiguration.Builder(); + Object disable = healthCheckAsMap.get("disable"); + if (disable != null && disable instanceof Boolean && (Boolean) disable) { + builder.mode(HealthCheckMode.none); + return builder.build(); + } + + Object test = healthCheckAsMap.get("test"); + if (test != null) { + if (test instanceof List) { + List cmd = (List) test; + if (cmd.size() > 0 && cmd.get(0).equalsIgnoreCase("NONE")) { + builder.mode(HealthCheckMode.none); + return builder.build(); + } else { + builder.cmd(new Arguments((List) test)); + builder.mode(HealthCheckMode.cmd); + } + } else { + builder.cmd(new Arguments(Arrays.asList("CMD-SHELL", (String) test))); + builder.mode(HealthCheckMode.cmd); + } + enableWaitCondition(WaitCondition.HEALTHY); + } + Object interval = healthCheckAsMap.get("interval"); + if (interval != null) { + builder.interval(interval.toString()); + } + Object timeout = healthCheckAsMap.get("timeout"); + if (timeout != null) { + builder.timeout(timeout.toString()); + } + Object retries = healthCheckAsMap.get("retries"); + if (retries != null && retries instanceof Number) { + builder.retries(((Number) retries).intValue()); + } + Object startPeriod = healthCheckAsMap.get("start_period"); + if (startPeriod != null) { + builder.startPeriod(startPeriod.toString()); + } + + return builder.build(); + } + /** * Docker Compose Spec v2.1+ defined conditions */ @@ -140,19 +191,19 @@ enum WaitCondition { HEALTHY("service_healthy"), COMPLETED("service_completed_successfully"), STARTED("service_started"); - + private final String condition; WaitCondition(String condition) { this.condition = condition; } - + static WaitCondition fromString(String string) { return Arrays.stream(WaitCondition.values()).filter(wc -> wc.condition.equals(string)).findFirst().orElseThrow( () -> new IllegalArgumentException("invalid condition \"" + string + "\"") ); } } - + /** * Extract a required condition of another (dependent) service from this service. * In a compose file following v2.1+ format this looks like this: @@ -175,15 +226,15 @@ static WaitCondition fromString(String string) { */ WaitCondition getWaitCondition(String dependentServiceName) { Objects.requireNonNull(dependentServiceName, "Dependent service's name may not be null"); - + Object dependsOnObj = asObject("depends_on"); if (dependsOnObj instanceof Map) { Map dependsOn = (Map) dependsOnObj; Object dependenSvcObj = dependsOn.get(dependentServiceName); - + if (dependenSvcObj instanceof Map) { Map dependency = (Map) dependenSvcObj; - + if (dependency.containsKey("condition")) { String condition = dependency.get("condition"); try { @@ -200,10 +251,10 @@ WaitCondition getWaitCondition(String dependentServiceName) { throwIllegalArgumentException("depends_on does not use long syntax, cannot retrieve condition"); return null; } - + private boolean healthyWaitRequested; private boolean successExitWaitRequested; - + /** * Switch on waiting conditions for this service. * It will not yet check for conflicting conditions, this is done in {@link #getWaitConfiguration()} @@ -212,7 +263,7 @@ WaitCondition getWaitCondition(String dependentServiceName) { */ void enableWaitCondition(WaitCondition condition) { Objects.requireNonNull(condition, "Condition may not be null"); - + // We do not check for conflicting conditions here - this is done when the wrapper is asked for its WaitConfig // Note: yes, we check here again, as we rely on Strings, not an enum switch (condition) { @@ -229,7 +280,7 @@ void enableWaitCondition(WaitCondition condition) { // Do nothing when unknown condition } } - + /** * Build the actual wait configuration for this service. *

Please note: while Docker Compose allows you to create a dependency graph which will allow to wait @@ -585,7 +636,7 @@ private Map convertToMap(List list) { void throwIllegalArgumentException(String msg) { throw new IllegalArgumentException(String.format("%s: %s - ", composeFile, name) + msg); } - + @Override public boolean equals(Object o) { if (this == o) return true; @@ -593,7 +644,7 @@ public boolean equals(Object o) { DockerComposeServiceWrapper that = (DockerComposeServiceWrapper) o; return Objects.equals(name, that.name); } - + @Override public int hashCode() { return Objects.hash(name); diff --git a/src/main/java/io/fabric8/maven/docker/service/RunService.java b/src/main/java/io/fabric8/maven/docker/service/RunService.java index cc6cd3d84..702aab4f6 100644 --- a/src/main/java/io/fabric8/maven/docker/service/RunService.java +++ b/src/main/java/io/fabric8/maven/docker/service/RunService.java @@ -376,7 +376,8 @@ ContainerCreateConfig createContainerConfig(String imageName, RunImageConfigurat .environment(runConfig.getEnvPropertyFile(), runConfig.getEnv(), mavenProps) .labels(mergeLabels(runConfig.getLabels(), gavLabel)) .command(runConfig.getCmd()) - .hostConfig(createContainerHostConfig(runConfig, mappedPorts, baseDir)); + .hostConfig(createContainerHostConfig(runConfig, mappedPorts, baseDir)) + .healthcheck(runConfig.getHealthCheckConfiguration()); RunVolumeConfiguration volumeConfig = runConfig.getVolumeConfiguration(); if (volumeConfig != null) { resolveRelativeVolumeBindings(baseDir, volumeConfig); diff --git a/src/main/java/io/fabric8/maven/docker/service/WaitService.java b/src/main/java/io/fabric8/maven/docker/service/WaitService.java index 206739a11..42931f2f3 100644 --- a/src/main/java/io/fabric8/maven/docker/service/WaitService.java +++ b/src/main/java/io/fabric8/maven/docker/service/WaitService.java @@ -29,6 +29,8 @@ import io.fabric8.maven.docker.wait.WaitTimeoutException; import io.fabric8.maven.docker.wait.WaitUtil; +import static io.fabric8.maven.docker.StartMojo.DOCKER_DEFAULT_CONTAINER_WAIT_TIMEOUT; + /** * @author roland * @since 03.05.17 @@ -58,6 +60,9 @@ public void wait(ImageConfiguration imageConfig, Properties projectProperties, S } return; } + if (timeout == 0 && projectProperties.containsKey(DOCKER_DEFAULT_CONTAINER_WAIT_TIMEOUT)) { + timeout = Integer.valueOf(projectProperties.get(DOCKER_DEFAULT_CONTAINER_WAIT_TIMEOUT).toString()); + } String logLine = extractCheckerLog(checkers); ContainerRunningPrecondition precondition = new ContainerRunningPrecondition(dockerAccess, containerId); diff --git a/src/test/java/io/fabric8/maven/docker/access/ContainerCreateConfigTest.java b/src/test/java/io/fabric8/maven/docker/access/ContainerCreateConfigTest.java index 7a610ff3e..26e4c8bf1 100644 --- a/src/test/java/io/fabric8/maven/docker/access/ContainerCreateConfigTest.java +++ b/src/test/java/io/fabric8/maven/docker/access/ContainerCreateConfigTest.java @@ -3,6 +3,9 @@ import com.google.gson.JsonArray; import com.google.gson.JsonObject; +import io.fabric8.maven.docker.config.Arguments; +import io.fabric8.maven.docker.config.HealthCheckConfiguration; +import io.fabric8.maven.docker.config.HealthCheckMode; import org.apache.commons.io.FileUtils; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -11,14 +14,12 @@ import java.io.File; import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import io.fabric8.maven.docker.util.JsonFactory; +import static org.junit.jupiter.api.Assertions.assertTrue; + /* * * Copyright 2014 Roland Huss @@ -51,12 +52,12 @@ void testEnvironment() throws Exception { Assertions.assertNotNull(env); Assertions.assertEquals(6, env.size()); List envAsString = convertToList(env); - Assertions.assertTrue(envAsString.contains("JAVA_OPTS=-Xmx512m")); - Assertions.assertTrue(envAsString.contains("TEST_SERVICE=SECURITY")); - Assertions.assertTrue(envAsString.contains("EXTERNAL_ENV=TRUE")); - Assertions.assertTrue(envAsString.contains("TEST_HTTP_ADDR=${docker.container.consul.ip}")); - Assertions.assertTrue(envAsString.contains("TEST_CONSUL_IP=+${docker.container.consul.ip}:8080")); - Assertions.assertTrue(envAsString.contains("TEST_CONSUL_IP_WITHOUT_DELIM=${docker.container.consul.ip}:8225")); + assertTrue(envAsString.contains("JAVA_OPTS=-Xmx512m")); + assertTrue(envAsString.contains("TEST_SERVICE=SECURITY")); + assertTrue(envAsString.contains("EXTERNAL_ENV=TRUE")); + assertTrue(envAsString.contains("TEST_HTTP_ADDR=${docker.container.consul.ip}")); + assertTrue(envAsString.contains("TEST_CONSUL_IP=+${docker.container.consul.ip}:8080")); + assertTrue(envAsString.contains("TEST_CONSUL_IP_WITHOUT_DELIM=${docker.container.consul.ip}:8225")); } @Test @@ -83,7 +84,7 @@ void testBind(String binding, String expectedContainerPath) { JsonObject volumes = (JsonObject) JsonFactory.newJsonObject(cc.toJson()).get("Volumes"); Assertions.assertEquals(1, volumes.size()); - Assertions.assertTrue(volumes.has(expectedContainerPath)); + assertTrue(volumes.has(expectedContainerPath)); } @@ -102,7 +103,7 @@ void testEnvNoMap() throws IOException { JsonArray env = getEnvArray(cc); Assertions.assertEquals(2, env.size()); List envAsString = convertToList(env); - Assertions.assertTrue(envAsString.contains("EXTERNAL_ENV=TRUE")); + assertTrue(envAsString.contains("EXTERNAL_ENV=TRUE")); } @Test @@ -118,6 +119,70 @@ void platform() { Assertions.assertEquals("linux/arm64", cc.getPlatform()); } + @Test + void testHealthCheckIsFullyPresent() { + ContainerCreateConfig cc = new ContainerCreateConfig("testImage"); + HealthCheckConfiguration healthCheckConfiguration = new HealthCheckConfiguration.Builder() + .cmd(new Arguments(Arrays.asList("CMD-SHELL", "some command 2>&1"))) + .startPeriod("1s") + .interval("500ms") + .timeout("2s") + .retries(20) + .mode(HealthCheckMode.cmd) + .build(); + cc.healthcheck(healthCheckConfiguration); + JsonObject config = JsonFactory.newJsonObject(cc.toJson()); + JsonObject healthCheck = (JsonObject) config.get("Healthcheck"); + Assertions.assertEquals(2, healthCheck.get("Test") + .getAsJsonArray().size()); + Assertions.assertEquals("some command 2>&1", healthCheck.get("Test") + .getAsJsonArray().get(1).getAsString()); + Assertions.assertEquals(20, healthCheck.get("Retries").getAsInt()); + Assertions.assertEquals(500000000, healthCheck.get("Interval").getAsInt()); + Assertions.assertEquals(1000000000, healthCheck.get("StartPeriod").getAsInt()); + Assertions.assertEquals(2000000000, healthCheck.get("Timeout").getAsInt()); + } + + @Test + void testHealthCheckIsInherited() { + ContainerCreateConfig cc = new ContainerCreateConfig("testImage"); + HealthCheckConfiguration healthCheckConfiguration = new HealthCheckConfiguration.Builder() + .cmd(new Arguments(Arrays.asList())) + .startPeriod("1s") + .mode(HealthCheckMode.cmd) + .build(); + cc.healthcheck(healthCheckConfiguration); + JsonObject config = JsonFactory.newJsonObject(cc.toJson()); + JsonObject healthCheck = (JsonObject) config.get("Healthcheck"); + Assertions.assertEquals(0, healthCheck.get("Test") + .getAsJsonArray().size()); + Assertions.assertNull(healthCheck.get("Retries")); + Assertions.assertNull(healthCheck.get("Interval")); + Assertions.assertEquals(1000000000, healthCheck.get("StartPeriod").getAsInt()); + Assertions.assertNull(healthCheck.get("Timeout")); + } + + @Test + void testHealthCheckIsDisabled() { + ContainerCreateConfig cc = new ContainerCreateConfig("testImage"); + HealthCheckConfiguration healthCheckConfiguration = new HealthCheckConfiguration.Builder() + .cmd(new Arguments(Arrays.asList("NONE"))) + .mode(HealthCheckMode.none) + .retries(2) + .build(); + cc.healthcheck(healthCheckConfiguration); + JsonObject config = JsonFactory.newJsonObject(cc.toJson()); + JsonObject healthCheck = (JsonObject) config.get("Healthcheck"); + Assertions.assertEquals(1, healthCheck.get("Test") + .getAsJsonArray().size()); + Assertions.assertEquals("NONE", healthCheck.get("Test") + .getAsJsonArray().get(0).getAsString()); + Assertions.assertNull(healthCheck.get("Retries")); + Assertions.assertNull(healthCheck.get("Interval")); + Assertions.assertNull(healthCheck.get("StartPeriod")); + Assertions.assertNull(healthCheck.get("Timeout")); + } + private JsonArray getEnvArray(ContainerCreateConfig cc) { JsonObject config = JsonFactory.newJsonObject(cc.toJson()); return (JsonArray) config.get("Env"); diff --git a/src/test/java/io/fabric8/maven/docker/access/util/ComposeDurationUtilTest.java b/src/test/java/io/fabric8/maven/docker/access/util/ComposeDurationUtilTest.java new file mode 100644 index 000000000..b9f990bc7 --- /dev/null +++ b/src/test/java/io/fabric8/maven/docker/access/util/ComposeDurationUtilTest.java @@ -0,0 +1,27 @@ +package io.fabric8.maven.docker.access.util; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class ComposeDurationUtilTest { + + @Test + void testCombinationsAreNotSupported() { + Assertions.assertThrows(IllegalArgumentException.class, ()-> ComposeDurationUtil.goDurationToNanoseconds("2h45m", "someField")); + } + @Test + void testUnknownTimeUnit() { + Assertions.assertThrows(IllegalArgumentException.class, ()-> ComposeDurationUtil.goDurationToNanoseconds("2x", "someField")); + } + @Test + void testDurationNanosecondsCorrect() { + assertEquals(0, ComposeDurationUtil.goDurationToNanoseconds("0", "someField")); + assertEquals(1, ComposeDurationUtil.goDurationToNanoseconds("1", "someField")); + assertEquals(3600000000000L, ComposeDurationUtil.goDurationToNanoseconds("1h", "someField")); + assertEquals(2000000000, ComposeDurationUtil.goDurationToNanoseconds("2s", "someField")); + assertEquals(259200000000000L, ComposeDurationUtil.goDurationToNanoseconds("3d", "someField")); + assertEquals(1000000, ComposeDurationUtil.goDurationToNanoseconds("1ms", "someField")); + } +} \ No newline at end of file diff --git a/src/test/java/io/fabric8/maven/docker/service/RunServiceTest.java b/src/test/java/io/fabric8/maven/docker/service/RunServiceTest.java index e41bc1dd4..158e43507 100644 --- a/src/test/java/io/fabric8/maven/docker/service/RunServiceTest.java +++ b/src/test/java/io/fabric8/maven/docker/service/RunServiceTest.java @@ -9,16 +9,7 @@ import io.fabric8.maven.docker.access.DockerAccessException; import io.fabric8.maven.docker.access.ExecException; import io.fabric8.maven.docker.access.PortMapping; -import io.fabric8.maven.docker.config.Arguments; -import io.fabric8.maven.docker.config.ImageConfiguration; -import io.fabric8.maven.docker.config.NetworkConfig; -import io.fabric8.maven.docker.config.RestartPolicy; -import io.fabric8.maven.docker.config.RunImageConfiguration; -import io.fabric8.maven.docker.config.RunVolumeConfiguration; -import io.fabric8.maven.docker.config.StopMode; -import io.fabric8.maven.docker.config.UlimitConfig; -import io.fabric8.maven.docker.config.VolumeConfiguration; -import io.fabric8.maven.docker.config.WaitConfiguration; +import io.fabric8.maven.docker.config.*; import io.fabric8.maven.docker.log.LogOutputSpec; import io.fabric8.maven.docker.log.LogOutputSpecFactory; import io.fabric8.maven.docker.model.Container; @@ -404,6 +395,7 @@ private void givenARunConfiguration() { .ports(ports()) .links(links()) .volumes(volumeConfiguration()) + .healthcheck(healthCheckConfiguration()) .dns(dns()) .dnsSearch(dnsSearch()) .privileged(true) @@ -419,6 +411,16 @@ private void givenARunConfiguration() { .build(); } + private HealthCheckConfiguration healthCheckConfiguration() { + return new HealthCheckConfiguration.Builder() + .retries(10) + .cmd(new Arguments(Arrays.asList("CMD", "healthcheck.sh"))) + .timeout("10s") + .interval("10s") + .startPeriod("20s") + .build(); + } + private NetworkConfig networkConfiguration() { NetworkConfig config = new NetworkConfig("custom_network"); config.addAlias("net-alias"); diff --git a/src/test/resources/compose/docker-compose.yml b/src/test/resources/compose/docker-compose.yml index a3b0b7567..f957b84c3 100644 --- a/src/test/resources/compose/docker-compose.yml +++ b/src/test/resources/compose/docker-compose.yml @@ -7,7 +7,7 @@ # - dns (list) # - dns_search (list) # -version: 2.2 +version: 2.4 services: service: cap_add: @@ -61,6 +61,12 @@ services: # ulimits restart: on-failure:1 user: tomcat + healthcheck: + test: curl -s --fail http://localhost:8080/status + interval: 1s + timeout: 3s + start_period: 5s + retries: 100 volumes: - /foo # mount /tmp with rw access control diff --git a/src/test/resources/docker/containerCreateConfigAll.json b/src/test/resources/docker/containerCreateConfigAll.json index 2b266f9d2..ccf400b25 100644 --- a/src/test/resources/docker/containerCreateConfigAll.json +++ b/src/test/resources/docker/containerCreateConfigAll.json @@ -82,6 +82,16 @@ ], "NetworkMode":"custom_network" }, + "Healthcheck": { + "Test": [ + "CMD", + "healthcheck.sh" + ], + "Retries": 10, + "Interval": 10000000000, + "StartPeriod": 20000000000, + "Timeout": 10000000000 + }, "Volumes":{ "/container_tmp":{ From 54c2dc85dfe58a3b93d480e46b5c8600c6e0a547 Mon Sep 17 00:00:00 2001 From: stanislav-shymov Date: Thu, 26 Sep 2024 22:28:13 +0300 Subject: [PATCH 2/9] update of the changelog --- doc/changelog.md | 4 ++-- src/main/java/io/fabric8/maven/docker/StartMojo.java | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/doc/changelog.md b/doc/changelog.md index 434483745..0eb75ab9f 100644 --- a/doc/changelog.md +++ b/doc/changelog.md @@ -4,8 +4,8 @@ * **0.45.1 (2024-09-29)**: - Make copy docker-buildx binary to temporary config directory work on windows too ([1819](https://github.com/fabric8io/docker-maven-plugin/pull/1819)) - Pull FROM images in relative path Dockerfiles ([1823](https://github.com/fabric8io/docker-maven-plugin/issues/1823)) - - Docker-compose healthcheck configuration support - - Docker container wait timeout default value made configurable using defaultContainerWaitTimeout configuration option + - Docker-compose healthcheck configuration support ([1825](https://github.com/fabric8io/docker-maven-plugin/pull/1825)) + - Docker container wait timeout default value made configurable using defaultContainerWaitTimeout configuration option ([1825](https://github.com/fabric8io/docker-maven-plugin/pull/1825)) * **0.45.0 (2024-07-27)**: - Automatically create parent directories of portPropertyFile path ([1761](https://github.com/fabric8io/docker-maven-plugin/pull/1761)) diff --git a/src/main/java/io/fabric8/maven/docker/StartMojo.java b/src/main/java/io/fabric8/maven/docker/StartMojo.java index 0e175c562..2d395af2e 100644 --- a/src/main/java/io/fabric8/maven/docker/StartMojo.java +++ b/src/main/java/io/fabric8/maven/docker/StartMojo.java @@ -100,9 +100,10 @@ public class StartMojo extends AbstractDockerMojo { protected boolean autoCreateCustomNetworks; /** - * Global across all the containers default wait time is milliseconds. + * Overrides the default across all the containers wait time is milliseconds. * Overriding that property might become particularly useful when docker-compose config defines - * the healthchecks, but some containers require more time to become healthy. + * the healthchecks, but the default wait timeout {@link io.fabric8.maven.docker.wait.WaitUtil#DEFAULT_MAX_WAIT} + * is too short for some containers to become healthy. */ @Parameter(property = DOCKER_DEFAULT_CONTAINER_WAIT_TIMEOUT, defaultValue = "10000") protected int defaultContainerWaitTimeout = 10000; From ef2441066adbc3b15285e44aed35838a96fde9aa Mon Sep 17 00:00:00 2001 From: stanislav-shymov Date: Fri, 27 Sep 2024 14:36:36 +0300 Subject: [PATCH 3/9] sonar updates --- .../io/fabric8/maven/docker/StartMojo.java | 4 +- .../docker/access/ContainerCreateConfig.java | 55 ++++++++++--------- .../access/util/ComposeDurationUtil.java | 33 ++++++----- .../compose/DockerComposeServiceWrapper.java | 32 ++++++----- 4 files changed, 70 insertions(+), 54 deletions(-) diff --git a/src/main/java/io/fabric8/maven/docker/StartMojo.java b/src/main/java/io/fabric8/maven/docker/StartMojo.java index 2d395af2e..074cb81bc 100644 --- a/src/main/java/io/fabric8/maven/docker/StartMojo.java +++ b/src/main/java/io/fabric8/maven/docker/StartMojo.java @@ -284,9 +284,7 @@ private void startImage(final ImageConfiguration imageConfig, final Properties projProperties = project.getProperties(); final RunImageConfiguration runConfig = imageConfig.getRunConfiguration(); final PortMapping portMapping = runService.createPortMapping(runConfig, projProperties); - if (!projProperties.containsKey(DOCKER_DEFAULT_CONTAINER_WAIT_TIMEOUT)) { - projProperties.put(DOCKER_DEFAULT_CONTAINER_WAIT_TIMEOUT, defaultContainerWaitTimeout); - } + projProperties.computeIfAbsent(DOCKER_DEFAULT_CONTAINER_WAIT_TIMEOUT, key -> defaultContainerWaitTimeout); final LogDispatcher dispatcher = getLogDispatcher(hub); StartContainerExecutor startExecutor = new StartContainerExecutor.Builder() diff --git a/src/main/java/io/fabric8/maven/docker/access/ContainerCreateConfig.java b/src/main/java/io/fabric8/maven/docker/access/ContainerCreateConfig.java index ab059bd12..3dba4e6c1 100644 --- a/src/main/java/io/fabric8/maven/docker/access/ContainerCreateConfig.java +++ b/src/main/java/io/fabric8/maven/docker/access/ContainerCreateConfig.java @@ -113,33 +113,38 @@ public ContainerCreateConfig exposedPorts(Set portSpecs) { } public ContainerCreateConfig healthcheck(HealthCheckConfiguration healthCheckConfiguration) { - if (healthCheckConfiguration != null) { - JsonObject healthcheck = new JsonObject(); - if (healthCheckConfiguration.getCmd() != null) { - healthcheck.add("Test", JsonFactory.newJsonArray(healthCheckConfiguration.getCmd().asStrings())); - } - if (healthCheckConfiguration.getMode() != HealthCheckMode.none) { - if (healthCheckConfiguration.getRetries() != null) { - healthcheck.add("Retries", new JsonPrimitive(healthCheckConfiguration.getRetries())); - } - if (healthCheckConfiguration.getInterval() != null) { - String intervalValue = healthCheckConfiguration.getInterval(); - String field = "Interval"; - healthcheck.add(field, new JsonPrimitive(goDurationToNanoseconds(intervalValue, field))); - } - if (healthCheckConfiguration.getStartPeriod() != null) { - String field = "StartPeriod"; - String intervalValue = healthCheckConfiguration.getStartPeriod(); - healthcheck.add(field, new JsonPrimitive(goDurationToNanoseconds(intervalValue, field))); - } - if (healthCheckConfiguration.getTimeout() != null) { - String field = "Timeout"; - String intervalValue = healthCheckConfiguration.getTimeout(); - healthcheck.add(field, new JsonPrimitive(goDurationToNanoseconds(intervalValue, field))); - } - } + if (healthCheckConfiguration == null) { + return this; + } + JsonObject healthcheck = new JsonObject(); + if (healthCheckConfiguration.getMode() == HealthCheckMode.none) { + healthcheck.add("Test", JsonFactory.newJsonArray(Collections.singletonList("NONE"))); createConfig.add("Healthcheck", healthcheck); + return this; } + + healthcheck.add("Test", JsonFactory.newJsonArray(healthCheckConfiguration.getCmd().asStrings())); + + if (healthCheckConfiguration.getRetries() != null) { + healthcheck.add("Retries", new JsonPrimitive(healthCheckConfiguration.getRetries())); + } + if (healthCheckConfiguration.getInterval() != null) { + String intervalValue = healthCheckConfiguration.getInterval(); + String field = "Interval"; + healthcheck.add(field, new JsonPrimitive(goDurationToNanoseconds(intervalValue, field))); + } + if (healthCheckConfiguration.getStartPeriod() != null) { + String field = "StartPeriod"; + String intervalValue = healthCheckConfiguration.getStartPeriod(); + healthcheck.add(field, new JsonPrimitive(goDurationToNanoseconds(intervalValue, field))); + } + if (healthCheckConfiguration.getTimeout() != null) { + String field = "Timeout"; + String intervalValue = healthCheckConfiguration.getTimeout(); + healthcheck.add(field, new JsonPrimitive(goDurationToNanoseconds(intervalValue, field))); + } + + createConfig.add("Healthcheck", healthcheck); return this; } diff --git a/src/main/java/io/fabric8/maven/docker/access/util/ComposeDurationUtil.java b/src/main/java/io/fabric8/maven/docker/access/util/ComposeDurationUtil.java index e1f221ea8..2a7b66af3 100644 --- a/src/main/java/io/fabric8/maven/docker/access/util/ComposeDurationUtil.java +++ b/src/main/java/io/fabric8/maven/docker/access/util/ComposeDurationUtil.java @@ -13,16 +13,23 @@ * This implementation doesn't support combinations of timeunits. */ public class ComposeDurationUtil { - private static Pattern SIMPLE_GO_DURATION_FORMAT = Pattern.compile("^([\\d]+)(ns|us|ms|s|m|h|d|w|y)?$"); - private static Map goTypesToJava = new HashMap() {{ - put("ns", TimeUnit.NANOSECONDS); - put("us", TimeUnit.MICROSECONDS); - put("ms", TimeUnit.MILLISECONDS); - put("s", TimeUnit.SECONDS); - put("m", TimeUnit.MINUTES); - put("h", TimeUnit.HOURS); - put("d", TimeUnit.DAYS); - }}; + + private ComposeDurationUtil() { + } + + private static final Pattern SIMPLE_GO_DURATION_FORMAT = Pattern.compile("^([\\d]+)(ns|us|ms|s|m|h|d|w|y)?$"); + private static final Map GO_TYPES_TO_JAVA = new HashMap<>(); + + static { + GO_TYPES_TO_JAVA.put("ns", TimeUnit.NANOSECONDS); + GO_TYPES_TO_JAVA.put("us", TimeUnit.MICROSECONDS); + GO_TYPES_TO_JAVA.put("ms", TimeUnit.MILLISECONDS); + GO_TYPES_TO_JAVA.put("s", TimeUnit.SECONDS); + GO_TYPES_TO_JAVA.put("m", TimeUnit.MINUTES); + GO_TYPES_TO_JAVA.put("h", TimeUnit.HOURS); + GO_TYPES_TO_JAVA.put("d", TimeUnit.DAYS); + } + public static long goDurationToNanoseconds(String goDuration, String field) { requireNonNull(goDuration); @@ -32,12 +39,12 @@ public static long goDurationToNanoseconds(String goDuration, String field) { String message = String.format("Unsupported duration value \"%s\" for the field \"%s\"", goDuration, field); throw new IllegalArgumentException(message); } - long duration = Long.valueOf(matcher.group(1)); + long duration = Long.parseLong(matcher.group(1)); if (matcher.groupCount() == 2 && matcher.group(2) != null) { String type = matcher.group(2); - if (goTypesToJava.containsKey(type)) { - duration = TimeUnit.NANOSECONDS.convert(duration, goTypesToJava.get(type)); + if (GO_TYPES_TO_JAVA.containsKey(type)) { + duration = TimeUnit.NANOSECONDS.convert(duration, GO_TYPES_TO_JAVA.get(type)); } else if ("w".equals(type)) { duration = 7 * TimeUnit.NANOSECONDS.convert(duration, TimeUnit.DAYS); } else if ("y".equals(type)) { diff --git a/src/main/java/io/fabric8/maven/docker/config/handler/compose/DockerComposeServiceWrapper.java b/src/main/java/io/fabric8/maven/docker/config/handler/compose/DockerComposeServiceWrapper.java index bdc6b3c90..c04b28636 100644 --- a/src/main/java/io/fabric8/maven/docker/config/handler/compose/DockerComposeServiceWrapper.java +++ b/src/main/java/io/fabric8/maven/docker/config/handler/compose/DockerComposeServiceWrapper.java @@ -142,28 +142,30 @@ public HealthCheckConfiguration getHealthCheckConfiguration() { Map healthCheckAsMap = (Map) configuration.get("healthcheck"); HealthCheckConfiguration.Builder builder = new HealthCheckConfiguration.Builder(); Object disable = healthCheckAsMap.get("disable"); - if (disable != null && disable instanceof Boolean && (Boolean) disable) { + if (isaBoolean(disable)) { builder.mode(HealthCheckMode.none); return builder.build(); } Object test = healthCheckAsMap.get("test"); - if (test != null) { - if (test instanceof List) { - List cmd = (List) test; - if (cmd.size() > 0 && cmd.get(0).equalsIgnoreCase("NONE")) { - builder.mode(HealthCheckMode.none); - return builder.build(); - } else { - builder.cmd(new Arguments((List) test)); - builder.mode(HealthCheckMode.cmd); - } + if (test == null) { + return null; + } + if (test instanceof List) { + List cmd = (List) test; + if (cmd.size() > 0 && cmd.get(0).equalsIgnoreCase("NONE")) { + builder.mode(HealthCheckMode.none); + return builder.build(); } else { - builder.cmd(new Arguments(Arrays.asList("CMD-SHELL", (String) test))); + builder.cmd(new Arguments((List) test)); builder.mode(HealthCheckMode.cmd); } - enableWaitCondition(WaitCondition.HEALTHY); + } else { + builder.cmd(new Arguments(Arrays.asList("CMD-SHELL", test.toString()))); + builder.mode(HealthCheckMode.cmd); } + enableWaitCondition(WaitCondition.HEALTHY); + Object interval = healthCheckAsMap.get("interval"); if (interval != null) { builder.interval(interval.toString()); @@ -184,6 +186,10 @@ public HealthCheckConfiguration getHealthCheckConfiguration() { return builder.build(); } + private static boolean isaBoolean(Object disable) { + return disable != null && disable instanceof Boolean && (Boolean) disable; + } + /** * Docker Compose Spec v2.1+ defined conditions */ From e5bd40d625ebeecee8dd1f154940675dd39724ae Mon Sep 17 00:00:00 2001 From: stanislav-shymov Date: Fri, 27 Sep 2024 16:36:43 +0300 Subject: [PATCH 4/9] sonar --- .../compose/DockerComposeServiceWrapper.java | 4 +- .../DockerComposeConfigHandlerTest.java | 111 ++++++++++++++---- src/test/resources/compose/docker-compose.yml | 4 +- .../compose/docker-compose_healthcheck.yml | 45 +++++++ 4 files changed, 139 insertions(+), 25 deletions(-) create mode 100644 src/test/resources/compose/docker-compose_healthcheck.yml diff --git a/src/main/java/io/fabric8/maven/docker/config/handler/compose/DockerComposeServiceWrapper.java b/src/main/java/io/fabric8/maven/docker/config/handler/compose/DockerComposeServiceWrapper.java index c04b28636..e19f828cd 100644 --- a/src/main/java/io/fabric8/maven/docker/config/handler/compose/DockerComposeServiceWrapper.java +++ b/src/main/java/io/fabric8/maven/docker/config/handler/compose/DockerComposeServiceWrapper.java @@ -175,7 +175,7 @@ public HealthCheckConfiguration getHealthCheckConfiguration() { builder.timeout(timeout.toString()); } Object retries = healthCheckAsMap.get("retries"); - if (retries != null && retries instanceof Number) { + if (retries instanceof Number) { builder.retries(((Number) retries).intValue()); } Object startPeriod = healthCheckAsMap.get("start_period"); @@ -187,7 +187,7 @@ public HealthCheckConfiguration getHealthCheckConfiguration() { } private static boolean isaBoolean(Object disable) { - return disable != null && disable instanceof Boolean && (Boolean) disable; + return disable instanceof Boolean && (Boolean) disable; } /** diff --git a/src/test/java/io/fabric8/maven/docker/config/handler/compose/DockerComposeConfigHandlerTest.java b/src/test/java/io/fabric8/maven/docker/config/handler/compose/DockerComposeConfigHandlerTest.java index 44769222a..1896146d6 100644 --- a/src/test/java/io/fabric8/maven/docker/config/handler/compose/DockerComposeConfigHandlerTest.java +++ b/src/test/java/io/fabric8/maven/docker/config/handler/compose/DockerComposeConfigHandlerTest.java @@ -1,10 +1,6 @@ package io.fabric8.maven.docker.config.handler.compose; -import io.fabric8.maven.docker.config.ImageConfiguration; -import io.fabric8.maven.docker.config.NetworkConfig; -import io.fabric8.maven.docker.config.RestartPolicy; -import io.fabric8.maven.docker.config.RunImageConfiguration; -import io.fabric8.maven.docker.config.RunVolumeConfiguration; +import io.fabric8.maven.docker.config.*; import io.fabric8.maven.docker.config.handler.ExternalConfigHandlerException; import org.apache.commons.io.FileUtils; import org.apache.maven.execution.MavenSession; @@ -27,11 +23,9 @@ import java.io.InputStream; import java.net.URL; import java.nio.file.Path; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; + +import static org.junit.jupiter.api.Assertions.fail; /** * @author roland @@ -77,7 +71,67 @@ void simpleExtraIn24() throws IOException, MavenFilteringException { RunImageConfiguration runConfig = configs.get(0).getRunConfiguration(); Assertions.assertEquals("linux/amd64", runConfig.getPlatform()); } - + + @Test + void testHealthcheckInherited() throws IOException, MavenFilteringException { + setupComposeExpectations("docker-compose_healthcheck.yml"); + List configs = handler.resolve(unresolved, project, session); + RunImageConfiguration runConfig = findRunConfigurationByAlias(configs, "serviceHealthcheckInherited"); + Assertions.assertEquals(HealthCheckMode.cmd, runConfig.getHealthCheckConfiguration().getMode()); + Assertions.assertEquals(0, runConfig.getHealthCheckConfiguration().getCmd().asStrings().size()); + Assertions.assertNull(runConfig.getHealthCheckConfiguration().getRetries()); + Assertions.assertEquals("1s", runConfig.getHealthCheckConfiguration().getInterval()); + Assertions.assertEquals("3s", runConfig.getHealthCheckConfiguration().getTimeout()); + Assertions.assertEquals("5s", runConfig.getHealthCheckConfiguration().getStartPeriod()); + } + + @Test + void testHealthcheckCmd() throws IOException, MavenFilteringException { + setupComposeExpectations("docker-compose_healthcheck.yml"); + List configs = handler.resolve(unresolved, project, session); + RunImageConfiguration runConfig = findRunConfigurationByAlias(configs, "serviceHealthcheck_CMD"); + Assertions.assertEquals(HealthCheckMode.cmd, runConfig.getHealthCheckConfiguration().getMode()); + Assertions.assertEquals(5, runConfig.getHealthCheckConfiguration().getCmd().asStrings().size()); + Assertions.assertEquals("CMD", runConfig.getHealthCheckConfiguration().getCmd().asStrings().get(0)); + Assertions.assertEquals(100, runConfig.getHealthCheckConfiguration().getRetries()); + Assertions.assertEquals("1s", runConfig.getHealthCheckConfiguration().getInterval()); + Assertions.assertEquals("3s", runConfig.getHealthCheckConfiguration().getTimeout()); + Assertions.assertEquals("5s", runConfig.getHealthCheckConfiguration().getStartPeriod()); + } + + @Test + void testHealthcheckCmdShell() throws IOException, MavenFilteringException { + setupComposeExpectations("docker-compose_healthcheck.yml"); + List configs = handler.resolve(unresolved, project, session); + RunImageConfiguration runConfig = findRunConfigurationByAlias(configs, "serviceHealthcheckSame_CMD-SHELL"); + Assertions.assertEquals(HealthCheckMode.cmd, runConfig.getHealthCheckConfiguration().getMode()); + Assertions.assertEquals(2, runConfig.getHealthCheckConfiguration().getCmd().asStrings().size()); + Assertions.assertEquals("CMD-SHELL", runConfig.getHealthCheckConfiguration().getCmd().asStrings().get(0)); + Assertions.assertEquals(100, runConfig.getHealthCheckConfiguration().getRetries()); + Assertions.assertEquals("1s", runConfig.getHealthCheckConfiguration().getInterval()); + Assertions.assertEquals("3s", runConfig.getHealthCheckConfiguration().getTimeout()); + Assertions.assertEquals("5s", runConfig.getHealthCheckConfiguration().getStartPeriod()); + } + + @Test + void testHealthcheckDisabled() throws IOException, MavenFilteringException { + setupComposeExpectations("docker-compose_healthcheck.yml"); + List configs = handler.resolve(unresolved, project, session); + Arrays.asList("serviceDisabledHealthcheck", "serviceDisabledHealthcheck2") + .stream() + .map(alias -> findRunConfigurationByAlias(configs, alias)) + .forEach(runConfig -> { + Assertions.assertNotNull(runConfig); + Assertions.assertEquals(HealthCheckMode.none, runConfig.getHealthCheckConfiguration().getMode()); + Assertions.assertNull(runConfig.getHealthCheckConfiguration().getCmd()); + Assertions.assertNull(runConfig.getHealthCheckConfiguration().getRetries()); + Assertions.assertNull(runConfig.getHealthCheckConfiguration().getInterval()); + Assertions.assertNull(runConfig.getHealthCheckConfiguration().getTimeout()); + Assertions.assertNull(runConfig.getHealthCheckConfiguration().getStartPeriod()); + }); + + } + @Test void networkAliases() throws IOException, MavenFilteringException { setupComposeExpectations("docker-compose-network-aliases.yml"); @@ -101,7 +155,7 @@ void networkAliases() throws IOException, MavenFilteringException { Assertions.assertEquals(1, netSvc.getAliases().size()); Assertions.assertEquals("alias1", netSvc.getAliases().get(0)); } - + @Test void longDependsOn() throws IOException, MavenFilteringException { setupComposeExpectations("dependsOn/long-valid.yml"); @@ -110,43 +164,43 @@ void longDependsOn() throws IOException, MavenFilteringException { Assertions.assertEquals(Arrays.asList("service2", "service3"), configs.get(0).getRunConfiguration().getDependsOn()); Assertions.assertEquals(Arrays.asList("service3", "service4", "service5"), configs.get(1).getRunConfiguration().getDependsOn()); } - + @Test void dependsOnUndefServiceRef() throws IOException, MavenFilteringException { setupComposeExpectations("dependsOn/long-undef-ref.yml"); Assertions.assertThrows(IllegalArgumentException.class, () -> handler.resolve(unresolved, project, session)); - + setupComposeExpectations("dependsOn/short-undef-ref.yml"); Assertions.assertThrows(IllegalArgumentException.class, () -> handler.resolve(unresolved, project, session)); } - + @Test void dependsOnServiceSelfRef() throws IOException, MavenFilteringException { setupComposeExpectations("dependsOn/long-selfref.yml"); Assertions.assertThrows(IllegalArgumentException.class, () -> handler.resolve(unresolved, project, session)); - + setupComposeExpectations("dependsOn/short-selfref.yml"); Assertions.assertThrows(IllegalArgumentException.class, () -> handler.resolve(unresolved, project, session)); } - + @Test void dependsOnServiceButNoConfig() throws IOException, MavenFilteringException { setupComposeExpectations("dependsOn/long-map-keys-only.yml"); Assertions.assertThrows(IllegalArgumentException.class, () -> handler.resolve(unresolved, project, session)); } - + @Test void dependsOnServiceButMissingCondition() throws IOException, MavenFilteringException { setupComposeExpectations("dependsOn/long-no-condition.yml"); Assertions.assertThrows(IllegalArgumentException.class, () -> handler.resolve(unresolved, project, session)); } - + @Test void dependsOnServiceButInvalidCondition() throws IOException, MavenFilteringException { setupComposeExpectations("dependsOn/long-invalid-condition.yml"); Assertions.assertThrows(IllegalArgumentException.class, () -> handler.resolve(unresolved, project, session)); } - + @Test void dependsOnServiceButConflictingConditions() throws IOException, MavenFilteringException { setupComposeExpectations("dependsOn/long-conflicting-condition.yml"); @@ -219,6 +273,13 @@ void validateRunConfiguration(RunImageConfiguration runConfig) { Assertions.assertEquals(1.5, runConfig.getCpus()); Assertions.assertEquals("default", runConfig.getIsolation()); Assertions.assertEquals((Long) 1L, runConfig.getCpuShares()); + + Assertions.assertEquals(HealthCheckMode.cmd, runConfig.getHealthCheckConfiguration().getMode()); + Assertions.assertEquals(Arrays.asList("CMD-SHELL", "curl -s --fail http://localhost:8080/status"), + runConfig.getHealthCheckConfiguration().getCmd().asStrings()); + Assertions.assertEquals("1s", runConfig.getHealthCheckConfiguration().getInterval()); + Assertions.assertEquals("3s", runConfig.getHealthCheckConfiguration().getTimeout()); + Assertions.assertNull(runConfig.getEnvPropertyFile()); Assertions.assertNull(runConfig.getPortPropertyFile()); @@ -302,4 +363,14 @@ protected List a(String... args) { return Arrays.asList(args); } + private static RunImageConfiguration findRunConfigurationByAlias(List configs, String alias) { + try { + return configs.stream() + .filter(rc -> rc.getAlias().equals(alias)) + .findFirst().get().getRunConfiguration(); + } catch (NoSuchElementException e) { + fail("Service configuration is not found for alias: "+alias); + return null; + } + } } diff --git a/src/test/resources/compose/docker-compose.yml b/src/test/resources/compose/docker-compose.yml index f957b84c3..79e76d38c 100644 --- a/src/test/resources/compose/docker-compose.yml +++ b/src/test/resources/compose/docker-compose.yml @@ -7,7 +7,7 @@ # - dns (list) # - dns_search (list) # -version: 2.4 +version: 2.2 services: service: cap_add: @@ -65,8 +65,6 @@ services: test: curl -s --fail http://localhost:8080/status interval: 1s timeout: 3s - start_period: 5s - retries: 100 volumes: - /foo # mount /tmp with rw access control diff --git a/src/test/resources/compose/docker-compose_healthcheck.yml b/src/test/resources/compose/docker-compose_healthcheck.yml new file mode 100644 index 000000000..e2d86a73e --- /dev/null +++ b/src/test/resources/compose/docker-compose_healthcheck.yml @@ -0,0 +1,45 @@ +version: 2.4 +services: + + serviceHealthcheck_CMD: + image: image + platform: linux/amd64 + healthcheck: + test: [ "CMD", "curl", "-s", "--fail", "http://localhost:8080/status" ] + interval: 1s + timeout: 3s + start_period: 5s + retries: 100 + + serviceHealthcheckSame_CMD-SHELL: + image: image + platform: linux/amd64 + healthcheck: + test: curl -s --fail http://localhost:8080/status 2>&1 + interval: 1s + timeout: 3s + start_period: 5s + retries: 100 + + serviceHealthcheckInherited: + image: image + platform: linux/amd64 + healthcheck: + test: [] # inherit value from the image + interval: 1s + timeout: 3s + start_period: 5s + + serviceDisabledHealthcheck: + image: image + platform: linux/amd64 + healthcheck: + disable: true # disable healthcheck + test: [] + + + serviceDisabledHealthcheck2: + image: image + platform: linux/amd64 + healthcheck: + test: [ "NONE" ] # another way to disable healthcheck From d6276888ac8419f6c590ef6972541f276a25fc1e Mon Sep 17 00:00:00 2001 From: stanislav-shymov Date: Fri, 27 Sep 2024 22:16:11 +0300 Subject: [PATCH 5/9] doc update --- doc/changelog.md | 2 +- src/main/asciidoc/inc/external/_docker_compose.adoc | 2 +- src/main/asciidoc/inc/start/_configuration.adoc | 6 ++++++ src/main/java/io/fabric8/maven/docker/StartMojo.java | 7 ++++--- .../maven/docker/access/util/ComposeDurationUtilTest.java | 4 +++- 5 files changed, 15 insertions(+), 6 deletions(-) diff --git a/doc/changelog.md b/doc/changelog.md index 0eb75ab9f..eb0d88230 100644 --- a/doc/changelog.md +++ b/doc/changelog.md @@ -5,7 +5,7 @@ - Make copy docker-buildx binary to temporary config directory work on windows too ([1819](https://github.com/fabric8io/docker-maven-plugin/pull/1819)) - Pull FROM images in relative path Dockerfiles ([1823](https://github.com/fabric8io/docker-maven-plugin/issues/1823)) - Docker-compose healthcheck configuration support ([1825](https://github.com/fabric8io/docker-maven-plugin/pull/1825)) - - Docker container wait timeout default value made configurable using defaultContainerWaitTimeout configuration option ([1825](https://github.com/fabric8io/docker-maven-plugin/pull/1825)) + - Docker container wait timeout default value made configurable using startContainerWaitTimeout configuration option ([1825](https://github.com/fabric8io/docker-maven-plugin/pull/1825)) * **0.45.0 (2024-07-27)**: - Automatically create parent directories of portPropertyFile path ([1761](https://github.com/fabric8io/docker-maven-plugin/pull/1761)) diff --git a/src/main/asciidoc/inc/external/_docker_compose.adoc b/src/main/asciidoc/inc/external/_docker_compose.adoc index 35707be13..abc613659 100644 --- a/src/main/asciidoc/inc/external/_docker_compose.adoc +++ b/src/main/asciidoc/inc/external/_docker_compose.adoc @@ -55,7 +55,7 @@ In addition to the `docker-compose.yml` you can add all known options for <> attached to dependent containers, while Docker Compose applies checks when starting the depending container. Keep in mind that execution of a container is continued as soon as any wait condition is fulfilled. \ No newline at end of file diff --git a/src/main/asciidoc/inc/start/_configuration.adoc b/src/main/asciidoc/inc/start/_configuration.adoc index 6966d7673..3feef06c0 100644 --- a/src/main/asciidoc/inc/start/_configuration.adoc +++ b/src/main/asciidoc/inc/start/_configuration.adoc @@ -19,6 +19,12 @@ In addition to the <>, this goal supports the following gl | Starts docker images in parallel while dependencies expressed as <> or <> are respected. This option can significantly reduce the startup time because independent containers do not need to wait for each other. | `docker.startParallel` +| *startContainerWaitTimeout* +| Overrides the default across all the containers wait timeout (