Files
linux-scripts/docker-smoke-tests.sh
chiefgeek a1a17e81a1 Sync all scripts from website downloads — 352 scripts total
Includes updated JS challenge scripts with Claude-User whitelist,
same-site referer bypass, Blackbox-Exporter allowed bot, and all
new exporters, cheat sheets, and automation scripts.
2026-05-25 03:31:08 +02:00

547 lines
23 KiB
Bash
Executable File

#!/usr/bin/env bash
#####################################################################################
#### docker-smoke-tests.sh — Verify Docker daemon and containers are healthy ####
#### Checks daemon, API, lifecycle, networking, volumes, DNS, compose, images. ####
#### ####
#### Author: Phil Connor ####
#### Contact: contact@mylinux.work ####
#### License: MIT ####
#### Version: 1.0 ####
#### ####
#### Usage: ./docker-smoke-tests.sh ####
#### COMPOSE_FILE=/path/to/docker-compose.yml ./docker-smoke-tests.sh ####
#### ####
#### See --help for all options. ####
#####################################################################################
set -euo pipefail
# ── Defaults ──────────────────────────────────────────────────────────
TEST_IMAGE="${TEST_IMAGE:-alpine:latest}"
COMPOSE_FILE="${COMPOSE_FILE:-}"
SKIP_LIFECYCLE="${SKIP_LIFECYCLE:-false}"
SKIP_NETWORK="${SKIP_NETWORK:-false}"
SKIP_VOLUME="${SKIP_VOLUME:-false}"
SKIP_BUILD="${SKIP_BUILD:-false}"
DNS_TEST_DOMAIN="${DNS_TEST_DOMAIN:-google.com}"
OUTPUT_FORMAT="${OUTPUT_FORMAT:-text}"
COLOR="${COLOR:-auto}"
VERBOSE="${VERBOSE:-false}"
# ── State ─────────────────────────────────────────────────────────────
PASS=0
FAIL=0
SKIP=0
TOTAL=0
RESULTS=()
START_TIME=""
# ── Test artifact names ──────────────────────────────────────────────
SMOKE_PREFIX="smoke-test-$$"
SMOKE_CONTAINER="${SMOKE_PREFIX}-ctr"
SMOKE_PORT_CONTAINER="${SMOKE_PREFIX}-port"
SMOKE_DNS_CONTAINER="${SMOKE_PREFIX}-dns"
SMOKE_VOL_CONTAINER="${SMOKE_PREFIX}-vol"
SMOKE_NET_CONTAINER="${SMOKE_PREFIX}-net"
SMOKE_MEM_CONTAINER="${SMOKE_PREFIX}-mem"
SMOKE_VOLUME="${SMOKE_PREFIX}-vol"
SMOKE_NETWORK="${SMOKE_PREFIX}-net"
SMOKE_BUILD_TAG="${SMOKE_PREFIX}-img"
SMOKE_PORT=$((47000 + (RANDOM % 1000)))
# ── Colors ────────────────────────────────────────────────────────────
setup_colors() {
if [[ "$COLOR" == "never" ]]; then
RED="" GREEN="" YELLOW="" BLUE="" BOLD="" RESET=""
return
fi
if [[ "$COLOR" == "always" ]] || [[ -t 1 ]]; then
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
BLUE='\033[0;34m'
BOLD='\033[1m'
RESET='\033[0m'
else
RED="" GREEN="" YELLOW="" BLUE="" BOLD="" RESET=""
fi
}
# ── Logging ───────────────────────────────────────────────────────────
log() { echo -e "${BLUE}[INFO]${RESET} $*"; }
warn() { echo -e "${YELLOW}[WARN]${RESET} $*" >&2; }
err() { echo -e "${RED}[ERROR]${RESET} $*" >&2; }
verbose() { if [[ "$VERBOSE" == "true" ]]; then echo -e "${BLUE}[DEBUG]${RESET} $*"; fi; }
# ── Test Result Recording ─────────────────────────────────────────────
record_pass() {
local name="$1" detail="${2:-}"
((PASS++)) || true; ((TOTAL++)) || true
RESULTS+=("PASS|${name}|${detail}")
if [[ "$OUTPUT_FORMAT" == "tap" ]]; then echo "ok ${TOTAL} - ${name}${detail:+ (${detail})}"
else echo -e " ${GREEN}${RESET} ${name}${detail:+ — ${detail}}"; fi
}
record_fail() {
local name="$1" detail="${2:-}"
((FAIL++)) || true; ((TOTAL++)) || true
RESULTS+=("FAIL|${name}|${detail}")
if [[ "$OUTPUT_FORMAT" == "tap" ]]; then
echo "not ok ${TOTAL} - ${name}"
[[ -n "$detail" ]] && echo " # ${detail}"
else echo -e " ${RED}${RESET} ${name}${detail:+ — ${detail}}"; fi
}
record_skip() {
local name="$1" reason="${2:-}"
((SKIP++)) || true; ((TOTAL++)) || true
RESULTS+=("SKIP|${name}|${reason}")
if [[ "$OUTPUT_FORMAT" == "tap" ]]; then echo "ok ${TOTAL} - ${name} # SKIP ${reason}"
else echo -e " ${YELLOW}${RESET} ${name}${reason:+ — ${reason}}"; fi
}
# ── Helpers ───────────────────────────────────────────────────────────
has_cmd() { command -v "$1" >/dev/null 2>&1; }
remove_container() {
local name="$1"
docker rm -f "$name" >/dev/null 2>&1 || true
}
section() {
if [[ "$OUTPUT_FORMAT" != "tap" ]]; then echo ""; echo -e "${BOLD}$1${RESET}"; fi
}
# ── Cleanup ───────────────────────────────────────────────────────────
# shellcheck disable=SC2317
cleanup() {
verbose "Cleaning up test artifacts..."
remove_container "$SMOKE_CONTAINER"
remove_container "$SMOKE_PORT_CONTAINER"
remove_container "$SMOKE_DNS_CONTAINER"
remove_container "$SMOKE_VOL_CONTAINER"
remove_container "$SMOKE_NET_CONTAINER"
remove_container "$SMOKE_MEM_CONTAINER"
docker volume rm -f "$SMOKE_VOLUME" >/dev/null 2>&1 || true
docker network rm "$SMOKE_NETWORK" >/dev/null 2>&1 || true
docker rmi -f "$SMOKE_BUILD_TAG" >/dev/null 2>&1 || true
}
trap cleanup EXIT
# ══════════════════════════════════════════════════════════════════════
# TEST FUNCTIONS
# ══════════════════════════════════════════════════════════════════════
# ── 1. Docker daemon running ─────────────────────────────────────────
test_daemon_running() {
if has_cmd systemctl; then
if systemctl is-active --quiet docker 2>/dev/null; then record_pass "Docker daemon running" "systemctl active"
else record_fail "Docker daemon running" "systemctl inactive"; fi
elif has_cmd service; then
if service docker status >/dev/null 2>&1; then record_pass "Docker daemon running" "service running"
else record_fail "Docker daemon running" "service stopped"; fi
elif docker info >/dev/null 2>&1; then record_pass "Docker daemon running" "docker info ok"
else record_fail "Docker daemon running" "cannot determine status"; fi
}
# ── 2. Docker API responsive ─────────────────────────────────────────
test_api_responsive() {
local output
if output=$(timeout 10 docker info 2>&1); then
local ver; ver=$(echo "$output" | grep -i "Server Version" | head -1 | awk '{print $NF}') || true
record_pass "Docker API responsive" "server ${ver:-unknown}"
else record_fail "Docker API responsive" "docker info timed out or failed"; fi
}
# ── 3. Docker socket accessible ──────────────────────────────────────
test_socket_accessible() {
local socket="/var/run/docker.sock"
if [[ -S "$socket" ]]; then
if [[ -r "$socket" && -w "$socket" ]]; then record_pass "Docker socket accessible" "$socket"
else record_fail "Docker socket accessible" "$socket not readable/writable"; fi
elif docker info >/dev/null 2>&1; then record_pass "Docker socket accessible" "non-default socket"
else record_fail "Docker socket accessible" "$socket not found"; fi
}
# ── 4. Container lifecycle ───────────────────────────────────────────
test_container_lifecycle() {
if [[ "$SKIP_LIFECYCLE" == "true" ]]; then record_skip "Container lifecycle" "SKIP_LIFECYCLE=true"; return; fi
remove_container "$SMOKE_CONTAINER"
if ! docker create --name "$SMOKE_CONTAINER" "$TEST_IMAGE" sleep 30 >/dev/null 2>&1; then
record_fail "Container lifecycle" "docker create failed"
return
fi
if ! docker start "$SMOKE_CONTAINER" >/dev/null 2>&1; then
record_fail "Container lifecycle" "docker start failed"
return
fi
local exec_output
exec_output=$(docker exec "$SMOKE_CONTAINER" echo "smoke-ok" 2>&1) || true
if [[ "$exec_output" != "smoke-ok" ]]; then
record_fail "Container lifecycle" "docker exec failed"
return
fi
if ! docker stop -t 5 "$SMOKE_CONTAINER" >/dev/null 2>&1; then
record_fail "Container lifecycle" "docker stop failed"
return
fi
if ! docker rm "$SMOKE_CONTAINER" >/dev/null 2>&1; then
record_fail "Container lifecycle" "docker rm failed"
return
fi
record_pass "Container lifecycle" "create/start/exec/stop/rm"
}
# ── 5. Port binding ──────────────────────────────────────────────────
test_port_binding() {
if [[ "$SKIP_LIFECYCLE" == "true" ]]; then record_skip "Port binding" "SKIP_LIFECYCLE=true"; return; fi
if ! has_cmd curl; then record_skip "Port binding" "curl not installed"; return; fi
remove_container "$SMOKE_PORT_CONTAINER"
if ! docker run -d --name "$SMOKE_PORT_CONTAINER" \
-p "${SMOKE_PORT}:80" \
"$TEST_IMAGE" sh -c 'mkdir -p /var/www && echo "smoke-ok" > /var/www/index.html && httpd -f -p 80 -h /var/www 2>/dev/null || { while true; do echo -e "HTTP/1.1 200 OK\r\nContent-Length: 9\r\n\r\nsmoke-ok\n" | nc -l -p 80 2>/dev/null || break; done; }' >/dev/null 2>&1; then
record_fail "Port binding" "failed to start container with port mapping"
return
fi
sleep 2
local response
response=$(curl -sf --max-time 5 "http://localhost:${SMOKE_PORT}/" 2>/dev/null) || true
remove_container "$SMOKE_PORT_CONTAINER"
if [[ "$response" == *"smoke-ok"* ]]; then
record_pass "Port binding" "curl localhost:${SMOKE_PORT}"
else
record_fail "Port binding" "no response on localhost:${SMOKE_PORT}"
fi
}
# ── 6. Container DNS ─────────────────────────────────────────────────
test_container_dns() {
if [[ "$SKIP_LIFECYCLE" == "true" ]]; then record_skip "Container DNS" "SKIP_LIFECYCLE=true"; return; fi
remove_container "$SMOKE_DNS_CONTAINER"
local dns_output
dns_output=$(docker run --rm --name "$SMOKE_DNS_CONTAINER" "$TEST_IMAGE" \
sh -c "nslookup ${DNS_TEST_DOMAIN} 2>/dev/null || getent hosts ${DNS_TEST_DOMAIN} 2>/dev/null || ping -c1 -W3 ${DNS_TEST_DOMAIN} 2>/dev/null" 2>&1) || true
if [[ -n "$dns_output" ]] && ! echo "$dns_output" | grep -qi "can't resolve\|not found\|failure\|NXDOMAIN"; then
record_pass "Container DNS" "${DNS_TEST_DOMAIN}"
else
record_fail "Container DNS" "failed to resolve ${DNS_TEST_DOMAIN}"
fi
}
# ── 7. Volume mount ──────────────────────────────────────────────────
test_volume_mount() {
if [[ "$SKIP_VOLUME" == "true" ]]; then record_skip "Volume mount" "SKIP_VOLUME=true"; return; fi
docker volume rm -f "$SMOKE_VOLUME" >/dev/null 2>&1 || true
remove_container "$SMOKE_VOL_CONTAINER"
if ! docker volume create "$SMOKE_VOLUME" >/dev/null 2>&1; then
record_fail "Volume mount" "docker volume create failed"
return
fi
local write_result
write_result=$(docker run --rm --name "$SMOKE_VOL_CONTAINER" \
-v "${SMOKE_VOLUME}:/data" "$TEST_IMAGE" \
sh -c 'echo "smoke-vol-ok" > /data/test.txt && cat /data/test.txt' 2>&1) || true
docker volume rm -f "$SMOKE_VOLUME" >/dev/null 2>&1 || true
if [[ "$write_result" == "smoke-vol-ok" ]]; then
record_pass "Volume mount" "write/read verified"
else
record_fail "Volume mount" "write/read mismatch"
fi
}
# ── 8. Network create/connect ────────────────────────────────────────
test_network_create() {
if [[ "$SKIP_NETWORK" == "true" ]]; then record_skip "Network create/connect" "SKIP_NETWORK=true"; return; fi
docker network rm "$SMOKE_NETWORK" >/dev/null 2>&1 || true
remove_container "$SMOKE_NET_CONTAINER"
if ! docker network create --driver bridge "$SMOKE_NETWORK" >/dev/null 2>&1; then
record_fail "Network create/connect" "docker network create failed"
return
fi
local net_output
net_output=$(docker run --rm --name "$SMOKE_NET_CONTAINER" \
--network "$SMOKE_NETWORK" "$TEST_IMAGE" \
sh -c 'ip addr show 2>/dev/null || ifconfig 2>/dev/null' 2>&1) || true
docker network rm "$SMOKE_NETWORK" >/dev/null 2>&1 || true
if [[ -n "$net_output" ]]; then
record_pass "Network create/connect" "bridge network"
else
record_fail "Network create/connect" "container failed to attach to network"
fi
}
# ── 9. Image pull ────────────────────────────────────────────────────
test_image_pull() {
if docker pull "$TEST_IMAGE" >/dev/null 2>&1; then
record_pass "Image pull" "$TEST_IMAGE"
else
record_fail "Image pull" "failed to pull $TEST_IMAGE"
fi
}
# ── 10. Image build ──────────────────────────────────────────────────
test_image_build() {
if [[ "$SKIP_BUILD" == "true" ]]; then record_skip "Image build" "SKIP_BUILD=true"; return; fi
docker rmi -f "$SMOKE_BUILD_TAG" >/dev/null 2>&1 || true
if echo "FROM alpine:latest" | docker build -t "$SMOKE_BUILD_TAG" - >/dev/null 2>&1; then
docker rmi -f "$SMOKE_BUILD_TAG" >/dev/null 2>&1 || true
record_pass "Image build" "inline Dockerfile"
else
record_fail "Image build" "docker build failed"
fi
}
# ── 11. Docker Compose ───────────────────────────────────────────────
test_compose_stack() {
if [[ -z "$COMPOSE_FILE" ]]; then
record_skip "Compose stack" "COMPOSE_FILE not set"
return
fi
if [[ ! -f "$COMPOSE_FILE" ]]; then
record_fail "Compose stack" "${COMPOSE_FILE} not found"
return
fi
local compose_cmd=""
if docker compose version >/dev/null 2>&1; then
compose_cmd="docker compose"
elif has_cmd docker-compose; then
compose_cmd="docker-compose"
else
record_skip "Compose stack" "neither 'docker compose' nor 'docker-compose' available"
return
fi
local ps_output expected_count running_count
ps_output=$($compose_cmd -f "$COMPOSE_FILE" ps --format json 2>/dev/null) || true
if [[ -z "$ps_output" ]]; then
ps_output=$($compose_cmd -f "$COMPOSE_FILE" ps 2>/dev/null) || true
if [[ -z "$ps_output" ]]; then
record_fail "Compose stack" "could not read compose project status"
return
fi
expected_count=$(echo "$ps_output" | tail -n +2 | wc -l)
running_count=$(echo "$ps_output" | tail -n +2 | grep -ciE "up|running" || true)
else
expected_count=$(echo "$ps_output" | grep -c '"Service"' 2>/dev/null || echo "$ps_output" | wc -l)
running_count=$(echo "$ps_output" | grep -ciE '"running"' 2>/dev/null || true)
fi
if [[ "$expected_count" -eq 0 ]]; then
record_fail "Compose stack" "no services found"
elif [[ "$running_count" -ge "$expected_count" ]]; then
record_pass "Compose stack" "${running_count}/${expected_count} services running"
else
record_fail "Compose stack" "${running_count}/${expected_count} services running"
fi
}
# ── 12. Resource limits ──────────────────────────────────────────────
test_resource_limits() {
if [[ "$SKIP_LIFECYCLE" == "true" ]]; then record_skip "Resource limits" "SKIP_LIFECYCLE=true"; return; fi
remove_container "$SMOKE_MEM_CONTAINER"
local mem_limit
mem_limit=$(docker run --rm --name "$SMOKE_MEM_CONTAINER" \
--memory=64m "$TEST_IMAGE" \
sh -c 'cat /sys/fs/cgroup/memory.max 2>/dev/null || cat /sys/fs/cgroup/memory/memory.limit_in_bytes 2>/dev/null' 2>&1) || true
if [[ -z "$mem_limit" ]]; then
record_skip "Resource limits" "cgroup memory info not available"
return
fi
local limit_bytes=67108864 # 64 MiB
if [[ "$mem_limit" =~ ^[0-9]+$ ]]; then
if [[ "$mem_limit" -le $((limit_bytes + 1048576)) ]]; then
local limit_mb=$((mem_limit / 1048576))
record_pass "Resource limits" "memory cgroup enforced (${limit_mb}M)"
else
record_fail "Resource limits" "memory limit not enforced (got ${mem_limit})"
fi
else
record_skip "Resource limits" "unexpected cgroup value: ${mem_limit}"
fi
}
# ── 13. Disk space ───────────────────────────────────────────────────
test_disk_space() {
local df_output
df_output=$(docker system df 2>/dev/null) || true
if [[ -z "$df_output" ]]; then
record_fail "Disk space" "docker system df failed"
return
fi
local docker_root used_pct
docker_root=$(docker info --format '{{.DockerRootDir}}' 2>/dev/null) || docker_root="/var/lib/docker"
used_pct=$(df "$docker_root" 2>/dev/null | tail -1 | awk '{print $5}' | tr -d '%') || used_pct=0
if [[ "$used_pct" -gt 80 ]]; then
record_fail "Disk space" "${used_pct}% used (threshold 80%)"
else
record_pass "Disk space" "${used_pct}% used"
fi
}
# ══════════════════════════════════════════════════════════════════════
# OUTPUT
# ══════════════════════════════════════════════════════════════════════
print_tap_header() {
echo "TAP version 13"
}
print_tap_footer() {
echo "1..${TOTAL}"
echo "# pass ${PASS}"
echo "# fail ${FAIL}"
echo "# skip ${SKIP}"
}
print_summary() {
local end_time; end_time=$(date +%s)
local duration=$(( end_time - START_TIME ))
echo ""
echo -e "${BOLD}────────────────────────────────────────${RESET}"
echo -e "${BOLD}Summary${RESET} Docker Smoke Tests"
echo -e " ${GREEN}${PASS} passed${RESET} ${RED}${FAIL} failed${RESET} ${YELLOW}${SKIP} skipped${RESET} (${duration}s)"
echo -e "${BOLD}────────────────────────────────────────${RESET}"
if [[ $FAIL -eq 0 ]]; then echo -e "${GREEN}${BOLD}All tests passed.${RESET}"
else echo -e "${RED}${BOLD}${FAIL} test(s) failed.${RESET}"; fi
}
# ══════════════════════════════════════════════════════════════════════
# MAIN
# ══════════════════════════════════════════════════════════════════════
usage() {
cat <<EOF
Usage: $(basename "$0") [OPTIONS]
Smoke-test Docker daemon, containers, networking, volumes, images, and compose.
Environment variables (all optional):
TEST_IMAGE Image for tests (default: alpine:latest)
COMPOSE_FILE docker-compose.yml path (default: none)
SKIP_LIFECYCLE Skip lifecycle tests (default: false)
SKIP_NETWORK Skip network tests (default: false)
SKIP_VOLUME Skip volume tests (default: false)
SKIP_BUILD Skip build test (default: false)
DNS_TEST_DOMAIN DNS domain to resolve (default: google.com)
OUTPUT_FORMAT text or tap (default: text)
COLOR auto, always, never (default: auto)
VERBOSE Show debug output (default: false)
Options:
--skip-lifecycle Skip container lifecycle tests
--skip-network Skip network tests
--skip-volume Skip volume tests
--skip-build Skip image build test
--format FORMAT Output format: text (default), tap
--verbose Show debug output
--no-color Disable colored output
--help Show this help
Examples:
./$(basename "$0")
COMPOSE_FILE=/opt/stacks/web/docker-compose.yml ./$(basename "$0")
OUTPUT_FORMAT=tap ./$(basename "$0") --skip-build
EOF
}
main() {
while [[ $# -gt 0 ]]; do
case "$1" in
--skip-lifecycle) SKIP_LIFECYCLE=true ;;
--skip-network) SKIP_NETWORK=true ;;
--skip-volume) SKIP_VOLUME=true ;;
--skip-build) SKIP_BUILD=true ;;
--format) OUTPUT_FORMAT="$2"; shift ;;
--verbose) VERBOSE=true ;;
--no-color) COLOR=never ;;
--help|-h) usage; exit 0 ;;
*) err "Unknown option: $1"; usage; exit 1 ;;
esac
shift
done
setup_colors
if ! has_cmd docker; then
err "docker CLI not found in PATH"
exit 1
fi
START_TIME=$(date +%s)
if [[ "$OUTPUT_FORMAT" == "tap" ]]; then
print_tap_header
else
echo ""
echo -e "${BOLD}Docker Smoke Tests${RESET}"
echo -e "Image: ${TEST_IMAGE} Port: ${SMOKE_PORT}"
echo -e "Time: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
fi
section "Daemon & Infrastructure"
test_daemon_running
test_api_responsive
test_socket_accessible
section "Image Operations"
test_image_pull
test_image_build
section "Container Operations"
test_container_lifecycle
test_port_binding
test_container_dns
section "Storage"
test_volume_mount
test_disk_space
section "Networking"
test_network_create
section "Resource Limits"
test_resource_limits
section "Compose"
test_compose_stack
# ── Results ──
if [[ "$OUTPUT_FORMAT" == "tap" ]]; then
print_tap_footer
else
print_summary
fi
[[ $FAIL -eq 0 ]] && exit 0 || exit 1
}
main "$@"