#!/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