#!/bin/bash # # FlyMQ Installation Script # Copyright (c) 2026 Firefly Software Solutions Inc. # Licensed under the Apache License, Version 2.0 # # Usage: # ./install.sh Interactive installation # ./install.sh --yes Non-interactive with defaults # ./install.sh --config-file FILE Use existing config file # ./install.sh --uninstall Uninstall FlyMQ # set -euo pipefail # ============================================================================= # Configuration # ============================================================================= readonly SCRIPT_VERSION="1.26.1" readonly FLYMQ_VERSION="${FLYMQ_VERSION:-1.26.1}" readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)" # Installation options PREFIX="" AUTO_CONFIRM=false UNINSTALL=false CONFIG_FILE="" # Configuration values (will be set during interactive setup) CFG_DATA_DIR="" CFG_BIND_ADDR=":9092" CFG_CLUSTER_ADDR=":9093" CFG_NODE_ID="" CFG_LOG_LEVEL="info" CFG_SEGMENT_BYTES="67108864" CFG_RETENTION_BYTES="0" CFG_TLS_ENABLED="false" CFG_TLS_CERT_FILE="" CFG_TLS_KEY_FILE="" CFG_ENCRYPTION_ENABLED="false" CFG_ENCRYPTION_KEY="" # Cluster Configuration CFG_DEPLOYMENT_MODE="standalone" # standalone or cluster CFG_CLUSTER_ROLE="bootstrap" # bootstrap (first node) or join CFG_CLUSTER_PEERS="" # comma-separated list of peer addresses CFG_REPLICATION_FACTOR="3" # number of replicas for each partition CFG_MIN_ISR="2" # minimum in-sync replicas # Schema validation CFG_SCHEMA_ENABLED="false" CFG_SCHEMA_VALIDATION="strict" CFG_SCHEMA_REGISTRY_DIR="" # Dead Letter Queue CFG_DLQ_ENABLED="false" CFG_DLQ_MAX_RETRIES="3" CFG_DLQ_RETRY_DELAY="1000" CFG_DLQ_TOPIC_SUFFIX="-dlq" # Message TTL CFG_TTL_DEFAULT="0" CFG_TTL_CLEANUP_INTERVAL="60" # Delayed Message Delivery CFG_DELAYED_ENABLED="false" CFG_DELAYED_MAX_DELAY="604800" # Transaction Support CFG_TXN_ENABLED="false" CFG_TXN_TIMEOUT="60" # Observability - Metrics CFG_METRICS_ENABLED="false" CFG_METRICS_ADDR=":9094" # Observability - Tracing CFG_TRACING_ENABLED="false" CFG_TRACING_ENDPOINT="localhost:4317" CFG_TRACING_SAMPLE_RATE="0.1" # Observability - Health Checks CFG_HEALTH_ENABLED="true" CFG_HEALTH_ADDR=":9095" # Observability - Admin API CFG_ADMIN_ENABLED="false" CFG_ADMIN_ADDR=":9096" # System service installation INSTALL_SYSTEMD="false" INSTALL_LAUNCHD="false" # Detected system info OS="" ARCH="" # ============================================================================= # Colors and Formatting # ============================================================================= if [[ -t 1 ]] && [[ -z "${NO_COLOR:-}" ]]; then readonly COLOR_ENABLED=true else readonly COLOR_ENABLED=false fi if [[ "$COLOR_ENABLED" == true ]]; then readonly RESET='\033[0m' readonly BOLD='\033[1m' readonly DIM='\033[2m' readonly RED='\033[31m' readonly GREEN='\033[32m' readonly YELLOW='\033[33m' readonly CYAN='\033[36m' readonly WHITE='\033[37m' else readonly RESET='' BOLD='' DIM='' RED='' GREEN='' YELLOW='' CYAN='' WHITE='' fi readonly ICON_SUCCESS="✓" readonly ICON_ERROR="✗" readonly ICON_WARNING="⚠" readonly ICON_INFO="ℹ" readonly ICON_ARROW="→" # ============================================================================= # Output Functions # ============================================================================= print_success() { echo -e "${GREEN}${ICON_SUCCESS}${RESET} $1"; } print_error() { echo -e "${RED}${ICON_ERROR}${RESET} ${RED}$1${RESET}" >&2; } print_warning() { echo -e "${YELLOW}${ICON_WARNING}${RESET} $1"; } print_info() { echo -e "${CYAN}${ICON_INFO}${RESET} $1"; } print_step() { echo -e "\n${CYAN}${BOLD}==>${RESET} ${BOLD}$1${RESET}"; } print_substep() { echo -e " ${CYAN}${ICON_ARROW}${RESET} $1"; } print_section() { echo -e "\n ${BOLD}${CYAN}━━━ $1 ━━━${RESET}\n"; } # Spinner for long-running tasks show_spinner() { local pid=$1 local message="$2" local spinner_chars="⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏" local i=0 echo -n " " while kill -0 "$pid" 2>/dev/null; do printf "\r ${CYAN}${spinner_chars:$i:1}${RESET} ${message}..." i=$(( (i + 1) % ${#spinner_chars} )) sleep 0.1 done printf "\r ${GREEN}${ICON_SUCCESS}${RESET} ${message}\n" } # ============================================================================= # Banner - Read from banner.txt # ============================================================================= print_banner() { local banner_file="${SCRIPT_DIR}/internal/banner/banner.txt" echo "" if [[ -f "$banner_file" ]]; then while IFS= read -r line; do # Output line with colors, then reset (avoids backslash escaping RESET) echo -ne "${CYAN}${BOLD}" echo -e "${line}" echo -ne "${RESET}" done < "$banner_file" else echo -e "${CYAN}${BOLD} FlyMQ${RESET}" fi echo "" echo -e " ${BOLD}High-Performance Message Queue${RESET}" echo -e " ${DIM}Version ${FLYMQ_VERSION}${RESET}" echo "" echo -e " ${DIM}Copyright (c) 2026 Firefly Software Solutions Inc.${RESET}" echo -e " ${DIM}Licensed under the Apache License 2.0${RESET}" echo "" } # ============================================================================= # System Detection # ============================================================================= detect_system() { OS=$(uname -s | tr '[:upper:]' '[:lower:]') ARCH=$(uname -m) CFG_NODE_ID=$(hostname) # Detect Windows environments case "$OS" in mingw*|msys*|cygwin*) OS="windows" # Check if running in WSL if grep -qEi "(Microsoft|WSL)" /proc/version 2>/dev/null; then OS="linux" print_info "Detected WSL environment" fi ;; esac case "$ARCH" in x86_64|amd64) ARCH="amd64" ;; aarch64|arm64) ARCH="arm64" ;; i386|i686) ARCH="386" ;; *) print_error "Unsupported architecture: $ARCH"; exit 1 ;; esac case "$OS" in linux|darwin|windows) ;; *) print_error "Unsupported OS: $OS"; exit 1 ;; esac } get_default_prefix() { if [[ "$OS" == "windows" ]]; then echo "$HOME/AppData/Local/FlyMQ" elif [[ $EUID -eq 0 ]]; then echo "/usr/local" else echo "$HOME/.local" fi } get_default_data_dir() { if [[ "$OS" == "windows" ]]; then echo "$HOME/AppData/Local/FlyMQ/data" elif [[ $EUID -eq 0 ]]; then echo "/var/lib/flymq" else echo "$HOME/.local/share/flymq" fi } get_default_config_dir() { if [[ "$OS" == "windows" ]]; then echo "$HOME/AppData/Local/FlyMQ/config" elif [[ $EUID -eq 0 ]]; then echo "/etc/flymq" else echo "$HOME/.config/flymq" fi } # ============================================================================= # Interactive Configuration # ============================================================================= prompt_value() { local prompt="$1" local default="$2" local result echo -en " ${prompt} [${DIM}${default}${RESET}]: " >&2 read -r result &2 else echo -en " ${prompt} [${DIM}y/N${RESET}]: " >&2 fi read -r result max )); then print_warning "Value must be at most $max" continue fi echo "$result" return 0 done } prompt_choice() { local prompt="$1" local default="$2" shift 2 local valid_choices=("$@") local result while true; do result=$(prompt_value "$prompt" "$default") # Check if result is in valid choices for choice in "${valid_choices[@]}"; do if [[ "$result" == "$choice" ]]; then echo "$result" return 0 fi done print_warning "Please enter one of: ${valid_choices[*]}" done } # ============================================================================= # Cluster Mode Configuration # ============================================================================= configure_cluster_mode() { print_section "Cluster Configuration" echo -e " ${DIM}FlyMQ uses Raft consensus for leader election and data replication.${RESET}" echo -e " ${DIM}A cluster requires at least 3 nodes for fault tolerance.${RESET}" echo "" # Determine if this is the first node or joining an existing cluster echo -e " ${BOLD}Cluster Role${RESET}" echo -e " ${DIM}1)${RESET} Bootstrap - First node in a new cluster" echo -e " ${DIM}2)${RESET} Join - Join an existing cluster" echo "" local role_choice role_choice=$(prompt_choice "Cluster role (1=bootstrap, 2=join)" "1" "1" "2" "bootstrap" "join") if [[ "$role_choice" == "2" ]] || [[ "$role_choice" == "join" ]]; then CFG_CLUSTER_ROLE="join" configure_cluster_join else CFG_CLUSTER_ROLE="bootstrap" configure_cluster_bootstrap fi } configure_cluster_bootstrap() { echo "" echo -e " ${BOLD}Bootstrap Node Configuration${RESET}" echo -e " ${DIM}This node will be the initial leader of the cluster.${RESET}" echo -e " ${DIM}Other nodes will join this cluster using this node's address.${RESET}" echo "" # Get the advertised address for this node local default_ip default_ip=$(get_local_ip) echo -e " ${BOLD}Advertised Address${RESET}" echo -e " ${DIM}This is the address other nodes will use to connect to this node.${RESET}" local advertised_host advertised_host=$(prompt_value "Advertised hostname/IP" "$default_ip") CFG_CLUSTER_ADDR="${advertised_host}:9093" echo "" # Replication settings echo -e " ${BOLD}Replication Settings${RESET}" CFG_REPLICATION_FACTOR=$(prompt_number "Replication factor (recommended: 3)" "3" "1") CFG_MIN_ISR=$(prompt_number "Minimum in-sync replicas" "2" "1") echo "" # Expected cluster size local expected_nodes expected_nodes=$(prompt_number "Expected number of nodes in cluster" "3" "1") echo "" print_info "Bootstrap node configured." echo "" echo -e " ${BOLD}Next Steps:${RESET}" echo -e " ${DIM}1. Complete this installation${RESET}" echo -e " ${DIM}2. Start this node: flymq --config /etc/flymq/flymq.conf${RESET}" echo -e " ${DIM}3. On other nodes, run install.sh and choose 'Join' mode${RESET}" echo -e " ${DIM}4. Use this address to join: ${CYAN}${CFG_CLUSTER_ADDR}${RESET}" echo "" } configure_cluster_join() { echo "" echo -e " ${BOLD}Join Existing Cluster${RESET}" echo -e " ${DIM}This node will join an existing FlyMQ cluster.${RESET}" echo "" # Get peer addresses echo -e " ${BOLD}Cluster Peers${RESET}" echo -e " ${DIM}Enter the addresses of existing cluster nodes (comma-separated).${RESET}" echo -e " ${DIM}Example: node1.example.com:9093,node2.example.com:9093${RESET}" echo "" CFG_CLUSTER_PEERS=$(prompt_value "Peer addresses" "") if [[ -z "$CFG_CLUSTER_PEERS" ]]; then print_error "At least one peer address is required to join a cluster." echo -e " ${DIM}You can configure peers later in flymq.conf${RESET}" fi echo "" # Get the advertised address for this node local default_ip default_ip=$(get_local_ip) echo -e " ${BOLD}Advertised Address${RESET}" echo -e " ${DIM}This is the address other nodes will use to connect to this node.${RESET}" local advertised_host advertised_host=$(prompt_value "Advertised hostname/IP" "$default_ip") CFG_CLUSTER_ADDR="${advertised_host}:9093" echo "" # Replication settings (should match the cluster) echo -e " ${BOLD}Replication Settings${RESET}" echo -e " ${DIM}These should match the existing cluster configuration.${RESET}" CFG_REPLICATION_FACTOR=$(prompt_number "Replication factor" "3" "1") CFG_MIN_ISR=$(prompt_number "Minimum in-sync replicas" "2" "1") echo "" print_info "Join configuration complete." echo "" echo -e " ${BOLD}Next Steps:${RESET}" echo -e " ${DIM}1. Complete this installation${RESET}" echo -e " ${DIM}2. Ensure the cluster is running${RESET}" echo -e " ${DIM}3. Start this node: flymq --config /etc/flymq/flymq.conf${RESET}" echo -e " ${DIM}4. This node will automatically join the cluster${RESET}" echo "" } get_local_ip() { # Try to get the primary IP address with timeout protection local ip="" # Check if timeout command exists (GNU coreutils) local use_timeout=false if command -v timeout &> /dev/null; then use_timeout=true elif command -v gtimeout &> /dev/null; then # macOS with GNU coreutils via Homebrew alias timeout='gtimeout' use_timeout=true fi # On macOS, use route and ifconfig if [[ "$OS" == "darwin" ]]; then # Try en0 or en1 directly (most common on macOS) for iface in en0 en1; do ip=$(ifconfig "$iface" 2>/dev/null | awk '/inet / && !/127\.0\.0\.1/ {print $2; exit}' || echo "") [[ -n "$ip" ]] && break done # If still no IP, try the default route interface if [[ -z "$ip" ]]; then local default_if=$(route -n get default 2>/dev/null | awk '/interface:/ {print $2; exit}' || echo "") if [[ -n "$default_if" ]]; then ip=$(ifconfig "$default_if" 2>/dev/null | awk '/inet / && !/127\.0\.0\.1/ {print $2; exit}' || echo "") fi fi # On Linux, try ip command first elif [[ "$OS" == "linux" ]] && command -v ip &> /dev/null; then if [[ "$use_timeout" == true ]]; then ip=$(timeout 2 ip route get 1.1.1.1 2>/dev/null | awk '/src/ {print $7; exit}' || echo "") else ip=$(ip route get 1.1.1.1 2>/dev/null | awk '/src/ {print $7; exit}' || echo "") fi # On Windows (Git Bash/MSYS2) elif [[ "$OS" == "windows" ]]; then # Try ipconfig on Windows ip=$(ipconfig 2>/dev/null | grep -Eo '([0-9]{1,3}\.){3}[0-9]{1,3}' | grep -v '127.0.0.1' | head -1 || echo "") fi # Generic ifconfig fallback (cross-platform) if [[ -z "$ip" ]] || [[ "$ip" == "127."* ]]; then if command -v ifconfig &> /dev/null; then ip=$(ifconfig 2>/dev/null | grep -Eo 'inet (addr:)?([0-9]*\.){3}[0-9]*' | grep -Eo '([0-9]*\.){3}[0-9]*' | grep -v '^127\.' | head -1 || echo "") fi fi # Final fallback to localhost if [[ -z "$ip" ]] || [[ "$ip" == "127."* ]]; then ip="localhost" fi echo "$ip" } format_peers_array() { # Convert comma-separated peers to TOML array format if [[ -z "$CFG_CLUSTER_PEERS" ]]; then echo "" return fi local result="" IFS=',' read -ra peers <<< "$CFG_CLUSTER_PEERS" for i in "${!peers[@]}"; do local peer="${peers[$i]}" peer=$(echo "$peer" | xargs) # trim whitespace if [[ -n "$peer" ]]; then if [[ -n "$result" ]]; then result="${result}, " fi result="${result}\"${peer}\"" fi done echo "$result" } configure_interactive() { echo "" print_step "Configuration" echo "" echo -e " ${DIM}Press Enter to accept defaults shown in brackets.${RESET}" echo -e " ${DIM}Press Ctrl+C at any time to cancel.${RESET}" echo "" # ========================================================================= # Deployment Mode Selection # ========================================================================= print_section "Deployment Mode" echo -e " ${DIM}Choose how you want to deploy FlyMQ:${RESET}" echo -e " ${DIM}1)${RESET} Standalone - Single node deployment (development/testing)" echo -e " ${DIM}2)${RESET} Cluster - Multi-node deployment (production)" echo "" local mode_choice mode_choice=$(prompt_choice "Deployment mode (1=standalone, 2=cluster)" "1" "1" "2" "standalone" "cluster") if [[ "$mode_choice" == "2" ]] || [[ "$mode_choice" == "cluster" ]]; then CFG_DEPLOYMENT_MODE="cluster" configure_cluster_mode else CFG_DEPLOYMENT_MODE="standalone" print_info "Configuring standalone mode..." fi echo "" # Network Configuration (only if not already configured in cluster mode) if [[ "$CFG_DEPLOYMENT_MODE" == "standalone" ]]; then echo "" echo -e " ${BOLD}Network Settings${RESET}" echo "" CFG_BIND_ADDR=$(prompt_value "Client bind address" ":9092") CFG_NODE_ID=$(prompt_value "Node ID" "$CFG_NODE_ID") echo "" fi # Storage Configuration echo -e " ${BOLD}Storage Settings${RESET}" echo "" CFG_DATA_DIR=$(prompt_value "Data directory" "$(get_default_data_dir)") CFG_SEGMENT_BYTES=$(prompt_number "Segment size (bytes)" "67108864" "1024") CFG_RETENTION_BYTES=$(prompt_number "Retention limit (0=unlimited)" "0" "0") echo "" # Logging Configuration echo -e " ${BOLD}Logging Settings${RESET}" echo -e " ${DIM}Available levels: debug, info, warn, error${RESET}" echo "" CFG_LOG_LEVEL=$(prompt_choice "Log level" "info" "debug" "info" "warn" "error") echo "" # Security Configuration echo -e " ${BOLD}Security Settings${RESET}" echo "" if prompt_yes_no "Enable TLS encryption" "n"; then CFG_TLS_ENABLED="true" CFG_TLS_CERT_FILE=$(prompt_value "TLS certificate file" "") CFG_TLS_KEY_FILE=$(prompt_value "TLS key file" "") if [[ -z "$CFG_TLS_CERT_FILE" ]] || [[ -z "$CFG_TLS_KEY_FILE" ]]; then print_warning "TLS files not specified. You can configure them later in flymq.conf" fi fi echo "" if prompt_yes_no "Enable data-at-rest encryption" "n"; then CFG_ENCRYPTION_ENABLED="true" CFG_ENCRYPTION_KEY=$(prompt_value "Encryption key (leave empty to auto-generate)" "") if [[ -z "$CFG_ENCRYPTION_KEY" ]]; then CFG_ENCRYPTION_KEY=$(openssl rand -hex 32 2>/dev/null || head -c 32 /dev/urandom | xxd -p -c 64) print_info "Generated encryption key (save this securely)" fi fi echo "" # ========================================================================= # Advanced Features # ========================================================================= print_section "Advanced Features" echo -e " ${DIM}Configure optional advanced features for enhanced functionality.${RESET}" echo "" # Schema Validation echo -e " ${BOLD}Schema Validation${RESET}" echo -e " ${DIM}Validate messages against JSON Schema, Avro, or Protobuf schemas${RESET}" if prompt_yes_no "Enable schema validation" "n"; then CFG_SCHEMA_ENABLED="true" echo -e " ${DIM}Validation modes: strict (reject invalid), lenient (warn only), none${RESET}" CFG_SCHEMA_VALIDATION=$(prompt_choice "Validation mode" "strict" "strict" "lenient" "none") CFG_SCHEMA_REGISTRY_DIR=$(prompt_value "Schema registry directory" "${CFG_DATA_DIR}/schemas") fi echo "" # Dead Letter Queue echo -e " ${BOLD}Dead Letter Queue (DLQ)${RESET}" echo -e " ${DIM}Route failed messages to a separate queue for later analysis${RESET}" if prompt_yes_no "Enable dead letter queues" "n"; then CFG_DLQ_ENABLED="true" CFG_DLQ_MAX_RETRIES=$(prompt_number "Max retry attempts before DLQ" "3" "1") CFG_DLQ_RETRY_DELAY=$(prompt_number "Retry delay (milliseconds)" "1000" "100") CFG_DLQ_TOPIC_SUFFIX=$(prompt_value "DLQ topic suffix" "-dlq") fi echo "" # Message TTL echo -e " ${BOLD}Message TTL (Time-To-Live)${RESET}" echo -e " ${DIM}Automatically expire messages after a specified time${RESET}" CFG_TTL_DEFAULT=$(prompt_number "Default TTL in seconds (0=no expiry)" "0" "0") if [[ "$CFG_TTL_DEFAULT" != "0" ]]; then CFG_TTL_CLEANUP_INTERVAL=$(prompt_number "Cleanup interval (seconds)" "60" "1") fi echo "" # Delayed Message Delivery echo -e " ${BOLD}Delayed Message Delivery${RESET}" echo -e " ${DIM}Schedule messages for future delivery${RESET}" if prompt_yes_no "Enable delayed message delivery" "n"; then CFG_DELAYED_ENABLED="true" CFG_DELAYED_MAX_DELAY=$(prompt_number "Maximum delay (seconds)" "604800" "1") print_info "Max delay: $(( CFG_DELAYED_MAX_DELAY / 86400 )) days" fi echo "" # Transaction Support echo -e " ${BOLD}Transaction Support${RESET}" echo -e " ${DIM}Enable exactly-once semantics with atomic message operations${RESET}" if prompt_yes_no "Enable transaction support" "n"; then CFG_TXN_ENABLED="true" CFG_TXN_TIMEOUT=$(prompt_number "Transaction timeout (seconds)" "60" "1") fi echo "" # ========================================================================= # Observability Features # ========================================================================= print_section "Observability Features" echo -e " ${DIM}Enable monitoring and debugging capabilities.${RESET}" echo "" # Prometheus Metrics echo -e " ${BOLD}Prometheus Metrics${RESET}" echo -e " ${DIM}Expose metrics for monitoring throughput, latency, and errors${RESET}" if prompt_yes_no "Enable Prometheus metrics endpoint" "y"; then CFG_METRICS_ENABLED="true" CFG_METRICS_ADDR=$(prompt_value "Metrics HTTP address" ":9094") fi echo "" # OpenTelemetry Tracing echo -e " ${BOLD}OpenTelemetry Tracing${RESET}" echo -e " ${DIM}Distributed tracing for debugging and performance analysis${RESET}" if prompt_yes_no "Enable OpenTelemetry tracing" "n"; then CFG_TRACING_ENABLED="true" CFG_TRACING_ENDPOINT=$(prompt_value "OTLP endpoint" "localhost:4317") CFG_TRACING_SAMPLE_RATE=$(prompt_value "Sample rate (0.0-1.0)" "0.1") fi echo "" # Health Check Endpoints echo -e " ${BOLD}Health Check Endpoints${RESET}" echo -e " ${DIM}Kubernetes-compatible liveness and readiness probes${RESET}" if prompt_yes_no "Enable health check endpoints" "y"; then CFG_HEALTH_ENABLED="true" CFG_HEALTH_ADDR=$(prompt_value "Health check HTTP address" ":9095") fi echo "" # Admin API echo -e " ${BOLD}Admin REST API${RESET}" echo -e " ${DIM}REST API for cluster management, topic operations, and monitoring${RESET}" if prompt_yes_no "Enable Admin REST API" "n"; then CFG_ADMIN_ENABLED="true" CFG_ADMIN_ADDR=$(prompt_value "Admin API HTTP address" ":9096") fi echo "" # ========================================================================= # System Integration # ========================================================================= # System Integration - Platform specific if [[ "$OS" == "linux" ]] && command -v systemctl &> /dev/null; then print_section "System Integration" echo -e " ${BOLD}Systemd Service${RESET}" echo -e " ${DIM}Install systemd service for automatic startup and management${RESET}" if prompt_yes_no "Install systemd service" "y"; then INSTALL_SYSTEMD="true" fi echo "" elif [[ "$OS" == "darwin" ]]; then print_section "System Integration" echo -e " ${BOLD}Launch Agent (macOS)${RESET}" echo -e " ${DIM}Install launchd service for automatic startup${RESET}" if prompt_yes_no "Install Launch Agent" "n"; then INSTALL_LAUNCHD="true" fi echo "" elif [[ "$OS" == "windows" ]]; then print_section "System Integration" echo -e " ${BOLD}Windows Service${RESET}" echo -e " ${DIM}Note: Windows service installation requires additional tools (NSSM or sc.exe)${RESET}" print_info "See documentation for Windows service setup" echo "" fi } # ============================================================================= # TOML Configuration Generation # ============================================================================= generate_config() { local config_dir="$1" local config_file="$config_dir/flymq.conf" print_step "Generating configuration" echo "" mkdir -p "$config_dir" cat > "$config_file" << EOF # FlyMQ Configuration File # Copyright (c) 2026 Firefly Software Solutions Inc. # Generated by install.sh on $(date -u +"%Y-%m-%dT%H:%M:%SZ") # ============================================================================= # Network Configuration # ============================================================================= # Address to listen for client connections bind_addr = "${CFG_BIND_ADDR}" # Address for cluster communication cluster_addr = "${CFG_CLUSTER_ADDR}" # Unique identifier for this node node_id = "${CFG_NODE_ID}" # ============================================================================= # Cluster Configuration # ============================================================================= [cluster] # Deployment mode: standalone or cluster mode = "${CFG_DEPLOYMENT_MODE}" # Cluster role: bootstrap (first node) or join role = "${CFG_CLUSTER_ROLE}" # List of peer nodes for clustering # Format: ["host1:port", "host2:port"] peers = [$(format_peers_array)] # Replication factor for partitions (cluster mode only) replication_factor = ${CFG_REPLICATION_FACTOR} # Minimum in-sync replicas required for writes min_isr = ${CFG_MIN_ISR} # ============================================================================= # Storage Configuration # ============================================================================= # Directory for data storage data_dir = "${CFG_DATA_DIR}" # Maximum size of each log segment in bytes (default: 64MB) segment_bytes = ${CFG_SEGMENT_BYTES} # Maximum bytes to retain per topic/partition (0 = unlimited) retention_bytes = ${CFG_RETENTION_BYTES} # ============================================================================= # Logging Configuration # ============================================================================= # Log level: debug, info, warn, error log_level = "${CFG_LOG_LEVEL}" # Output logs in JSON format log_json = false # ============================================================================= # Security Configuration # ============================================================================= [security] # Enable TLS for client connections tls_enabled = ${CFG_TLS_ENABLED} # Path to TLS certificate file tls_cert_file = "${CFG_TLS_CERT_FILE}" # Path to TLS private key file tls_key_file = "${CFG_TLS_KEY_FILE}" # Enable data-at-rest encryption encryption_enabled = ${CFG_ENCRYPTION_ENABLED} # AES-256 encryption key (hex-encoded, 64 characters) # IMPORTANT: Keep this key secure and backed up! encryption_key = "${CFG_ENCRYPTION_KEY}" # ============================================================================= # Advanced Configuration # ============================================================================= [advanced] # Maximum message size in bytes (default: 32MB) max_message_size = 33554432 # Connection idle timeout in seconds idle_timeout = 300 # Enable fsync after each write (slower but more durable) sync_writes = true # ============================================================================= # Schema Validation # ============================================================================= [schema] # Enable schema validation for messages enabled = ${CFG_SCHEMA_ENABLED} # Validation mode: strict (reject invalid), lenient (warn only), none validation = "${CFG_SCHEMA_VALIDATION}" # Directory for schema registry storage registry_dir = "${CFG_SCHEMA_REGISTRY_DIR:-${CFG_DATA_DIR}/schemas}" # ============================================================================= # Dead Letter Queue # ============================================================================= [dlq] # Enable dead letter queues for failed messages enabled = ${CFG_DLQ_ENABLED} # Maximum retry attempts before routing to DLQ max_retries = ${CFG_DLQ_MAX_RETRIES} # Delay between retry attempts (milliseconds) retry_delay = ${CFG_DLQ_RETRY_DELAY} # Suffix appended to topic names for DLQ topics topic_suffix = "${CFG_DLQ_TOPIC_SUFFIX}" # ============================================================================= # Message TTL # ============================================================================= [ttl] # Default message TTL in seconds (0 = no expiry) default_ttl = ${CFG_TTL_DEFAULT} # Interval for expired message cleanup (seconds) cleanup_interval = ${CFG_TTL_CLEANUP_INTERVAL} # ============================================================================= # Delayed Message Delivery # ============================================================================= [delayed] # Enable delayed/scheduled message delivery enabled = ${CFG_DELAYED_ENABLED} # Maximum allowed delay in seconds (default: 7 days) max_delay = ${CFG_DELAYED_MAX_DELAY} # ============================================================================= # Transaction Support # ============================================================================= [transaction] # Enable transaction support for exactly-once semantics enabled = ${CFG_TXN_ENABLED} # Transaction timeout in seconds timeout = ${CFG_TXN_TIMEOUT} # ============================================================================= # Observability - Prometheus Metrics # ============================================================================= [observability.metrics] # Enable Prometheus metrics endpoint enabled = ${CFG_METRICS_ENABLED} # HTTP address for metrics endpoint addr = "${CFG_METRICS_ADDR}" # ============================================================================= # Observability - OpenTelemetry Tracing # ============================================================================= [observability.tracing] # Enable OpenTelemetry distributed tracing enabled = ${CFG_TRACING_ENABLED} # OTLP exporter endpoint endpoint = "${CFG_TRACING_ENDPOINT}" # Sampling rate (0.0 to 1.0) sample_rate = ${CFG_TRACING_SAMPLE_RATE} # ============================================================================= # Observability - Health Checks # ============================================================================= [observability.health] # Enable health check endpoints enabled = ${CFG_HEALTH_ENABLED} # HTTP address for health endpoints addr = "${CFG_HEALTH_ADDR}" # ============================================================================= # Observability - Admin API # ============================================================================= [observability.admin] # Enable Admin REST API enabled = ${CFG_ADMIN_ENABLED} # HTTP address for Admin API addr = "${CFG_ADMIN_ADDR}" EOF print_success "Generated: ${CYAN}$config_file${RESET}" # Create schema registry directory if schema validation is enabled if [[ "$CFG_SCHEMA_ENABLED" == "true" ]]; then local schema_dir="${CFG_SCHEMA_REGISTRY_DIR:-${CFG_DATA_DIR}/schemas}" mkdir -p "$schema_dir" print_success "Created schema registry: ${CYAN}$schema_dir${RESET}" fi } # ============================================================================= # Installation # ============================================================================= check_dependencies() { print_step "Checking dependencies" echo "" if ! command -v go &> /dev/null; then print_error "Go is not installed. Please install Go 1.21+ first." print_info "Visit: ${CYAN}https://go.dev/dl/${RESET}" exit 1 fi GO_VERSION=$(go version | grep -oE 'go[0-9]+\.[0-9]+' | sed 's/go//') print_success "Go $GO_VERSION found" } build_binaries() { print_step "Building FlyMQ" echo "" print_info "Target: ${CYAN}${OS}/${ARCH}${RESET}" echo "" # Create bin directory mkdir -p bin # Set binary extensions for Windows local server_bin="bin/flymq" local cli_bin="bin/flymq-cli" if [[ "$OS" == "windows" ]]; then server_bin="bin/flymq.exe" cli_bin="bin/flymq-cli.exe" fi # Build server echo -n " " if ! GOOS="${OS}" GOARCH="${ARCH}" go build -o "$server_bin" cmd/flymq/main.go 2>&1; then print_error "Failed to build flymq server" exit 1 fi print_success "Built flymq server" # Build CLI echo -n " " if ! GOOS="${OS}" GOARCH="${ARCH}" go build -o "$cli_bin" cmd/flymq-cli/main.go 2>&1; then print_error "Failed to build flymq-cli" exit 1 fi print_success "Built flymq-cli" } install_binaries() { local prefix="$1" local bin_dir="$prefix/bin" print_step "Installing binaries" echo "" print_info "Install location: ${CYAN}$bin_dir${RESET}" echo "" mkdir -p "$bin_dir" # Set binary names based on OS local server_src="bin/flymq" local cli_src="bin/flymq-cli" local server_dst="$bin_dir/flymq" local cli_dst="$bin_dir/flymq-cli" if [[ "$OS" == "windows" ]]; then server_src="bin/flymq.exe" cli_src="bin/flymq-cli.exe" server_dst="$bin_dir/flymq.exe" cli_dst="$bin_dir/flymq-cli.exe" fi cp "$server_src" "$server_dst" [[ "$OS" != "windows" ]] && chmod +x "$server_dst" print_success "Installed flymq" cp "$cli_src" "$cli_dst" [[ "$OS" != "windows" ]] && chmod +x "$cli_dst" print_success "Installed flymq-cli" # On Windows, add a note about PATH if [[ "$OS" == "windows" ]]; then echo "" print_warning "Windows detected: You may need to add the bin directory to your PATH manually" print_info "Add this to your environment variables: ${CYAN}$bin_dir${RESET}" fi } create_data_dir() { print_step "Creating data directory" echo "" mkdir -p "$CFG_DATA_DIR" print_success "Created: ${CYAN}$CFG_DATA_DIR${RESET}" } install_system_service() { local config_dir="$1" local prefix="$2" # Linux systemd if [[ "$OS" == "linux" ]] && [[ "$INSTALL_SYSTEMD" == "true" ]] && command -v systemctl &> /dev/null; then print_step "Installing systemd service" echo "" local service_file local service_name if [[ "$CFG_DEPLOYMENT_MODE" == "cluster" ]]; then service_file="deploy/systemd/flymq-cluster@.service" service_name="flymq-cluster@${CFG_NODE_ID}" else service_file="deploy/systemd/flymq.service" service_name="flymq" fi if [[ ! -f "$service_file" ]]; then print_warning "Systemd service file not found: $service_file" return 1 fi # Copy service file if [[ "$CFG_DEPLOYMENT_MODE" == "cluster" ]]; then sudo cp "$service_file" /etc/systemd/system/flymq-cluster@.service else sudo cp "$service_file" /etc/systemd/system/flymq.service fi # Reload systemd sudo systemctl daemon-reload print_success "Installed systemd service: ${CYAN}$service_name${RESET}" echo "" print_info "Enable: ${CYAN}sudo systemctl enable $service_name${RESET}" print_info "Start: ${CYAN}sudo systemctl start $service_name${RESET}" # macOS launchd elif [[ "$OS" == "darwin" ]] && [[ "$INSTALL_LAUNCHD" == "true" ]]; then print_step "Installing Launch Agent" echo "" local launch_agents_dir="$HOME/Library/LaunchAgents" local plist_file="$launch_agents_dir/com.firefly.flymq.plist" mkdir -p "$launch_agents_dir" # Generate plist file cat > "$plist_file" << EOF Label com.firefly.flymq ProgramArguments $prefix/bin/flymq --config $config_dir/flymq.conf RunAtLoad KeepAlive StandardOutPath $HOME/Library/Logs/flymq.log StandardErrorPath $HOME/Library/Logs/flymq.error.log EOF print_success "Installed Launch Agent: ${CYAN}$plist_file${RESET}" echo "" print_info "Load: ${CYAN}launchctl load $plist_file${RESET}" print_info "Unload: ${CYAN}launchctl unload $plist_file${RESET}" fi } print_post_install() { local prefix="$1" local config_dir="$2" local bin_dir="$prefix/bin" echo "" echo -e "${GREEN}${BOLD}════════════════════════════════════════════════════════════════${RESET}" print_success "FlyMQ v${FLYMQ_VERSION} installed successfully!" echo -e "${GREEN}${BOLD}════════════════════════════════════════════════════════════════${RESET}" echo "" echo -e "${BOLD}Installation Summary:${RESET}" echo -e " ${ICON_ARROW} Binaries: ${CYAN}$bin_dir${RESET}" echo -e " ${ICON_ARROW} Configuration: ${CYAN}$config_dir/flymq.conf${RESET}" echo -e " ${ICON_ARROW} Data: ${CYAN}$CFG_DATA_DIR${RESET}" echo "" # Show enabled features echo -e "${BOLD}Enabled Features:${RESET}" echo -e " ${ICON_ARROW} TLS Encryption: ${CYAN}$CFG_TLS_ENABLED${RESET}" echo -e " ${ICON_ARROW} Data Encryption: ${CYAN}$CFG_ENCRYPTION_ENABLED${RESET}" echo -e " ${ICON_ARROW} Schema Validation: ${CYAN}$CFG_SCHEMA_ENABLED${RESET}" echo -e " ${ICON_ARROW} Dead Letter Queue: ${CYAN}$CFG_DLQ_ENABLED${RESET}" echo -e " ${ICON_ARROW} Delayed Delivery: ${CYAN}$CFG_DELAYED_ENABLED${RESET}" echo -e " ${ICON_ARROW} Transactions: ${CYAN}$CFG_TXN_ENABLED${RESET}" echo -e " ${ICON_ARROW} Prometheus Metrics: ${CYAN}$CFG_METRICS_ENABLED${RESET}" echo -e " ${ICON_ARROW} Health Checks: ${CYAN}$CFG_HEALTH_ENABLED${RESET}" echo -e " ${ICON_ARROW} Admin API: ${CYAN}$CFG_ADMIN_ENABLED${RESET}" echo "" echo -e "${BOLD}Quick Start:${RESET}" echo "" if [[ "$CFG_DEPLOYMENT_MODE" == "cluster" ]]; then if [[ "$CFG_CLUSTER_ROLE" == "bootstrap" ]]; then echo " ${BOLD}Bootstrap Node (First Node):${RESET}" echo "" echo " 1. Start this node:" echo -e " ${CYAN}flymq --config $config_dir/flymq.conf${RESET}" echo "" echo " 2. On other nodes, run install.sh and choose 'Join' mode" echo " Use this address to join: ${CYAN}${CFG_CLUSTER_ADDR}${RESET}" echo "" echo " 3. Once all nodes are running, check cluster status:" echo -e " ${CYAN}flymq-cli cluster status${RESET}" echo "" else echo " ${BOLD}Joining Node:${RESET}" echo "" echo " 1. Ensure the cluster is running" echo "" echo " 2. Start this node:" echo -e " ${CYAN}flymq --config $config_dir/flymq.conf${RESET}" echo "" echo " 3. This node will automatically join the cluster" echo "" echo " 4. Check cluster status:" echo -e " ${CYAN}flymq-cli cluster status${RESET}" echo "" fi else echo " 1. Start the server:" echo -e " ${CYAN}flymq --config $config_dir/flymq.conf${RESET}" echo "" fi echo " 2. Produce a message:" echo -e " ${CYAN}flymq-cli produce my-topic \"Hello World\"${RESET}" echo "" echo " 3. Subscribe to messages:" echo -e " ${CYAN}flymq-cli subscribe my-topic --from earliest${RESET}" echo "" echo " 4. List topics:" echo -e " ${CYAN}flymq-cli topics${RESET}" echo "" # Show advanced feature examples if enabled if [[ "$CFG_SCHEMA_ENABLED" == "true" ]] || [[ "$CFG_DLQ_ENABLED" == "true" ]] || \ [[ "$CFG_DELAYED_ENABLED" == "true" ]] || [[ "$CFG_TXN_ENABLED" == "true" ]]; then echo -e "${BOLD}Advanced Feature Examples:${RESET}" echo "" if [[ "$CFG_SCHEMA_ENABLED" == "true" ]]; then echo " # Register a JSON schema" echo -e " ${CYAN}flymq-cli schema register user-schema json '{\"type\":\"object\"}'${RESET}" echo "" fi if [[ "$CFG_DLQ_ENABLED" == "true" ]]; then echo " # View dead letter queue messages" echo -e " ${CYAN}flymq-cli dlq list my-topic${RESET}" echo "" fi if [[ "$CFG_DELAYED_ENABLED" == "true" ]]; then echo " # Send a delayed message (5 second delay)" echo -e " ${CYAN}flymq-cli produce-delayed my-topic \"Delayed message\" 5000${RESET}" echo "" fi if [[ "$CFG_TXN_ENABLED" == "true" ]]; then echo " # Send messages in a transaction" echo -e " ${CYAN}flymq-cli txn my-topic \"Message 1\" \"Message 2\"${RESET}" echo "" fi fi # Show observability endpoints if enabled if [[ "$CFG_METRICS_ENABLED" == "true" ]] || [[ "$CFG_HEALTH_ENABLED" == "true" ]] || \ [[ "$CFG_ADMIN_ENABLED" == "true" ]]; then echo -e "${BOLD}Observability Endpoints:${RESET}" echo "" if [[ "$CFG_METRICS_ENABLED" == "true" ]]; then echo -e " ${ICON_ARROW} Prometheus Metrics: ${CYAN}http://localhost${CFG_METRICS_ADDR}/metrics${RESET}" fi if [[ "$CFG_HEALTH_ENABLED" == "true" ]]; then echo -e " ${ICON_ARROW} Health Check: ${CYAN}http://localhost${CFG_HEALTH_ADDR}/health${RESET}" echo -e " ${ICON_ARROW} Liveness Probe: ${CYAN}http://localhost${CFG_HEALTH_ADDR}/health/live${RESET}" echo -e " ${ICON_ARROW} Readiness Probe: ${CYAN}http://localhost${CFG_HEALTH_ADDR}/health/ready${RESET}" fi if [[ "$CFG_ADMIN_ENABLED" == "true" ]]; then echo -e " ${ICON_ARROW} Admin API: ${CYAN}http://localhost${CFG_ADMIN_ADDR}/api/v1${RESET}" fi echo "" fi # Check if bin_dir is in PATH if [[ ":$PATH:" != *":$bin_dir:"* ]]; then print_warning "Add $bin_dir to your PATH:" echo "" echo -e " ${CYAN}export PATH=\"$bin_dir:\$PATH\"${RESET}" echo "" fi if [[ "$CFG_ENCRYPTION_ENABLED" == "true" ]]; then print_warning "IMPORTANT: Save your encryption key securely!" echo -e " ${DIM}Key: $CFG_ENCRYPTION_KEY${RESET}" echo "" fi echo -e "${DIM}For more information, visit: https://github.com/firefly-oss/flymq${RESET}" echo "" } # ============================================================================= # Uninstallation # ============================================================================= uninstall() { print_step "Uninstalling FlyMQ..." local prefix="${PREFIX:-$(get_default_prefix)}" local bin_dir="$prefix/bin" local config_dir="$(get_default_config_dir)" if [[ -f "$bin_dir/flymq" ]]; then rm -f "$bin_dir/flymq" print_success "Removed flymq" fi if [[ -f "$bin_dir/flymq-cli" ]]; then rm -f "$bin_dir/flymq-cli" print_success "Removed flymq-cli" fi print_success "FlyMQ uninstalled" print_info "Configuration and data directories were not removed." print_info "Remove manually if needed:" echo -e " ${DIM}rm -rf $config_dir${RESET}" echo -e " ${DIM}rm -rf $(get_default_data_dir)${RESET}" } # ============================================================================= # Signal Handling # ============================================================================= cleanup() { echo "" print_warning "Installation cancelled by user" exit 130 } trap cleanup SIGINT SIGTERM # ============================================================================= # Main # ============================================================================= parse_args() { while [[ $# -gt 0 ]]; do case "$1" in --prefix) PREFIX="$2" shift 2 ;; --yes|-y) AUTO_CONFIRM=true shift ;; --config-file) CONFIG_FILE="$2" AUTO_CONFIRM=true shift 2 ;; --uninstall) UNINSTALL=true shift ;; --help|-h) print_banner echo "Usage: $0 [OPTIONS]" echo "" echo "Options:" echo " --prefix PATH Installation prefix (default: ~/.local or /usr/local)" echo " --yes, -y Non-interactive mode with default configuration" echo " --config-file FILE Use existing configuration file (implies --yes)" echo " --uninstall Uninstall FlyMQ" echo " --help, -h Show this help" echo "" echo "Examples:" echo " ./install.sh # Interactive installation" echo " ./install.sh --yes # Quick install with defaults" echo " ./install.sh --config-file my.conf # Use existing config" echo " ./install.sh --prefix /opt/flymq # Custom install location" exit 0 ;; *) print_error "Unknown option: $1" exit 1 ;; esac done } main() { parse_args "$@" print_banner detect_system if [[ "$UNINSTALL" == true ]]; then uninstall exit 0 fi print_info "Detected: $OS/$ARCH" # Set default prefix if not specified if [[ -z "$PREFIX" ]]; then PREFIX=$(get_default_prefix) fi # Set default data dir CFG_DATA_DIR=$(get_default_data_dir) local config_dir=$(get_default_config_dir) # Interactive configuration or use defaults if [[ "$AUTO_CONFIRM" != true ]]; then echo "" echo -e " ${BOLD}Welcome to the FlyMQ installer!${RESET}" echo -e " ${DIM}This wizard will guide you through the installation process.${RESET}" echo "" if prompt_yes_no "Run interactive configuration" "y"; then configure_interactive else print_info "Using default configuration" fi echo "" echo -e " ${BOLD}Installation Summary:${RESET}" echo -e " ${ICON_ARROW} Install prefix: ${CYAN}$PREFIX${RESET}" echo -e " ${ICON_ARROW} Config dir: ${CYAN}$config_dir${RESET}" echo -e " ${ICON_ARROW} Data dir: ${CYAN}$CFG_DATA_DIR${RESET}" echo -e " ${ICON_ARROW} Bind address: ${CYAN}$CFG_BIND_ADDR${RESET}" echo "" echo -e " ${BOLD}Deployment:${RESET}" echo -e " ${ICON_ARROW} Mode: ${CYAN}$CFG_DEPLOYMENT_MODE${RESET}" if [[ "$CFG_DEPLOYMENT_MODE" == "cluster" ]]; then echo -e " ${ICON_ARROW} Role: ${CYAN}$CFG_CLUSTER_ROLE${RESET}" echo -e " ${ICON_ARROW} Cluster addr: ${CYAN}$CFG_CLUSTER_ADDR${RESET}" if [[ -n "$CFG_CLUSTER_PEERS" ]]; then echo -e " ${ICON_ARROW} Peers: ${CYAN}$CFG_CLUSTER_PEERS${RESET}" fi echo -e " ${ICON_ARROW} Replication: ${CYAN}$CFG_REPLICATION_FACTOR${RESET}" echo -e " ${ICON_ARROW} Min ISR: ${CYAN}$CFG_MIN_ISR${RESET}" fi echo "" echo -e " ${BOLD}Security:${RESET}" echo -e " ${ICON_ARROW} TLS enabled: ${CYAN}$CFG_TLS_ENABLED${RESET}" echo -e " ${ICON_ARROW} Encryption: ${CYAN}$CFG_ENCRYPTION_ENABLED${RESET}" echo "" echo -e " ${BOLD}Advanced Features:${RESET}" echo -e " ${ICON_ARROW} Schema Validation: ${CYAN}$CFG_SCHEMA_ENABLED${RESET}" echo -e " ${ICON_ARROW} Dead Letter Queue: ${CYAN}$CFG_DLQ_ENABLED${RESET}" echo -e " ${ICON_ARROW} Delayed Delivery: ${CYAN}$CFG_DELAYED_ENABLED${RESET}" echo -e " ${ICON_ARROW} Transactions: ${CYAN}$CFG_TXN_ENABLED${RESET}" echo "" echo -e " ${BOLD}Observability:${RESET}" echo -e " ${ICON_ARROW} Metrics (${CFG_METRICS_ADDR}): ${CYAN}$CFG_METRICS_ENABLED${RESET}" echo -e " ${ICON_ARROW} Health (${CFG_HEALTH_ADDR}): ${CYAN}$CFG_HEALTH_ENABLED${RESET}" echo -e " ${ICON_ARROW} Admin API (${CFG_ADMIN_ADDR}): ${CYAN}$CFG_ADMIN_ENABLED${RESET}" echo "" if ! prompt_yes_no "Proceed with installation" "y"; then print_info "Installation cancelled" exit 0 fi fi echo "" check_dependencies build_binaries install_binaries "$PREFIX" create_data_dir generate_config "$config_dir" install_system_service "$config_dir" "$PREFIX" print_post_install "$PREFIX" "$config_dir" } main "$@"