Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 48 additions & 30 deletions .github/scripts/smoke-test-deb.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,19 @@
# - Binary is functional (--help, --config-init-only)
# - systemd unit file is installed (part of the .deb package)
# - Default configuration file is generated
# - Config directory has secure permissions
# - Service starts, responds to health check, and stops cleanly
# - Package uninstall removes files but preserves config
#
# Environment variables (required):
# PACKAGE_FILE Absolute path to the .deb file inside the container.
# VERSION Expected package version (e.g. 2026.1.0).
# PACKAGE_NAME Package name (e.g. devolutions-gateway).
#
# LIMITATION — systemd in containers:
# Docker containers do not normally run systemd, so the postinst script
# skips config initialization and service enablement (both gated on
# /run/systemd/system). This script compensates by running
# --config-init-only manually. Full service start/stop validation is
# best-effort and only attempted when systemd is detected.
# Docker containers do not normally run systemd. The postinst gates
# service enable/start on /run/systemd/system. When systemd is not
# detected, the service is started directly for the health check.
# ──────────────────────────────────────────────────────────────────────────────

set -euo pipefail
Expand Down Expand Up @@ -61,30 +62,13 @@ diagnostics() {
echo "── Diagnostics ──────────────────────────────────────────────"
echo ""
echo "Package metadata:"
dpkg -s "$PACKAGE_NAME" 2>/dev/null || echo " (not installed)"
dpkg-deb -I "$PACKAGE_FILE" 2>/dev/null || echo " (dpkg-deb query failed)"
echo ""
echo "Package file list:"
dpkg -L "$PACKAGE_NAME" 2>/dev/null || echo " (not installed)"
dpkg-deb -c "$PACKAGE_FILE" 2>/dev/null || echo " (dpkg-deb query failed)"
echo ""
echo "Config directory:"
ls -la "$CONFIG_DIR/" 2>/dev/null || echo " (not found)"
echo ""
echo "Binary info:"
ls -la "$BINARY" 2>/dev/null || echo " (not found)"
file "$BINARY" 2>/dev/null || true
echo ""
echo "Dynamic library dependencies (ldd):"
ldd "$BINARY" 2>/dev/null || echo " (ldd failed or binary not found)"
echo ""
echo "Webapp directory:"
ls -laR "$WEBAPP_DIR/" 2>/dev/null | head -40 || echo " (not found)"
echo ""
echo "Library directory:"
ls -la "$LIB_DIR/" 2>/dev/null || echo " (not found)"
echo ""
echo "systemd unit files:"
UNIT_FILES=$(find /lib/systemd /usr/lib/systemd /etc/systemd -name '*devolutions*' 2>/dev/null || true)
if [ -n "$UNIT_FILES" ]; then echo "$UNIT_FILES"; else echo " (none found)"; fi
echo "────────────────────────────────────────────────────────────"
}

Expand All @@ -104,13 +88,13 @@ info "Updating apt and installing prerequisites…"
export DEBIAN_FRONTEND=noninteractive
apt-get update -qq
PREREQ_LOG=$(mktemp)
if apt-get install -y -qq file python3 > "$PREREQ_LOG" 2>&1; then
if apt-get install -y -qq file python3 curl openssl > "$PREREQ_LOG" 2>&1; then
rm -f "$PREREQ_LOG"
else
echo "Prerequisites installation output:"
cat "$PREREQ_LOG"
rm -f "$PREREQ_LOG"
fail "Prerequisites installation failed (file, python3)"
fail "Prerequisites installation failed (file, python3, curl, openssl)"
diagnostics
summary
fi
Expand Down Expand Up @@ -148,28 +132,62 @@ check_native_library
check_webapp
check_config_dir

# ── Config directory permissions ──────────────────────────────────────────────

info "Checking config directory permissions…"
check_config_dir_permissions

# ── Binary functionality ──────────────────────────────────────────────────────

info "Checking binary functionality…"
check_binary_help

# ── Config initialization ─────────────────────────────────────────────────────
# The postinst runs --config-init-only only when systemd is present.
# In a container without systemd we run it manually.
# The postinst always runs --config-init-only regardless of systemd presence.

info "Checking config initialization…"
check_config_init

# ── Config file permissions ───────────────────────────────────────────────────

info "Checking config file permissions…"
check_config_file_permissions

# ── systemd unit file ─────────────────────────────────────────────────────────
# The .deb package installs the unit file via dh_installsystemd,
# so it must be present regardless of whether systemd is running.

info "Checking systemd unit file…"
check_unit_file "fail"

# ── Service startup (best-effort) ─────────────────────────────────────────────
info "Checking service file has exactly one ExecStart directive…"
check_single_execstart

# ── Provisioner key ───────────────────────────────────────────────────────────
# The gateway requires a provisioner public key to start.
# Generate a key pair and place the public key where gateway.json points.

info "Generating provisioner key…"
check_provisioner_key

# ── Service health ────────────────────────────────────────────────────────────

check_service_startup
info "Checking service health…"
check_service_health

# ── Uninstall ─────────────────────────────────────────────────────────────────

info "Checking package uninstall…"
REMOVE_LOG=$(mktemp)
if apt-get remove -y "$PACKAGE_NAME" >"$REMOVE_LOG" 2>&1; then
pass "Package removal succeeded"
else
echo "Removal output:"
cat "$REMOVE_LOG"
fail "Package removal failed"
fi
rm -f "$REMOVE_LOG"
check_post_uninstall

# ── Final output ──────────────────────────────────────────────────────────────

Expand Down
191 changes: 162 additions & 29 deletions .github/scripts/smoke-test-lib.sh
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,17 @@ summary() {
fi
}

# ── Helpers ───────────────────────────────────────────────────────────────────

# Returns 0 if systemd is running AND the unit file is installed on disk.
systemd_and_unit_available() {
[ -d /run/systemd/system ] && command -v systemctl >/dev/null 2>&1 || return 1
for path in "${UNIT_FILE_PATHS[@]}"; do
[ -f "$path" ] && return 0
done
return 1
}

# ── Check functions ───────────────────────────────────────────────────────────

check_binary_executable() {
Expand All @@ -61,10 +72,10 @@ check_webapp() {
fail "Webapp directory missing: $WEBAPP_DIR"
fi
for app in client player; do
if find "$WEBAPP_DIR/$app" -name 'index.html' 2>/dev/null | grep -q .; then
pass "Webapp $app contains index.html"
if [ -f "$WEBAPP_DIR/$app/index.html" ]; then
pass "Webapp $app entry point exists: $WEBAPP_DIR/$app/index.html"
else
fail "Webapp $app missing index.html"
fail "Webapp $app entry point missing: $WEBAPP_DIR/$app/index.html"
fi
done
}
Expand All @@ -77,6 +88,16 @@ check_config_dir() {
fi
}

check_config_dir_permissions() {
local perms
perms=$(stat -c '%a' "$CONFIG_DIR" 2>/dev/null)
if [ "$perms" = "750" ]; then
pass "Config directory has secure permissions ($perms): $CONFIG_DIR"
else
fail "Config directory has insecure permissions ($perms, expected 750): $CONFIG_DIR"
fi
}

check_binary_help() {
HELP_OUTPUT=$("$BINARY" --help 2>&1) && HELP_RC=$? || HELP_RC=$?
if [ "$HELP_RC" -eq 0 ] || echo "$HELP_OUTPUT" | grep -qi 'gateway\|usage\|help'; then
Expand All @@ -87,20 +108,6 @@ check_binary_help() {
}

check_config_init() {
if [ ! -f "$CONFIG_FILE" ]; then
info "Config file not generated by postinst (expected without systemd)."
info "Running config initialization manually…"
CONFIG_INIT_LOG=$(mktemp)
if "$BINARY" --config-init-only > "$CONFIG_INIT_LOG" 2>&1; then
pass "Config initialization command succeeded"
else
echo "config-init-only output:"
cat "$CONFIG_INIT_LOG"
fail "Config initialization command failed"
fi
rm -f "$CONFIG_INIT_LOG"
fi

if [ -f "$CONFIG_FILE" ]; then
pass "Default config file exists: $CONFIG_FILE"
if python3 -c "import json; json.load(open('$CONFIG_FILE'))" 2>/dev/null; then
Expand All @@ -109,7 +116,7 @@ check_config_init() {
fail "$(basename "$CONFIG_FILE") exists but is not valid JSON"
fi
else
fail "Default config file missing after initialization: $CONFIG_FILE"
fail "Default config file missing after installation: $CONFIG_FILE"
fi
}

Expand Down Expand Up @@ -140,19 +147,145 @@ check_unit_file() {
fi
}

check_service_startup() {
info "[Best-effort] Checking service startup…"
warn "systemd service startup testing is best-effort in containers."
warn "Full service validation requires a real systemd environment."
if [ -d /run/systemd/system ]; then
info "systemd detected; attempting service start…"
if systemctl start devolutions-gateway 2>&1; then
pass "[Best-effort] Service started successfully"
systemctl status devolutions-gateway 2>&1 || true
check_single_execstart() {
local unit_file="" count
for path in "${UNIT_FILE_PATHS[@]}"; do
if [ -f "$path" ]; then
unit_file="$path"
break
fi
done
if [ -z "$unit_file" ]; then
warn "Skipping ExecStart check: no unit file found (check_unit_file already reported this)."
return
fi
# Match only non-empty ExecStart= lines; bare 'ExecStart=' is a reset directive.
count=$(grep -c '^ExecStart=[^[:space:]]' "$unit_file" 2>/dev/null || true)
if [ "$count" -eq 1 ]; then
pass "Service file has exactly one ExecStart directive"
else
fail "Service file has $count ExecStart directives (expected exactly 1)"
fi
Comment thread
CBenoit marked this conversation as resolved.
}

check_config_file_permissions() {
if [ ! -f "$CONFIG_FILE" ]; then
fail "Config file not found, cannot check permissions: $CONFIG_FILE"
return
fi
local perms
perms=$(stat -c '%a' "$CONFIG_FILE" 2>/dev/null)
if [ "$perms" = "600" ]; then
pass "Config file has secure permissions ($perms): $CONFIG_FILE"
else
fail "Config file has insecure permissions ($perms, expected 600): $CONFIG_FILE"
fi
}

check_provisioner_key() {
info "Generating RSA-2048 provisioner key pair with openssl…"
KEY_LOG=$(mktemp)
if openssl genrsa -out "$CONFIG_DIR/provisioner.key" 2048 >"$KEY_LOG" 2>&1 \
&& openssl rsa -in "$CONFIG_DIR/provisioner.key" \
-pubout -out "$CONFIG_DIR/provisioner.pem" >>"$KEY_LOG" 2>&1; then
chmod 600 "$CONFIG_DIR/provisioner.key"
pass "Provisioner key pair generated: $CONFIG_DIR/provisioner.pem"
else
echo "openssl output:"
cat "$KEY_LOG"
fail "Failed to generate provisioner key pair"
fi
rm -f "$KEY_LOG"
}

check_service_health() {
local health_url="http://localhost:7171/jet/health"
local gateway_pid=""
local gateway_log=""

if systemd_and_unit_available; then
info "systemd available — using systemctl start/stop"
if ! systemctl start devolutions-gateway >/dev/null 2>&1; then
fail "systemctl start devolutions-gateway failed"
echo "Service logs:"
journalctl -u devolutions-gateway --no-pager -n 50 2>/dev/null || true
return
fi
else
info "systemd not available — starting binary directly"
gateway_log=$(mktemp)
"$BINARY" 2>"$gateway_log" &
gateway_pid=$!
fi

# Wait for the service to be ready (up to 10 s).
local i=0
while [ "$i" -lt 10 ]; do
curl -sf -H 'Accept: application/json' "$health_url" >/dev/null 2>&1 && break
sleep 1
i=$((i + 1))
done

local health_output health_rc
health_output=$(curl -sf -H 'Accept: application/json' "$health_url" 2>/dev/null) && health_rc=$? || health_rc=$?

# Stop the service.
if systemd_and_unit_available; then
systemctl stop devolutions-gateway >/dev/null 2>&1 || true
elif [ -n "$gateway_pid" ]; then
kill "$gateway_pid" 2>/dev/null || true
wait "$gateway_pid" 2>/dev/null || true
fi

if [ "$health_rc" -eq 0 ]; then
pass "Health endpoint responded: $health_output"
# Verify the version field in the health response matches expected.
local health_version
health_version=$(python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('version',''))" <<< "$health_output" 2>/dev/null) || health_version=""
if [ -n "$health_version" ] && echo "$health_version" | grep -qF "$VERSION"; then
pass "Health response version ($health_version) matches expected ($VERSION)"
elif [ -n "$health_version" ]; then
fail "Health response version ($health_version) does not match expected ($VERSION)"
else
warn "Service start failed (expected in some container environments)."
warn "Could not extract version from health response"
fi
else
fail "Health endpoint did not respond at $health_url after 10 s"
if systemd_and_unit_available; then
echo "Service logs:"
journalctl -u devolutions-gateway --no-pager -n 50 2>/dev/null || true
elif [ -n "$gateway_log" ] && [ -f "$gateway_log" ]; then
echo "Gateway process output:"
cat "$gateway_log"
fi
fi

[ -n "$gateway_log" ] && rm -f "$gateway_log"
}

check_post_uninstall() {
if [ ! -f "$BINARY" ]; then
pass "Binary removed after uninstall"
else
fail "Binary still present after uninstall: $BINARY"
fi

local unit_file_found=0
for path in "${UNIT_FILE_PATHS[@]}"; do
if [ -f "$path" ]; then
unit_file_found=1
break
fi
done
if [ "$unit_file_found" -eq 0 ]; then
pass "Unit file removed after uninstall"
else
fail "Unit file still present after uninstall"
fi

if [ -d "$CONFIG_DIR" ]; then
pass "Config directory preserved after uninstall: $CONFIG_DIR"
else
info "No systemd detected; skipping service startup test."
fail "Config directory was removed after uninstall (should be preserved)"
fi
}
Loading
Loading