mirror of
https://github.com/dokku/dokku.git
synced 2026-05-18 05:05:46 +02:00
test: cover dokku container healthcheck wiring
Adds a bats lint test that guards the static wiring (nginx conf, dokku-restore finish-script ordering, my_init sentinel reset, and the Dockerfile HEALTHCHECK line) plus a docker smoke test that boots the built image, waits for the health flip, exercises the loopback endpoint, asserts the port is not published to the host, and verifies the negative path. The smoke test is invoked via a new `make test-image-healthcheck` target and runs automatically in the build-image action after `docker buildx --load`.
This commit is contained in:
6
.github/actions/build-image/action.yml
vendored
6
.github/actions/build-image/action.yml
vendored
@@ -26,6 +26,12 @@ runs:
|
||||
shell: bash
|
||||
run: |
|
||||
docker buildx build \
|
||||
--load \
|
||||
--progress plain \
|
||||
--tag "dokku/dokku:latest" \
|
||||
.
|
||||
|
||||
- name: smoke test image
|
||||
shell: bash
|
||||
run: |
|
||||
make test-image-healthcheck IMAGE_TAG=dokku/dokku:latest
|
||||
|
||||
5
Makefile
5
Makefile
@@ -226,3 +226,8 @@ vagrant-acl-add:
|
||||
|
||||
vagrant-dokku:
|
||||
vagrant ssh -- "sudo -H -u root bash -c 'dokku $(RUN_ARGS)'"
|
||||
|
||||
IMAGE_TAG ?= dokku/dokku:latest
|
||||
|
||||
test-image-healthcheck:
|
||||
IMAGE_TAG=$(IMAGE_TAG) ./tests/image/healthcheck.sh
|
||||
|
||||
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