Skip to content

Commit

Permalink
ISSUE-1204: Add HEALTHCHECK instruction for Dockerfiles
Browse files Browse the repository at this point in the history
  • Loading branch information
marcospereira committed Nov 29, 2023
1 parent ab86493 commit 3a29e42
Show file tree
Hide file tree
Showing 3 changed files with 241 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ LABEL [email protected]
workingDir('/tmp')
onBuild('RUN echo "Hello World"')
instruction('LABEL env=prod')
healthcheck(new Dockerfile.Healthcheck('/bin/check-running'))
}
"""

Expand All @@ -180,6 +181,7 @@ USER root
WORKDIR /tmp
ONBUILD RUN echo "Hello World"
LABEL env=prod
HEALTHCHECK CMD /bin/check-running
""")
}

Expand All @@ -202,6 +204,7 @@ LABEL env=prod
workingDir(project.provider { '/path/to/workdir' })
onBuild(project.provider { 'ADD . /app/src' })
instruction(project.provider { 'LABEL env=prod' })
healthcheck(project.provider { new Dockerfile.Healthcheck('/bin/check-running') })
}
"""

Expand All @@ -224,6 +227,7 @@ USER patrick
WORKDIR /path/to/workdir
ONBUILD ADD . /app/src
LABEL env=prod
HEALTHCHECK CMD /bin/check-running
""")
}

Expand All @@ -233,6 +237,7 @@ LABEL env=prod
task ${DOCKERFILE_TASK_NAME}(type: Dockerfile) {
instructions.add(new Dockerfile.FromInstruction(new Dockerfile.From('$TEST_IMAGE_WITH_TAG')))
instructions.add(new Dockerfile.LabelInstruction(['maintainer': '[email protected]']))
instructions.add(new Dockerfile.HealthcheckInstruction(new Dockerfile.Healthcheck('/bin/check-running')))
}
"""

Expand All @@ -242,6 +247,7 @@ LABEL env=prod
then:
assertDockerfileContent("""FROM $TEST_IMAGE_WITH_TAG
LABEL [email protected]
HEALTHCHECK CMD /bin/check-running
""")
}

Expand Down
227 changes: 224 additions & 3 deletions src/main/java/com/bmuschko/gradle/docker/tasks/image/Dockerfile.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,13 @@
import org.gradle.api.tasks.OutputFile;
import org.gradle.api.tasks.TaskAction;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.file.Files;
import java.time.Duration;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
Expand Down Expand Up @@ -296,7 +298,7 @@ public void from(String image) {
* FROM ubuntu:14.04
* </pre>
*
* @param from From definition
* @param from From definition
* @see #from(String)
* @see #from(Provider)
*/
Expand Down Expand Up @@ -1104,6 +1106,62 @@ public void label(Provider<Map<String, String>> provider) {
instructions.add(new LabelInstruction(provider));
}

/**
* The <a href="https://docs.docker.com/engine/reference/builder/#healthcheck">HEALTHCHECK instruction</a> tells
* Docker how to test a container to check that it is still working.
*
* <p>
* Example in Groovy DSL:
* <p>
* <pre>
* task createDockerfile(type: Dockerfile) {
* healthcheck(new Healthcheck("curl -f http://localhost/ || exit 1").withRetries(5))
* }
* </pre>
* The produced instruction looks as follows:
* <p>
* <pre>
* HEALTHCHECK --retries=5 CMD curl -f http://localhost/ || exit 1
* </pre>
*
* @param healthcheck the healthcheck configuration
* @see #healthcheck(Provider)
* @see Healthcheck
*/
public void healthcheck(Healthcheck healthcheck) {
instructions.add(new HealthcheckInstruction(healthcheck));
}

/**
* The <a href="https://docs.docker.com/engine/reference/builder/#healthcheck">HEALTHCHECK instruction</a> tells
* Docker how to test a container to check that it is still working.
*
* <p>
* Example in Groovy DSL:
* <p>
* <pre>
* task createDockerfile(type: Dockerfile) {
* from(project.provider(new Callable&#60;Dockerfile.Healthcheck&#62;() {
* {@literal @}Override
* Dockerfile.Healthcheck call() throws Exception {
* new Dockerfile.Healthcheck("curl -f http://localhost/ || exit 1")
* }
* }))
* }
* </pre>
* The produced instruction looks as follows:
* <p>
* <pre>
* HEALTHCHECK CMD curl -f http://localhost/ || exit 1
* </pre>
*
* @param provider Healthcheck information as Provider
* @see #healthcheck(Healthcheck)
*/
public void healthcheck(Provider<Healthcheck> provider) {
instructions.add(new HealthcheckInstruction(provider));
}

/**
* A representation of an instruction in a Dockerfile.
*/
Expand Down Expand Up @@ -1243,7 +1301,7 @@ private interface ItemJoiner {
private static class MultiItemJoiner implements ItemJoiner {
@Override
public String join(Map<String, String> map) {
return map.entrySet().stream().map( entry -> {
return map.entrySet().stream().map(entry -> {
String key = ItemJoinerUtil.isUnquotedStringWithWhitespaces(entry.getKey()) ? ItemJoinerUtil.toQuotedString(entry.getKey()) : entry.getKey();
String value = ItemJoinerUtil.isUnquotedStringWithWhitespaces(entry.getValue()) ? ItemJoinerUtil.toQuotedString(entry.getValue()) : entry.getValue();
value = value.replaceAll("(\r)*\n", "\\\\\n");
Expand Down Expand Up @@ -1296,7 +1354,7 @@ public String getText() {

private void validateKeysAreNotBlank(Map<String, String> command) throws IllegalArgumentException {
command.entrySet().forEach(entry -> {
if (entry.getKey().trim().length() == 0) {
if (entry.getKey().trim().isEmpty()) {
throw new IllegalArgumentException("blank keys for a key=value pair are not allowed: please check instruction " + getKeyword() + " and given pair `" + String.valueOf(entry) + "`");
}
});
Expand Down Expand Up @@ -1756,6 +1814,59 @@ public String getKeyword() {
}
}

public static class HealthcheckInstruction implements Instruction {

public static final String KEYWORD = "HEALTHCHECK";

private final Provider<Healthcheck> provider;

public HealthcheckInstruction(Healthcheck healthcheck) {
this.provider = Providers.ofNullable(healthcheck);
}

public HealthcheckInstruction(Provider<Healthcheck> provider) {
this.provider = provider;
}

@Nullable
@Override
public String getKeyword() {
return KEYWORD;
}

@Nullable
@Override
public String getText() {
return buildTextInstruction(provider.getOrNull());
}

private String buildTextInstruction(Healthcheck healthcheck) {
if (healthcheck != null) {
StringBuilder result = new StringBuilder(getKeyword());
if (healthcheck.getInterval() != null) {
result.append(" --interval=").append(healthcheck.getInterval().toSeconds()).append("s");
}
if (healthcheck.getTimeout() != null) {
result.append(" --timeout=").append(healthcheck.getTimeout().toSeconds()).append("s");
}
if (healthcheck.getStartPeriod() != null) {
result.append(" --start-period=").append(healthcheck.getStartPeriod().toSeconds()).append("s");
}
if (healthcheck.getStartInterval() != null) {
result.append(" --start-interval=").append(healthcheck.getStartInterval().toSeconds()).append("s");
}

if (healthcheck.getRetries() != null) {
result.append(" --retries=").append(healthcheck.getRetries());
}

result.append(" CMD ").append(healthcheck.getCmd());
return result.toString();
}
return null;
}
}

/**
* Input data for a {@link AddFileInstruction} or {@link CopyFileInstruction}.
*
Expand Down Expand Up @@ -1916,4 +2027,114 @@ public String getPlatform() {
return platform;
}
}

/**
* Input data for a {@link HealthcheckInstruction}.
*
* @see <a href="https://docs.docker.com/engine/reference/builder/#healthcheck">Dockerfile reference / HEALTHCHECK</a>.
* @since ???
*/
public static class Healthcheck {
@Nullable
private Duration interval;
@Nullable
private Duration timeout;
@Nullable
private Duration startPeriod;
@Nullable
private Duration startInterval = null;
@Nullable
private Integer retries;
@Nonnull
private final String cmd;

public Healthcheck(@Nonnull String cmd) {
this.cmd = cmd;
}

/**
* Sets the healthcheck interval by adding {@code --interval} to Healthcheck instruction.
*
* @param interval a {@link Duration} in seconds.
* @return this healthcheck.
*/
public Healthcheck withInterval(Duration interval) {
this.interval = interval;
return this;
}

/**
* Sets the healthcheck timeout by adding {@code --timeout} to Healthcheck instruction.
*
* @param timeout a {@link Duration} in seconds.
* @return this healthcheck.
*/
public Healthcheck withTimeout(Duration timeout) {
this.timeout = timeout;
return this;
}

/**
* Sets the healthcheck startPeriod by adding {@code --start-period} to Healthcheck instruction.
*
* @param startPeriod a {@link Duration} in seconds.
* @return this healthcheck.
*/
public Healthcheck withStartPeriod(Duration startPeriod) {
this.startPeriod = startPeriod;
return this;
}

/**
* This option requires Docker Engine version 25.0 or later.
* Sets the healthcheck startInterval by adding {@code --start-interval} to Healthcheck instruction.
*
* @param startInterval a {@link Duration} in seconds.
* @return this healthcheck.
*/
public Healthcheck withStartInterval(@Nullable Duration startInterval) {
this.startInterval = startInterval;
return this;
}

/**
* Sets the healthcheck number of retries by adding {@code --retries} to Healthcheck instruction.
*
* @param retries the number of retries. Must be greater than 0, or it will fallback to the default (3).
* @return this healthcheck.
*/
public Healthcheck withRetries(int retries) {
this.retries = retries;
return this;
}

@Nullable
public Duration getInterval() {
return interval;
}

@Nullable
public Duration getTimeout() {
return timeout;
}

@Nullable
public Duration getStartPeriod() {
return startPeriod;
}

@Nullable
public Duration getStartInterval() {
return startInterval;
}

@Nullable
public Integer getRetries() {
return retries;
}

public String getCmd() {
return cmd;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import java.util.logging.Level
import java.util.logging.Logger

import static com.bmuschko.gradle.docker.tasks.image.Dockerfile.*
import static java.time.Duration.ofSeconds

class DockerfileTest extends Specification {
private static final Logger LOG = Logger.getLogger(DockerfileTest.class.getCanonicalName())
Expand Down Expand Up @@ -55,11 +56,11 @@ class DockerfileTest extends Specification {
new EnvironmentVariableInstruction(' ', 'Linux') | 'ENV' | IllegalArgumentException.class
new EnvironmentVariableInstruction('OS', '"Linux"') | 'ENV' | 'ENV OS="Linux"'
new EnvironmentVariableInstruction('OS', 'Linux or Windows') | 'ENV' | 'ENV OS="Linux or Windows"'
new EnvironmentVariableInstruction('long', '''Multiple line env
new EnvironmentVariableInstruction('long', '''Multiple line env
with linebreaks in between''') | 'ENV' | "ENV long=\"Multiple line env \\\n\
with linebreaks in between\""
new EnvironmentVariableInstruction(['OS': 'Linux']) | 'ENV' | 'ENV OS=Linux'
new EnvironmentVariableInstruction(['long': '''Multiple line env
new EnvironmentVariableInstruction(['long': '''Multiple line env
with linebreaks in between''']) | 'ENV' | "ENV long=\"Multiple line env \\\n\
with linebreaks in between\""
new EnvironmentVariableInstruction(['OS': 'Linux', 'TZ': 'UTC']) | 'ENV' | 'ENV OS=Linux TZ=UTC'
Expand All @@ -74,5 +75,13 @@ with linebreaks in between\""
new LabelInstruction(['description': 'Single label' ]) | 'LABEL' | 'LABEL description="Single label"'
new LabelInstruction(['"un subscribe"': 'true' ]) | 'LABEL' | 'LABEL "un subscribe"=true'
new LabelInstruction(['description': 'Multiple labels', 'version': '1.0' ]) | 'LABEL' | 'LABEL description="Multiple labels" version=1.0'
new HealthcheckInstruction(new Healthcheck("/bin/check-running")) | 'HEALTHCHECK' | 'HEALTHCHECK CMD /bin/check-running'
new HealthcheckInstruction(new Healthcheck("/bin/check-running").withInterval(ofSeconds(10))) | 'HEALTHCHECK' | 'HEALTHCHECK --interval=10s CMD /bin/check-running'
new HealthcheckInstruction(new Healthcheck("/bin/check-running").withTimeout(ofSeconds(20))) | 'HEALTHCHECK' | 'HEALTHCHECK --timeout=20s CMD /bin/check-running'
new HealthcheckInstruction(new Healthcheck("/bin/check-running")
.withStartInterval(ofSeconds(30))) | 'HEALTHCHECK' | 'HEALTHCHECK --start-interval=30s CMD /bin/check-running'
new HealthcheckInstruction(new Healthcheck("/bin/check-running")
.withStartPeriod(ofSeconds(40))) | 'HEALTHCHECK' | 'HEALTHCHECK --start-period=40s CMD /bin/check-running'
new HealthcheckInstruction(new Healthcheck("/bin/check-running").withRetries(5)) | 'HEALTHCHECK' | 'HEALTHCHECK --retries=5 CMD /bin/check-running'
}
}

0 comments on commit 3a29e42

Please sign in to comment.