19 changed files with 565 additions and 376 deletions
@ -0,0 +1,22 @@ |
|||||
|
.git |
||||
|
.gitignore |
||||
|
.sisyphus/ |
||||
|
.venv/ |
||||
|
.pytest_cache/ |
||||
|
__pycache__/ |
||||
|
*.py[cod] |
||||
|
node_modules/ |
||||
|
instance/ |
||||
|
app/static/css/ |
||||
|
playwright-report/ |
||||
|
test-results/ |
||||
|
ui-audit-home-dark.png |
||||
|
ui-audit-home-default.png |
||||
|
ui-audit-home-sidebar-collapsed.png |
||||
|
ui-polish-desktop-collapsed.png |
||||
|
ui-polish-desktop-default.png |
||||
|
ui-polish-mobile-final-check.png |
||||
|
ui-polish-mobile-hidden-current.png |
||||
|
tests/ |
||||
|
docs/ |
||||
|
README.md |
||||
@ -0,0 +1,39 @@ |
|||||
|
FROM node:20-bookworm-slim AS assets |
||||
|
|
||||
|
WORKDIR /opt/hw |
||||
|
|
||||
|
COPY package.json package-lock.json ./ |
||||
|
RUN npm ci |
||||
|
|
||||
|
COPY app/static/src ./app/static/src |
||||
|
COPY app/templates ./app/templates |
||||
|
RUN mkdir -p ./app/static/css && npm run build:css |
||||
|
|
||||
|
FROM python:3.13-slim-bookworm |
||||
|
|
||||
|
ENV PYTHONDONTWRITEBYTECODE=1 \ |
||||
|
PYTHONUNBUFFERED=1 \ |
||||
|
PIP_NO_CACHE_DIR=1 |
||||
|
|
||||
|
WORKDIR /opt/hw |
||||
|
|
||||
|
RUN groupadd --system --gid 10001 hw \ |
||||
|
&& useradd --system --uid 10001 --gid 10001 --create-home --home-dir /opt/hw hw |
||||
|
|
||||
|
COPY requirements.txt ./requirements.txt |
||||
|
RUN pip install -r requirements.txt |
||||
|
|
||||
|
COPY . . |
||||
|
COPY --from=assets /opt/hw/app/static/css/main.css /opt/hw/app/static/css/main.css |
||||
|
|
||||
|
RUN chmod +x /opt/hw/docker/entrypoint.sh \ |
||||
|
&& mkdir -p /opt/hw/instance/csv_previews \ |
||||
|
&& chown -R hw:hw /opt/hw |
||||
|
|
||||
|
USER hw |
||||
|
|
||||
|
EXPOSE 8000 |
||||
|
|
||||
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 CMD python -c "import json, urllib.request; payload = json.load(urllib.request.urlopen('http://127.0.0.1:8000/health', timeout=3)); raise SystemExit(0 if payload.get('status') == 'ok' else 1)" |
||||
|
|
||||
|
ENTRYPOINT ["/opt/hw/docker/entrypoint.sh"] |
||||
@ -1,13 +0,0 @@ |
|||||
example.com { |
|
||||
encode zstd gzip |
|
||||
|
|
||||
log { |
|
||||
output stderr |
|
||||
} |
|
||||
|
|
||||
reverse_proxy 127.0.0.1:8000 { |
|
||||
header_up X-Forwarded-For {remote_host} |
|
||||
header_up X-Forwarded-Proto {scheme} |
|
||||
header_up Host {host} |
|
||||
} |
|
||||
} |
|
||||
@ -1,16 +0,0 @@ |
|||||
[Unit] |
|
||||
Description=HappyWedding Gunicorn Service |
|
||||
After=network.target |
|
||||
|
|
||||
[Service] |
|
||||
Type=simple |
|
||||
User=hw |
|
||||
Group=hw |
|
||||
WorkingDirectory=/opt/hw |
|
||||
EnvironmentFile=/etc/hw/hw.env |
|
||||
ExecStart=/opt/hw/.venv/bin/gunicorn --workers 2 --bind 127.0.0.1:8000 run:app |
|
||||
Restart=on-failure |
|
||||
RestartSec=5 |
|
||||
|
|
||||
[Install] |
|
||||
WantedBy=multi-user.target |
|
||||
@ -0,0 +1,34 @@ |
|||||
|
#!/bin/sh |
||||
|
set -eu |
||||
|
|
||||
|
APP_DIR="${APP_DIR:-/opt/hw}" |
||||
|
CONFIG_DIR="${CONFIG_DIR:-/opt/hw.conf.d}" |
||||
|
ENV_FILE="${ENV_FILE:-${CONFIG_DIR}/hw.env}" |
||||
|
INSTANCE_DIR="${INSTANCE_DIR:-${APP_DIR}/instance}" |
||||
|
SEED_MARKER_FILE="${SEED_MARKER_FILE:-${INSTANCE_DIR}/.seed-all-complete}" |
||||
|
|
||||
|
if [ -f "$ENV_FILE" ]; then |
||||
|
set -a |
||||
|
# shellcheck disable=SC1090 |
||||
|
. "$ENV_FILE" |
||||
|
set +a |
||||
|
fi |
||||
|
|
||||
|
export FLASK_APP="${FLASK_APP:-run.py}" |
||||
|
export HW_CONFIG="${HW_CONFIG:-production}" |
||||
|
export PYTHONPATH="${PYTHONPATH:-${APP_DIR}}" |
||||
|
|
||||
|
mkdir -p "$INSTANCE_DIR/csv_previews" |
||||
|
|
||||
|
python -m flask db upgrade |
||||
|
|
||||
|
if [ "${RUN_SEED_ALL:-0}" = "1" ] && [ ! -f "$SEED_MARKER_FILE" ]; then |
||||
|
python -m flask seed-all |
||||
|
: > "$SEED_MARKER_FILE" |
||||
|
fi |
||||
|
|
||||
|
if [ "$#" -eq 0 ]; then |
||||
|
set -- gunicorn --workers "${HW_GUNICORN_WORKERS:-2}" --bind "0.0.0.0:${HW_GUNICORN_PORT:-8000}" run:app |
||||
|
fi |
||||
|
|
||||
|
exec "$@" |
||||
@ -0,0 +1,81 @@ |
|||||
|
#!/usr/bin/env bash |
||||
|
set -euo pipefail |
||||
|
|
||||
|
APP_DIR="${APP_DIR:-/opt/hw}" |
||||
|
CONFIG_DIR="${CONFIG_DIR:-/opt/hw.conf.d}" |
||||
|
ENV_FILE="${ENV_FILE:-$CONFIG_DIR/hw.env}" |
||||
|
CONTAINER_NAME="${CONTAINER_NAME:-hw}" |
||||
|
IMAGE_NAME="${IMAGE_NAME:-hw}" |
||||
|
IMAGE_TAG="${IMAGE_TAG:-py313}" |
||||
|
APP_PORT="${APP_PORT:-8000}" |
||||
|
RUN_SEED_ALL="${RUN_SEED_ALL:-0}" |
||||
|
HOST_PORT="${HOST_PORT:-8000}" |
||||
|
ALLOW_NON_ROOT="${ALLOW_NON_ROOT:-0}" |
||||
|
CONTAINER_UID="${CONTAINER_UID:-10001}" |
||||
|
CONTAINER_GID="${CONTAINER_GID:-10001}" |
||||
|
|
||||
|
require_root() { |
||||
|
if [[ "$ALLOW_NON_ROOT" == "1" ]]; then |
||||
|
return |
||||
|
fi |
||||
|
if [[ "${EUID}" -ne 0 ]]; then |
||||
|
printf 'Please run this script as root.\n' >&2 |
||||
|
exit 1 |
||||
|
fi |
||||
|
} |
||||
|
|
||||
|
require_file() { |
||||
|
local path="$1" |
||||
|
if [[ ! -f "$path" ]]; then |
||||
|
printf 'Required file not found: %s\n' "$path" >&2 |
||||
|
exit 1 |
||||
|
fi |
||||
|
} |
||||
|
|
||||
|
require_docker_daemon() { |
||||
|
if ! docker info >/dev/null 2>&1; then |
||||
|
printf 'Docker daemon is not available. Please start Docker and re-run this script.\n' >&2 |
||||
|
exit 1 |
||||
|
fi |
||||
|
} |
||||
|
|
||||
|
main() { |
||||
|
require_root |
||||
|
require_file "$ENV_FILE" |
||||
|
require_file "$APP_DIR/Dockerfile" |
||||
|
require_file "$APP_DIR/run.py" |
||||
|
require_docker_daemon |
||||
|
|
||||
|
install -d -m 755 "$APP_DIR/instance" |
||||
|
install -d -m 755 "$APP_DIR/instance/csv_previews" |
||||
|
|
||||
|
if [[ "$ALLOW_NON_ROOT" == "1" ]]; then |
||||
|
chown -R "$CONTAINER_UID:$CONTAINER_GID" "$APP_DIR/instance" >/dev/null 2>&1 || true |
||||
|
else |
||||
|
chown -R "$CONTAINER_UID:$CONTAINER_GID" "$APP_DIR/instance" |
||||
|
fi |
||||
|
|
||||
|
docker build -t "${IMAGE_NAME}:${IMAGE_TAG}" "$APP_DIR" |
||||
|
|
||||
|
if docker ps -a --format '{{.Names}}' | grep -Fxq "$CONTAINER_NAME"; then |
||||
|
docker rm -f "$CONTAINER_NAME" >/dev/null |
||||
|
fi |
||||
|
|
||||
|
docker run -d \ |
||||
|
--name "$CONTAINER_NAME" \ |
||||
|
--env-file "$ENV_FILE" \ |
||||
|
-e RUN_SEED_ALL="$RUN_SEED_ALL" \ |
||||
|
-e HW_GUNICORN_PORT="$APP_PORT" \ |
||||
|
-p "$HOST_PORT:$APP_PORT" \ |
||||
|
-v "$APP_DIR/instance:/opt/hw/instance" \ |
||||
|
-v "$CONFIG_DIR:/opt/hw.conf.d:ro" \ |
||||
|
--restart unless-stopped \ |
||||
|
"${IMAGE_NAME}:${IMAGE_TAG}" |
||||
|
|
||||
|
printf 'Container deployment completed.\n' |
||||
|
printf 'Container: %s\n' "$CONTAINER_NAME" |
||||
|
printf 'Image: %s:%s\n' "$IMAGE_NAME" "$IMAGE_TAG" |
||||
|
printf 'Published port: %s -> %s\n' "$HOST_PORT" "$APP_PORT" |
||||
|
} |
||||
|
|
||||
|
main "$@" |
||||
@ -1,55 +0,0 @@ |
|||||
#!/usr/bin/env bash |
|
||||
set -euo pipefail |
|
||||
|
|
||||
APP_DIR="${APP_DIR:-/opt/hw}" |
|
||||
ENV_FILE="${ENV_FILE:-/etc/hw/hw.env}" |
|
||||
SERVICE_NAME="${SERVICE_NAME:-hw}" |
|
||||
RUN_SEED_ALL="${RUN_SEED_ALL:-0}" |
|
||||
|
|
||||
require_root() { |
|
||||
if [[ "${EUID}" -ne 0 ]]; then |
|
||||
printf 'Please run this script as root.\n' >&2 |
|
||||
exit 1 |
|
||||
fi |
|
||||
} |
|
||||
|
|
||||
require_file() { |
|
||||
local path="$1" |
|
||||
if [[ ! -f "$path" ]]; then |
|
||||
printf 'Required file not found: %s\n' "$path" >&2 |
|
||||
exit 1 |
|
||||
fi |
|
||||
} |
|
||||
|
|
||||
main() { |
|
||||
require_root |
|
||||
require_file "$ENV_FILE" |
|
||||
require_file "$APP_DIR/requirements.txt" |
|
||||
require_file "$APP_DIR/run.py" |
|
||||
|
|
||||
set -a |
|
||||
# shellcheck disable=SC1090 |
|
||||
source "$ENV_FILE" |
|
||||
set +a |
|
||||
|
|
||||
"$APP_DIR/.venv/bin/python" -m pip install --upgrade pip |
|
||||
"$APP_DIR/.venv/bin/python" -m pip install -r "$APP_DIR/requirements.txt" |
|
||||
|
|
||||
export FLASK_APP=run.py |
|
||||
export HW_CONFIG="${HW_CONFIG:-production}" |
|
||||
|
|
||||
(cd "$APP_DIR" && "$APP_DIR/.venv/bin/python" -m flask db upgrade) |
|
||||
|
|
||||
if [[ "$RUN_SEED_ALL" == "1" ]]; then |
|
||||
(cd "$APP_DIR" && "$APP_DIR/.venv/bin/python" -m flask seed-all) |
|
||||
fi |
|
||||
|
|
||||
systemctl daemon-reload |
|
||||
systemctl restart "$SERVICE_NAME" |
|
||||
systemctl enable "$SERVICE_NAME" |
|
||||
|
|
||||
printf 'Deployment completed.\n' |
|
||||
printf 'Use: systemctl status %s --no-pager\n' "$SERVICE_NAME" |
|
||||
} |
|
||||
|
|
||||
main "$@" |
|
||||
@ -0,0 +1,101 @@ |
|||||
|
#!/usr/bin/env bash |
||||
|
set -euo pipefail |
||||
|
|
||||
|
APP_DIR="${APP_DIR:-/opt/hw}" |
||||
|
CONFIG_DIR="${CONFIG_DIR:-/opt/hw.conf.d}" |
||||
|
DEFAULT_HOST_USER="${SUDO_USER:-$(id -un)}" |
||||
|
APP_USER="${APP_USER:-$DEFAULT_HOST_USER}" |
||||
|
APP_GROUP="${APP_GROUP:-$(id -gn "$APP_USER" 2>/dev/null || id -gn)}" |
||||
|
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" |
||||
|
ENV_TEMPLATE_SOURCE="${ENV_TEMPLATE_SOURCE:-$PROJECT_ROOT/deploy/env/hw.env.example}" |
||||
|
ENV_FILE_NAME="${ENV_FILE_NAME:-hw.env}" |
||||
|
DOCKER_INSTALL_SCRIPT_URL="https://get.docker.com" |
||||
|
ALLOW_NON_ROOT="${ALLOW_NON_ROOT:-0}" |
||||
|
CONTAINER_UID="${CONTAINER_UID:-10001}" |
||||
|
CONTAINER_GID="${CONTAINER_GID:-10001}" |
||||
|
|
||||
|
require_root() { |
||||
|
if [[ "$ALLOW_NON_ROOT" == "1" ]]; then |
||||
|
return |
||||
|
fi |
||||
|
if [[ "${EUID}" -ne 0 ]]; then |
||||
|
printf 'Please run this script as root.\n' >&2 |
||||
|
exit 1 |
||||
|
fi |
||||
|
} |
||||
|
|
||||
|
command_exists() { |
||||
|
command -v "$1" >/dev/null 2>&1 |
||||
|
} |
||||
|
|
||||
|
require_command() { |
||||
|
if ! command_exists "$1"; then |
||||
|
printf 'Required command not found: %s\n' "$1" >&2 |
||||
|
exit 1 |
||||
|
fi |
||||
|
} |
||||
|
|
||||
|
install_docker_if_missing() { |
||||
|
if command_exists docker; then |
||||
|
return |
||||
|
fi |
||||
|
|
||||
|
if command_exists apt-get; then |
||||
|
printf 'Docker not found. Installing Docker via %s ...\n' "$DOCKER_INSTALL_SCRIPT_URL" |
||||
|
curl -fsSL "$DOCKER_INSTALL_SCRIPT_URL" | sh |
||||
|
return |
||||
|
fi |
||||
|
|
||||
|
printf 'Docker is not installed and automatic installation is only supported on apt-based hosts.\n' >&2 |
||||
|
printf 'Please install Docker manually, then re-run this script.\n' >&2 |
||||
|
exit 1 |
||||
|
} |
||||
|
|
||||
|
main() { |
||||
|
require_root |
||||
|
require_command curl |
||||
|
install_docker_if_missing |
||||
|
|
||||
|
install -d -m 755 -o "$APP_USER" -g "$APP_GROUP" "$APP_DIR" |
||||
|
install -d -m 755 -o "$APP_USER" -g "$APP_GROUP" "$APP_DIR/instance" |
||||
|
install -d -m 755 -o "$APP_USER" -g "$APP_GROUP" "$APP_DIR/instance/csv_previews" |
||||
|
|
||||
|
if [[ "$ALLOW_NON_ROOT" == "1" ]]; then |
||||
|
install -d -m 750 "$CONFIG_DIR" |
||||
|
else |
||||
|
install -d -m 750 -o root -g "$APP_GROUP" "$CONFIG_DIR" |
||||
|
fi |
||||
|
|
||||
|
if [[ "$ALLOW_NON_ROOT" == "1" ]]; then |
||||
|
chown -R "$CONTAINER_UID:$CONTAINER_GID" "$APP_DIR/instance" >/dev/null 2>&1 || true |
||||
|
else |
||||
|
chown -R "$CONTAINER_UID:$CONTAINER_GID" "$APP_DIR/instance" |
||||
|
fi |
||||
|
|
||||
|
if [[ ! -f "$CONFIG_DIR/$ENV_FILE_NAME" ]]; then |
||||
|
if [[ "$ALLOW_NON_ROOT" == "1" ]]; then |
||||
|
install -m 640 "$ENV_TEMPLATE_SOURCE" "$CONFIG_DIR/$ENV_FILE_NAME" |
||||
|
else |
||||
|
install -m 640 -o root -g "$APP_GROUP" "$ENV_TEMPLATE_SOURCE" "$CONFIG_DIR/$ENV_FILE_NAME" |
||||
|
fi |
||||
|
printf 'Created %s from template. Please edit it before deployment.\n' "$CONFIG_DIR/$ENV_FILE_NAME" |
||||
|
else |
||||
|
printf 'Keeping existing %s\n' "$CONFIG_DIR/$ENV_FILE_NAME" |
||||
|
fi |
||||
|
|
||||
|
if [[ "$ALLOW_NON_ROOT" != "1" ]] && id -u "$APP_USER" >/dev/null 2>&1; then |
||||
|
usermod -aG docker "$APP_USER" >/dev/null 2>&1 || true |
||||
|
fi |
||||
|
|
||||
|
printf '\nContainer deployment prerequisites are ready.\n' |
||||
|
printf 'Next steps:\n' |
||||
|
printf '1. Sync project files into %s\n' "$APP_DIR" |
||||
|
printf '2. Edit %s/%s with real production values\n' "$CONFIG_DIR" "$ENV_FILE_NAME" |
||||
|
if [[ "$ALLOW_NON_ROOT" == "1" ]]; then |
||||
|
printf '3. Run ALLOW_NON_ROOT=1 bash scripts/deploy-container.sh\n' |
||||
|
else |
||||
|
printf '3. Run sudo bash scripts/deploy-container.sh\n' |
||||
|
fi |
||||
|
} |
||||
|
|
||||
|
main "$@" |
||||
@ -1,55 +0,0 @@ |
|||||
#!/usr/bin/env bash |
|
||||
set -euo pipefail |
|
||||
|
|
||||
APP_USER="${APP_USER:-hw}" |
|
||||
APP_GROUP="${APP_GROUP:-$APP_USER}" |
|
||||
APP_DIR="${APP_DIR:-/opt/hw}" |
|
||||
ENV_DIR="${ENV_DIR:-/etc/hw}" |
|
||||
SERVICE_NAME="${SERVICE_NAME:-hw}" |
|
||||
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" |
|
||||
|
|
||||
require_root() { |
|
||||
if [[ "${EUID}" -ne 0 ]]; then |
|
||||
printf 'Please run this script as root.\n' >&2 |
|
||||
exit 1 |
|
||||
fi |
|
||||
} |
|
||||
|
|
||||
ensure_user() { |
|
||||
if ! id -u "$APP_USER" >/dev/null 2>&1; then |
|
||||
useradd --system --create-home --shell /usr/sbin/nologin "$APP_USER" |
|
||||
fi |
|
||||
} |
|
||||
|
|
||||
main() { |
|
||||
require_root |
|
||||
ensure_user |
|
||||
|
|
||||
install -d -m 755 -o "$APP_USER" -g "$APP_GROUP" "$APP_DIR" |
|
||||
install -d -m 755 -o "$APP_USER" -g "$APP_GROUP" "$APP_DIR/instance" |
|
||||
install -d -m 755 -o "$APP_USER" -g "$APP_GROUP" "$APP_DIR/instance/csv_previews" |
|
||||
install -d -m 750 -o root -g "$APP_GROUP" "$ENV_DIR" |
|
||||
|
|
||||
if [[ ! -d "$APP_DIR/.venv" ]]; then |
|
||||
python3 -m venv "$APP_DIR/.venv" |
|
||||
fi |
|
||||
|
|
||||
"$APP_DIR/.venv/bin/python" -m pip install --upgrade pip |
|
||||
|
|
||||
install -m 644 "$PROJECT_ROOT/deploy/systemd/hw.service" "/etc/systemd/system/${SERVICE_NAME}.service" |
|
||||
|
|
||||
if [[ ! -f "$ENV_DIR/hw.env" ]]; then |
|
||||
install -m 640 -o root -g "$APP_GROUP" "$PROJECT_ROOT/deploy/env/hw.env.example" "$ENV_DIR/hw.env" |
|
||||
printf 'Created %s/hw.env from template. Please edit it before starting the service.\n' "$ENV_DIR" |
|
||||
else |
|
||||
printf 'Keeping existing %s/hw.env\n' "$ENV_DIR" |
|
||||
fi |
|
||||
|
|
||||
printf '\nNext steps:\n' |
|
||||
printf '1. Sync project files into %s\n' "$APP_DIR" |
|
||||
printf '2. Edit %s/hw.env with real production values\n' "$ENV_DIR" |
|
||||
printf '3. Copy deploy/caddy/Caddyfile into your Caddy config and replace the domain\n' |
|
||||
printf '4. Run scripts/deploy-systemd.sh as root\n' |
|
||||
} |
|
||||
|
|
||||
main "$@" |
|
||||
@ -0,0 +1,72 @@ |
|||||
|
#!/usr/bin/env bash |
||||
|
set -euo pipefail |
||||
|
|
||||
|
CONTAINER_NAME="${CONTAINER_NAME:-hw}" |
||||
|
HOST_PORT="${HOST_PORT:-${APP_PORT:-8000}}" |
||||
|
HEALTHCHECK_URL="${HEALTHCHECK_URL:-http://127.0.0.1:${HOST_PORT}/health}" |
||||
|
ALLOW_NON_ROOT="${ALLOW_NON_ROOT:-0}" |
||||
|
|
||||
|
fail() { |
||||
|
printf 'Verification failed: %s\n' "$1" >&2 |
||||
|
exit 1 |
||||
|
} |
||||
|
|
||||
|
command_exists() { |
||||
|
command -v "$1" >/dev/null 2>&1 |
||||
|
} |
||||
|
|
||||
|
require_docker_daemon() { |
||||
|
if ! docker info >/dev/null 2>&1; then |
||||
|
fail "docker daemon is not available" |
||||
|
fi |
||||
|
} |
||||
|
|
||||
|
port_is_listening() { |
||||
|
local port="$1" |
||||
|
|
||||
|
if command_exists ss; then |
||||
|
ss -tln | grep -q ":${port} " |
||||
|
return |
||||
|
fi |
||||
|
|
||||
|
if command_exists lsof; then |
||||
|
lsof -nP -iTCP:"${port}" -sTCP:LISTEN >/dev/null 2>&1 |
||||
|
return |
||||
|
fi |
||||
|
|
||||
|
if command_exists netstat; then |
||||
|
netstat -an | grep -E -q "[\.:]${port}[[:space:]].*LISTEN" |
||||
|
return |
||||
|
fi |
||||
|
|
||||
|
fail "no supported port inspection command found (need one of: ss, lsof, netstat)" |
||||
|
} |
||||
|
|
||||
|
main() { |
||||
|
require_docker_daemon |
||||
|
if ! docker ps --format '{{.Names}}' | grep -Fxq "$CONTAINER_NAME"; then |
||||
|
fail "container '$CONTAINER_NAME' is not running" |
||||
|
fi |
||||
|
|
||||
|
health_status="$(docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' "$CONTAINER_NAME")" |
||||
|
if [[ "$health_status" != "healthy" && "$health_status" != "none" ]]; then |
||||
|
fail "container health status is '$health_status'" |
||||
|
fi |
||||
|
|
||||
|
if ! port_is_listening "$HOST_PORT"; then |
||||
|
fail "nothing is listening on TCP port ${HOST_PORT}" |
||||
|
fi |
||||
|
|
||||
|
health_payload="$(curl --fail --silent --show-error --max-time 10 "$HEALTHCHECK_URL")" || fail "healthcheck request failed: $HEALTHCHECK_URL" |
||||
|
|
||||
|
if ! printf '%s' "$health_payload" | grep -q '"status":"ok"\|"status": "ok"'; then |
||||
|
fail "healthcheck response does not contain status=ok" |
||||
|
fi |
||||
|
|
||||
|
printf 'Verification succeeded.\n' |
||||
|
printf 'Container: %s\n' "$CONTAINER_NAME" |
||||
|
printf 'Port: %s\n' "$HOST_PORT" |
||||
|
printf 'Healthcheck: %s\n' "$HEALTHCHECK_URL" |
||||
|
} |
||||
|
|
||||
|
main "$@" |
||||
@ -1,34 +0,0 @@ |
|||||
#!/usr/bin/env bash |
|
||||
set -euo pipefail |
|
||||
|
|
||||
SERVICE_NAME="${SERVICE_NAME:-hw}" |
|
||||
APP_PORT="${APP_PORT:-8000}" |
|
||||
HEALTHCHECK_URL="${HEALTHCHECK_URL:-http://127.0.0.1/health}" |
|
||||
|
|
||||
fail() { |
|
||||
printf 'Verification failed: %s\n' "$1" >&2 |
|
||||
exit 1 |
|
||||
} |
|
||||
|
|
||||
main() { |
|
||||
if ! systemctl is-active --quiet "$SERVICE_NAME"; then |
|
||||
fail "systemd service '$SERVICE_NAME' is not active" |
|
||||
fi |
|
||||
|
|
||||
if ! ss -tln | grep -q ":${APP_PORT} "; then |
|
||||
fail "nothing is listening on TCP port ${APP_PORT}" |
|
||||
fi |
|
||||
|
|
||||
health_payload="$(curl --fail --silent --show-error --max-time 10 "$HEALTHCHECK_URL")" || fail "healthcheck request failed: $HEALTHCHECK_URL" |
|
||||
|
|
||||
if ! printf '%s' "$health_payload" | grep -q '"status":"ok"\|"status": "ok"'; then |
|
||||
fail "healthcheck response does not contain status=ok" |
|
||||
fi |
|
||||
|
|
||||
printf 'Verification succeeded.\n' |
|
||||
printf 'Service: %s\n' "$SERVICE_NAME" |
|
||||
printf 'Port: %s\n' "$APP_PORT" |
|
||||
printf 'Healthcheck: %s\n' "$HEALTHCHECK_URL" |
|
||||
} |
|
||||
|
|
||||
main "$@" |
|
||||
Loading…
Reference in new issue