Files
act_runner/scripts/test-dind.sh
silverwind ad9eeb668a fix: job container, daemon startup, action cache and exec fixes (#1294)
- Keep `GITHUB_ENV`, `GITHUB_OUTPUT`, `GITHUB_STATE` and `GITHUB_PATH` when a job times out or is cancelled, so post steps still get their state
- Retry the startup Declare while Gitea is unreachable instead of exiting, still failing fast on rejected registrations, auth and misconfiguration
- Reuse a local image when a platform variant such as `linux/arm/v7` is requested instead of pulling again
- Read `PATH` from the created job container so cross-architecture jobs get tool paths for the right architecture
- Extract action copies relative to the act volume so they never cross an image's `/var/run` symlink, which rootless Podman rejects
- Keep the job container's entrypoint when container options set `--entrypoint`, warning instead of appending it to the keep-alive command
- Lock action cache entries across runner processes that share a cache directory
- `exec`: omit `GITHUB_SERVER_URL` and `GITHUB_API_URL` without `--gitea-instance` instead of exporting a bare `https://`, and read owner and repository from any origin remote

Reviewed-on: https://gitea.com/gitea/runner/pulls/1294
Reviewed-by: bircni <bircni@icloud.com>
Co-authored-by: silverwind <me@silverwind.io>
2026-10-10 18:11:03 +00:00

177 lines
8.1 KiB
Bash
Executable File

#!/usr/bin/env bash
# Generic docker-in-docker test harness.
#
# Builds a dind image variant from the repo Dockerfile, starts its docker daemon over a
# local TCP port, and runs a Go test command against that daemon via DOCKER_HOST. This
# validates the actual docker version and behaviour shipped in the dind image, so any
# daemon-level regression surfaces here (e.g. the "docker cp" break in gitea/runner#981).
# It is deliberately generic: point it at any package/test to exercise the dind daemon.
#
# Usage: scripts/test-dind.sh [target] [-- go-test-args...]
# target: dind (default), dind-rootless, or podman to run PODMAN_TEST_IMAGE's API service instead
# go-test-args: passed verbatim to `go test`. Defaults cover image platform detection,
# symlink copying, a mounted Docker job using cached images and the
# image's s6 supervision, or the Docker proxy probe for podman.
#
# Env:
# DIND_TEST_PORT host port for the daemon (default 32375)
# DIND_TEST_IMAGE skip the build and use this prebuilt image instead
# DIND_TEST_PRELOAD space-separated images to copy from the host daemon into the fresh one
set -euo pipefail
target="dind"
case "${1:-}" in
dind|dind-rootless|podman) target="$1"; shift ;;
esac
[ "${1:-}" = "--" ] && shift
default_tests=false
port="${DIND_TEST_PORT:-32375}"
name="gitea-runner-dind-test-$$"
# The host daemon endpoint, captured before DOCKER_HOST is pointed at the fresh dind daemon.
host_docker="${DOCKER_HOST:-$(docker context inspect --format '{{.Endpoints.docker.Host}}')}"
test_dir=""
cleanup() {
docker -H "$host_docker" rm -fv "$name" "$name-s6" >/dev/null 2>&1 || true
if [ -n "$test_dir" ]; then
rm -rf "$test_dir"
fi
}
trap cleanup EXIT
if [ "$target" = podman ]; then
image="${PODMAN_TEST_IMAGE:?}"
daemon_args=("$image" podman system service --time=0 tcp://0.0.0.0:2375)
[ $# -gt 0 ] || set -- -count=1 -race -run '^TestDockerProxyWithDaemon$' ./act/container/
else
image="${DIND_TEST_IMAGE:-gitea-runner-${target}:dind-test}"
if [ -z "${DIND_TEST_IMAGE:-}" ]; then
echo "==> Building ${target} image"
docker build --target "$target" -t "$image" .
fi
# Override the image entrypoint (s6) and run only dockerd, exposed over insecure TCP.
# We are testing the daemon the image ships, not the runner supervision tree.
daemon_args=(-e DOCKER_TLS_CERTDIR= --entrypoint dockerd-entrypoint.sh "$image" --host=tcp://0.0.0.0:2375)
if [ $# -eq 0 ]; then
default_tests=true
set -- -count=1 -race -run '^TestImageExistsLocally$' ./act/container/
fi
fi
# How the test process reaches the daemon depends on where it runs:
# - plain host: publish 2375 on loopback and connect to 127.0.0.1.
# - inside a container (CI), the daemon is a sibling container, so its published port is on
# the host, not our loopback; instead attach it to our own network and reach it by name.
self_container=""
if [ -f /.dockerenv ]; then
self_container="$(cat /proc/sys/kernel/hostname 2>/dev/null || cat /etc/hostname)"
fi
self_network=""
if [ -n "$self_container" ]; then
self_network="$(docker inspect -f '{{range $k,$v := .NetworkSettings.Networks}}{{$k}}{{"\n"}}{{end}}' "$self_container" 2>/dev/null | head -1)"
fi
# The two cases differ only in how the daemon is exposed and addressed; everything else
# (privileged, name, daemon_args) is shared, so collect just the differing run args and the
# resulting DOCKER_HOST here.
if [ -n "$self_network" ]; then
echo "==> Starting ${target} daemon on network ${self_network} (reached as ${name}:2375)"
run_args=(--network "$self_network")
daemon_host="tcp://${name}:2375"
else
echo "==> Starting ${target} daemon on tcp://127.0.0.1:${port}"
run_args=(-p "127.0.0.1:${port}:2375")
daemon_host="tcp://127.0.0.1:${port}"
fi
# Create the dind container on the host daemon first, then repoint DOCKER_HOST at it: exporting
# DOCKER_HOST before `docker run` would make this `docker run` target the not-yet-existent dind.
docker -H "$host_docker" run -d --privileged --name "$name" "${run_args[@]}" "${daemon_args[@]}" >/dev/null
export DOCKER_HOST="$daemon_host"
echo "==> Waiting for daemon"
for _ in $(seq 1 60); do
docker version --format 'server docker {{.Server.Version}}' 2>/dev/null && break
sleep 1
done
if ! docker version --format 'server docker {{.Server.Version}}'; then
docker -H "$host_docker" logs "$name" >&2
exit 1
fi
# Seed the fresh daemon with images the host already has (the CI job pulls them in the
# preceding `make test`), so the daemon-facing tests run without registry access.
echo "==> Seeding daemon with cached host images"
preload="${DIND_TEST_PRELOAD:-alpine:latest}"
job_image="${ACT_TEST_IMAGE:-node:24-bookworm-slim}"
if [ "$default_tests" = true ]; then
preload="$preload $job_image"
fi
for img in $preload; do
if docker -H "$host_docker" image inspect "$img" >/dev/null 2>&1; then
docker -H "$host_docker" save "$img" | docker load >/dev/null 2>&1 && echo " loaded $img" || true
fi
done
echo "==> Running tests against ${target} daemon"
go test "$@"
if [ "$default_tests" = true ]; then
if ! docker image inspect "$job_image" >/dev/null 2>&1; then
echo "mounted Docker test requires ${job_image}, pull it on the host before running this harness" >&2
exit 1
fi
test_dir="$(mktemp -d)"
echo "==> Building mounted Docker test for the dind container"
CGO_ENABLED=0 GOOS=linux GOARCH="$(docker -H "$host_docker" image inspect "$image" --format '{{.Architecture}}')" \
go test -c -o "$test_dir/runner.test" ./act/runner/
docker -H "$host_docker" exec "$name" mkdir -p /tmp/gitea-runner-proxy-test/testdata
tar -C "$test_dir" -cf - runner.test -C "$PWD/act/runner" testdata/docker-proxy | \
docker -H "$host_docker" exec -i "$name" tar -x -C /tmp/gitea-runner-proxy-test
socket="unix:///var/run/docker.sock"
users=(0 1000:2375)
if [ "$target" = dind-rootless ]; then
socket="unix:///run/user/1000/docker.sock"
users=(1000 0)
fi
for user in "${users[@]}"; do
proxy_mode="proxy"
if [ "$user" = 1000:2375 ]; then
proxy_mode="direct"
fi
echo "==> Running mounted Docker job inside ${target} as UID ${user}, expecting ${proxy_mode} access"
docker -H "$host_docker" exec --user "$user" -w /tmp/gitea-runner-proxy-test \
-e DOCKER_HOST="$socket" -e ACT_TEST_DOCKER_PROXY="$proxy_mode" -e ACT_TEST_IMAGE="$job_image" \
"$name" ./runner.test -test.v -test.run '^TestDockerProxyMountedJob$' -test.timeout 3m
done
echo "==> Running mounted Docker job in a container given the ${target} socket, expecting proxy access"
docker -H "$host_docker" exec -e DOCKER_HOST="$socket" "$name" docker run --rm -v "${socket#unix://}:/var/run/docker.sock" -v /tmp/gitea-runner-proxy-test:/data -w /data \
-e ACT_TEST_DOCKER_PROXY=proxy -e ACT_TEST_IMAGE="$job_image" "$job_image" ./runner.test -test.v -test.run '^TestDockerProxyMountedJob$' -test.timeout 3m
supervised="$name-s6"
supervised_exit_is() {
if [ "$(docker -H "$host_docker" inspect -f '{{.State.ExitCode}}' "$supervised")" != "$1" ] ||
! docker -H "$host_docker" logs "$supervised" 2>&1 | grep -F "$2" >/dev/null; then
docker -H "$host_docker" logs "$supervised" >&2
echo "${target} supervision: expected exit $1 after \"$2\"" >&2
exit 1
fi
}
echo "==> Checking that a runner failure sets the ${target} container exit code"
docker -H "$host_docker" run -d --privileged --name "$supervised" -e DOCKER_TLS_CERTDIR= -e RUNNER_STATE_FILE=/etc/hostname "$image" >/dev/null
for _ in $(seq 1 120); do
[ "$(docker -H "$host_docker" inspect -f '{{.State.Running}}' "$supervised")" = false ] && break
sleep 1
done
supervised_exit_is 1 'registration file not found'
docker -H "$host_docker" rm -fv "$supervised" >/dev/null
echo "==> Checking that stopping a started ${target} runner exits 0"
docker -H "$host_docker" run -d --privileged --name "$supervised" -e DOCKER_TLS_CERTDIR= "$image" >/dev/null
for _ in $(seq 1 120); do
docker -H "$host_docker" logs "$supervised" 2>&1 | grep -F 'is missing or not a regular file' >/dev/null && break
sleep 1
done
docker -H "$host_docker" stop "$supervised" >/dev/null
supervised_exit_is 0 'is missing or not a regular file'
fi