diff --git a/ssh_router.sh b/ssh_router.sh index 810454a..69b8588 100755 --- a/ssh_router.sh +++ b/ssh_router.sh @@ -1,210 +1,209 @@ -#!/bin/bash +#!/usr/bin/env bash +set -euo pipefail -PERSON="$1" -WORKSPACE="$SSH_ORIGINAL_COMMAND" +PERSON="${1:?Usage: $0 }" +WORKSPACE="${SSH_ORIGINAL_COMMAND:-}" IMAGE="localhost/analytics-backend-workspace:latest" DEV_USER="devuser" - XDG_RUNTIME_DIR="/run/user/$(id -u)" LOG_FILE="/tmp/.ssh-router-${PERSON}.log" +# ───────────────────────────────────────────── +# ANSI colors & emojis +readonly C_RESET='\033[0m' +readonly C_INFO='\033[1;34m' # blue +readonly C_WARN='\033[1;33m' # yellow +readonly C_ERROR='\033[1;31m' # red + log() { - echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >>"$LOG_FILE" + local level="${1^^}" + shift + local icon color + case "$level" in + INFO) icon="ℹ️" color="$C_INFO" ;; + WARN) icon="⚠️" color="$C_WARN" ;; + ERROR) icon="❌" color="$C_ERROR" ;; + *) icon="🔹" color="$C_RESET" ;; + esac + local ts + ts="$(date '+%Y-%m-%d %H:%M:%S')" + printf '%b%s [%s] %s%b\n' \ + "$color" "$icon" "$ts" "[$level] $*" "$C_RESET" | + tee -a "$LOG_FILE" } +# ───────────────────────────────────────────── +# Check for interactive TTY if [[ ! -t 0 ]]; then - log "❌ No TTY allocated — refusing to run tmux without an interactive terminal" + log ERROR "No TTY allocated—refusing to run without an interactive terminal" echo "Error: No TTY. Use 'ssh -t'" >&2 exit 1 fi -# log "🧩 IMAGE = '$IMAGE'" -# log "🧩 WORKSPACE = '$WORKSPACE'" -# log "🧩 PERSON = '$PERSON'" - -# Fallbacks -if [[ -z "${WORKSPACE:-}" ]]; then +# ───────────────────────────────────────────── +# Default WORKSPACE if empty +if [[ -z "$WORKSPACE" ]]; then WORKSPACE="$PERSON" - log "ℹ️ Defaulted WORKSPACE to $WORKSPACE" + log INFO "Defaulted WORKSPACE → $WORKSPACE" fi +TMUX_SESSION="${WORKSPACE}|analytics-backend" -TMUX_SESSION="$WORKSPACE|analytics-backend" - -# Start podman socket service if it's not running -if [[ ! -S "$XDG_RUNTIME_DIR/podman/podman.sock" ]]; then - log "🔄 Starting Podman socket service for user $USER" - systemctl --user start podman.socket || { - log "❌ Failed to start podman.socket via systemd" +# ───────────────────────────────────────────── +# Ensure Podman socket is up +ensure_podman() { + local sock="$XDG_RUNTIME_DIR/podman/podman.sock" + if [[ ! -S "$sock" ]]; then + log INFO "Starting podman.socket for user $(id -un)" + systemctl --user start podman.socket || { + log ERROR "Failed to start podman.socket" + exit 1 + } + sleep 1 + fi + [[ -S "$sock" ]] || { + log ERROR "Podman socket still missing" exit 1 } +} +ensure_podman - # Wait briefly for socket to appear - sleep 1 -fi - -if [[ ! -S "$XDG_RUNTIME_DIR/podman/podman.sock" ]]; then - log "❌ Podman socket still missing after startup attempt" - exit 1 -fi - -# Check if image exists locally -if ! podman image exists "$IMAGE"; then - log "📦 Image $IMAGE not found locally. Pulling from registry..." - - # Attempt to pull the image from the local registry (insecure HTTP) - if ! podman pull --tls-verify=false "$IMAGE"; then - log "❌ Failed to pull image from $IMAGE" - exit 1 +# ───────────────────────────────────────────── +# Ensure IMAGE is present +ensure_image() { + if ! podman image exists "$IMAGE"; then + log WARN "Image $IMAGE not found—pulling" + podman pull --tls-verify=false "$IMAGE" || { + log ERROR "Failed to pull $IMAGE" + exit 1 + } + log INFO "Pulled $IMAGE" fi +} +ensure_image - log "✅ Successfully pulled $IMAGE" -fi - +# ───────────────────────────────────────────── +# Disallow file transfers case "$SSH_ORIGINAL_COMMAND" in *scp* | *sftp* | *rsync* | *tar*) - log "❌ File transfers are disabled" + log ERROR "File transfers are disabled" exit 1 ;; esac +# ───────────────────────────────────────────── +# Generate per-user gitconfig generate_gitconfig() { - access="$HOME/access.yml" - template="$HOME/gitconfig.template" - user_dir="$HOME/secrets/$PERSON" + local access="$HOME/access.yml" + local template="$HOME/gitconfig.template" + local userdir="$HOME/secrets/$PERSON" + local name email - # Extract user fields from YAML - name=$(yq ".\"$PERSON\".name" "$access") - email=$(yq ".\"$PERSON\".email" "$access") + name=$(yq -r ".\"$PERSON\".name" "$access" 2>/dev/null || echo) + email=$(yq -r ".\"$PERSON\".email" "$access" 2>/dev/null || echo) - # Ensure fields are not empty if [[ -z "$name" || -z "$email" ]]; then - echo "❌ Error: User '$PERSON' not found or missing name/email in $access" + log ERROR "Missing name/email for '$PERSON' in $access" exit 1 fi - # Generate .gitconfig - env GIT_NAME="$name" GIT_EMAIL="$email" \ - envsubst <"$template" >"$user_dir/gitconfig" - - echo "✅ .gitconfig created at $user_dir/gitconfig" + mkdir -p "$userdir" + GIT_NAME="$name" GIT_EMAIL="$email" \ + envsubst <"$template" >"$userdir/gitconfig" + log INFO ".gitconfig created → $userdir/gitconfig" } -# Function to start the container if not running +# ───────────────────────────────────────────── +# Start container if absent or stopped start_container_if_needed() { if ! podman container exists "$WORKSPACE"; then - log "🚀 Creating container $WORKSPACE..." + log INFO "Creating container '$WORKSPACE'" generate_gitconfig podman run -dit \ - --userns=keep-id \ --name "$WORKSPACE" \ + --userns=keep-id \ --user "$DEV_USER" \ --hostname "$WORKSPACE" \ --label auto-cleanup=true \ - -v "${XDG_RUNTIME_DIR}"/podman/podman.sock:/run/podman/podman.sock \ - -v /home/infilytics/data/"$WORKSPACE":/app \ - -v /home/infilytics/secrets/"$WORKSPACE"/gitconfig:/home/"$DEV_USER"/.gitconfig:ro \ - -v /home/infilytics/secrets/"$WORKSPACE"/id_ed25519:/home/"$DEV_USER"/.ssh/id_ed25519:ro \ - -v /home/infilytics/secrets/"$WORKSPACE"/id_ed25519.pub:/home/"$DEV_USER"/.ssh/id_ed25519.pub:ro \ + -v "${XDG_RUNTIME_DIR}/podman/podman.sock:/run/podman/podman.sock:Z" \ + -v "/home/infilytics/data/$WORKSPACE:/app:Z" \ + -v "/home/infilytics/secrets/$WORKSPACE/gitconfig:/home/$DEV_USER/.gitconfig:ro,Z" \ + -v "/home/infilytics/secrets/$WORKSPACE/id_ed25519:/home/$DEV_USER/.ssh/id_ed25519:ro,Z" \ + -v "/home/infilytics/secrets/$WORKSPACE/id_ed25519.pub:/home/$DEV_USER/.ssh/id_ed25519.pub:ro,Z" \ --entrypoint "/home/$DEV_USER/start.sh" \ - "$IMAGE" "${TMUX_SESSION}" + "$IMAGE" "$TMUX_SESSION" elif ! podman inspect -f '{{.State.Running}}' "$WORKSPACE" | grep -q true; then - log "⚡ Starting existing container $WORKSPACE..." - podman start "$WORKSPACE" >/dev/null 2>&1 + log INFO "Starting existing container '$WORKSPACE'" + podman start "$WORKSPACE" >/dev/null fi sleep 1 } -# After devuser exits... +# ───────────────────────────────────────────── +# Detach logic: stop container when devuser has left check_devuser_attached() { - # Get list of clients - client_users=$(podman exec "$WORKSPACE" tmux list-clients -t "$TMUX_SESSION" -F "#{client_user}" 2>/dev/null) - - if echo "$client_users" | grep -q "$DEV_USER"; then - log "💡 devuser still attached — container stays running" - return 0 + local clients + clients=$(podman exec "$WORKSPACE" tmux list-clients -t "$TMUX_SESSION" -F "#{client_user}" 2>/dev/null) + if grep -q "^${DEV_USER}\$" <<<"$clients"; then + log INFO "devuser still attached—keeping container running" else - log "🏃 $PERSON has logged out — stopping container" - podman stop "$WORKSPACE" >/dev/null 2>&1 - return 1 + log INFO "devuser detached—stopping container" + podman stop "$WORKSPACE" >/dev/null fi } +# ───────────────────────────────────────────── +# Determine access mode (rw|ro) or exit get_access_mode() { - local yaml_file="access.yml" - local workspace="$1" - local person="$2" - - if [[ ! "$workspace" =~ ^[a-zA-Z0-9._-]+$ ]]; then - log "❌ Invalid container name: $WORKSPACE" + local yaml="access.yml" user="$PERSON" ws="$WORKSPACE" + [[ ! "$ws" =~ ^[A-Za-z0-9._-]+$ ]] && { + log ERROR "Invalid workspace name" + exit 1 + } + if [[ "$user" == "$ws" ]]; then + echo rw + elif yq -e '.["'"$user"'"].rw[]?' "$yaml" | grep -qx "$ws"; then + echo rw + elif yq -e '.["'"$user"'"].ro[]?' "$yaml" | grep -qx "$ws"; then + echo ro + else + log ERROR "$user has no access to $ws" exit 1 fi - - # Special case: user accessing their own workspace - if [[ "$workspace" == "$person" ]]; then - echo "access=rw" - return 0 - fi - - # Check rw - if yq '.["'"$person"'"].rw // []' "$yaml_file" | grep -q "\b$workspace\b"; then - echo "access=rw" - return 0 - fi - - # Check ro - if yq '.["'"$person"'"].ro // []' "$yaml_file" | grep -q "\b$workspace\b"; then - echo "access=ro" - return 0 - fi - - # No access → exit with error - log "❌ $person has no access to $workspace" >&2 - exit 1 } -# === Main === - -read -r access_line < <(get_access_mode "$WORKSPACE" "$PERSON") || exit 1 -MODE="${access_line#access=}" +MODE="$(get_access_mode)" +# ───────────────────────────────────────────── +# Main dispatch case "$MODE" in rw) start_container_if_needed - # Run tmux session inside the container - if ! podman exec -it --user "$DEV_USER" "$WORKSPACE" tmux has-session -t "$TMUX_SESSION" >/dev/null 2>&1; then - if ! podman exec -it -e EDITOR=nvim --user "$DEV_USER" "$WORKSPACE" tmux new-session -d -s "$TMUX_SESSION" >/dev/null 2>&1; then - log "❌ Could not create new tmux session. Please contact admin or try again later." - exit 1 - fi + # Ensure tmux session exists + if ! podman exec -it --user "$DEV_USER" "$WORKSPACE" tmux has-session -t "$TMUX_SESSION" 2>/dev/null; then + podman exec -it --user "$DEV_USER" "$WORKSPACE" \ + tmux new-session -d -s "$TMUX_SESSION" fi - log "⚡ $PERSON is working on $WORKSPACE's workspace" - if ! podman exec -it -e TERM="$TERM" --user "$DEV_USER" "$WORKSPACE" tmux attach -t "$TMUX_SESSION"; then - log "❌ Could not attach to tmux session. Please contact admin or try again later." - exit 1 - fi - log "⚡ $PERSON finished working on $WORKSPACE's worksapce" - + log INFO "$PERSON attaching to workspace '$WORKSPACE'" + podman exec -it -e TERM="$TERM" --user "$DEV_USER" "$WORKSPACE" \ + tmux attach -t "$TMUX_SESSION" + log INFO "$PERSON detached from '$WORKSPACE'" check_devuser_attached - exit 0 ;; ro) - if (podman container exists "$WORKSPACE" && podman inspect -f '{{.State.Running}}' "$WORKSPACE" | grep -q true) >/dev/null 2>&1; then - log "📜 $PERSON is viewing $WORKSPACE's workspace" - if ! podman exec -it -e TERM="$TERM" --user "$DEV_USER" "$WORKSPACE" tmux attach -r -t "$TMUX_SESSION"; then - log "❌ Could not attach to tmux session. Please contact admin or try again later." - exit 1 - fi - log "🏃 $PERSON stopped viewing $WORKSPACE's workspace" - exit 0 + if podman inspect -f '{{.State.Running}}' "$WORKSPACE" 2>/dev/null | grep -q true; then + log INFO "$PERSON viewing workspace '$WORKSPACE'" + podman exec -it -e TERM="$TERM" --user "$DEV_USER" "$WORKSPACE" \ + tmux attach -r -t "$TMUX_SESSION" + log INFO "$PERSON stopped viewing '$WORKSPACE'" else - log "❌ Workspace for $WORKSPACE does not exist." + log ERROR "Workspace '$WORKSPACE' is not running" exit 1 fi ;; *) - log "❌ Invalid access mode: $MODE" + log ERROR "Unknown access mode: '$MODE'" exit 1 ;; esac