Merge pull request #8599 from dokku/dokku-container-healthcheck
Add docker healthcheck to dokku container
This commit is contained in:
128
tests/image/healthcheck.sh
Executable file
128
tests/image/healthcheck.sh
Executable file
@@ -0,0 +1,128 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
IMAGE_TAG="${IMAGE_TAG:-dokku/dokku:latest}"
|
||||
CONTAINER_NAME="${CONTAINER_NAME:-dokku-healthcheck-smoke}"
|
||||
START_TIMEOUT_SECONDS="${START_TIMEOUT_SECONDS:-360}"
|
||||
UNHEALTHY_TIMEOUT_SECONDS="${UNHEALTHY_TIMEOUT_SECONDS:-180}"
|
||||
|
||||
log-info() {
|
||||
echo "-----> $*"
|
||||
}
|
||||
|
||||
log-fail() {
|
||||
echo " ! $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
require-image() {
|
||||
if ! docker image inspect "$IMAGE_TAG" >/dev/null 2>&1; then
|
||||
log-fail "Image '$IMAGE_TAG' not found in local docker daemon. Build it first (eg. 'docker build -t $IMAGE_TAG .')."
|
||||
fi
|
||||
}
|
||||
|
||||
start-container() {
|
||||
log-info "Starting container '$CONTAINER_NAME' from image '$IMAGE_TAG'"
|
||||
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
|
||||
docker run -d \
|
||||
--name "$CONTAINER_NAME" \
|
||||
--env DOKKU_HOSTNAME=dokku.me \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
"$IMAGE_TAG" >/dev/null
|
||||
}
|
||||
|
||||
wait-for-catchall-vhost() {
|
||||
local deadline=$((SECONDS + 30))
|
||||
while ((SECONDS < deadline)); do
|
||||
if docker exec "$CONTAINER_NAME" test -f /etc/nginx/conf.d/00-default-vhost.conf; then
|
||||
return 0
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
log-fail "Catch-all default vhost /etc/nginx/conf.d/00-default-vhost.conf was never created. \
|
||||
Likely cause: a stale build/package/dokku-*.deb that predates the catch-all template shipped in \
|
||||
plugins/nginx-vhosts/templates/default-site.conf. Rebuild the deb before running this test."
|
||||
}
|
||||
|
||||
wait-for-healthy() {
|
||||
local deadline=$((SECONDS + START_TIMEOUT_SECONDS))
|
||||
local status
|
||||
while ((SECONDS < deadline)); do
|
||||
status=$(docker inspect --format='{{.State.Health.Status}}' "$CONTAINER_NAME" 2>/dev/null || echo "missing")
|
||||
if [[ "$status" == "healthy" ]]; then
|
||||
log-info "Container reached healthy status"
|
||||
return 0
|
||||
fi
|
||||
sleep 5
|
||||
done
|
||||
|
||||
log-info "Container failed to become healthy within ${START_TIMEOUT_SECONDS}s. Last logs:"
|
||||
docker logs --tail 100 "$CONTAINER_NAME" || true
|
||||
log-fail "Container '$CONTAINER_NAME' did not reach healthy status (last status: ${status:-unknown})"
|
||||
}
|
||||
|
||||
assert-endpoint-ok() {
|
||||
log-info "Verifying /_dokku/health returns ok from inside the container"
|
||||
local body
|
||||
body=$(docker exec "$CONTAINER_NAME" curl -fsS http://127.0.0.1:18080/_dokku/health)
|
||||
if [[ "$body" != "ok" ]]; then
|
||||
log-fail "Expected health endpoint body 'ok', got: ${body}"
|
||||
fi
|
||||
}
|
||||
|
||||
assert-port-not-published() {
|
||||
log-info "Verifying port 18080 is not published to the host"
|
||||
local ports
|
||||
ports=$(docker inspect --format='{{json .NetworkSettings.Ports}}' "$CONTAINER_NAME")
|
||||
if echo "$ports" | grep -q "18080/tcp"; then
|
||||
log-fail "Port 18080/tcp should not be published to the host. Inspect output: $ports"
|
||||
fi
|
||||
|
||||
if curl --max-time 2 -fsS http://127.0.0.1:18080/_dokku/health >/dev/null 2>&1; then
|
||||
log-fail "Health endpoint is reachable from the host on 127.0.0.1:18080 - it must be loopback-only inside the container."
|
||||
fi
|
||||
}
|
||||
|
||||
assert-unhealthy-after-sentinel-removed() {
|
||||
log-info "Removing /var/run/dokku/ready and confirming endpoint returns failure"
|
||||
docker exec "$CONTAINER_NAME" rm -f /var/run/dokku/ready
|
||||
|
||||
local exit_code=0
|
||||
docker exec "$CONTAINER_NAME" curl -fsS http://127.0.0.1:18080/_dokku/health >/dev/null 2>&1 || exit_code=$?
|
||||
if ((exit_code == 0)); then
|
||||
log-fail "Health endpoint still returned success after the readiness sentinel was removed"
|
||||
fi
|
||||
|
||||
log-info "Waiting for docker to flip the health status to unhealthy"
|
||||
local deadline=$((SECONDS + UNHEALTHY_TIMEOUT_SECONDS))
|
||||
local status
|
||||
while ((SECONDS < deadline)); do
|
||||
status=$(docker inspect --format='{{.State.Health.Status}}' "$CONTAINER_NAME" 2>/dev/null || echo "missing")
|
||||
if [[ "$status" == "unhealthy" ]]; then
|
||||
log-info "Container flipped to unhealthy"
|
||||
return 0
|
||||
fi
|
||||
sleep 5
|
||||
done
|
||||
|
||||
log-fail "Container did not flip to unhealthy within ${UNHEALTHY_TIMEOUT_SECONDS}s (last status: ${status:-unknown})"
|
||||
}
|
||||
|
||||
main() {
|
||||
require-image
|
||||
start-container
|
||||
wait-for-catchall-vhost
|
||||
wait-for-healthy
|
||||
assert-endpoint-ok
|
||||
assert-port-not-published
|
||||
assert-unhealthy-after-sentinel-removed
|
||||
log-info "All healthcheck smoke tests passed."
|
||||
}
|
||||
|
||||
main "$@"
|
||||
57
tests/unit/docker-image-healthcheck.bats
Normal file
57
tests/unit/docker-image-healthcheck.bats
Normal file
@@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env bats
|
||||
|
||||
REPO_ROOT="$(cd "$BATS_TEST_DIRNAME/../.." && pwd)"
|
||||
|
||||
NGINX_CONF="${REPO_ROOT}/docker/etc/nginx/conf.d/zz-dokku-health.conf"
|
||||
FINISH_SCRIPT="${REPO_ROOT}/docker/etc/runit/runsvdir/default/dokku-restore/finish"
|
||||
MY_INIT_SCRIPT="${REPO_ROOT}/docker/etc/my_init.d/10_dokku_init"
|
||||
DOCKERFILE="${REPO_ROOT}/Dockerfile"
|
||||
|
||||
@test "(docker-image) [healthcheck] nginx health conf is shipped via the docker overlay" {
|
||||
[[ -f "$NGINX_CONF" ]]
|
||||
grep -qE "listen[[:space:]]+127\.0\.0\.1:18080" "$NGINX_CONF"
|
||||
grep -qE "location = /_dokku/health" "$NGINX_CONF"
|
||||
grep -qE "/var/run/dokku/ready" "$NGINX_CONF"
|
||||
grep -qE "default_type[[:space:]]+text/plain" "$NGINX_CONF"
|
||||
}
|
||||
|
||||
@test "(docker-image) [healthcheck] finish script touches sentinel after waiting for sshd and nginx" {
|
||||
[[ -f "$FINISH_SCRIPT" ]]
|
||||
|
||||
local sshd_line nginx_line touch_line
|
||||
sshd_line=$(grep -nE "/dev/tcp/127\.0\.0\.1/22" "$FINISH_SCRIPT" | head -n1 | cut -d: -f1)
|
||||
nginx_line=$(grep -nE "/dev/tcp/127\.0\.0\.1/80" "$FINISH_SCRIPT" | head -n1 | cut -d: -f1)
|
||||
touch_line=$(grep -nE "touch /var/run/dokku/ready" "$FINISH_SCRIPT" | head -n1 | cut -d: -f1)
|
||||
|
||||
[[ -n "$sshd_line" ]]
|
||||
[[ -n "$nginx_line" ]]
|
||||
[[ -n "$touch_line" ]]
|
||||
((touch_line > sshd_line))
|
||||
((touch_line > nginx_line))
|
||||
}
|
||||
|
||||
@test "(docker-image) [healthcheck] my_init clears stale sentinel on container boot" {
|
||||
[[ -f "$MY_INIT_SCRIPT" ]]
|
||||
|
||||
local rm_line mkdir_line plugin_line hostname_line
|
||||
rm_line=$(grep -nE "rm -f /var/run/dokku/ready" "$MY_INIT_SCRIPT" | head -n1 | cut -d: -f1)
|
||||
mkdir_line=$(grep -nE "mkdir -p /var/run/dokku( |$)" "$MY_INIT_SCRIPT" | head -n1 | cut -d: -f1)
|
||||
plugin_line=$(grep -nE "plugin:install" "$MY_INIT_SCRIPT" | head -n1 | cut -d: -f1)
|
||||
hostname_line=$(grep -nE "domains:set-global" "$MY_INIT_SCRIPT" | head -n1 | cut -d: -f1)
|
||||
|
||||
[[ -n "$rm_line" ]]
|
||||
[[ -n "$mkdir_line" ]]
|
||||
[[ -n "$plugin_line" ]]
|
||||
[[ -n "$hostname_line" ]]
|
||||
((rm_line < plugin_line))
|
||||
((rm_line < hostname_line))
|
||||
((mkdir_line < plugin_line))
|
||||
((mkdir_line < hostname_line))
|
||||
}
|
||||
|
||||
@test "(docker-image) [healthcheck] Dockerfile declares HEALTHCHECK with the expected endpoint" {
|
||||
[[ -f "$DOCKERFILE" ]]
|
||||
grep -qE "^HEALTHCHECK" "$DOCKERFILE"
|
||||
grep -qE "http://127\.0\.0\.1:18080/_dokku/health" "$DOCKERFILE"
|
||||
grep -qE "\-\-start-period" "$DOCKERFILE"
|
||||
}
|
||||
Reference in New Issue
Block a user