Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

healthcheck support in docker-compose configuration #1825

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading