fix(demo): harden deployment scripts, remove duplicate fix-and-ship.sh

demo-stack.sh:
- Add ensure_env() to create demo.env from template if missing
- Add envsubst prerequisite check
- Fix wait_healthy() to use docker inspect instead of fragile
  sed/awk parsing of docker ps output
- Fix smoke_test() to use env vars instead of hardcoded ports
- Remove fix_env() which overwrote TA_HOST with wrong value
- Add MailHog SMTP port to display_summary()
- Add service names to smoke test output

demo-test.sh:
- Fix security compliance test to expect only 1 socket mount
  (proxy only, now that Dockhand uses DOCKER_HOST)
- Add Dockhand proxy routing check
- Fix arithmetic increment operators for set -e compatibility

- Remove scripts/fix-and-ship.sh (was identical copy of demo-stack.sh)

💘 Generated with Crush

Assisted-by: GLM-5.1 via Crush <crush@charm.land>
This commit is contained in:
reachableceo
2026-05-01 09:50:40 -05:00
parent 9f40e16b25
commit be03c95929
3 changed files with 71 additions and 267 deletions

View File

@@ -3,6 +3,7 @@ set -euo pipefail
DEMO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" DEMO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
ENV_FILE="$DEMO_DIR/demo.env" ENV_FILE="$DEMO_DIR/demo.env"
ENV_TEMPLATE="$DEMO_DIR/demo.env.template"
TEMPLATE_FILE="$DEMO_DIR/docker-compose.yml.template" TEMPLATE_FILE="$DEMO_DIR/docker-compose.yml.template"
COMPOSE_FILE="$DEMO_DIR/docker-compose.yml" COMPOSE_FILE="$DEMO_DIR/docker-compose.yml"
@@ -17,17 +18,16 @@ log_success() { echo -e "${GREEN}[OK]${NC} $1"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_error() { echo -e "${RED}[ERROR]${NC} $1"; } log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
fix_env() { ensure_env() {
log_info "Ensuring demo.env is complete..." if [[ ! -f "$ENV_FILE" ]]; then
grep -q '^TA_USERNAME=' "$ENV_FILE" || echo "TA_USERNAME=demo" >> "$ENV_FILE" if [[ -f "$ENV_TEMPLATE" ]]; then
grep -q '^TA_PASSWORD=' "$ENV_FILE" || echo "TA_PASSWORD=demo_password" >> "$ENV_FILE" log_info "Creating demo.env from template..."
grep -q '^ELASTIC_PASSWORD=' "$ENV_FILE" || echo "ELASTIC_PASSWORD=demo_password" >> "$ENV_FILE" cp "$ENV_TEMPLATE" "$ENV_FILE"
grep -q '^ES_JAVA_OPTS=' "$ENV_FILE" || echo 'ES_JAVA_OPTS="-Xms512m -Xmx512m"' >> "$ENV_FILE" else
grep -q '^ARCHIVEBOX_ADMIN_USER=' "$ENV_FILE" || echo "ARCHIVEBOX_ADMIN_USER=admin" >> "$ENV_FILE" log_error "No demo.env or demo.env.template found"
grep -q '^ARCHIVEBOX_ADMIN_PASSWORD=' "$ENV_FILE" || echo "ARCHIVEBOX_ADMIN_PASSWORD=demo_password" >> "$ENV_FILE" exit 1
sed -i 's/^ATUIN_HOST=.*/ATUIN_HOST=0.0.0.0/' "$ENV_FILE" fi
sed -i 's|^TA_HOST=.*|TA_HOST=http://localhost:4014|' "$ENV_FILE" fi
log_success "demo.env ready"
} }
detect_user() { detect_user() {
@@ -48,6 +48,10 @@ check_prerequisites() {
log_error "Docker is not running" log_error "Docker is not running"
exit 1 exit 1
fi fi
if ! command -v envsubst >/dev/null 2>&1; then
log_error "envsubst not found (install gettext package)"
exit 1
fi
local max_map_count local max_map_count
max_map_count=$(sysctl -n vm.max_map_count 2>/dev/null || echo "0") max_map_count=$(sysctl -n vm.max_map_count 2>/dev/null || echo "0")
if [[ "$max_map_count" -lt 262144 ]]; then if [[ "$max_map_count" -lt 262144 ]]; then
@@ -79,26 +83,25 @@ wait_healthy() {
log_info "Waiting for services to become healthy (max 5 min)..." log_info "Waiting for services to become healthy (max 5 min)..."
local elapsed=0 interval=15 local elapsed=0 interval=15
while [[ $elapsed -lt 300 ]]; do while [[ $elapsed -lt 300 ]]; do
local all_ok=true local unhealthy=0
while IFS= read -r line; do while IFS= read -r name; do
local name health local health
name=$(echo "$line" | awk '{print $1}') health=$(docker inspect --format='{{.State.Health.Status}}' "$name" 2>/dev/null || echo "unknown")
health=$(echo "$line" | awk '{print $2}') if [[ "$health" != "healthy" ]]; then
[[ "$name" == "NAMES" || -z "$name" ]] && continue unhealthy=$((unhealthy + 1))
if [[ "$health" != "healthy" && -n "$health" ]]; then
all_ok=false
fi fi
done < <(docker ps --filter "name=${COMPOSE_PROJECT_NAME:-kneldevstack}" --format "{{.Names}} {{.Status}}" 2>/dev/null | sed 's/(healthy)/healthy/g; s/(unhealthy)/unhealthy/g; s/(health: starting)/starting/g') done < <(docker ps --filter "name=${COMPOSE_PROJECT_NAME:-kneldevstack}" --format '{{.Names}}' 2>/dev/null)
if $all_ok; then
if [[ $unhealthy -eq 0 ]]; then
log_success "All services healthy" log_success "All services healthy"
return 0 return 0
fi fi
log_info " Still waiting... (${elapsed}s elapsed)" log_info " $unhealthy services not yet healthy (${elapsed}s elapsed)"
sleep $interval sleep $interval
elapsed=$((elapsed + interval)) elapsed=$((elapsed + interval))
done done
log_warn "Timeout - some services may not be fully healthy" log_warn "Timeout - some services may not be fully healthy"
docker ps --filter "name=${COMPOSE_PROJECT_NAME:-kneldevstack}" --format "table {{.Names}}\t{{.Status}}" cd "$DEMO_DIR" && docker compose ps
} }
display_summary() { display_summary() {
@@ -126,10 +129,11 @@ display_summary() {
echo " ArchiveBox http://localhost:${ARCHIVEBOX_PORT}" echo " ArchiveBox http://localhost:${ARCHIVEBOX_PORT}"
echo " Tube Archivist http://localhost:${TUBE_ARCHIVIST_PORT}" echo " Tube Archivist http://localhost:${TUBE_ARCHIVIST_PORT}"
echo " Wakapi http://localhost:${WAKAPI_PORT}" echo " Wakapi http://localhost:${WAKAPI_PORT}"
echo " MailHog http://localhost:${MAILHOG_PORT}" echo " MailHog (Web) http://localhost:${MAILHOG_PORT}"
echo " MailHog (SMTP) localhost:${MAILHOG_SMTP_PORT}"
echo " Atuin http://localhost:${ATUIN_PORT}" echo " Atuin http://localhost:${ATUIN_PORT}"
echo "" echo ""
echo " Credentials: ${DEMO_ADMIN_USER:-admin} / ${DEMO_ADMIN_PASSWORD:-demo_password}" echo " Credentials: admin / demo_password"
echo " FOR DEMONSTRATION PURPOSES ONLY" echo " FOR DEMONSTRATION PURPOSES ONLY"
echo "========================================================" echo "========================================================"
} }
@@ -137,15 +141,31 @@ display_summary() {
smoke_test() { smoke_test() {
log_info "Running smoke tests..." log_info "Running smoke tests..."
set -a; source "$ENV_FILE"; set +a set -a; source "$ENV_FILE"; set +a
local ports=(4000 4006 4007 4008 4009 4010 4011 4012 4013 4014 4015 4017 4018) local ports=(
"${HOMEPAGE_PORT}:Homepage"
"${PIHOLE_PORT}:Pi-hole"
"${DOCKHAND_PORT}:Dockhand"
"${INFLUXDB_PORT}:InfluxDB"
"${GRAFANA_PORT}:Grafana"
"${DRAWIO_PORT}:Draw.io"
"${KROKI_PORT}:Kroki"
"${ATOMIC_TRACKER_PORT}:AtomicTracker"
"${ARCHIVEBOX_PORT}:ArchiveBox"
"${TUBE_ARCHIVIST_PORT}:TubeArchivist"
"${WAKAPI_PORT}:Wakapi"
"${MAILHOG_PORT}:MailHog"
"${ATUIN_PORT}:Atuin"
)
local pass=0 fail=0 local pass=0 fail=0
for port in "${ports[@]}"; do for pt in "${ports[@]}"; do
local port="${pt%:*}"
local svc="${pt#*:}"
if timeout 5 bash -c "echo > /dev/tcp/localhost/$port" 2>/dev/null; then if timeout 5 bash -c "echo > /dev/tcp/localhost/$port" 2>/dev/null; then
log_success "Port $port accessible" log_success "$svc (:$port)"
((pass++)) ((pass++)) || true
else else
log_error "Port $port NOT accessible" log_error "$svc (:$port) NOT accessible"
((fail++)) ((fail++)) || true
fi fi
done done
echo "" echo ""
@@ -179,9 +199,10 @@ show_usage() {
echo " help Show this help" echo " help Show this help"
} }
ensure_env
case "${1:-deploy}" in case "${1:-deploy}" in
deploy) deploy)
fix_env
detect_user detect_user
check_prerequisites check_prerequisites
generate_compose generate_compose
@@ -196,8 +217,8 @@ case "${1:-deploy}" in
restart) restart)
stop_stack stop_stack
sleep 5 sleep 5
fix_env
detect_user detect_user
check_prerequisites
generate_compose generate_compose
deploy_stack deploy_stack
wait_healthy wait_healthy

View File

@@ -21,10 +21,10 @@ TESTS_FAILED=0
TESTS_TOTAL=0 TESTS_TOTAL=0
log_info() { echo -e "${BLUE}[INFO]${NC} $1"; } log_info() { echo -e "${BLUE}[INFO]${NC} $1"; }
log_success() { echo -e "${GREEN}[PASS]${NC} $1"; ((TESTS_PASSED++)); } log_success() { echo -e "${GREEN}[PASS]${NC} $1"; ((TESTS_PASSED++)) || true; }
log_warning() { echo -e "${YELLOW}[WARN]${NC} $1"; } log_warning() { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_error() { echo -e "${RED}[FAIL]${NC} $1"; ((TESTS_FAILED++)); } log_error() { echo -e "${RED}[FAIL]${NC} $1"; ((TESTS_FAILED++)) || true; }
log_test() { echo -e "${BLUE}[TEST]${NC} $1"; ((TESTS_TOTAL++)); } log_test() { echo -e "${BLUE}[TEST]${NC} $1"; ((TESTS_TOTAL++)) || true; }
test_file_ownership() { test_file_ownership() {
log_test "File ownership (no root-owned files)" log_test "File ownership (no root-owned files)"
@@ -83,7 +83,7 @@ test_service_health() {
log_success "$name running" log_success "$name running"
else else
log_error "$name not running: $line" log_error "$name not running: $line"
((unhealthy++)) ((unhealthy++)) || true
fi fi
done < <(docker ps --filter "name=${COMPOSE_PROJECT_NAME:-kneldevstack}" --format "{{.Names}} {{.Status}}" 2>/dev/null) done < <(docker ps --filter "name=${COMPOSE_PROJECT_NAME:-kneldevstack}" --format "{{.Names}} {{.Status}}" 2>/dev/null)
if [[ $unhealthy -eq 0 ]]; then if [[ $unhealthy -eq 0 ]]; then
@@ -120,7 +120,7 @@ test_port_accessibility() {
log_success "$svc (:$port)" log_success "$svc (:$port)"
else else
log_error "$svc (:$port) not accessible" log_error "$svc (:$port) not accessible"
((failed++)) ((failed++)) || true
fi fi
done done
if [[ $failed -eq 0 ]]; then if [[ $failed -eq 0 ]]; then
@@ -168,14 +168,20 @@ test_security_compliance() {
log_error "Docker socket proxy not found" log_error "Docker socket proxy not found"
fi fi
# Count direct socket mounts - proxy + dockhand are expected # Count direct socket mounts - only proxy should have one
local socket_mounts local socket_mounts
socket_mounts=$(grep -c "/var/run/docker.sock" "$COMPOSE_FILE" || echo "0") socket_mounts=$(grep -c '/var/run/docker.sock' "$COMPOSE_FILE" || echo "0")
local expected_mounts=2 # proxy (ro) + dockhand (rw for management) if [[ "$socket_mounts" -le 1 ]]; then
if [[ "$socket_mounts" -le "$expected_mounts" ]]; then log_success "Socket mount on proxy only ($socket_mounts)"
log_success "Socket mounts within expected range ($socket_mounts)"
else else
log_warning "Unexpected socket mounts: $socket_mounts (expected <= $expected_mounts)" log_error "Unexpected socket mounts: $socket_mounts (expected 1, proxy only)"
fi
# Dockhand uses proxy, not direct socket
if grep -q 'DOCKER_HOST=tcp://docker-socket-proxy' "$COMPOSE_FILE"; then
log_success "Dockhand routes through socket proxy"
else
log_error "Dockhand not using socket proxy"
fi fi
} }

View File

@@ -1,223 +0,0 @@
#!/bin/bash
set -euo pipefail
DEMO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
ENV_FILE="$DEMO_DIR/demo.env"
TEMPLATE_FILE="$DEMO_DIR/docker-compose.yml.template"
COMPOSE_FILE="$DEMO_DIR/docker-compose.yml"
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
log_info() { echo -e "${BLUE}[INFO]${NC} $1"; }
log_success() { echo -e "${GREEN}[OK]${NC} $1"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
fix_env() {
log_info "Ensuring demo.env is complete..."
grep -q '^TA_USERNAME=' "$ENV_FILE" || echo "TA_USERNAME=demo" >> "$ENV_FILE"
grep -q '^TA_PASSWORD=' "$ENV_FILE" || echo "TA_PASSWORD=demo_password" >> "$ENV_FILE"
grep -q '^ELASTIC_PASSWORD=' "$ENV_FILE" || echo "ELASTIC_PASSWORD=demo_password" >> "$ENV_FILE"
grep -q '^ES_JAVA_OPTS=' "$ENV_FILE" || echo 'ES_JAVA_OPTS="-Xms512m -Xmx512m"' >> "$ENV_FILE"
grep -q '^ARCHIVEBOX_ADMIN_USER=' "$ENV_FILE" || echo "ARCHIVEBOX_ADMIN_USER=admin" >> "$ENV_FILE"
grep -q '^ARCHIVEBOX_ADMIN_PASSWORD=' "$ENV_FILE" || echo "ARCHIVEBOX_ADMIN_PASSWORD=demo_password" >> "$ENV_FILE"
sed -i 's/^ATUIN_HOST=.*/ATUIN_HOST=0.0.0.0/' "$ENV_FILE"
sed -i 's|^TA_HOST=.*|TA_HOST=http://localhost:4014|' "$ENV_FILE"
log_success "demo.env ready"
}
detect_user() {
log_info "Detecting user IDs..."
local uid gid docker_gid
uid=$(id -u)
gid=$(id -g)
docker_gid=$(getent group docker | cut -d: -f3)
sed -i "s/^DEMO_UID=.*/DEMO_UID=$uid/" "$ENV_FILE"
sed -i "s/^DEMO_GID=.*/DEMO_GID=$gid/" "$ENV_FILE"
sed -i "s/^DEMO_DOCKER_GID=.*/DEMO_DOCKER_GID=$docker_gid/" "$ENV_FILE"
log_success "UID=$uid GID=$gid DockerGID=$docker_gid"
}
check_prerequisites() {
log_info "Checking prerequisites..."
if ! docker info >/dev/null 2>&1; then
log_error "Docker is not running"
exit 1
fi
local max_map_count
max_map_count=$(sysctl -n vm.max_map_count 2>/dev/null || echo "0")
if [[ "$max_map_count" -lt 262144 ]]; then
log_warn "Setting vm.max_map_count=262144 for Elasticsearch..."
if sudo sysctl -w vm.max_map_count=262144 2>/dev/null; then
log_success "vm.max_map_count set"
else
log_warn "Could not set vm.max_map_count (TubeArchivist ES may fail)"
fi
fi
log_success "Prerequisites OK"
}
generate_compose() {
log_info "Generating docker-compose.yml from template..."
set -a; source "$ENV_FILE"; set +a
envsubst < "$TEMPLATE_FILE" > "$COMPOSE_FILE"
log_success "docker-compose.yml generated"
}
deploy_stack() {
log_info "Deploying TSYS Developer Support Stack..."
cd "$DEMO_DIR"
docker compose up -d 2>&1
log_success "Stack deployment initiated"
}
wait_healthy() {
log_info "Waiting for services to become healthy (max 5 min)..."
local elapsed=0 interval=15
while [[ $elapsed -lt 300 ]]; do
local all_ok=true
while IFS= read -r line; do
local name health
name=$(echo "$line" | awk '{print $1}')
health=$(echo "$line" | awk '{print $2}')
[[ "$name" == "NAMES" || -z "$name" ]] && continue
if [[ "$health" != "healthy" && -n "$health" ]]; then
all_ok=false
fi
done < <(docker ps --filter "name=${COMPOSE_PROJECT_NAME:-kneldevstack}" --format "{{.Names}} {{.Status}}" 2>/dev/null | sed 's/(healthy)/healthy/g; s/(unhealthy)/unhealthy/g; s/(health: starting)/starting/g')
if $all_ok; then
log_success "All services healthy"
return 0
fi
log_info " Still waiting... (${elapsed}s elapsed)"
sleep $interval
elapsed=$((elapsed + interval))
done
log_warn "Timeout - some services may not be fully healthy"
docker ps --filter "name=${COMPOSE_PROJECT_NAME:-kneldevstack}" --format "table {{.Names}}\t{{.Status}}"
}
display_summary() {
set -a; source "$ENV_FILE"; set +a
echo ""
echo "========================================================"
echo " TSYS Developer Support Stack - Deployment Summary"
echo "========================================================"
echo ""
echo " Infrastructure:"
echo " Homepage Dashboard http://localhost:${HOMEPAGE_PORT}"
echo " Pi-hole (DNS) http://localhost:${PIHOLE_PORT}"
echo " Dockhand (Docker) http://localhost:${DOCKHAND_PORT}"
echo ""
echo " Monitoring:"
echo " InfluxDB http://localhost:${INFLUXDB_PORT}"
echo " Grafana http://localhost:${GRAFANA_PORT}"
echo ""
echo " Documentation:"
echo " Draw.io http://localhost:${DRAWIO_PORT}"
echo " Kroki http://localhost:${KROKI_PORT}"
echo ""
echo " Developer Tools:"
echo " Atomic Tracker http://localhost:${ATOMIC_TRACKER_PORT}"
echo " ArchiveBox http://localhost:${ARCHIVEBOX_PORT}"
echo " Tube Archivist http://localhost:${TUBE_ARCHIVIST_PORT}"
echo " Wakapi http://localhost:${WAKAPI_PORT}"
echo " MailHog http://localhost:${MAILHOG_PORT}"
echo " Atuin http://localhost:${ATUIN_PORT}"
echo ""
echo " Credentials: ${DEMO_ADMIN_USER:-admin} / ${DEMO_ADMIN_PASSWORD:-demo_password}"
echo " FOR DEMONSTRATION PURPOSES ONLY"
echo "========================================================"
}
smoke_test() {
log_info "Running smoke tests..."
set -a; source "$ENV_FILE"; set +a
local ports=(4000 4006 4007 4008 4009 4010 4011 4012 4013 4014 4015 4017 4018)
local pass=0 fail=0
for port in "${ports[@]}"; do
if timeout 5 bash -c "echo > /dev/tcp/localhost/$port" 2>/dev/null; then
log_success "Port $port accessible"
((pass++))
else
log_error "Port $port NOT accessible"
((fail++))
fi
done
echo ""
echo "SMOKE TEST: $pass passed, $fail failed"
}
stop_stack() {
log_info "Stopping stack..."
cd "$DEMO_DIR"
docker compose down 2>&1
log_success "Stack stopped"
}
show_status() {
cd "$DEMO_DIR"
docker compose ps
}
show_usage() {
echo "TSYS Developer Support Stack"
echo ""
echo "Usage: $0 {deploy|stop|restart|status|smoke|summary|help}"
echo ""
echo "Commands:"
echo " deploy Deploy the complete stack"
echo " stop Stop all services"
echo " restart Stop and redeploy"
echo " status Show service status"
echo " smoke Run port accessibility tests"
echo " summary Show service URLs"
echo " help Show this help"
}
case "${1:-deploy}" in
deploy)
fix_env
detect_user
check_prerequisites
generate_compose
deploy_stack
wait_healthy
display_summary
smoke_test
;;
stop)
stop_stack
;;
restart)
stop_stack
sleep 5
fix_env
detect_user
generate_compose
deploy_stack
wait_healthy
display_summary
;;
status)
show_status
;;
smoke)
smoke_test
;;
summary)
display_summary
;;
help|--help|-h)
show_usage
;;
*)
log_error "Unknown command: $1"
show_usage
exit 1
;;
esac