Skip to content

Commit

Permalink
healthcheck support in docker-compose configuration (#1825)
Browse files Browse the repository at this point in the history
* healthcheck suppor in docker-compose configuration

* update of the changelog

* sonar updates

* sonar

* doc update

* changelog.md update

* renaming

* fix wildcard import

* add healthcheck to integration test

---------

Co-authored-by: stanislav-shymov <[email protected]>
  • Loading branch information
StasShymov and stanislav-shymov authored Oct 12, 2024
1 parent 28981d4 commit 1d9cf08
Show file tree
Hide file tree
Showing 19 changed files with 490 additions and 60 deletions.
2 changes: 2 additions & 0 deletions doc/changelog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# ChangeLog
* **0.46-SNAPSHOT**:
- Docker-compose healthcheck configuration support ([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.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))
Expand Down
6 changes: 6 additions & 0 deletions it/docker-compose-dependon/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ services:
build:
context: .
dockerfile: Postgres.Dockerfile
healthcheck:
test: pg_isready -U postgres
interval: 2s
timeout: 2s
retries: 10
start_period: 1s
environment:
POSTGRES_PASSWORD: supersecret
tmpfs:
Expand Down
2 changes: 1 addition & 1 deletion src/main/asciidoc/inc/external/_docker_compose.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ In addition to the `docker-compose.yml` you can add all known options for <<buil

The following Docker Compose file keywords are not yet supported:

* `cgroup_parent`, `devices`, `env_file`, `expose`, `pid`, `security_opt`, `stop_signal`, `cpu_quota`, `ipc`, `mac_address`, `read_only`, `healthcheck` are not yet supported (but might be in a future version).
* `cgroup_parent`, `devices`, `env_file`, `expose`, `pid`, `security_opt`, `stop_signal`, `cpu_quota`, `ipc`, `mac_address`, `read_only` are not yet supported (but might be in a future version).
* `extend` for including other Docker Compose files is not yet implemented.
* Only **services** are currently evaluated, there is no supported yet for **volumes** and **networks**.
* When using https://docs.docker.com/compose/compose-file/compose-file-v2/#depends_on[`depends_on` with long syntax] in a Docker Compose file, be advised the plugin cannot apply all usage constellations expressible in it. The root cause is this plugin uses the concept of pausing execution based on <<start-wait,wait conditions>> 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.
6 changes: 6 additions & 0 deletions src/main/asciidoc/inc/start/_configuration.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ In addition to the <<global-configuration>>, this goal supports the following gl
| Starts docker images in parallel while dependencies expressed as <<start-links,Link>> or <<start-depends-on,dependsOn>> 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 (<wait><time>) is milliseconds.
Overriding that property might become particularly useful when docker-compose config defines the healthchecks,
but the default wait timeout of 10000ms is too short for some containers to become healthy.
| `docker.startContainerWaitTimeout`

|===

The `<run>` configuration element knows the following sub elements:
Expand Down
12 changes: 12 additions & 0 deletions src/main/java/io/fabric8/maven/docker/StartMojo.java
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,17 @@ public class StartMojo extends AbstractDockerMojo {
@Parameter(property = "docker.autoCreateCustomNetworks", defaultValue = "false")
protected boolean autoCreateCustomNetworks;

public static final String DOCKER_START_CONTAINER_WAIT_TIMEOUT = "docker.startContainerWaitTimeout";

/**
* 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 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_START_CONTAINER_WAIT_TIMEOUT, defaultValue = "10000")
protected int startContainerWaitTimeout = 10000;

// property file to write out with port mappings
@Parameter
protected String portPropertyFile;
Expand Down Expand Up @@ -274,6 +285,7 @@ private void startImage(final ImageConfiguration imageConfig,
final Properties projProperties = project.getProperties();
final RunImageConfiguration runConfig = imageConfig.getRunConfiguration();
final PortMapping portMapping = runService.createPortMapping(runConfig, projProperties);
projProperties.computeIfAbsent(DOCKER_START_CONTAINER_WAIT_TIMEOUT, key -> String.valueOf(startContainerWaitTimeout));
final LogDispatcher dispatcher = getLogDispatcher(hub);

StartContainerExecutor startExecutor = new StartContainerExecutor.Builder()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,15 @@
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.Collections;
import java.util.Enumeration;
import java.util.List;
import java.util.Map;
Expand All @@ -17,6 +21,8 @@
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();
Expand Down Expand Up @@ -72,7 +78,7 @@ public ContainerCreateConfig environment(String envPropsFile, Map<String, String
String value = entry.getValue();
if (value == null) {
value = "";
} else if(value.matches("^\\+\\$\\{.*}$")) {
} else if (value.matches("^\\+\\$\\{.*}$")) {
/*
* This case is to handle the Maven interpolation issue which used
* to occur when using ${..} only without any suffix.
Expand All @@ -93,7 +99,7 @@ public ContainerCreateConfig environment(String envPropsFile, Map<String, String
return this;
}

public ContainerCreateConfig labels(Map<String,String> labels) {
public ContainerCreateConfig labels(Map<String, String> labels) {
if (labels != null && labels.size() > 0) {
createConfig.add("Labels", JsonFactory.newJsonObject(labels));
}
Expand All @@ -111,6 +117,42 @@ public ContainerCreateConfig exposedPorts(Set<String> portSpecs) {
return this;
}

public ContainerCreateConfig healthcheck(HealthCheckConfiguration healthCheckConfiguration) {
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;
}

public String getImageName() {
return imageName;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
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 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<String, TimeUnit> 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);

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.parseLong(matcher.group(1));
if (matcher.groupCount() == 2 && matcher.group(2) != null) {
String type = matcher.group(2);

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)) {
duration = 365 * TimeUnit.NANOSECONDS.convert(duration, TimeUnit.DAYS);
} else {
throw new IllegalArgumentException("Unsupported time unit: " + type);
}
}

return duration;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ public class RunImageConfiguration implements Serializable {
@Parameter
private List<String> dependsOn;

// healthcheck
@Parameter
private HealthCheckConfiguration healthCheckConfiguration;

/**
* container entry point
*
Expand Down Expand Up @@ -264,6 +268,10 @@ public List<String> getDependsOn() {
return EnvUtil.splitAtCommasAndTrim(dependsOn);
}

public HealthCheckConfiguration getHealthCheckConfiguration() {
return healthCheckConfiguration;
}

public String getUser() {
return user;
}
Expand Down Expand Up @@ -582,6 +590,11 @@ public Builder dependsOn(List<String> dependsOn) {
return this;
}

public Builder healthcheck(HealthCheckConfiguration healthCheckConfiguration) {
config.healthCheckConfiguration = healthCheckConfiguration;
return this;
}

public Builder dns(List<String> dns) {
config.dns = dns;
return this;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
Loading

0 comments on commit 1d9cf08

Please sign in to comment.