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..e22aadc26 100644 --- a/src/main/java/io/fabric8/maven/docker/access/ContainerCreateConfig.java +++ b/src/main/java/io/fabric8/maven/docker/access/ContainerCreateConfig.java @@ -138,6 +138,10 @@ public ContainerCreateConfig hostConfig(ContainerHostConfig startConfig) { public ContainerCreateConfig networkingConfig(ContainerNetworkingConfig networkingConfig) { return add("NetworkingConfig", networkingConfig.toJsonObject()); } + + public ContainerCreateConfig healthCheck(ContainerHealthCheckConfig healthCheckConfig) { + return add("Healthcheck", healthCheckConfig.toJsonObject()); + } /** * Get JSON which is used for creating a container diff --git a/src/main/java/io/fabric8/maven/docker/access/ContainerHealthCheckConfig.java b/src/main/java/io/fabric8/maven/docker/access/ContainerHealthCheckConfig.java new file mode 100644 index 000000000..933a69793 --- /dev/null +++ b/src/main/java/io/fabric8/maven/docker/access/ContainerHealthCheckConfig.java @@ -0,0 +1,54 @@ +package io.fabric8.maven.docker.access; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import io.fabric8.maven.docker.config.HealthCheckConfiguration; +import io.fabric8.maven.docker.config.HealthCheckConfiguration.DurationParser; + +public class ContainerHealthCheckConfig { + + private final JsonObject healthcheck = new JsonObject(); + + public ContainerHealthCheckConfig(HealthCheckConfiguration configuration) { + JsonArray test = new JsonArray(); + switch (configuration.getMode()) { + case none: + test.add("NONE"); + break; + case cmd: + test.add("CMD"); + for (String arg : configuration.getCmd().asStrings()) { + test.add(arg); + } + break; + case shell: + test.add("CMD-SHELL"); + test.add(configuration.getCmd().getShell()); + break; + case inherit: + // case inherit can be ignored here - equivalent to an empty JSON array + } + this.healthcheck.add("Test", test); + + if (configuration.getInterval() != null) { + this.healthcheck.addProperty("Interval", DurationParser.parseDuration(configuration.getInterval()).toNanos()); + } + if (configuration.getTimeout() != null) { + this.healthcheck.addProperty("Timeout", DurationParser.parseDuration(configuration.getTimeout()).toNanos()); + } + if (configuration.getStartPeriod() != null) { + this.healthcheck.addProperty("StartPeriod", DurationParser.parseDuration(configuration.getStartPeriod()).toNanos()); + } + if (configuration.getRetries() != null) { + this.healthcheck.addProperty("Retries", configuration.getRetries()); + } + } + + public String toJson() { + return healthcheck.toString(); + } + + public JsonObject toJsonObject() { + return healthcheck; + } +} diff --git a/src/main/java/io/fabric8/maven/docker/assembly/DockerFileBuilder.java b/src/main/java/io/fabric8/maven/docker/assembly/DockerFileBuilder.java index 1560ccac5..4d9255199 100644 --- a/src/main/java/io/fabric8/maven/docker/assembly/DockerFileBuilder.java +++ b/src/main/java/io/fabric8/maven/docker/assembly/DockerFileBuilder.java @@ -8,6 +8,7 @@ import com.google.common.base.Joiner; import io.fabric8.maven.docker.config.Arguments; +import io.fabric8.maven.docker.config.BuildImageConfiguration; import io.fabric8.maven.docker.config.HealthCheckConfiguration; import org.codehaus.plexus.util.FileUtils; @@ -134,20 +135,24 @@ private void addUser(StringBuilder b) { private void addHealthCheck(StringBuilder b) { if (healthCheck != null) { StringBuilder healthString = new StringBuilder(); - + + // Context is image building, thus default to Dockerfile CMD mode (unequal to runtime version!) + // Note: usually done via BuildImageConfiguration.initAndValidate(), but not with low-level unit tests. + healthCheck.setModeIfNotPresent(BuildImageConfiguration.HC_BUILDTIME_DEFAULT); + switch (healthCheck.getMode()) { - case cmd: - buildOption(healthString, DockerFileOption.HEALTHCHECK_INTERVAL, healthCheck.getInterval()); - buildOption(healthString, DockerFileOption.HEALTHCHECK_TIMEOUT, healthCheck.getTimeout()); - buildOption(healthString, DockerFileOption.HEALTHCHECK_START_PERIOD, healthCheck.getStartPeriod()); - buildOption(healthString, DockerFileOption.HEALTHCHECK_RETRIES, healthCheck.getRetries()); - buildArguments(healthString, DockerFileKeyword.CMD, false, healthCheck.getCmd()); - break; - case none: - DockerFileKeyword.NONE.addTo(healthString, false); - break; - default: - throw new IllegalArgumentException("Unsupported health check mode: " + healthCheck.getMode()); + case cmd: + buildOption(healthString, DockerFileOption.HEALTHCHECK_INTERVAL, healthCheck.getInterval()); + buildOption(healthString, DockerFileOption.HEALTHCHECK_TIMEOUT, healthCheck.getTimeout()); + buildOption(healthString, DockerFileOption.HEALTHCHECK_START_PERIOD, healthCheck.getStartPeriod()); + buildOption(healthString, DockerFileOption.HEALTHCHECK_RETRIES, healthCheck.getRetries()); + buildArguments(healthString, DockerFileKeyword.CMD, false, healthCheck.getCmd()); + break; + case none: + DockerFileKeyword.NONE.addTo(healthString, false); + break; + default: + throw new IllegalArgumentException("Unsupported build time health check mode: " + healthCheck.getMode() + " - use 'cmd' or 'none'"); } DockerFileKeyword.HEALTHCHECK.addTo(b, healthString.toString()); diff --git a/src/main/java/io/fabric8/maven/docker/config/BuildImageConfiguration.java b/src/main/java/io/fabric8/maven/docker/config/BuildImageConfiguration.java index 7c378851a..598c3a86f 100644 --- a/src/main/java/io/fabric8/maven/docker/config/BuildImageConfiguration.java +++ b/src/main/java/io/fabric8/maven/docker/config/BuildImageConfiguration.java @@ -753,6 +753,8 @@ public BuildImageConfiguration build() { return config; } } + + public static final HealthCheckMode HC_BUILDTIME_DEFAULT = HealthCheckMode.cmd; public String initAndValidate(Logger log) throws IllegalArgumentException { if (entryPoint != null) { @@ -762,6 +764,8 @@ public String initAndValidate(Logger log) throws IllegalArgumentException { cmd.validate(); } if (healthCheck != null) { + // Context is image building, thus default to Dockerfile CMD mode (unequal to runtime version!) + healthCheck.setModeIfNotPresent(HC_BUILDTIME_DEFAULT); healthCheck.validate(); } diff --git a/src/main/java/io/fabric8/maven/docker/config/ConfigHelper.java b/src/main/java/io/fabric8/maven/docker/config/ConfigHelper.java index df30b9c87..ed67c2630 100644 --- a/src/main/java/io/fabric8/maven/docker/config/ConfigHelper.java +++ b/src/main/java/io/fabric8/maven/docker/config/ConfigHelper.java @@ -117,12 +117,20 @@ public static String getExternalConfigActivationProperty(MavenProject project) { * @param nameFormatter formatter for image names * @param log a logger for printing out diagnostic messages * @return the minimal API Docker API required to be used for the given configuration. + * @throws IllegalArgumentException When an image validation fails, the thrown exception will contain + * the image's name as its message and wrap the underlying exception as cause. */ public static String initAndValidate(List images, String apiVersion, NameFormatter nameFormatter, Logger log) { // Init and validate configs. After this step, getResolvedImages() contains the valid configuration. for (ImageConfiguration imageConfiguration : images) { - apiVersion = EnvUtil.extractLargerVersion(apiVersion, imageConfiguration.initAndValidate(nameFormatter, log)); + try { + apiVersion = EnvUtil.extractLargerVersion(apiVersion, imageConfiguration.initAndValidate(nameFormatter, log)); + } catch (IllegalArgumentException e) { + // Wrap the underlying validation and add the image's name/alias. + // Maven will properly unpack the wrapped exception. + throw new IllegalArgumentException(imageConfiguration.getName(), e); + } } return apiVersion; } diff --git a/src/main/java/io/fabric8/maven/docker/config/HealthCheckConfiguration.java b/src/main/java/io/fabric8/maven/docker/config/HealthCheckConfiguration.java index 7cd8154d1..358e6b8bf 100644 --- a/src/main/java/io/fabric8/maven/docker/config/HealthCheckConfiguration.java +++ b/src/main/java/io/fabric8/maven/docker/config/HealthCheckConfiguration.java @@ -1,13 +1,19 @@ package io.fabric8.maven.docker.config; +import org.apache.commons.lang3.StringUtils; + import java.io.Serializable; +import java.time.Duration; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** * Build configuration for health checks. */ public class HealthCheckConfiguration implements Serializable { - private HealthCheckMode mode = HealthCheckMode.cmd; + // Default values are applied differently in build or runtime context, no default here + private HealthCheckMode mode; private String interval; @@ -19,6 +25,7 @@ public class HealthCheckConfiguration implements Serializable { private Arguments cmd; + // This constructor must remain "public" as this class is deserialized from XML config public HealthCheckConfiguration() {} public String getInterval() { @@ -48,7 +55,19 @@ public Arguments getCmd() { public HealthCheckMode getMode() { return mode; } - + + /** + * Use this method to apply a default mode depending on context (build or runtime) + * @param mode The default mode to set + * @return The configuration, making the call chainable + */ + public HealthCheckConfiguration setModeIfNotPresent(HealthCheckMode mode) { + if (this.mode == null) { + this.mode = mode; + } + return this; + } + public Integer getRetries() { return retries; } @@ -59,23 +78,59 @@ public void validate() throws IllegalArgumentException { } switch(mode) { - case none: - if (interval != null || timeout != null || startPeriod != null || retries != null || cmd != null) { - throw new IllegalArgumentException("HealthCheck: no parameters are allowed when the health check mode is set to 'none'"); - } - break; - case cmd: - if (cmd == null) { - throw new IllegalArgumentException("HealthCheck: the parameter 'cmd' is mandatory when the health check mode is set to 'cmd' (default)"); - } + case none: + if (interval != null || timeout != null || startPeriod != null || retries != null || cmd != null) { + throw new IllegalArgumentException("HealthCheck: no parameters are allowed when the health check mode is set to 'none'"); + } + break; + case cmd: + case shell: + if (cmd == null) { + throw new IllegalArgumentException("HealthCheck: parameter 'cmd' is mandatory for mode set to 'cmd' (default for builds) or 'shell'"); + } + // cmd.getExec() == null can be ignored here - we will simply parse the string into arguments + if (mode == HealthCheckMode.shell && cmd.getShell() == null) { + throw new IllegalArgumentException("HealthCheck: parameter 'cmd' for mode 'shell' must be given as one string, not arguments"); + } + // Now fallthrough to mode inherit (which has needs the same validations for options, but not the test) + case inherit: + if (retries != null && retries < 0) { + throw new IllegalArgumentException("HealthCheck: the parameter 'retries' may not be negative"); + } + if (interval != null && ! DurationParser.matchesDuration(interval)) { + throw new IllegalArgumentException("HealthCheck: illegal duration specified for interval"); + } + if (timeout != null && ! DurationParser.matchesDuration(timeout)) { + throw new IllegalArgumentException("HealthCheck: illegal duration specified for timeout"); + } + if (startPeriod != null && ! DurationParser.matchesDuration(startPeriod)) { + throw new IllegalArgumentException("HealthCheck: illegal duration specified for start period"); + } + // Must limit check to inherit *again* because shell and cmd fall through to this case! + if (mode == HealthCheckMode.inherit && cmd != null) { + throw new IllegalArgumentException("HealthCheck: parameter 'cmd' not allowed for mode set to 'inherit'"); + } + break; } } - + + @Override + public String toString() { + return "HealthCheckConfiguration{" + + "mode=" + mode + + ", interval='" + interval + '\'' + + ", timeout='" + timeout + '\'' + + ", startPeriod='" + startPeriod + '\'' + + ", retries=" + retries + + ", cmd=" + cmd + + '}'; + } + // =========================================== public static class Builder { - private HealthCheckConfiguration config = new HealthCheckConfiguration(); + private final HealthCheckConfiguration config; public Builder() { this.config = new HealthCheckConfiguration(); @@ -109,7 +164,7 @@ public Builder retries(Integer retries) { } public Builder mode(String mode) { - return this.mode(mode != null ? HealthCheckMode.valueOf(mode) : (HealthCheckMode) null); + return this.mode(mode != null ? HealthCheckMode.valueOf(mode) : null); } public Builder mode(HealthCheckMode mode) { @@ -121,4 +176,77 @@ public HealthCheckConfiguration build() { return config; } } + + public static final class DurationParser { + + // No instances allowed + private DurationParser() {} + + /** + * This complex regex allows duration in the special Docker format, + * which is not ISO-8601 compatible, and thus not parseable directly. + * (For example, it does not allow using days or even longer periods) + * @implSpec See Docker Compose durations for supported duration formats. + * Dockerfile HEALTHCHECK has only very limited specification about allowed duration formatting. + * @implNote Note that the Docker API requires nanosecond precision (int64/long). + * A conversion is easily done using {@link Duration#toNanos()}. + * Examples of allowed values: 23h17m1s, 10ms, 1s, 0h10ms, 1h2m1.3432s + */ + @SuppressWarnings("java:S5843") + private static final String DURATION_REGEX = "^((?0\\d|1\\d|2[0-3]|\\d)h)?((?[0-5]?\\d)m)?(((?[0-5]?\\d)s)?((?\\d{1,3})ms)?((?\\d{1,3})us)?|(?[0-5]?\\d)\\.(?\\d{1,9})s)$"; + private static final Matcher durationMatcher = Pattern.compile(DURATION_REGEX).matcher(""); + + public static boolean matchesDuration(String durationString) { + if (durationString == null || durationString.isEmpty()) { + return false; + } + return durationMatcher.reset(durationString).matches() || durationString.equals("0"); + } + + public static Duration parseDuration(String durationString) { + if (durationString == null || durationString.isEmpty()) { + return null; + } + + if (durationString.equals("0")) { + return Duration.ZERO; + } + + if (durationMatcher.reset(durationString).matches()) { + Duration duration = Duration.ZERO; + // Add hours + if (durationMatcher.group("hours") != null) { + duration = duration.plusHours(Long.parseLong(durationMatcher.group("hours"))); + } + // Add minutes + if (durationMatcher.group("mins") != null) { + duration = duration.plusMinutes(Long.parseLong(durationMatcher.group("mins"))); + } + // When seconds are given as an (optional) fraction + if (durationMatcher.group("fsecs") != null) { + duration = duration.plusSeconds(Long.parseLong(durationMatcher.group("fsecs"))); + + String fraction = durationMatcher.group("fraction"); + // Append enough zeros to make it nanosecond precision, then tune the duration + fraction += StringUtils.repeat("0", 9 - fraction.length()); + duration = duration.plusNanos(Long.parseLong(fraction)); + } else { + // Add seconds + if (durationMatcher.group("secs") != null) { + duration = duration.plusSeconds(Long.parseLong(durationMatcher.group("secs"))); + } + // Add milliseconds + if (durationMatcher.group("msecs") != null) { + duration = duration.plusMillis(Long.parseLong(durationMatcher.group("msecs"))); + } + // Add microseconds (make them fake nanoseconds first, as Duration does not support adding micros) + if (durationMatcher.group("usecs") != null) { + duration = duration.plusNanos(Long.parseLong(durationMatcher.group("usecs") + "000")); + } + } + return duration; + } + return null; + } + } } diff --git a/src/main/java/io/fabric8/maven/docker/config/HealthCheckMode.java b/src/main/java/io/fabric8/maven/docker/config/HealthCheckMode.java index cce8a4b17..c7278caa5 100644 --- a/src/main/java/io/fabric8/maven/docker/config/HealthCheckMode.java +++ b/src/main/java/io/fabric8/maven/docker/config/HealthCheckMode.java @@ -1,16 +1,30 @@ package io.fabric8.maven.docker.config; +@SuppressWarnings("java:S115") public enum HealthCheckMode { /** * Mainly used to disable any health check provided by the base image. + * This mode is supported at build and run time. */ none, /** - * A command based health check. + * A command-based health check. + * This mode is supported at build and run time. */ - cmd; + cmd, + + /** + * A shell-wrapped command-based health check. + * This mode is supported at runtime only. + */ + shell, + + /** + * Runtime-only mode, used to change options, but not the test itself + */ + inherit } diff --git a/src/main/java/io/fabric8/maven/docker/config/ImageConfiguration.java b/src/main/java/io/fabric8/maven/docker/config/ImageConfiguration.java index 78cbb68c3..6630549a3 100644 --- a/src/main/java/io/fabric8/maven/docker/config/ImageConfiguration.java +++ b/src/main/java/io/fabric8/maven/docker/config/ImageConfiguration.java @@ -197,7 +197,7 @@ public String toString() { return String.format("ImageConfiguration {name='%s', alias='%s'}", name, alias); } - public String initAndValidate(ConfigHelper.NameFormatter nameFormatter, Logger log) { + public String initAndValidate(ConfigHelper.NameFormatter nameFormatter, Logger log) throws IllegalArgumentException { name = nameFormatter.format(name); String minimalApiVersion = null; if (build != null) { 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..3293b4b20 100644 --- a/src/main/java/io/fabric8/maven/docker/config/RunImageConfiguration.java +++ b/src/main/java/io/fabric8/maven/docker/config/RunImageConfiguration.java @@ -208,6 +208,12 @@ public class RunImageConfiguration implements Serializable { // How to stop a container @Parameter private StopMode stopMode; + + /** + * Add new or override build time provided healthcheck + */ + @Parameter + private HealthCheckConfiguration healthCheck; public RunImageConfiguration() { } @@ -222,14 +228,27 @@ public String initAndValidate() { if (cmd != null) { cmd.validate(); } + if (healthCheck != null) { + // Context is running an image, thus default to inheriting a check defined at build time or parent image(s) + // (Which still allows to change any option while keeping the test itself!) + healthCheck.setModeIfNotPresent(HealthCheckMode.inherit); + healthCheck.validate(); + } + + String minimalApiVersion = null; // Custom networks are available since API 1.21 (Docker 1.9) NetworkConfig config = getNetworkingConfig(); if (config != null && config.isCustomNetwork()) { - return "1.21"; + minimalApiVersion = "1.21"; + } + + // Runtime provided healthchecks are available since API 1.24 (Docker 1.12) + if (healthCheck != null) { + minimalApiVersion = "1.24"; } - return null; + return minimalApiVersion; } public Map getEnv() { @@ -447,6 +466,10 @@ public StopMode getStopMode() { } return stopMode; } + + public HealthCheckConfiguration getHealthCheck() { + return this.healthCheck; + } /** * @deprecated use {@link #getContainerNamePattern} instead @@ -725,6 +748,11 @@ public Builder autoRemove(Boolean autoRemove) { config.autoRemove = autoRemove; return this; } + + public Builder healthCheck(HealthCheckConfiguration configuration) { + config.healthCheck = configuration; + return this; + } public RunImageConfiguration build() { return config; diff --git a/src/main/java/io/fabric8/maven/docker/config/handler/property/PropertyConfigHandler.java b/src/main/java/io/fabric8/maven/docker/config/handler/property/PropertyConfigHandler.java index 366e8f5e6..3a1d02d40 100644 --- a/src/main/java/io/fabric8/maven/docker/config/handler/property/PropertyConfigHandler.java +++ b/src/main/java/io/fabric8/maven/docker/config/handler/property/PropertyConfigHandler.java @@ -314,7 +314,8 @@ private HealthCheckConfiguration extractHealthCheck(HealthCheckConfiguration con return config; } if (config == null) { - config = new HealthCheckConfiguration(); + // Create empty config to allow null safe fallback value requests during extraction + config = new HealthCheckConfiguration.Builder().build(); } return new HealthCheckConfiguration.Builder() 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..2d4a7e305 100644 --- a/src/main/java/io/fabric8/maven/docker/service/RunService.java +++ b/src/main/java/io/fabric8/maven/docker/service/RunService.java @@ -36,6 +36,7 @@ import java.util.concurrent.ExecutionException; import io.fabric8.maven.docker.access.ContainerCreateConfig; +import io.fabric8.maven.docker.access.ContainerHealthCheckConfig; import io.fabric8.maven.docker.access.ContainerHostConfig; import io.fabric8.maven.docker.access.ContainerNetworkingConfig; import io.fabric8.maven.docker.access.DockerAccess; @@ -44,6 +45,7 @@ import io.fabric8.maven.docker.access.NetworkCreateConfig; import io.fabric8.maven.docker.access.PortMapping; import io.fabric8.maven.docker.config.Arguments; +import io.fabric8.maven.docker.config.HealthCheckConfiguration; import io.fabric8.maven.docker.config.ImageConfiguration; import io.fabric8.maven.docker.config.NetworkConfig; import io.fabric8.maven.docker.config.RestartPolicy; @@ -390,6 +392,11 @@ ContainerCreateConfig createContainerConfig(String imageName, RunImageConfigurat .aliases(networkConfig); config.networkingConfig(networkingConfig); } + + HealthCheckConfiguration healthCheckConfiguration = runConfig.getHealthCheck(); + if (healthCheckConfiguration != null) { + config.healthCheck(new ContainerHealthCheckConfig(healthCheckConfiguration)); + } return config; } catch (IllegalArgumentException e) { diff --git a/src/test/java/io/fabric8/maven/docker/access/ContainerHealthCheckConfigTest.java b/src/test/java/io/fabric8/maven/docker/access/ContainerHealthCheckConfigTest.java new file mode 100644 index 000000000..baa9056b9 --- /dev/null +++ b/src/test/java/io/fabric8/maven/docker/access/ContainerHealthCheckConfigTest.java @@ -0,0 +1,72 @@ +package io.fabric8.maven.docker.access; + +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.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class ContainerHealthCheckConfigTest { + + @Test + void testNanoSecondTransition() { + HealthCheckConfiguration hcc = new HealthCheckConfiguration.Builder() + .mode(HealthCheckMode.cmd) + .cmd(Arguments.Builder.get().withShell("test").build()) + .timeout("3s") + .interval("2s") + .startPeriod("1s") + .build(); + + ContainerHealthCheckConfig chcc = new ContainerHealthCheckConfig(hcc); + JsonObject sut = chcc.toJsonObject(); + + Assertions.assertEquals(3000000000L, sut.get("Timeout").getAsLong()); + Assertions.assertEquals(2000000000L, sut.get("Interval").getAsLong()); + Assertions.assertEquals(1000000000L, sut.get("StartPeriod").getAsLong()); + } + + + // Zero as duration option means "inherit" the options from the image / base image options + // See also https://docs.docker.com/engine/api/latest/#tag/Container/operation/ContainerCreate + @Test + void testZeroAsDuration() { + HealthCheckConfiguration hcc = new HealthCheckConfiguration.Builder() + .mode(HealthCheckMode.cmd) + .cmd(Arguments.Builder.get().withShell("test").build()) + .timeout("0") + .interval("0") + .startPeriod("0") + .build(); + + ContainerHealthCheckConfig chcc = new ContainerHealthCheckConfig(hcc); + JsonObject sut = chcc.toJsonObject(); + + Assertions.assertEquals(0, sut.get("Timeout").getAsLong()); + Assertions.assertEquals(0, sut.get("Interval").getAsLong()); + Assertions.assertEquals(0, sut.get("StartPeriod").getAsLong()); + } + + @Test + void testCmdSplitting() { + HealthCheckConfiguration hcc = new HealthCheckConfiguration.Builder() + .mode(HealthCheckMode.cmd) + .cmd(Arguments.Builder.get().withShell("test -f /bin/bash").build()) + .build(); + + ContainerHealthCheckConfig chcc = new ContainerHealthCheckConfig(hcc); + JsonObject sut = chcc.toJsonObject(); + + JsonArray expected = new JsonArray(); + expected.add("CMD"); + expected.add("test"); + expected.add("-f"); + expected.add("/bin/bash"); + + Assertions.assertEquals(expected, sut.get("Test").getAsJsonArray()); + } +} \ No newline at end of file diff --git a/src/test/java/io/fabric8/maven/docker/assembly/DockerFileBuilderTest.java b/src/test/java/io/fabric8/maven/docker/assembly/DockerFileBuilderTest.java index 615a4d5a1..f067338c6 100644 --- a/src/test/java/io/fabric8/maven/docker/assembly/DockerFileBuilderTest.java +++ b/src/test/java/io/fabric8/maven/docker/assembly/DockerFileBuilderTest.java @@ -195,6 +195,13 @@ void testHealthCheckNone() { String dockerfileContent = new DockerFileBuilder().healthCheck(hc).content(); Assertions.assertEquals("NONE", dockerfileToMap(dockerfileContent).get("HEALTHCHECK")); } + + @Test + void testHealthCheckShell() { + HealthCheckConfiguration hc = new HealthCheckConfiguration.Builder().mode(HealthCheckMode.shell).build(); + DockerFileBuilder dfb = new DockerFileBuilder().healthCheck(hc); + Assertions.assertThrows(IllegalArgumentException.class, dfb::content); + } @Test void testNoRootExport() { diff --git a/src/test/java/io/fabric8/maven/docker/config/HealthCheckConfigTest.java b/src/test/java/io/fabric8/maven/docker/config/HealthCheckConfigTest.java index 2e562f489..c748c3d0b 100644 --- a/src/test/java/io/fabric8/maven/docker/config/HealthCheckConfigTest.java +++ b/src/test/java/io/fabric8/maven/docker/config/HealthCheckConfigTest.java @@ -1,145 +1,130 @@ package io.fabric8.maven.docker.config; +import io.fabric8.maven.docker.config.HealthCheckConfiguration.DurationParser; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; + +import java.time.Duration; +import java.util.stream.Stream; /** * Tests the health check configuration */ class HealthCheckConfigTest { - - @Test - void testGoodHealthCheck1() { - new HealthCheckConfiguration.Builder() - .cmd(new Arguments("exit 0")) - .build() - .validate(); + + static Stream goodExamples() { + return Stream.of( + new HealthCheckConfiguration.Builder().mode(HealthCheckMode.cmd).cmd(new Arguments("exit 0")).build(), + new HealthCheckConfiguration.Builder().mode(HealthCheckMode.cmd).cmd(new Arguments("exit 0")).retries(1).build(), + new HealthCheckConfiguration.Builder().mode(HealthCheckMode.cmd).cmd(new Arguments("exit 0")).retries(1).interval("2s").build(), + new HealthCheckConfiguration.Builder().mode(HealthCheckMode.cmd).cmd(new Arguments("exit 0")).retries(1).interval("2s").timeout("3s").build(), + new HealthCheckConfiguration.Builder().mode(HealthCheckMode.shell).cmd(new Arguments("exit 0")).retries(1).interval("2s").timeout("3s").startPeriod("30s").build(), + new HealthCheckConfiguration.Builder().mode(HealthCheckMode.shell).cmd(new Arguments("exit 0")).retries(1).interval("2s").timeout("3s").startPeriod("4s").build(), + new HealthCheckConfiguration.Builder().mode(HealthCheckMode.inherit).retries(1).interval("2s").timeout("3s").startPeriod("4s").build(), + new HealthCheckConfiguration.Builder().mode(HealthCheckMode.none).build() + ); } - - @Test - void testGoodHealthCheck2() { - new HealthCheckConfiguration.Builder() - .cmd(new Arguments("exit 0")) - .retries(1) - .build() - .validate(); + + @ParameterizedTest + @MethodSource("goodExamples") + void goodHealthCheck(HealthCheckConfiguration sut) { + Assertions.assertDoesNotThrow(sut::validate); } - - @Test - void testGoodHealthCheck3() { - new HealthCheckConfiguration.Builder() - .cmd(new Arguments("exit 0")) - .retries(1) - .interval("2s") - .build() - .validate(); + + static Stream badExamples() { + return Stream.of( + // No completely empty config is valid + new HealthCheckConfiguration.Builder().build(), + + // No mode given is invalid + new HealthCheckConfiguration.Builder().interval("2s").build(), + + // When mode is "none" there should be no options nor commands + new HealthCheckConfiguration.Builder().mode(HealthCheckMode.none).interval("2s").build(), + new HealthCheckConfiguration.Builder().mode(HealthCheckMode.none).retries(1).build(), + new HealthCheckConfiguration.Builder().mode(HealthCheckMode.none).timeout("3s").build(), + new HealthCheckConfiguration.Builder().mode(HealthCheckMode.none).startPeriod("30s").cmd(new Arguments("echo a")).build(), + new HealthCheckConfiguration.Builder().mode(HealthCheckMode.none).cmd(new Arguments("echo a")).build(), + + // No empty command when cmd or shell mode + new HealthCheckConfiguration.Builder().mode(HealthCheckMode.cmd).build(), + new HealthCheckConfiguration.Builder().mode(HealthCheckMode.shell).build(), + + // No command when inherit mode + new HealthCheckConfiguration.Builder().mode(HealthCheckMode.inherit).cmd(new Arguments("echo a")).build(), + + // No invalid durations + new HealthCheckConfiguration.Builder().mode(HealthCheckMode.cmd).cmd(new Arguments("echo a")).interval("1m2h").build(), + new HealthCheckConfiguration.Builder().mode(HealthCheckMode.cmd).cmd(new Arguments("echo a")).timeout("1m2h").build(), + new HealthCheckConfiguration.Builder().mode(HealthCheckMode.cmd).cmd(new Arguments("echo a")).startPeriod("1m2h").build(), + new HealthCheckConfiguration.Builder().mode(HealthCheckMode.shell).cmd(new Arguments("echo a")).interval("1m2h").build(), + new HealthCheckConfiguration.Builder().mode(HealthCheckMode.shell).cmd(new Arguments("echo a")).timeout("1m2h").build(), + new HealthCheckConfiguration.Builder().mode(HealthCheckMode.shell).cmd(new Arguments("echo a")).startPeriod("1m2h").build(), + new HealthCheckConfiguration.Builder().mode(HealthCheckMode.inherit).interval("1m2h").build(), + new HealthCheckConfiguration.Builder().mode(HealthCheckMode.inherit).timeout("1m2h").build(), + new HealthCheckConfiguration.Builder().mode(HealthCheckMode.inherit).startPeriod("1m2h").build(), + + // No invalid retries + new HealthCheckConfiguration.Builder().mode(HealthCheckMode.cmd).cmd(new Arguments("echo a")).retries(-1).build(), + new HealthCheckConfiguration.Builder().mode(HealthCheckMode.shell).cmd(new Arguments("echo a")).retries(-1).build() + ); } - - @Test - void testGoodHealthCheck4() { - new HealthCheckConfiguration.Builder() - .cmd(new Arguments("exit 0")) - .retries(1) - .interval("2s") - .timeout("3s") - .build() - .validate(); - } - - @Test - void testGoodHealthCheck5() { - new HealthCheckConfiguration.Builder() - .mode(HealthCheckMode.cmd) - .cmd(new Arguments("exit 0")) - .retries(1) - .interval("2s") - .timeout("3s") - .startPeriod("30s") - .build() - .validate(); - } - - @Test - void testGoodHealthCheck6() { - new HealthCheckConfiguration.Builder() - .mode(HealthCheckMode.cmd) - .cmd(new Arguments("exit 0")) - .retries(1) - .interval("2s") - .timeout("3s") - .startPeriod("4s") - .build() - .validate(); - } - - @Test - void testGoodHealthCheck7() { - new HealthCheckConfiguration.Builder() - .mode(HealthCheckMode.none) - .build() - .validate(); - } - - @Test - void testBadHealthCheck1() { - HealthCheckConfiguration healthCheckConfiguration = new HealthCheckConfiguration.Builder() - .mode(HealthCheckMode.none) - .interval("2s") - .build(); - Assertions.assertThrows(IllegalArgumentException.class, healthCheckConfiguration::validate); + + @ParameterizedTest + @MethodSource("badExamples") + void badHealthCheck(HealthCheckConfiguration sut) { + Assertions.assertThrows(IllegalArgumentException.class, sut::validate); } - - @Test - void testBadHealthCheck2() { - HealthCheckConfiguration healthCheckConfiguration = new HealthCheckConfiguration.Builder() - .mode(HealthCheckMode.none) - .retries(1) - .build(); - Assertions.assertThrows(IllegalArgumentException.class, healthCheckConfiguration::validate); - - } - - @Test - void testBadHealthCheck3() { - HealthCheckConfiguration healthCheckConfiguration = new HealthCheckConfiguration.Builder() - .mode(HealthCheckMode.none) - .timeout("3s") - .build(); - Assertions.assertThrows(IllegalArgumentException.class, healthCheckConfiguration::validate); - } - - @Test - void testBadHealthCheck4() { - HealthCheckConfiguration healthCheckConfiguration = new HealthCheckConfiguration.Builder() - .mode(HealthCheckMode.none) - .startPeriod("30s") - .cmd(new Arguments("echo a")) - .build(); - Assertions.assertThrows(IllegalArgumentException.class, healthCheckConfiguration::validate); - } - - @Test - void testBadHealthCheck5() { - HealthCheckConfiguration healthCheckConfiguration = new HealthCheckConfiguration.Builder() - .mode(HealthCheckMode.none) - .cmd(new Arguments("echo a")) - .build(); - Assertions.assertThrows(IllegalArgumentException.class, healthCheckConfiguration::validate); - } - - @Test - void testBadHealthCheck6() { - HealthCheckConfiguration healthCheckConfiguration = new HealthCheckConfiguration.Builder() - .build(); - Assertions.assertThrows(IllegalArgumentException.class, healthCheckConfiguration::validate); - } - - @Test - void testBadHealthCheck7() { - HealthCheckConfiguration healthCheckConfiguration = new HealthCheckConfiguration.Builder() - .mode(HealthCheckMode.cmd) - .build(); - Assertions.assertThrows(IllegalArgumentException.class, healthCheckConfiguration::validate); + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + class DurationParserTest { + + Stream goodExamples() { + return Stream.of( + org.junit.jupiter.params.provider.Arguments.of("0", Duration.ZERO), + org.junit.jupiter.params.provider.Arguments.of("0h30m1s", Duration.ofMinutes(30).plusSeconds(1)), + org.junit.jupiter.params.provider.Arguments.of("1h30m1s", Duration.ofHours(1).plusMinutes(30).plusSeconds(1)), + org.junit.jupiter.params.provider.Arguments.of("1h1s", Duration.ofHours(1).plusSeconds(1)), + org.junit.jupiter.params.provider.Arguments.of("01h01s", Duration.ofHours(1).plusSeconds(1)), + org.junit.jupiter.params.provider.Arguments.of("10h30m", Duration.ofHours(10).plusMinutes(30)), + org.junit.jupiter.params.provider.Arguments.of("20h30m", Duration.ofHours(20).plusMinutes(30)), + org.junit.jupiter.params.provider.Arguments.of("23h30m", Duration.ofHours(23).plusMinutes(30)), + org.junit.jupiter.params.provider.Arguments.of("30m1s", Duration.ofMinutes(30).plusSeconds(1)), + org.junit.jupiter.params.provider.Arguments.of("1s", Duration.ofSeconds(1)), + org.junit.jupiter.params.provider.Arguments.of("10ms", Duration.ofMillis(10)), + org.junit.jupiter.params.provider.Arguments.of("30m30ms", Duration.ofMinutes(30).plusMillis(30)), + org.junit.jupiter.params.provider.Arguments.of("30m1us", Duration.ofMinutes(30).plusNanos(1000)), + org.junit.jupiter.params.provider.Arguments.of("1h30m1.2s", Duration.ofHours(1).plusMinutes(30).plusSeconds(1).plusMillis(200)), + org.junit.jupiter.params.provider.Arguments.of("1.234s", Duration.ofSeconds(1).plusMillis(234)) + ); + } + + @ParameterizedTest + @MethodSource("goodExamples") + void success(String sut, Duration expected) { + Assertions.assertTrue(DurationParser.matchesDuration(sut)); + Assertions.assertEquals(expected, DurationParser.parseDuration(sut)); + } + + @Test + void nullSafe() { + Assertions.assertFalse(DurationParser.matchesDuration(null)); + Assertions.assertNull(DurationParser.parseDuration(null)); + } + + @ParameterizedTest + @ValueSource(strings = {"", " ", "test", "1d1m", "1s1m", "1.2h", "24h1m", "1h60m", "3m100s", "3m1s2000ms", "1ns"}) + void failure(String sut) { + Assertions.assertFalse(DurationParser.matchesDuration(sut)); + Assertions.assertNull(DurationParser.parseDuration(sut)); + } + } }