Files
silo-mod/scripts/deploy.sh
forbes 802368c583 fix(deploy): add database migration step and auth env vars
Add run_migrations function to deploy.sh that automatically applies
pending SQL migrations during deployment. Migrations are run after
config installation and before service restart.

Migration runner:
- Sources /etc/silo/silod.env for SILO_DB_PASSWORD
- Reads DB host/port/name/user from production config.yaml
- Waits for database connectivity (5 retries)
- Applies each migration file in order, skipping already-applied ones
- Gracefully degrades if psql is missing or DB password is not set

This fixes the missing migration 009 (auth tables) that caused:
- 'column created_by of relation projects does not exist'
- 'relation api_tokens does not exist'

Also adds auth environment variables to silod.env.example:
- SILO_SESSION_SECRET
- SILO_ADMIN_USERNAME / SILO_ADMIN_PASSWORD
- SILO_OIDC_CLIENT_SECRET, SILO_LDAP_BIND_PASSWORD
2026-01-31 12:09:17 -06:00

459 lines
13 KiB
Bash
Executable File

#!/usr/bin/env bash
#
# Silo Deployment Script
# Pulls from git and deploys silod on the local machine
#
# Usage:
# sudo ./scripts/deploy.sh [options]
#
# Options:
# --no-pull Skip git pull (use current checkout)
# --no-build Skip build (use existing binary)
# --restart-only Only restart the service
# --status Show service status and exit
# --help Show this help message
#
# This script should be run on silo.kindred.internal as root or with sudo.
set -euo pipefail
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Configuration
REPO_URL="${SILO_REPO_URL:-https://gitea.kindred.internal/kindred/silo-0062.git}"
REPO_BRANCH="${SILO_BRANCH:-main}"
INSTALL_DIR="/opt/silo"
CONFIG_DIR="/etc/silo"
BINARY_NAME="silod"
SERVICE_NAME="silod"
# Flags
DO_PULL=true
DO_BUILD=true
RESTART_ONLY=false
STATUS_ONLY=false
# Functions
log_info() {
echo -e "${BLUE}[INFO]${NC} $*"
}
log_success() {
echo -e "${GREEN}[OK]${NC} $*"
}
log_warn() {
echo -e "${YELLOW}[WARN]${NC} $*"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $*" >&2
}
die() {
log_error "$*"
exit 1
}
show_help() {
head -18 "$0" | grep -E '^#' | sed 's/^# *//'
exit 0
}
check_root() {
if [[ $EUID -ne 0 ]]; then
die "This script must be run as root (use sudo)"
fi
}
check_dependencies() {
log_info "Checking dependencies..."
# Add common Go install locations to PATH
if [[ -d /usr/local/go/bin ]]; then
export PATH=$PATH:/usr/local/go/bin
fi
if [[ -d /opt/go/bin ]]; then
export PATH=$PATH:/opt/go/bin
fi
local missing=()
command -v git >/dev/null 2>&1 || missing+=("git")
command -v go >/dev/null 2>&1 || missing+=("go (golang)")
command -v systemctl >/dev/null 2>&1 || missing+=("systemctl")
if [[ ${#missing[@]} -gt 0 ]]; then
die "Missing required commands: ${missing[*]}"
fi
# Check Go version
local go_version
go_version=$(go version | grep -oP 'go\d+\.\d+' | head -1)
log_info "Found Go version: ${go_version}"
log_success "All dependencies available"
}
setup_directories() {
log_info "Setting up directories..."
# Create directories if they don't exist
mkdir -p "${INSTALL_DIR}/bin"
mkdir -p "${INSTALL_DIR}/src"
mkdir -p "${CONFIG_DIR}/schemas"
mkdir -p /var/log/silo
# Create silo user if it doesn't exist
if ! id -u silo >/dev/null 2>&1; then
useradd -r -m -d "${INSTALL_DIR}" -s /sbin/nologin -c "Silo Service" silo
log_info "Created silo user"
fi
log_success "Directories ready"
}
git_pull() {
log_info "Pulling latest code from ${REPO_BRANCH}..."
local src_dir="${INSTALL_DIR}/src"
if [[ -d "${src_dir}/.git" ]]; then
# Existing checkout - pull updates
cd "${src_dir}"
git fetch origin
git checkout "${REPO_BRANCH}"
git reset --hard "origin/${REPO_BRANCH}"
log_success "Updated to $(git rev-parse --short HEAD)"
else
# Fresh clone
log_info "Cloning repository..."
rm -rf "${src_dir}"
git clone --branch "${REPO_BRANCH}" "${REPO_URL}" "${src_dir}"
cd "${src_dir}"
log_success "Cloned $(git rev-parse --short HEAD)"
fi
# Show version info
local version
version=$(git describe --tags --always --dirty 2>/dev/null || git rev-parse --short HEAD)
log_info "Version: ${version}"
}
build_binary() {
log_info "Building ${BINARY_NAME}..."
local src_dir="${INSTALL_DIR}/src"
cd "${src_dir}"
# Get version from git
local version
version=$(git describe --tags --always --dirty 2>/dev/null || git rev-parse --short HEAD)
local ldflags="-w -s -X main.Version=${version}"
# Build
CGO_ENABLED=0 go build -ldflags="${ldflags}" -o "${INSTALL_DIR}/bin/${BINARY_NAME}" ./cmd/silod
if [[ ! -f "${INSTALL_DIR}/bin/${BINARY_NAME}" ]]; then
die "Build failed: binary not found"
fi
chmod 755 "${INSTALL_DIR}/bin/${BINARY_NAME}"
local size
size=$(du -h "${INSTALL_DIR}/bin/${BINARY_NAME}" | cut -f1)
log_success "Built ${BINARY_NAME} (${size})"
}
install_config() {
log_info "Installing configuration..."
local src_dir="${INSTALL_DIR}/src"
# Install config file if it doesn't exist or is different
if [[ ! -f "${CONFIG_DIR}/config.yaml" ]]; then
cp "${src_dir}/deployments/config.prod.yaml" "${CONFIG_DIR}/config.yaml"
chmod 644 "${CONFIG_DIR}/config.yaml"
chown root:silo "${CONFIG_DIR}/config.yaml"
log_info "Installed config.yaml"
else
log_info "Config file exists, not overwriting"
fi
# Install schemas (always update)
rm -rf "${CONFIG_DIR}/schemas/"*
cp -r "${src_dir}/schemas/"* "${CONFIG_DIR}/schemas/"
chmod -R 644 "${CONFIG_DIR}/schemas/"*
chown -R root:silo "${CONFIG_DIR}/schemas"
log_success "Schemas installed"
# Check environment file
if [[ ! -f "${CONFIG_DIR}/silod.env" ]]; then
cp "${src_dir}/deployments/systemd/silod.env.example" "${CONFIG_DIR}/silod.env"
chmod 600 "${CONFIG_DIR}/silod.env"
chown root:silo "${CONFIG_DIR}/silod.env"
log_warn "Created ${CONFIG_DIR}/silod.env - EDIT THIS FILE WITH CREDENTIALS!"
fi
}
install_systemd() {
log_info "Installing systemd service..."
local src_dir="${INSTALL_DIR}/src"
local service_file="${src_dir}/deployments/systemd/silod.service"
if [[ -f "${service_file}" ]]; then
cp "${service_file}" /etc/systemd/system/silod.service
chmod 644 /etc/systemd/system/silod.service
systemctl daemon-reload
log_success "Systemd service installed"
else
die "Service file not found: ${service_file}"
fi
}
set_permissions() {
log_info "Setting permissions..."
chown -R silo:silo "${INSTALL_DIR}"
chown root:silo "${CONFIG_DIR}"
chmod 750 "${CONFIG_DIR}"
chown silo:silo /var/log/silo
chmod 750 /var/log/silo
# Binary should be owned by root but executable by silo
chown root:root "${INSTALL_DIR}/bin/${BINARY_NAME}"
chmod 755 "${INSTALL_DIR}/bin/${BINARY_NAME}"
log_success "Permissions set"
}
restart_service() {
log_info "Restarting ${SERVICE_NAME} service..."
# Enable if not already
systemctl enable "${SERVICE_NAME}" >/dev/null 2>&1 || true
# Restart
systemctl restart "${SERVICE_NAME}"
# Wait for startup
sleep 2
if systemctl is-active --quiet "${SERVICE_NAME}"; then
log_success "Service started successfully"
else
log_error "Service failed to start"
journalctl -u "${SERVICE_NAME}" -n 20 --no-pager || true
die "Deployment failed: service not running"
fi
}
run_migrations() {
log_info "Running database migrations..."
local src_dir="${INSTALL_DIR}/src"
local migrations_dir="${src_dir}/migrations"
local env_file="${CONFIG_DIR}/silod.env"
if [[ ! -d "${migrations_dir}" ]]; then
die "Migrations directory not found: ${migrations_dir}"
fi
# Source env file for DB password
if [[ -f "${env_file}" ]]; then
# shellcheck disable=SC1090
source "${env_file}"
fi
# Read DB config from production config
local db_host db_port db_name db_user db_password
db_host=$(grep -A10 '^database:' "${CONFIG_DIR}/config.yaml" | grep 'host:' | head -1 | awk '{print $2}' | tr -d '"')
db_port=$(grep -A10 '^database:' "${CONFIG_DIR}/config.yaml" | grep 'port:' | head -1 | awk '{print $2}')
db_name=$(grep -A10 '^database:' "${CONFIG_DIR}/config.yaml" | grep 'name:' | head -1 | awk '{print $2}' | tr -d '"')
db_user=$(grep -A10 '^database:' "${CONFIG_DIR}/config.yaml" | grep 'user:' | head -1 | awk '{print $2}' | tr -d '"')
db_password="${SILO_DB_PASSWORD:-}"
db_host="${db_host:-localhost}"
db_port="${db_port:-5432}"
db_name="${db_name:-silo}"
db_user="${db_user:-silo}"
if [[ -z "${db_password}" ]]; then
log_warn "SILO_DB_PASSWORD not set — skipping migrations"
log_warn "Run migrations manually: psql -h ${db_host} -U ${db_user} -d ${db_name} -f migrations/009_auth.sql"
return 0
fi
# Check psql is available
if ! command -v psql >/dev/null 2>&1; then
log_warn "psql not found — skipping automatic migrations"
log_warn "Run migrations manually: psql -h ${db_host} -U ${db_user} -d ${db_name} -f migrations/009_auth.sql"
return 0
fi
# Wait for database to be reachable
local retries=0
while ! PGPASSWORD="${db_password}" psql -h "${db_host}" -p "${db_port}" -U "${db_user}" -d "${db_name}" -c '\q' 2>/dev/null; do
retries=$((retries + 1))
if [[ ${retries} -ge 5 ]]; then
log_warn "Could not connect to database after 5 attempts — skipping migrations"
return 0
fi
log_info "Waiting for database... (attempt ${retries}/5)"
sleep 2
done
# Apply each migration, skipping ones that have already been applied
local applied=0
local skipped=0
for migration in "${migrations_dir}"/*.sql; do
if [[ ! -f "${migration}" ]]; then
continue
fi
local name
name=$(basename "${migration}")
if PGPASSWORD="${db_password}" psql -h "${db_host}" -p "${db_port}" \
-U "${db_user}" -d "${db_name}" -f "${migration}" 2>/dev/null; then
log_info "Applied: ${name}"
applied=$((applied + 1))
else
skipped=$((skipped + 1))
fi
done
log_success "Migrations complete (${applied} applied, ${skipped} already present)"
}
verify_deployment() {
log_info "Verifying deployment..."
# Wait a moment for service to fully start
sleep 2
# Check health endpoint
local health_status
health_status=$(curl -sf http://localhost:8080/health 2>/dev/null || echo "FAILED")
if [[ "${health_status}" == *"ok"* ]] || [[ "${health_status}" == *"healthy"* ]] || [[ "${health_status}" == "{}" ]]; then
log_success "Health check passed"
else
log_warn "Health check returned: ${health_status}"
fi
# Check ready endpoint (includes DB and MinIO)
local ready_status
ready_status=$(curl -sf http://localhost:8080/ready 2>/dev/null || echo "FAILED")
if [[ "${ready_status}" == *"ok"* ]] || [[ "${ready_status}" == *"ready"* ]] || [[ "${ready_status}" == "{}" ]]; then
log_success "Readiness check passed (DB and MinIO connected)"
else
log_warn "Readiness check returned: ${ready_status}"
log_warn "Check credentials in ${CONFIG_DIR}/silod.env"
fi
# Show version
log_info "Deployed version: $("${INSTALL_DIR}/bin/${BINARY_NAME}" --version 2>/dev/null || echo 'unknown')"
}
show_status() {
echo ""
log_info "Service Status"
echo "============================================"
systemctl status "${SERVICE_NAME}" --no-pager -l || true
echo ""
echo "Recent logs:"
journalctl -u "${SERVICE_NAME}" -n 10 --no-pager || true
}
# Parse arguments
while [[ $# -gt 0 ]]; do
case $1 in
--no-pull)
DO_PULL=false
shift
;;
--no-build)
DO_BUILD=false
shift
;;
--restart-only)
RESTART_ONLY=true
shift
;;
--status)
STATUS_ONLY=true
shift
;;
--help|-h)
show_help
;;
*)
die "Unknown option: $1"
;;
esac
done
# Main execution
main() {
echo ""
log_info "Silo Deployment Script"
log_info "======================"
echo ""
check_root
if [[ "${STATUS_ONLY}" == "true" ]]; then
show_status
exit 0
fi
if [[ "${RESTART_ONLY}" == "true" ]]; then
restart_service
verify_deployment
exit 0
fi
check_dependencies
setup_directories
if [[ "${DO_PULL}" == "true" ]]; then
git_pull
else
log_info "Skipping git pull (--no-pull)"
cd "${INSTALL_DIR}/src"
fi
if [[ "${DO_BUILD}" == "true" ]]; then
build_binary
else
log_info "Skipping build (--no-build)"
fi
install_config
run_migrations
install_systemd
set_permissions
restart_service
verify_deployment
echo ""
log_success "============================================"
log_success "Deployment complete!"
log_success "============================================"
echo ""
echo "Useful commands:"
echo " sudo systemctl status silod # Check service status"
echo " sudo journalctl -u silod -f # Follow logs"
echo " curl http://localhost:8080/health # Health check"
echo ""
}
main "$@"