446 lines
14 KiB
Bash
Executable File
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
|