Files

446 lines
14 KiB
Bash
Executable File

#!/usr/bin/env bash
set -Eeuo pipefail
die() {
printf 'error: %s\n' "$*" >&2
exit 1
}
script_path=${BASH_SOURCE[0]}
case "$script_path" in
*/*) script_dir=${script_path%/*} ;;
*) script_dir=. ;;
esac
cd "$script_dir/.."
repo_root=$PWD
if command -v podman >/dev/null 2>&1; then
engine=podman
elif command -v docker >/dev/null 2>&1; then
engine=docker
else
die 'podman or docker required'
fi
run_e2e=${DEV_RUN_E2E:-0}
case "${1:-}" in
--e2e) run_e2e=1 ;;
'') ;;
*) die "unknown argument: $1" ;;
esac
dev_realm=${DEV_REALM:-DEV.TEST}
dev_workgroup=${DEV_WORKGROUP:-DEV}
dev_dns_domain=${DEV_DNS_DOMAIN:-dev.test}
dev_base_dn=${DEV_BASE_DN:-DC=dev,DC=test}
dev_web_hostname=${DEV_WEB_HOSTNAME:-files.localhost}
dev_dc_password=${DEV_DC_PASSWORD:-PreviewDc123!}
dev_user_password=${DEV_USER_PASSWORD:-PreviewUser123!}
dev_admin_user=${DEV_ADMIN_USER:-previewadmin}
dev_admin_password=${DEV_ADMIN_PASSWORD:-PreviewAdmin123!}
dev_backup_user=${DEV_BACKUP_USER:-preview}
dev_backup_password=${DEV_BACKUP_PASSWORD:-PreviewBackup123!}
dev_archive_password=${DEV_ARCHIVE_PASSWORD:-PreviewArchive123!}
dev_backup_auto_enabled=${DEV_BACKUP_AUTO_ENABLED:-true}
dev_ca_password=${DEV_CA_PASSWORD:-PreviewCa123!}
dev_ca_provisioner=${DEV_CA_PROVISIONER:-preview}
dev_https_port=${DEV_HTTPS_PORT:-8443}
dev_activity_interval=${DEV_ACTIVITY_INTERVAL_SECONDS:-4}
dev_backup_interval=${DEV_BACKUP_INTERVAL_SECONDS:-120}
server_image=${DEV_SERVER_IMAGE:-ad-ds-simple-file-server-dev:latest}
ad_image=${DEV_AD_IMAGE:-ad-ds-simple-file-server-ad-dev:latest}
backup_image=${DEV_BACKUP_IMAGE:-ad-ds-simple-file-server-backup-dev:latest}
ca_image=${DEV_CA_IMAGE:-docker.io/smallstep/step-ca:latest}
run_id="ad-file-server-dev-$$-$RANDOM"
network_name="$run_id-net"
dc_container="$run_id-dc"
ca_container="$run_id-ca"
backup_container="$run_id-backup"
files_container="$run_id-files"
client_container="$run_id-client"
ca_volume="$run_id-ca-state"
backup_volume="$run_id-backup-data"
private_volume="$run_id-private-data"
fslogix_volume="$run_id-fslogix-data"
groups_volume="$run_id-group-data"
state_volume="$run_id-state-data"
samba_volume="$run_id-samba-lib"
containers=(
"$client_container"
"$files_container"
"$dc_container"
"$backup_container"
"$ca_container"
)
volumes=(
"$ca_volume"
"$backup_volume"
"$private_volume"
"$fslogix_volume"
"$groups_volume"
"$state_volume"
"$samba_volume"
)
network_created=0
watchdog_pid=
log_pid=
backup_driver_pid=
ca_root_file=
stop_resources() {
local container
local volume
for container in "${containers[@]}"; do
"$engine" stop -t 3 "$container" >/dev/null 2>&1 || true
done
for container in "${containers[@]}"; do
"$engine" rm -f "$container" >/dev/null 2>&1 || true
done
if [[ $network_created == 1 ]]; then
"$engine" network rm "$network_name" >/dev/null 2>&1 || true
fi
for volume in "${volumes[@]}"; do
"$engine" volume rm "$volume" >/dev/null 2>&1 || true
done
}
start_watchdog() {
local parent_pid=$$
(
while kill -0 "$parent_pid" >/dev/null 2>&1; do
sleep 1
done
stop_resources
) &
watchdog_pid=$!
}
cleanup() {
local status=$?
trap - EXIT INT TERM HUP QUIT
if [[ -n ${backup_driver_pid:-} ]]; then
kill "$backup_driver_pid" >/dev/null 2>&1 || true
wait "$backup_driver_pid" 2>/dev/null || true
fi
if [[ -n ${log_pid:-} ]]; then
kill "$log_pid" >/dev/null 2>&1 || true
wait "$log_pid" 2>/dev/null || true
fi
if [[ -n ${watchdog_pid:-} ]]; then
kill "$watchdog_pid" >/dev/null 2>&1 || true
wait "$watchdog_pid" 2>/dev/null || true
fi
stop_resources
if [[ -n ${ca_root_file:-} && -f $ca_root_file ]]; then
rm -f -- "$ca_root_file"
fi
exit "$status"
}
trap cleanup EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
trap 'exit 129' HUP
trap 'exit 131' QUIT
show_logs() {
local container
for container in "$dc_container" "$ca_container" "$backup_container" "$files_container" "$client_container"; do
if "$engine" inspect "$container" >/dev/null 2>&1; then
printf '\n===== %s =====\n' "$container" >&2
"$engine" logs --tail 160 "$container" >&2 || true
fi
done
}
wait_for_exec() {
local container=$1
local seconds=$2
shift 2
local elapsed=0
while (( elapsed < seconds )); do
if "$engine" exec "$container" "$@" >/dev/null 2>&1; then
return 0
fi
if [[ $("$engine" inspect -f '{{.State.Running}}' "$container" 2>/dev/null || true) != true ]]; then
return 1
fi
sleep 1
elapsed=$((elapsed + 1))
done
return 1
}
create_network() {
local attempt
local second
local third
for attempt in $(seq 0 31); do
second=$((20 + ((RANDOM + attempt) % 10)))
third=$(((RANDOM + $$ + attempt) % 240 + 8))
subnet="172.${second}.${third}.0/24"
if "$engine" network create --subnet "$subnet" "$network_name" >/dev/null 2>&1; then
network_created=1
subnet_prefix="172.${second}.${third}."
return 0
fi
done
return 1
}
start_watchdog
printf 'engine: %s\n' "$engine"
if [[ ${DEV_SKIP_BUILD:-0} != 1 ]]; then
printf 'building file server image: %s\n' "$server_image"
"$engine" build -t "$server_image" -f "$repo_root/Dockerfile" "$repo_root"
printf 'building AD/client image: %s\n' "$ad_image"
"$engine" build -t "$ad_image" -f "$repo_root/dev/ad-dc.Dockerfile" "$repo_root"
printf 'building backup image: %s\n' "$backup_image"
"$engine" build -t "$backup_image" -f "$repo_root/dev/backup.Dockerfile" "$repo_root"
fi
printf 'creating isolated network: %s\n' "$network_name"
create_network || die 'unable to allocate an isolated container subnet'
dc_ip="${subnet_prefix}10"
ca_ip="${subnet_prefix}20"
files_ip="${subnet_prefix}30"
backup_ip="${subnet_prefix}40"
for volume in "${volumes[@]}"; do
"$engine" volume create "$volume" >/dev/null
done
printf 'starting local CA: %s\n' "$ca_image"
"$engine" run -d \
--name "$ca_container" \
--hostname "ca.${dev_dns_domain}" \
--network "$network_name" \
--ip "$ca_ip" \
--add-host "${dev_web_hostname}:${files_ip}" \
-e "DOCKER_STEPCA_INIT_NAME=AD file server preview CA" \
-e "DOCKER_STEPCA_INIT_DNS_NAMES=ca.${dev_dns_domain},localhost,127.0.0.1" \
-e "DOCKER_STEPCA_INIT_PROVISIONER_NAME=${dev_ca_provisioner}" \
-e "DOCKER_STEPCA_INIT_PASSWORD=${dev_ca_password}" \
-e "DOCKER_STEPCA_INIT_ACME=true" \
-v "$ca_volume:/home/step" \
"$ca_image" >/dev/null
if ! wait_for_exec "$ca_container" 120 step ca health \
--ca-url=https://localhost:9000 \
--root=/home/step/certs/root_ca.crt; then
show_logs
die 'local CA did not become healthy'
fi
preview_tmp_root=${TMPDIR:-/tmp}
ca_root_file="${preview_tmp_root%/}/${run_id}-root-ca.crt"
"$engine" exec "$ca_container" cat /home/step/certs/root_ca.crt > "$ca_root_file"
printf 'starting disposable rsync backup target\n'
"$engine" run -d \
--name "$backup_container" \
--hostname "backup.${dev_dns_domain}" \
--network "$network_name" \
--ip "$backup_ip" \
-e "BACKUP_USERNAME=${dev_backup_user}" \
-e "BACKUP_PASSWORD=${dev_backup_password}" \
-e "PREVIEW_ARCHIVE_PASSWORD=${dev_archive_password}" \
-v "$backup_volume:/backup" \
"$backup_image" >/dev/null
printf 'starting disposable Samba AD DC: %s\n' "$dev_realm"
"$engine" run -d \
--name "$dc_container" \
--hostname "dc.${dev_dns_domain}" \
--network "$network_name" \
--ip "$dc_ip" \
-e "AD_REALM=${dev_realm}" \
-e "AD_DOMAIN=${dev_workgroup}" \
-e "AD_DNS_DOMAIN=${dev_dns_domain}" \
-e "AD_BASE_DN=${dev_base_dn}" \
-e "AD_ADMIN_PASSWORD=${dev_dc_password}" \
-e "AD_USER_PASSWORD=${dev_user_password}" \
-e "AD_WEB_ADMIN_USER=${dev_admin_user}" \
-e "AD_WEB_ADMIN_PASSWORD=${dev_admin_password}" \
-e "DEV_CA_IP=${ca_ip}" \
-e "DEV_BACKUP_IP=${backup_ip}" \
-e "DEV_FILESERVER_IP=${files_ip}" \
"$ad_image" >/dev/null
if ! wait_for_exec "$dc_container" 150 test -f /run/preview-ready; then
show_logs
die 'dummy AD domain did not become ready'
fi
domain_sid=$("$engine" exec "$dc_container" cat /run/domain-sid)
printf 'seeding preview files\n'
"$engine" run --rm \
--network "$network_name" \
-e "DEV_SEED_MB=${DEV_SEED_MB:-8}" \
-v "$private_volume:/data/private" \
-v "$fslogix_volume:/data/fslogix" \
-v "$groups_volume:/data/groups" \
-v "$state_volume:/state" \
--entrypoint /usr/local/bin/preview-seed-files \
"$ad_image"
printf 'starting actual file server and HTTPS web UI\n'
"$engine" run -d \
--name "$files_container" \
--hostname files \
--network "$network_name" \
--ip "$files_ip" \
--dns "$dc_ip" \
--dns-search "$dev_dns_domain" \
-p "127.0.0.1:${dev_https_port}:8443" \
-e "REALM=${dev_realm}" \
-e "WORKGROUP=${dev_workgroup}" \
-e "DOMAIN=dc.${dev_dns_domain}" \
-e "LDAP_URI=ldap://dc.${dev_dns_domain}" \
-e "LDAP_BASE_DN=${dev_base_dn}" \
-e "JOIN_USER=Administrator" \
-e "JOIN_PASSWORD=${dev_dc_password}" \
-e "DOMAIN_USERS_SID=${domain_sid}-513" \
-e "DOMAIN_ADMINS_SID=${domain_sid}-512" \
-e "FSLOGIX_GROUP_SID=${domain_sid}-513" \
-e "SAMBA_HOSTNAME=files" \
-e "NETBIOS_NAME=FILES" \
-e "AD_DNS_NAME=files.${dev_dns_domain}" \
-e "WEB_ENABLED=true" \
-e "WEB_HOSTNAME=${dev_web_hostname}" \
-e "WEB_BIND_PORT=8443" \
-e "WEB_JWT_SECRET=preview-only-jwt-secret-with-at-least-32-bytes" \
-e "WEB_TLS_MODE=acme" \
-e "ACME_CA_SERVER=https://ca.${dev_dns_domain}:9000/acme/acme/directory" \
-e "ACME_CA_CERTIFICATES=/state/tls/acme-ca-certificates.pem" \
-e "ACME_CA_CERTIFICATES_URL=https://ca.${dev_dns_domain}:9000/roots.pem" \
-e "ACME_CA_CERTIFICATES_INSECURE_DOWNLOAD=true" \
-e "ACME_HTTP_LISTEN=:80" \
-e "WEB_USAGE_SCAN_INTERVAL_SECONDS=60" \
-e "WEB_DIRECTORY_CACHE_SECONDS=30" \
-e "AUDIT_POLL_SECONDS=0.25" \
-e "BACKUP_DESTINATION=rsync://${dev_backup_user}:${dev_backup_password}@backup.${dev_dns_domain}/backups/fileserver" \
-e "BACKUP_ARCHIVE_PASSWORD=${dev_archive_password}" \
-e "BACKUP_AUTO_ENABLED=${dev_backup_auto_enabled}" \
-e "BACKUP_PROGRESS=never" \
-e "BACKUP_PROGRESS_INTERVAL_SECONDS=1" \
-e "BACKUP_RETENTION_DAILY=3" \
-e "BACKUP_RETENTION_WEEKLY=2" \
-e "BACKUP_RETENTION_MONTHLY=1" \
-e "BACKUP_RETENTION_YEARLY=1" \
-v "$private_volume:/data/private" \
-v "$fslogix_volume:/data/fslogix" \
-v "$groups_volume:/data/groups" \
-v "$state_volume:/state" \
-v "$samba_volume:/var/lib/samba" \
"$server_image" >/dev/null
if ! wait_for_exec "$files_container" 240 python3 -c \
'import ssl,urllib.request; print(urllib.request.urlopen("https://127.0.0.1:8443/healthz", context=ssl._create_unverified_context(), timeout=3).status)'; then
show_logs
die 'file server web UI did not become healthy'
fi
if [[ $run_e2e == 1 ]]; then
printf 'validating one-time untrusted CA root retrieval across a restart\n'
initial_ca_bundle_stat=$("$engine" exec "$files_container" stat -c '%i:%s:%Y' /state/tls/acme-ca-certificates.pem)
"$engine" restart "$files_container" >/dev/null
if ! wait_for_exec "$files_container" 240 python3 -c \
'import ssl,urllib.request; print(urllib.request.urlopen("https://127.0.0.1:8443/healthz", context=ssl._create_unverified_context(), timeout=3).status)'; then
show_logs
die 'file server web UI did not become healthy after the one-time CA download restart check'
fi
restarted_ca_bundle_stat=$("$engine" exec "$files_container" stat -c '%i:%s:%Y' /state/tls/acme-ca-certificates.pem)
if [[ "$initial_ca_bundle_stat" != "$restarted_ca_bundle_stat" ]]; then
show_logs
die 'persisted ACME CA certificate bundle changed during restart'
fi
ca_bundle_download_count=$("$engine" logs "$files_container" 2>&1 | awk '/Stored the initial ACME CA certificate bundle/ { count++ } END { print count + 0 }')
if [[ "$ca_bundle_download_count" != 1 ]]; then
show_logs
die "expected exactly one ACME CA certificate download, got ${ca_bundle_download_count}"
fi
fi
printf 'starting continuous authenticated SMB activity client\n'
"$engine" run -d \
--name "$client_container" \
--hostname preview-client \
--network "$network_name" \
--dns "$dc_ip" \
--dns-search "$dev_dns_domain" \
--add-host "files.${dev_dns_domain}:${files_ip}" \
-e "AD_DOMAIN=${dev_workgroup}" \
-e "AD_USER_PASSWORD=${dev_user_password}" \
-e "FILESERVER_HOST=files.${dev_dns_domain}" \
-e "DEV_ACTIVITY_INTERVAL_SECONDS=${dev_activity_interval}" \
"$ad_image" /usr/local/bin/preview-client >/dev/null
if ! wait_for_exec "$client_container" 120 test -f /run/preview-client-ready; then
show_logs
die 'preview SMB client did not complete its initial activity'
fi
run_backup_loop() {
local elapsed
while "$engine" inspect "$files_container" >/dev/null 2>&1; do
"$engine" exec "$files_container" /bin/bash -lc \
'source /app/runtime.env && BACKUP_TRIGGER=automatic exec /usr/bin/python3 /app/backup_to_destination.py' || true
elapsed=0
while (( elapsed < dev_backup_interval )); do
"$engine" inspect "$files_container" >/dev/null 2>&1 || return 0
sleep 1
elapsed=$((elapsed + 1))
done
done
}
run_backup_loop &
backup_driver_pid=$!
if [[ $run_e2e == 1 ]]; then
printf 'running end-to-end assertions\n'
if ! PREVIEW_ENGINE="$engine" \
PREVIEW_FILES_CONTAINER="$files_container" \
PREVIEW_CLIENT_CONTAINER="$client_container" \
PREVIEW_BACKUP_CONTAINER="$backup_container" \
PREVIEW_CA_ROOT="$ca_root_file" \
PREVIEW_HTTPS_PORT="$dev_https_port" \
PREVIEW_HTTPS_HOST="$dev_web_hostname" \
PREVIEW_REALM="$dev_realm" \
PREVIEW_WORKGROUP="$dev_workgroup" \
PREVIEW_DNS_DOMAIN="$dev_dns_domain" \
PREVIEW_DOMAIN_SID="$domain_sid" \
PREVIEW_ADMIN_USER="$dev_admin_user" \
PREVIEW_ADMIN_PASSWORD="$dev_admin_password" \
PREVIEW_USER_PASSWORD="$dev_user_password" \
python3 "$repo_root/dev/e2e.py"; then
show_logs
exit 1
fi
exit 0
fi
printf '\nPreview ready\n'
printf ' URL: https://%s:%s\n' "$dev_web_hostname" "$dev_https_port"
printf ' Username: %s\\%s\n' "$dev_workgroup" "$dev_admin_user"
printf ' Password: %s\n' "$dev_admin_password"
printf ' CA root: %s\n' "$ca_root_file"
printf '\nThe CA is disposable, so import the displayed root only as temporary trust.\n'
printf 'SMB activity and backups repeat in the background. Press Ctrl-C to remove everything.\n\n'
"$engine" logs -f "$files_container" &
log_pid=$!
file_status=$("$engine" wait "$files_container" 2>/dev/null || printf '1')
case "$file_status" in
''|*[!0-9]*) exit 1 ;;
*) exit "$file_status" ;;
esac