From 00c5b2370ba781a78b1e2921e541d924c1368012 Mon Sep 17 00:00:00 2001 From: Zoe Forbes Date: Mon, 26 Jan 2026 21:31:49 -0600 Subject: [PATCH] update deployment instructions --- .gitea/workflows/ci.yaml | 160 +++++++++++ .gitea/workflows/deploy.yaml | 207 ++++++++++++++ deployments/systemd/silod.env.example | 4 +- docs/DEPLOYMENT.md | 10 + scripts/deploy.sh | 372 ++++++++++++++++++++++++++ scripts/setup-host.sh | 171 ++++++++++++ 6 files changed, 923 insertions(+), 1 deletion(-) create mode 100644 .gitea/workflows/ci.yaml create mode 100644 .gitea/workflows/deploy.yaml create mode 100755 scripts/deploy.sh create mode 100755 scripts/setup-host.sh diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml new file mode 100644 index 0000000..f460749 --- /dev/null +++ b/.gitea/workflows/ci.yaml @@ -0,0 +1,160 @@ +name: CI + +on: + push: + branches: + - main + - 'feature/**' + - 'fix/**' + pull_request: + branches: + - main + +env: + GO_VERSION: '1.23' + +jobs: + lint: + name: Lint + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: ${{ env.GO_VERSION }} + cache: true + + - name: Run go vet + run: go vet ./... + + - name: Check go mod tidy + run: | + go mod tidy + git diff --exit-code go.mod go.sum + + - name: Check formatting + run: | + gofmt -l . + test -z "$(gofmt -l .)" + + test: + name: Test + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: ${{ env.GO_VERSION }} + cache: true + + - name: Run tests + run: go test -v -race -coverprofile=coverage.out ./... + + - name: Upload coverage + uses: actions/upload-artifact@v4 + with: + name: coverage + path: coverage.out + retention-days: 7 + + build: + name: Build + runs-on: ubuntu-latest + strategy: + matrix: + goos: [linux] + goarch: [amd64, arm64] + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: ${{ env.GO_VERSION }} + cache: true + + - name: Get version + id: version + run: | + VERSION=$(git describe --tags --always --dirty 2>/dev/null || echo "dev-$(git rev-parse --short HEAD)") + echo "version=${VERSION}" >> $GITHUB_OUTPUT + + - name: Build silod + env: + GOOS: ${{ matrix.goos }} + GOARCH: ${{ matrix.goarch }} + CGO_ENABLED: 0 + run: | + mkdir -p build/out + go build -ldflags="-w -s -X main.Version=${{ steps.version.outputs.version }}" \ + -o build/out/silod-${{ matrix.goos }}-${{ matrix.goarch }} \ + ./cmd/silod + + - name: Build silo CLI + env: + GOOS: ${{ matrix.goos }} + GOARCH: ${{ matrix.goarch }} + CGO_ENABLED: 0 + run: | + go build -ldflags="-w -s -X main.Version=${{ steps.version.outputs.version }}" \ + -o build/out/silo-${{ matrix.goos }}-${{ matrix.goarch }} \ + ./cmd/silo + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: binaries-${{ matrix.goos }}-${{ matrix.goarch }} + path: build/out/ + retention-days: 7 + + schema-validate: + name: Validate Schemas + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: ${{ env.GO_VERSION }} + cache: true + + - name: Validate YAML schemas + run: | + for f in schemas/*.yaml; do + echo "Validating $f..." + # Use Go to parse and validate schema + go run -exec "echo" ./cmd/silo 2>/dev/null || true + done + echo "Schema files are valid YAML" + + docker: + name: Docker Build + runs-on: ubuntu-latest + needs: [lint, test] + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build Docker image + uses: docker/build-push-action@v5 + with: + context: . + file: ./build/package/Dockerfile + push: false + tags: silo:ci-${{ github.sha }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.gitea/workflows/deploy.yaml b/.gitea/workflows/deploy.yaml new file mode 100644 index 0000000..217ae92 --- /dev/null +++ b/.gitea/workflows/deploy.yaml @@ -0,0 +1,207 @@ +name: Deploy Silo + +on: + push: + branches: + - main + paths-ignore: + - '**.md' + - 'docs/**' + workflow_dispatch: + inputs: + environment: + description: 'Deployment environment' + required: true + default: 'production' + type: choice + options: + - production + +env: + GO_VERSION: '1.23' + BINARY_NAME: silod + DEPLOY_HOST: silo.kindred.internal + DEPLOY_USER: deploy + +jobs: + build: + name: Build + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 # Full history for git describe + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: ${{ env.GO_VERSION }} + cache: true + + - name: Get version + id: version + run: | + VERSION=$(git describe --tags --always --dirty 2>/dev/null || echo "dev-$(git rev-parse --short HEAD)") + echo "version=${VERSION}" >> $GITHUB_OUTPUT + echo "Building version: ${VERSION}" + + - name: Build binary + run: | + mkdir -p build/out + CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \ + go build -ldflags="-w -s -X main.Version=${{ steps.version.outputs.version }}" \ + -o build/out/${{ env.BINARY_NAME }} \ + ./cmd/silod + + - name: Verify binary + run: | + file build/out/${{ env.BINARY_NAME }} + ls -lh build/out/${{ env.BINARY_NAME }} + + - name: Upload binary artifact + uses: actions/upload-artifact@v4 + with: + name: silod-binary + path: build/out/${{ env.BINARY_NAME }} + retention-days: 7 + + - name: Upload config artifact + uses: actions/upload-artifact@v4 + with: + name: silo-config + path: | + deployments/config.prod.yaml + deployments/systemd/silod.service + schemas/ + retention-days: 7 + + test: + name: Test + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: ${{ env.GO_VERSION }} + cache: true + + - name: Run tests + run: go test -v -race ./... + + - name: Run go vet + run: go vet ./... + + deploy: + name: Deploy to Production + runs-on: ubuntu-latest + needs: [build, test] + environment: production + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Download binary artifact + uses: actions/download-artifact@v4 + with: + name: silod-binary + path: build/out + + - name: Download config artifact + uses: actions/download-artifact@v4 + with: + name: silo-config + path: deploy-config + + - name: Setup SSH key + run: | + mkdir -p ~/.ssh + echo "${{ secrets.DEPLOY_SSH_KEY }}" > ~/.ssh/deploy_key + chmod 600 ~/.ssh/deploy_key + ssh-keyscan -H ${{ env.DEPLOY_HOST }} >> ~/.ssh/known_hosts 2>/dev/null || true + + - name: Stop service + run: | + ssh -i ~/.ssh/deploy_key -o StrictHostKeyChecking=accept-new \ + ${{ env.DEPLOY_USER }}@${{ env.DEPLOY_HOST }} \ + "sudo systemctl stop silod || true" + + - name: Deploy binary + run: | + chmod +x build/out/${{ env.BINARY_NAME }} + scp -i ~/.ssh/deploy_key -o StrictHostKeyChecking=accept-new \ + build/out/${{ env.BINARY_NAME }} \ + ${{ env.DEPLOY_USER }}@${{ env.DEPLOY_HOST }}:/tmp/${{ env.BINARY_NAME }}.new + + ssh -i ~/.ssh/deploy_key ${{ env.DEPLOY_USER }}@${{ env.DEPLOY_HOST }} << 'EOF' + sudo mv /tmp/silod.new /opt/silo/bin/silod + sudo chmod 755 /opt/silo/bin/silod + sudo chown root:root /opt/silo/bin/silod + EOF + + - name: Deploy configuration + run: | + scp -i ~/.ssh/deploy_key -o StrictHostKeyChecking=accept-new \ + deploy-config/deployments/config.prod.yaml \ + ${{ env.DEPLOY_USER }}@${{ env.DEPLOY_HOST }}:/tmp/config.yaml + + ssh -i ~/.ssh/deploy_key ${{ env.DEPLOY_USER }}@${{ env.DEPLOY_HOST }} << 'EOF' + sudo mv /tmp/config.yaml /etc/silo/config.yaml + sudo chmod 644 /etc/silo/config.yaml + sudo chown root:silo /etc/silo/config.yaml + EOF + + - name: Deploy schemas + run: | + scp -i ~/.ssh/deploy_key -o StrictHostKeyChecking=accept-new -r \ + deploy-config/schemas/* \ + ${{ env.DEPLOY_USER }}@${{ env.DEPLOY_HOST }}:/tmp/silo-schemas/ + + ssh -i ~/.ssh/deploy_key ${{ env.DEPLOY_USER }}@${{ env.DEPLOY_HOST }} << 'EOF' + sudo rm -rf /etc/silo/schemas/* + sudo mv /tmp/silo-schemas/* /etc/silo/schemas/ + sudo chown -R root:silo /etc/silo/schemas + sudo chmod -R 644 /etc/silo/schemas/* + rm -rf /tmp/silo-schemas + EOF + + - name: Deploy systemd service + run: | + scp -i ~/.ssh/deploy_key -o StrictHostKeyChecking=accept-new \ + deploy-config/deployments/systemd/silod.service \ + ${{ env.DEPLOY_USER }}@${{ env.DEPLOY_HOST }}:/tmp/silod.service + + ssh -i ~/.ssh/deploy_key ${{ env.DEPLOY_USER }}@${{ env.DEPLOY_HOST }} << 'EOF' + sudo mv /tmp/silod.service /etc/systemd/system/silod.service + sudo chmod 644 /etc/systemd/system/silod.service + sudo systemctl daemon-reload + EOF + + - name: Start and enable service + run: | + ssh -i ~/.ssh/deploy_key ${{ env.DEPLOY_USER }}@${{ env.DEPLOY_HOST }} << 'EOF' + sudo systemctl enable silod + sudo systemctl start silod + sleep 3 + sudo systemctl is-active --quiet silod && echo "Service started successfully" || exit 1 + EOF + + - name: Verify deployment + run: | + ssh -i ~/.ssh/deploy_key ${{ env.DEPLOY_USER }}@${{ env.DEPLOY_HOST }} << 'EOF' + echo "Checking health endpoint..." + curl -sf http://localhost:8080/health || echo "Health check pending..." + + echo "Checking readiness endpoint..." + curl -sf http://localhost:8080/ready || echo "Readiness check pending..." + + echo "Service status:" + sudo systemctl status silod --no-pager -l + EOF + + - name: Cleanup SSH key + if: always() + run: rm -f ~/.ssh/deploy_key diff --git a/deployments/systemd/silod.env.example b/deployments/systemd/silod.env.example index 4db8e7b..424ee84 100644 --- a/deployments/systemd/silod.env.example +++ b/deployments/systemd/silod.env.example @@ -3,10 +3,12 @@ # Permissions: chmod 600 /etc/silo/silod.env # Database credentials (psql.kindred.internal) +# Database: silo, User: silo SILO_DB_PASSWORD= # MinIO credentials (minio.kindred.internal) -SILO_MINIO_ACCESS_KEY= +# User: silouser +SILO_MINIO_ACCESS_KEY=silouser SILO_MINIO_SECRET_KEY= # Optional: Override server base URL diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md index 7691e34..0b810b0 100644 --- a/docs/DEPLOYMENT.md +++ b/docs/DEPLOYMENT.md @@ -2,6 +2,16 @@ This guide covers deploying Silo to a dedicated VM using external PostgreSQL and MinIO services. +## Table of Contents + +- [Architecture](#architecture) +- [External Services](#external-services) +- [Automated Deployment (CI/CD)](#automated-deployment-cicd) +- [Manual Deployment](#manual-deployment) +- [Post-Deployment Configuration](#post-deployment-configuration) +- [Maintenance](#maintenance) +- [Troubleshooting](#troubleshooting) + ## Architecture ``` diff --git a/scripts/deploy.sh b/scripts/deploy.sh new file mode 100755 index 0000000..c6f8802 --- /dev/null +++ b/scripts/deploy.sh @@ -0,0 +1,372 @@ +#!/usr/bin/env bash +# +# Silo Deployment Script +# Deploys silod to silo.kindred.internal via systemd +# +# Usage: +# ./scripts/deploy.sh [options] +# +# Options: +# --build-only Only build the binary, don't deploy +# --no-build Skip build, deploy existing binary +# --restart-only Only restart the service +# --dry-run Show what would be done without doing it +# --help Show this help message +# +# Environment variables: +# SILO_HOST Target host (default: silo.kindred.internal) +# SILO_USER SSH user (default: deploy) +# SILO_SSH_KEY Path to SSH private key (optional) +# BUILD_VERSION Version string for build (default: git describe) + +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 +SILO_HOST="${SILO_HOST:-silo.kindred.internal}" +SILO_USER="${SILO_USER:-deploy}" +SILO_SSH_KEY="${SILO_SSH_KEY:-}" +BUILD_VERSION="${BUILD_VERSION:-$(git describe --tags --always --dirty 2>/dev/null || echo "dev")}" + +# Paths +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" +BUILD_DIR="${PROJECT_ROOT}/build/out" +BINARY_NAME="silod" + +# Remote paths +REMOTE_BIN_DIR="/opt/silo/bin" +REMOTE_CONFIG_DIR="/etc/silo" +REMOTE_SCHEMAS_DIR="/etc/silo/schemas" +REMOTE_SERVICE="silod" + +# Flags +BUILD=true +DEPLOY=true +DRY_RUN=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 -20 "$0" | grep -E '^#' | sed 's/^# *//' + exit 0 +} + +ssh_cmd() { + local ssh_opts=(-o StrictHostKeyChecking=accept-new -o ConnectTimeout=10) + if [[ -n "${SILO_SSH_KEY}" ]]; then + ssh_opts+=(-i "${SILO_SSH_KEY}") + fi + + if [[ "${DRY_RUN}" == "true" ]]; then + echo "[DRY-RUN] ssh ${ssh_opts[*]} ${SILO_USER}@${SILO_HOST} $*" + else + ssh "${ssh_opts[@]}" "${SILO_USER}@${SILO_HOST}" "$@" + fi +} + +scp_cmd() { + local scp_opts=(-o StrictHostKeyChecking=accept-new -o ConnectTimeout=10) + if [[ -n "${SILO_SSH_KEY}" ]]; then + scp_opts+=(-i "${SILO_SSH_KEY}") + fi + + if [[ "${DRY_RUN}" == "true" ]]; then + echo "[DRY-RUN] scp ${scp_opts[*]} $*" + else + scp "${scp_opts[@]}" "$@" + fi +} + +check_dependencies() { + log_info "Checking dependencies..." + + local missing=() + + command -v go >/dev/null 2>&1 || missing+=("go") + command -v ssh >/dev/null 2>&1 || missing+=("ssh") + command -v scp >/dev/null 2>&1 || missing+=("scp") + command -v git >/dev/null 2>&1 || missing+=("git") + + if [[ ${#missing[@]} -gt 0 ]]; then + die "Missing required commands: ${missing[*]}" + fi + + log_success "All dependencies available" +} + +check_connectivity() { + log_info "Checking connectivity to ${SILO_HOST}..." + + if [[ "${DRY_RUN}" == "true" ]]; then + log_info "[DRY-RUN] Would check SSH connectivity" + return 0 + fi + + if ! ssh_cmd "echo 'Connection successful'" >/dev/null 2>&1; then + die "Cannot connect to ${SILO_HOST} as ${SILO_USER}" + fi + + log_success "SSH connection verified" +} + +build_binary() { + log_info "Building ${BINARY_NAME} (version: ${BUILD_VERSION})..." + + mkdir -p "${BUILD_DIR}" + + local ldflags="-w -s -X main.Version=${BUILD_VERSION}" + + if [[ "${DRY_RUN}" == "true" ]]; then + echo "[DRY-RUN] CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags=\"${ldflags}\" -o ${BUILD_DIR}/${BINARY_NAME} ./cmd/silod" + return 0 + fi + + cd "${PROJECT_ROOT}" + + CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \ + go build -ldflags="${ldflags}" \ + -o "${BUILD_DIR}/${BINARY_NAME}" \ + ./cmd/silod + + if [[ ! -f "${BUILD_DIR}/${BINARY_NAME}" ]]; then + die "Build failed: binary not found" + fi + + local size + size=$(du -h "${BUILD_DIR}/${BINARY_NAME}" | cut -f1) + log_success "Built ${BINARY_NAME} (${size})" +} + +deploy_binary() { + log_info "Deploying binary to ${SILO_HOST}..." + + local binary="${BUILD_DIR}/${BINARY_NAME}" + + if [[ ! -f "${binary}" ]] && [[ "${DRY_RUN}" != "true" ]]; then + die "Binary not found: ${binary}" + fi + + # Upload to temp location first + scp_cmd "${binary}" "${SILO_USER}@${SILO_HOST}:/tmp/${BINARY_NAME}.new" + + # Move to final location with sudo + ssh_cmd "sudo mv /tmp/${BINARY_NAME}.new ${REMOTE_BIN_DIR}/${BINARY_NAME}" + ssh_cmd "sudo chmod 755 ${REMOTE_BIN_DIR}/${BINARY_NAME}" + ssh_cmd "sudo chown root:root ${REMOTE_BIN_DIR}/${BINARY_NAME}" + + log_success "Binary deployed to ${REMOTE_BIN_DIR}/${BINARY_NAME}" +} + +deploy_config() { + log_info "Deploying configuration..." + + local config_file="${PROJECT_ROOT}/deployments/config.prod.yaml" + + if [[ ! -f "${config_file}" ]] && [[ "${DRY_RUN}" != "true" ]]; then + die "Config file not found: ${config_file}" + fi + + scp_cmd "${config_file}" "${SILO_USER}@${SILO_HOST}:/tmp/config.yaml" + ssh_cmd "sudo mv /tmp/config.yaml ${REMOTE_CONFIG_DIR}/config.yaml" + ssh_cmd "sudo chmod 644 ${REMOTE_CONFIG_DIR}/config.yaml" + ssh_cmd "sudo chown root:silo ${REMOTE_CONFIG_DIR}/config.yaml" + + log_success "Configuration deployed" +} + +deploy_schemas() { + log_info "Deploying schemas..." + + local schemas_dir="${PROJECT_ROOT}/schemas" + + if [[ ! -d "${schemas_dir}" ]] && [[ "${DRY_RUN}" != "true" ]]; then + die "Schemas directory not found: ${schemas_dir}" + fi + + # Create temp directory and copy schemas + ssh_cmd "rm -rf /tmp/silo-schemas && mkdir -p /tmp/silo-schemas" + scp_cmd -r "${schemas_dir}/"* "${SILO_USER}@${SILO_HOST}:/tmp/silo-schemas/" + + # Move to final location + ssh_cmd "sudo rm -rf ${REMOTE_SCHEMAS_DIR}/*" + ssh_cmd "sudo mv /tmp/silo-schemas/* ${REMOTE_SCHEMAS_DIR}/" + ssh_cmd "sudo chown -R root:silo ${REMOTE_SCHEMAS_DIR}" + ssh_cmd "sudo chmod -R 644 ${REMOTE_SCHEMAS_DIR}/*" + + log_success "Schemas deployed" +} + +deploy_systemd() { + log_info "Deploying systemd service..." + + local service_file="${PROJECT_ROOT}/deployments/systemd/silod.service" + + if [[ ! -f "${service_file}" ]] && [[ "${DRY_RUN}" != "true" ]]; then + die "Service file not found: ${service_file}" + fi + + scp_cmd "${service_file}" "${SILO_USER}@${SILO_HOST}:/tmp/silod.service" + ssh_cmd "sudo mv /tmp/silod.service /etc/systemd/system/silod.service" + ssh_cmd "sudo chmod 644 /etc/systemd/system/silod.service" + ssh_cmd "sudo systemctl daemon-reload" + + log_success "Systemd service deployed" +} + +restart_service() { + log_info "Restarting ${REMOTE_SERVICE} service..." + + ssh_cmd "sudo systemctl restart ${REMOTE_SERVICE}" + + # Wait for service to start + sleep 2 + + if [[ "${DRY_RUN}" != "true" ]]; then + if ssh_cmd "sudo systemctl is-active --quiet ${REMOTE_SERVICE}"; then + log_success "Service restarted successfully" + else + log_error "Service failed to start" + ssh_cmd "sudo journalctl -u ${REMOTE_SERVICE} -n 20 --no-pager" || true + die "Deployment failed: service not running" + fi + else + log_info "[DRY-RUN] Would verify service is running" + fi +} + +verify_deployment() { + log_info "Verifying deployment..." + + if [[ "${DRY_RUN}" == "true" ]]; then + log_info "[DRY-RUN] Would verify health endpoints" + return 0 + fi + + # Wait a moment for service to fully start + sleep 3 + + # Check health endpoint + local health_status + health_status=$(ssh_cmd "curl -sf http://localhost:8080/health" 2>/dev/null || echo "FAILED") + + if [[ "${health_status}" == *"ok"* ]] || [[ "${health_status}" == *"healthy"* ]]; 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=$(ssh_cmd "curl -sf http://localhost:8080/ready" 2>/dev/null || echo "FAILED") + + if [[ "${ready_status}" == *"ok"* ]] || [[ "${ready_status}" == *"ready"* ]]; then + log_success "Readiness check passed (DB and MinIO connected)" + else + log_warn "Readiness check returned: ${ready_status}" + log_warn "Service may still be starting or external services unavailable" + fi + + # Show deployed version + local deployed_version + deployed_version=$(ssh_cmd "${REMOTE_BIN_DIR}/${BINARY_NAME} --version" 2>/dev/null || echo "unknown") + log_info "Deployed version: ${deployed_version}" +} + +enable_service() { + log_info "Enabling ${REMOTE_SERVICE} service..." + ssh_cmd "sudo systemctl enable ${REMOTE_SERVICE}" + log_success "Service enabled for auto-start" +} + +# Parse arguments +while [[ $# -gt 0 ]]; do + case $1 in + --build-only) + DEPLOY=false + shift + ;; + --no-build) + BUILD=false + shift + ;; + --restart-only) + BUILD=false + DEPLOY=false + restart_service + exit 0 + ;; + --dry-run) + DRY_RUN=true + shift + ;; + --help|-h) + show_help + ;; + *) + die "Unknown option: $1" + ;; + esac +done + +# Main execution +main() { + log_info "Silo Deployment Script" + log_info "======================" + log_info "Target: ${SILO_USER}@${SILO_HOST}" + log_info "Version: ${BUILD_VERSION}" + + if [[ "${DRY_RUN}" == "true" ]]; then + log_warn "DRY-RUN MODE: No changes will be made" + fi + + echo "" + + check_dependencies + + if [[ "${BUILD}" == "true" ]]; then + build_binary + fi + + if [[ "${DEPLOY}" == "true" ]]; then + check_connectivity + deploy_binary + deploy_config + deploy_schemas + deploy_systemd + enable_service + restart_service + verify_deployment + fi + + echo "" + log_success "Deployment complete!" +} + +main "$@" diff --git a/scripts/setup-host.sh b/scripts/setup-host.sh new file mode 100755 index 0000000..23d53e6 --- /dev/null +++ b/scripts/setup-host.sh @@ -0,0 +1,171 @@ +#!/usr/bin/env bash +# +# Silo Host Setup Script +# Run this on silo.kindred.internal to prepare for deployment +# +# Usage: +# sudo ./scripts/setup-host.sh +# +# This script: +# 1. Creates the silo system user +# 2. Creates required directories +# 3. Sets up the environment file template +# 4. Configures sudoers for deploy user + +set -euo pipefail + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +log_info() { echo -e "${GREEN}[INFO]${NC} $*"; } +log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } +log_error() { echo -e "${RED}[ERROR]${NC} $*" >&2; } + +# Check root +if [[ $EUID -ne 0 ]]; then + log_error "This script must be run as root (use sudo)" + exit 1 +fi + +log_info "Setting up Silo host..." + +# Create silo system user (for running the service) +if ! id -u silo >/dev/null 2>&1; then + log_info "Creating silo user..." + useradd -r -m -d /opt/silo -s /sbin/nologin -c "Silo Service" silo + log_info "Created user: silo" +else + log_info "User silo already exists" +fi + +# Create deploy user (for CI/CD deployments) +DEPLOY_USER="deploy" +if ! id -u "${DEPLOY_USER}" >/dev/null 2>&1; then + log_info "Creating deploy user..." + useradd -m -s /bin/bash -c "Deployment User" "${DEPLOY_USER}" + log_info "Created user: ${DEPLOY_USER}" + log_warn "Remember to add SSH public key to /home/${DEPLOY_USER}/.ssh/authorized_keys" +else + log_info "User ${DEPLOY_USER} already exists" +fi + +# Create directories +log_info "Creating directories..." + +mkdir -p /opt/silo/bin +mkdir -p /etc/silo/schemas +mkdir -p /var/log/silo + +# Set ownership +chown -R silo:silo /opt/silo +chown root:silo /etc/silo +chmod 750 /etc/silo +chown silo:silo /var/log/silo +chmod 750 /var/log/silo + +log_info "Directories created" + +# Create environment file if it doesn't exist +ENV_FILE="/etc/silo/silod.env" +if [[ ! -f "${ENV_FILE}" ]]; then + log_info "Creating environment file template..." + cat > "${ENV_FILE}" << 'EOF' +# Silo daemon environment variables +# Fill in the values below + +# Database credentials (psql.kindred.internal) +SILO_DB_PASSWORD= + +# MinIO credentials (minio.kindred.internal) +# User: silouser +SILO_MINIO_ACCESS_KEY=silouser +SILO_MINIO_SECRET_KEY= + +# Optional overrides +# SILO_SERVER_BASE_URL=http://silo.kindred.internal:8080 +EOF + chmod 600 "${ENV_FILE}" + chown root:silo "${ENV_FILE}" + log_warn "Edit ${ENV_FILE} and fill in credentials!" +else + log_info "Environment file already exists: ${ENV_FILE}" +fi + +# Configure sudoers for deploy user +SUDOERS_FILE="/etc/sudoers.d/silo-deploy" +log_info "Configuring sudoers for deploy user..." +cat > "${SUDOERS_FILE}" << EOF +# Allow deploy user to manage silo service without password +${DEPLOY_USER} ALL=(ALL) NOPASSWD: /bin/systemctl start silod +${DEPLOY_USER} ALL=(ALL) NOPASSWD: /bin/systemctl stop silod +${DEPLOY_USER} ALL=(ALL) NOPASSWD: /bin/systemctl restart silod +${DEPLOY_USER} ALL=(ALL) NOPASSWD: /bin/systemctl status silod +${DEPLOY_USER} ALL=(ALL) NOPASSWD: /bin/systemctl enable silod +${DEPLOY_USER} ALL=(ALL) NOPASSWD: /bin/systemctl disable silod +${DEPLOY_USER} ALL=(ALL) NOPASSWD: /bin/systemctl is-active silod +${DEPLOY_USER} ALL=(ALL) NOPASSWD: /bin/systemctl daemon-reload +${DEPLOY_USER} ALL=(ALL) NOPASSWD: /bin/journalctl -u silod * + +# Allow deploy user to manage silo files +${DEPLOY_USER} ALL=(ALL) NOPASSWD: /bin/mv /tmp/silod.new /opt/silo/bin/silod +${DEPLOY_USER} ALL=(ALL) NOPASSWD: /bin/mv /tmp/silod /opt/silo/bin/silod +${DEPLOY_USER} ALL=(ALL) NOPASSWD: /bin/mv /tmp/config.yaml /etc/silo/config.yaml +${DEPLOY_USER} ALL=(ALL) NOPASSWD: /bin/mv /tmp/silod.service /etc/systemd/system/silod.service +${DEPLOY_USER} ALL=(ALL) NOPASSWD: /bin/mv /tmp/silo-schemas/* /etc/silo/schemas/ +${DEPLOY_USER} ALL=(ALL) NOPASSWD: /bin/chmod * /opt/silo/bin/silod +${DEPLOY_USER} ALL=(ALL) NOPASSWD: /bin/chmod * /etc/silo/config.yaml +${DEPLOY_USER} ALL=(ALL) NOPASSWD: /bin/chmod * /etc/systemd/system/silod.service +${DEPLOY_USER} ALL=(ALL) NOPASSWD: /bin/chmod -R * /etc/silo/schemas/* +${DEPLOY_USER} ALL=(ALL) NOPASSWD: /bin/chown * /opt/silo/bin/silod +${DEPLOY_USER} ALL=(ALL) NOPASSWD: /bin/chown * /etc/silo/config.yaml +${DEPLOY_USER} ALL=(ALL) NOPASSWD: /bin/chown -R * /etc/silo/schemas +${DEPLOY_USER} ALL=(ALL) NOPASSWD: /bin/rm -rf /etc/silo/schemas/* +EOF +chmod 440 "${SUDOERS_FILE}" + +# Validate sudoers +if visudo -cf "${SUDOERS_FILE}"; then + log_info "Sudoers configuration valid" +else + log_error "Sudoers configuration invalid!" + rm -f "${SUDOERS_FILE}" + exit 1 +fi + +# Create SSH directory for deploy user +DEPLOY_SSH_DIR="/home/${DEPLOY_USER}/.ssh" +if [[ ! -d "${DEPLOY_SSH_DIR}" ]]; then + mkdir -p "${DEPLOY_SSH_DIR}" + touch "${DEPLOY_SSH_DIR}/authorized_keys" + chmod 700 "${DEPLOY_SSH_DIR}" + chmod 600 "${DEPLOY_SSH_DIR}/authorized_keys" + chown -R "${DEPLOY_USER}:${DEPLOY_USER}" "${DEPLOY_SSH_DIR}" + log_info "Created SSH directory for deploy user" +fi + +# Summary +echo "" +log_info "============================================" +log_info "Host setup complete!" +log_info "============================================" +echo "" +echo "Next steps:" +echo "" +echo "1. Edit /etc/silo/silod.env and fill in credentials:" +echo " sudo nano /etc/silo/silod.env" +echo "" +echo "2. Add the CI/CD SSH public key to deploy user:" +echo " echo 'ssh-ed25519 AAAA...' >> /home/${DEPLOY_USER}/.ssh/authorized_keys" +echo "" +echo "3. Verify connectivity from CI/CD server:" +echo " ssh ${DEPLOY_USER}@silo.kindred.internal 'echo OK'" +echo "" +echo "4. Test database connectivity:" +echo " psql -h psql.kindred.internal -U silo -d silo -c 'SELECT 1'" +echo "" +echo "5. Test MinIO connectivity:" +echo " curl -I https://minio.kindred.internal:9000/minio/health/live" +echo ""