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

Add healthy wait condition that waits until a container becomes healthy #719

Merged
merged 8 commits into from
Mar 29, 2017
Merged
1 change: 1 addition & 0 deletions doc/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
- Add new mojo "docker:save" for saving the image to a file (#687)
- Check whether a temporary tag could be removed and throw an error if not (#725)
- Allow multi line matches in log output (#628)
- Add a wait condition on a healthcheck when starting up containers (#719)
- Don't use authentication from config when no "auth" is set (#731)

* **0.20.0** (2017-02-17)
Expand Down
10 changes: 10 additions & 0 deletions samples/healthcheck/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@
<cmd>curl -f http://localhost/ || exit 1</cmd>
</healthCheck>
</build>
<run>
<wait>
<healthy>true</healthy>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder what <healthy>false</healthy> would imply ;-)

I know, Maven doesn't allow empty tags, so that's probably fine.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be honest, I have no clue what particularly maven does to merge those plugin configs into mojo parameters :) I am open to any other suggestion (maybe as attribute on wait?)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's more a complaint against Maven ;-). No, attributes are not possible at all for Maven configurations, no empty tags, too.

So its fine for me as it is now, except when we would find something 'useful' to put as value (like a timeout, but thats also available otherwise). But no worries here ...

</wait>
</run>
</image>
<image>
<alias>healthybox2</alias>
Expand All @@ -64,6 +69,11 @@
</cmd>
</healthCheck>
</build>
<run>
<wait>
<healthy>true</healthy>
</wait>
</run>
</image>
<image>
<alias>healthybox4</alias>
Expand Down
6 changes: 6 additions & 0 deletions src/main/asciidoc/inc/start/_wait.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ a| TCP port check which periodically polls given tcp ports. It knows the followi
* *mode* can be either `mapped` which uses the mapped ports or `direct` in which case the container ports are addressed directly. In the later case the host field should be left empty in order to select the container ip (which must be routed which is only the case when running on the Docker daemon's host directly). Default is `direct` when host is _localhost_, `mapped` otherwise. The direct mode might help when a so called _user-proxy_ is enabled on the Docker daemon which makes the mapped ports directly available even when the container is not ready yet.
* *host* is the hostname or the IP address. It defaults to `${docker.host.address}` for a mapped mode and the container ip address for the direct mode.
* *ports* is a list of TCP ports to check. These are supposed to be the container internal ports.

| *healthy*
a| Check that waits until the container health state becomes `healthy`. A container is considered healthy when its <<build-healthcheck, configured healtcheck>> succeeds.

This behaviour mimics the docker compose dependsOn `condition: service_healthy`.
|===

As soon as one condition is met the build continues. If you add a `<time>` constraint this works more or less as a timeout for other conditions. The build will abort if you wait on an url or log output and reach the timeout. If only a `<time>` is specified, the build will wait that amount of milliseconds and then continues.
Expand Down Expand Up @@ -68,6 +73,7 @@ As soon as one condition is met the build continues. If you add a `<time>` const
<port>9999</port>
</ports>
</tcp>
<healthy>true</healthy>
</wait>
----

Expand Down
4 changes: 4 additions & 0 deletions src/main/java/io/fabric8/maven/docker/StartMojo.java
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,10 @@ private void waitIfRequested(ServiceHub hub, ImageConfiguration imageConfig,
}
}

if (wait.getHealthy()) {
checkers.add(new HealthCheckChecker(hub.getDockerAccess(), containerId, imageConfig.getDescription(), logOut, log));
}

if (checkers.isEmpty()) {
if (wait.getTime() > 0) {
log.info("%s: Pausing for %d ms", imageConfig.getDescription(), wait.getTime());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import io.fabric8.maven.docker.config.Arguments;
import io.fabric8.maven.docker.log.LogOutputSpec;
import io.fabric8.maven.docker.model.Container;
import io.fabric8.maven.docker.model.InspectedContainer;
import io.fabric8.maven.docker.model.Network;

/**
Expand All @@ -35,7 +36,7 @@ public interface DockerAccess {
* @return <code>ContainerDetails<code> representing the container or null if none could be found
* @throws DockerAccessException if the container could not be inspected
*/
Container getContainer(String containerIdOrName) throws DockerAccessException;
InspectedContainer getContainer(String containerIdOrName) throws DockerAccessException;

/**
* Check whether the given name exists as image at the docker daemon
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,7 @@ public List<Container> getContainersForImage(String image) throws DockerAccessEx
}

@Override
public Container getContainer(String containerIdOrName) throws DockerAccessException {
public InspectedContainer getContainer(String containerIdOrName) throws DockerAccessException {
HttpBodyAndStatus response = inspectContainer(containerIdOrName);
if (response.getStatusCode() == HTTP_NOT_FOUND) {
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ public class WaitConfiguration implements Serializable {
@Parameter
private TcpConfiguration tcp;

@Parameter boolean healthy;

@Parameter
private String log;

Expand All @@ -49,11 +51,12 @@ public class WaitConfiguration implements Serializable {

public WaitConfiguration() {}

private WaitConfiguration(int time, ExecConfiguration exec, HttpConfiguration http, TcpConfiguration tcp, String log, int shutdown, int kill) {
private WaitConfiguration(int time, ExecConfiguration exec, HttpConfiguration http, TcpConfiguration tcp, boolean healthy, String log, int shutdown, int kill) {
this.time = time;
this.exec = exec;
this.http = http;
this.tcp = tcp;
this.healthy = healthy;
this.log = log;
this.shutdown = shutdown;
this.kill = kill;
Expand All @@ -79,6 +82,10 @@ public TcpConfiguration getTcp() {
return tcp;
}

public boolean getHealthy() {
return healthy;
}

public String getLog() {
return log;
}
Expand All @@ -96,6 +103,7 @@ public int getKill() {
public static class Builder {
private int time = 0,shutdown = 0, kill = 0;
private String url,log,status;
boolean healthy;
private String method;
private String preStop;
private String postStart;
Expand Down Expand Up @@ -123,6 +131,11 @@ public Builder status(String status) {
return this;
}

public Builder healthy(boolean healthy) {
this.healthy = healthy;
return this;
}

public Builder log(String log) {
this.log = log;
return this;
Expand Down Expand Up @@ -161,6 +174,7 @@ public WaitConfiguration build() {
postStart != null || preStop != null ? new ExecConfiguration(postStart, preStop) : null,
url != null ? new HttpConfiguration(url,method,status) : null,
tcpPorts != null ? new TcpConfiguration(tcpMode, tcpHost, tcpPorts) : null,
healthy,
log,
shutdown,
kill);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import java.util.*;


public class ContainerDetails implements Container {
public class ContainerDetails implements InspectedContainer {

static final String CONFIG = "Config";
static final String CREATED = "Created";
Expand All @@ -23,6 +23,11 @@ public class ContainerDetails implements Container {
static final String PORTS = "Ports";
static final String SLASH = "/";
static final String STATE = "State";
static final String HEALTH = "Health";
static final String STATUS = "Status";
static final String HEALTH_STATUS_HEALTHY = "healthy";
static final String HEALTHCHECK = "Healthcheck";
static final String TEST = "Test";

private static final String RUNNING = "Running";

Expand Down Expand Up @@ -121,6 +126,22 @@ public boolean isRunning() {
return state.getBoolean(RUNNING);
}

@Override
public boolean isHealthy() {
final JSONObject state = json.getJSONObject(STATE);
// always indicate healthy for docker hosts that do not support health checks.
return !state.has(HEALTH) || HEALTH_STATUS_HEALTHY.equals(state.getJSONObject(HEALTH).getString(STATUS));
}

@Override
public String getHealthcheck() {
if (!json.getJSONObject(CONFIG).has(HEALTHCHECK) ||
!json.getJSONObject(CONFIG).getJSONObject(HEALTHCHECK).has(TEST)) {
return null;
}
return json.getJSONObject(CONFIG).getJSONObject(HEALTHCHECK).getJSONArray(TEST).join(", ");
}

private void addPortMapping(String port, JSONObject hostConfig, Map<String, PortBinding> portBindings) {
String hostIp = hostConfig.getString(HOST_IP);
Integer hostPort = Integer.valueOf(hostConfig.getString(HOST_PORT));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package io.fabric8.maven.docker.model;
/*
*
* Copyright 2014 Roland Huss
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

/**
* Interface representing an inspected container
*
* @author daniel
* @since 17/02/15
*/
public interface InspectedContainer extends Container {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the benefit to extend Container over simplify adding the method directly to Container ?

Please add also some javadocs to public interface methods.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ContainersListElement can not provide these properties since they are only available on the docker inspect endpoint (single container) and not the list container endpoint. To be honest, right now i can not even find the healthy field documented it in the official docker api docs, I took the path directly from the docker source. I'll try to provide the original reference asap.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, understand.


/**
* The Health Status of this container (if applicable).
* @return {@code false} if the container has a configured healthcheck and has not the
* Health Status "healthy". Returns {@code true} otherwise.
*/
boolean isHealthy();

/**
* @return the docker healthcheck command that is configured for this container.
*/
String getHealthcheck();

}
63 changes: 63 additions & 0 deletions src/main/java/io/fabric8/maven/docker/wait/HealthCheckChecker.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package io.fabric8.maven.docker.wait;

import java.util.List;

import io.fabric8.maven.docker.access.DockerAccess;
import io.fabric8.maven.docker.access.DockerAccessException;
import io.fabric8.maven.docker.model.InspectedContainer;
import io.fabric8.maven.docker.util.Logger;

/**
* @author roland
* @since 29/03/2017
*/
public class HealthCheckChecker implements WaitChecker {

private boolean first = true;

private final List<String> logOut;

private DockerAccess docker;
private String containerId;
private Logger log;
private final String imageConfigDesc;

public HealthCheckChecker(DockerAccess docker, String containerId, String imageConfigDesc, List<String> logOut, Logger log) {
this.docker = docker;
this.containerId = containerId;
this.imageConfigDesc = imageConfigDesc;
this.logOut = logOut;
this.log = log;
}

@Override
public boolean check() {
try {
final InspectedContainer container = docker.getContainer(containerId);
if (container == null) {
log.debug("HealthyWaitChecker: Container %s not found");
return false;
}

final String healthcheck = container.getHealthcheck();
if (first) {
if (healthcheck == null) {
throw new IllegalArgumentException("Can not wait for healthy state of " + imageConfigDesc +". No HEALTHCHECK configured.");
}
log.info("%s: Waiting to become healthy", imageConfigDesc);
log.debug("HealthyWaitChecker: Waiting for healthcheck: '%s'", healthcheck);
logOut.add("on healthcheck '" + healthcheck+ "'");
first = false;
} else if (log.isDebugEnabled()) {
log.debug("HealthyWaitChecker: Waiting on healthcheck '%s'", healthcheck);
}

return container.isHealthy();
} catch(DockerAccessException e) {
return false;
}
}

@Override
public void cleanUp() {}
};