#!/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.11" readonly FLYMQ_VERSION="${FLYMQ_VERSION:-1.26.11}" readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)" # Installation options PREFIX="" AUTO_CONFIRM=false UNINSTALL=false CONFIG_FILE="" UPGRADE=false FORCE_REINSTALL=false # ============================================================================= # Smart Defaults - Production-Ready Out of the Box # ============================================================================= # These defaults provide a secure, feature-rich standalone deployment. # Users can customize by section if needed. # Configuration values (will be set during interactive setup) CFG_DATA_DIR="" CFG_CONFIG_DIR="" # Track detected config directory CFG_BIND_ADDR=":9092" CFG_CLUSTER_ADDR=":9093" CFG_NODE_ID="" CFG_LOG_LEVEL="info" CFG_SEGMENT_BYTES="67108864" CFG_RETENTION_BYTES="0" # TLS/SSL - ENABLED by default with auto-generated self-signed certificate CFG_TLS_ENABLED="true" CFG_TLS_CERT_FILE="" CFG_TLS_KEY_FILE="" # Data-at-Rest Encryption - ENABLED by default (auto-generated key) CFG_ENCRYPTION_ENABLED="true" CFG_ENCRYPTION_KEY="" # Cluster Configuration - Standalone by default CFG_DEPLOYMENT_MODE="standalone" # standalone or cluster CFG_CLUSTER_PEERS="" # comma-separated list of peer addresses CFG_ADVERTISE_CLUSTER="" # advertised cluster address CFG_REPLICATION_FACTOR="1" # number of replicas for data CFG_CLUSTER_ENABLED="false" # whether clustering is enabled # Performance Configuration CFG_ACKS="leader" # Durability mode: all, leader, none CFG_DEFAULT_SERDE="binary" # Default SerDe: binary, json, string, avro, protobuf # Schema validation - ENABLED by default (Server-side validation) CFG_SCHEMA_ENABLED="true" CFG_SCHEMA_VALIDATION="strict" CFG_SCHEMA_REGISTRY_DIR="" # Dead Letter Queue - ENABLED by default CFG_DLQ_ENABLED="true" CFG_DLQ_MAX_RETRIES="3" CFG_DLQ_RETRY_DELAY="1000" CFG_DLQ_TOPIC_SUFFIX="-dlq" # Message TTL - ENABLED by default (7 days) CFG_TTL_DEFAULT="604800" CFG_TTL_CLEANUP_INTERVAL="60" # Delayed Message Delivery - ENABLED by default CFG_DELAYED_ENABLED="true" CFG_DELAYED_MAX_DELAY="604800" # Transaction Support - ENABLED by default CFG_TXN_ENABLED="true" CFG_TXN_TIMEOUT="60" # Observability - Metrics - ENABLED by default CFG_METRICS_ENABLED="true" CFG_METRICS_ADDR=":9094" # Observability - Tracing - ENABLED by default CFG_TRACING_ENABLED="true" CFG_TRACING_ENDPOINT="localhost:4317" CFG_TRACING_SAMPLE_RATE="0.1" # Observability - Health Checks - ENABLED by default CFG_HEALTH_ENABLED="true" CFG_HEALTH_ADDR=":9095" CFG_HEALTH_TLS_ENABLED="false" CFG_HEALTH_TLS_CERT_FILE="" CFG_HEALTH_TLS_KEY_FILE="" CFG_HEALTH_TLS_AUTO_GENERATE="false" CFG_HEALTH_TLS_USE_ADMIN_CERT="false" # Observability - Admin API - ENABLED by default CFG_ADMIN_ENABLED="true" CFG_ADMIN_ADDR=":9096" CFG_ADMIN_TLS_ENABLED="false" CFG_ADMIN_TLS_CERT_FILE="" CFG_ADMIN_TLS_KEY_FILE="" CFG_ADMIN_TLS_AUTO_GENERATE="false" # gRPC API - ENABLED by default CFG_GRPC_ENABLED="true" CFG_GRPC_ADDR=":9097" # WebSocket Gateway - ENABLED by default CFG_WS_ENABLED="true" CFG_WS_ADDR=":9098" # MQTT Bridge - ENABLED by default CFG_MQTT_ENABLED="true" CFG_MQTT_ADDR=":1883" # Bridge TLS configuration CFG_GRPC_TLS_ENABLED="false" CFG_GRPC_TLS_USE_GLOBAL="true" CFG_GRPC_TLS_CERT="" CFG_GRPC_TLS_KEY="" CFG_WS_TLS_ENABLED="false" CFG_WS_TLS_USE_GLOBAL="true" CFG_WS_TLS_CERT="" CFG_WS_TLS_KEY="" CFG_MQTT_TLS_ENABLED="false" CFG_MQTT_TLS_USE_GLOBAL="true" CFG_MQTT_TLS_CERT="" CFG_MQTT_TLS_KEY="" # Authentication - ENABLED by default (auto-generated credentials) CFG_AUTH_ENABLED="true" CFG_AUTH_ADMIN_USER="admin" CFG_AUTH_ADMIN_PASS="" CFG_AUTH_ALLOW_ANONYMOUS="true" # Allow anonymous connections to public topics CFG_AUTH_DEFAULT_PUBLIC="false" # Topics are private by default (secure) # Partition Management (Horizontal Scaling) - Sensible defaults CFG_PARTITION_DISTRIBUTION_STRATEGY="round-robin" CFG_PARTITION_DEFAULT_REPLICATION_FACTOR="1" CFG_PARTITION_DEFAULT_PARTITIONS="1" CFG_PARTITION_AUTO_REBALANCE_ENABLED="false" CFG_PARTITION_AUTO_REBALANCE_INTERVAL="300" CFG_PARTITION_REBALANCE_THRESHOLD="0.2" # Service Discovery - Enable for cluster auto-discovery CFG_DISCOVERY_ENABLED="false" CFG_DISCOVERY_CLUSTER_ID="" # Audit Trail - ENABLED by default for security compliance CFG_AUDIT_ENABLED="true" CFG_AUDIT_LOG_DIR="" # Default: data_dir/audit CFG_AUDIT_MAX_FILE_SIZE="104857600" # 100MB CFG_AUDIT_RETENTION_DAYS="90" # System service installation INSTALL_SYSTEMD="false" INSTALL_LAUNCHD="false" # Detected system info OS="" ARCH="" # Track if values were user-provided USER_PROVIDED_PASSWORD=false USER_PROVIDED_ENCRYPTION_KEY=false # ============================================================================= # 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}$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 - Premium Welcome Experience # ============================================================================= print_banner() { local banner_file="${SCRIPT_DIR}/internal/banner/banner.txt" # Clear screen for a clean start (optional, only in interactive mode) if [[ -t 1 ]] && [[ "$AUTO_CONFIRM" != true ]]; then clear 2>/dev/null || true fi echo "" echo -e "${CYAN}${BOLD}" if [[ -f "$banner_file" ]]; then while IFS= read -r line; do echo " $line" done < "$banner_file" else echo " F L Y M Q" fi echo -e "${RESET}" echo "" echo -e " ${GREEN}${BOLD}FlyMQ Installer${RESET} ${DIM}v${FLYMQ_VERSION}${RESET}" echo -e " ${DIM}High-Performance Message Queue for Modern Applications${RESET}" echo "" echo -e " ${CYAN}✓${RESET} Sub-millisecond latency ${CYAN}✓${RESET} Zero external dependencies" echo -e " ${CYAN}✓${RESET} Single binary deployment ${CYAN}✓${RESET} TLS encryption by default" echo -e " ${CYAN}✓${RESET} Consumer groups & offsets ${CYAN}✓${RESET} Production-ready defaults" 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${RESET}/${GREEN}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 # ============================================================================= # Generate a unique node ID for cluster deployment generate_cluster_node_id() { local hostname hostname=$(hostname | tr '[:upper:]' '[:lower:]' | tr -cd '[:alnum:]-') # Add a short random suffix for uniqueness local suffix suffix=$(head -c 4 /dev/urandom 2>/dev/null | xxd -p 2>/dev/null || echo "$(date +%s)" | tail -c 5) suffix="${suffix:0:4}" echo "${hostname}-${suffix}" } # Validate address format (hostname or IP) validate_address_format() { local addr="$1" # Check if it's a valid IPv4 address if [[ "$addr" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then return 0 fi # Check if it's a valid hostname (basic check) if [[ "$addr" =~ ^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?)*$ ]]; then return 0 fi # Check for localhost if [[ "$addr" == "localhost" ]]; then return 0 fi return 1 } # Validate peer address format (host:port) validate_peer_address() { local peer="$1" # Check format: host:port if [[ ! "$peer" =~ ^.+:[0-9]+$ ]]; then return 1 fi local host="${peer%:*}" local port="${peer##*:}" # Validate host if ! validate_address_format "$host"; then return 1 fi # Validate port range if (( port < 1 || port > 65535 )); then return 1 fi return 0 } # Validate cluster size recommendations validate_cluster_size() { local expected_nodes="$1" # Check for single-node cluster warnings if (( expected_nodes == 1 )); then print_warning "Single-node cluster provides no fault tolerance" print_info "Consider adding more nodes for production use" fi # Check for even-numbered cluster (split-brain risk) if (( expected_nodes > 1 && expected_nodes % 2 == 0 )); then print_warning "Even number of nodes ($expected_nodes) may cause split-brain scenarios" print_info "Consider using an odd number of nodes (3, 5, 7) for better fault tolerance" fi } 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 " ${CYAN}1${RESET}) Bootstrap - First node in a new cluster ${DIM}(no peers needed)${RESET}" echo -e " ${CYAN}2${RESET}) Join - Join an existing cluster ${DIM}(peers required)${RESET}" 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 configure_cluster_join else configure_cluster_bootstrap fi # Configure partition management for horizontal scaling configure_partition_management } configure_partition_management() { print_section "Partition Management (Horizontal Scaling)" echo -e " ${DIM}FlyMQ distributes partition leaders across cluster nodes for horizontal scaling.${RESET}" echo -e " ${DIM}Each partition can have a different leader, enabling parallel writes.${RESET}" echo "" if prompt_yes_no "Configure partition management settings" "n"; then echo "" echo -e " ${BOLD}Default Topic Settings${RESET}" echo -e " ${DIM}These defaults apply when creating new topics.${RESET}" echo "" CFG_PARTITION_DEFAULT_PARTITIONS=$(prompt_value "Default partitions per topic" "${CFG_PARTITION_DEFAULT_PARTITIONS}") CFG_PARTITION_DEFAULT_REPLICATION_FACTOR=$(prompt_value "Default replication factor" "${CFG_PARTITION_DEFAULT_REPLICATION_FACTOR}") echo "" echo -e " ${BOLD}Distribution Strategy${RESET}" echo -e " ${DIM}How partition leaders are distributed across nodes:${RESET}" echo -e " ${CYAN}round-robin${RESET} - Distribute evenly in order (default)" echo -e " ${CYAN}least-loaded${RESET} - Assign to node with fewest leaders" echo -e " ${CYAN}rack-aware${RESET} - Consider rack placement for fault tolerance" echo "" local strategy_choice strategy_choice=$(prompt_choice "Distribution strategy (1=round-robin, 2=least-loaded, 3=rack-aware)" "1" "1" "2" "3" "round-robin" "least-loaded" "rack-aware") case "$strategy_choice" in 1|round-robin) CFG_PARTITION_DISTRIBUTION_STRATEGY="round-robin" ;; 2|least-loaded) CFG_PARTITION_DISTRIBUTION_STRATEGY="least-loaded" ;; 3|rack-aware) CFG_PARTITION_DISTRIBUTION_STRATEGY="rack-aware" ;; esac echo "" echo -e " ${BOLD}Automatic Rebalancing${RESET}" echo -e " ${DIM}Automatically redistribute partition leaders when nodes join/leave.${RESET}" echo "" if prompt_yes_no "Enable automatic rebalancing" "n"; then CFG_PARTITION_AUTO_REBALANCE_ENABLED="true" CFG_PARTITION_AUTO_REBALANCE_INTERVAL=$(prompt_value "Rebalance check interval (seconds)" "${CFG_PARTITION_AUTO_REBALANCE_INTERVAL}") CFG_PARTITION_REBALANCE_THRESHOLD=$(prompt_value "Imbalance threshold (0.0-1.0, e.g., 0.2 = 20%)" "${CFG_PARTITION_REBALANCE_THRESHOLD}") else CFG_PARTITION_AUTO_REBALANCE_ENABLED="false" fi echo "" print_success "Partition management configured" echo "" echo -e " ${BOLD}Partition Settings:${RESET}" echo -e " Default Partitions: ${CYAN}${CFG_PARTITION_DEFAULT_PARTITIONS}${RESET}" echo -e " Replication Factor: ${CYAN}${CFG_PARTITION_DEFAULT_REPLICATION_FACTOR}${RESET}" echo -e " Distribution Strategy: ${CYAN}${CFG_PARTITION_DISTRIBUTION_STRATEGY}${RESET}" echo -e " Auto Rebalance: ${CYAN}${CFG_PARTITION_AUTO_REBALANCE_ENABLED}${RESET}" echo "" else print_info "Using default partition settings (1 partition, 1 replica, round-robin)" 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 using this node's cluster address.${RESET}" echo "" # Generate unique node ID for cluster echo -e " ${BOLD}Node Identity${RESET}" local default_node_id default_node_id=$(generate_cluster_node_id) CFG_NODE_ID=$(prompt_value "Node ID (must be unique in cluster)" "$default_node_id") echo "" # Get the advertised address for this node local default_ip default_ip=$(get_local_ip) echo -e " ${BOLD}Cluster Address${RESET}" echo -e " ${DIM}This is the address other nodes will use to connect to this node.${RESET}" echo -e " ${DIM}Use a hostname or IP that is reachable from other cluster nodes.${RESET}" local advertised_host advertised_host=$(prompt_value "Advertised hostname/IP" "$default_ip") # Validate the advertised address format if ! validate_address_format "$advertised_host"; then print_warning "Address format may be invalid. Proceeding anyway." fi local advertised_port advertised_port=$(prompt_value "Cluster port" "9093") CFG_ADVERTISE_CLUSTER="${advertised_host}:${advertised_port}" CFG_CLUSTER_ADDR=":${advertised_port}" echo "" # Expected cluster size for validation echo -e " ${BOLD}Cluster Size${RESET}" local expected_nodes expected_nodes=$(prompt_number "Expected number of nodes in cluster" "3" "1") validate_cluster_size "$expected_nodes" echo "" print_info "Bootstrap node configured." echo "" echo -e " ${GREEN}${BOLD}✓ BOOTSTRAP NODE SETUP COMPLETE${RESET}" echo "" echo -e " ${BOLD}This Node:${RESET}" echo -e " Node ID: ${CYAN}${CFG_NODE_ID}${RESET}" echo -e " Cluster Address: ${CYAN}${CFG_ADVERTISE_CLUSTER}${RESET}" echo "" echo -e " ${BOLD}Next Steps After Installation:${RESET}" echo "" echo -e " ${YELLOW}1.${RESET} Start this bootstrap node first:" echo -e " ${CYAN}flymq --config /etc/flymq/flymq.json${RESET}" echo "" echo -e " ${YELLOW}2.${RESET} On each additional node, run the installer and choose 'Join' mode" echo "" echo -e " ${YELLOW}3.${RESET} When prompted for peer addresses on joining nodes, use:" echo -e " ${CYAN}${CFG_ADVERTISE_CLUSTER}${RESET}" echo "" echo -e " ${YELLOW}4.${RESET} After all nodes are running, verify cluster status:" echo -e " ${CYAN}flymq-cli cluster status${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 "" # Generate unique node ID for this joining node echo -e " ${BOLD}Node Identity${RESET}" local default_node_id default_node_id=$(generate_cluster_node_id) CFG_NODE_ID=$(prompt_value "Node ID (must be unique in cluster)" "$default_node_id") echo "" # Get peer addresses with validation echo -e " ${BOLD}Cluster Peers${RESET}" echo -e " ${DIM}Enter the addresses of existing cluster nodes (comma-separated).${RESET}" echo -e " ${DIM}Include at least the bootstrap node address.${RESET}" echo -e " ${DIM}Format: host1:port,host2:port${RESET}" echo -e " ${DIM}Example: 192.168.1.10:9093,192.168.1.11:9093${RESET}" echo "" local peers_valid=false while [[ "$peers_valid" == false ]]; do 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." if prompt_yes_no "Continue without peers (configure later)" "n"; then print_warning "You must configure peers in flymq.json before starting" peers_valid=true fi else # Validate each peer address local all_valid=true local invalid_peers="" IFS=',' read -ra peer_array <<< "$CFG_CLUSTER_PEERS" for peer in "${peer_array[@]}"; do peer=$(echo "$peer" | xargs) # trim whitespace if [[ -n "$peer" ]] && ! validate_peer_address "$peer"; then all_valid=false invalid_peers="${invalid_peers} ${peer}" fi done if [[ "$all_valid" == true ]]; then peers_valid=true print_success "Peer addresses validated: ${#peer_array[@]} peer(s)" else print_warning "Invalid peer address format:${invalid_peers}" print_info "Expected format: hostname:port or ip:port" if prompt_yes_no "Continue anyway" "n"; then peers_valid=true fi fi fi done echo "" # Get the advertised address for this node local default_ip default_ip=$(get_local_ip) echo -e " ${BOLD}Cluster Address${RESET}" echo -e " ${DIM}This is the address other cluster nodes will use to connect to this node.${RESET}" echo -e " ${DIM}Must be reachable from all other cluster nodes.${RESET}" local advertised_host advertised_host=$(prompt_value "Advertised hostname/IP" "$default_ip") # Validate the advertised address format if ! validate_address_format "$advertised_host"; then print_warning "Address format may be invalid. Proceeding anyway." fi local advertised_port advertised_port=$(prompt_value "Cluster port" "9093") CFG_ADVERTISE_CLUSTER="${advertised_host}:${advertised_port}" CFG_CLUSTER_ADDR=":${advertised_port}" echo "" print_info "Join configuration complete." echo "" echo -e " ${GREEN}${BOLD}✓ JOINING NODE SETUP COMPLETE${RESET}" echo "" echo -e " ${BOLD}This Node:${RESET}" echo -e " Node ID: ${CYAN}${CFG_NODE_ID}${RESET}" echo -e " Cluster Address: ${CYAN}${CFG_ADVERTISE_CLUSTER}${RESET}" echo -e " Joining Peers: ${CYAN}${CFG_CLUSTER_PEERS}${RESET}" echo "" echo -e " ${BOLD}Pre-Start Checklist:${RESET}" echo -e " ${DIM}☐ Bootstrap node is running and healthy${RESET}" echo -e " ${DIM}☐ Network connectivity to peers verified${RESET}" echo -e " ${DIM}☐ Firewall allows port ${advertised_port} (cluster) and 9092 (client)${RESET}" echo "" echo -e " ${BOLD}Next Steps After Installation:${RESET}" echo "" echo -e " ${YELLOW}1.${RESET} Verify the cluster is running:" echo -e " ${CYAN}flymq-cli --server ${CFG_CLUSTER_PEERS%%,*} cluster status${RESET}" echo "" echo -e " ${YELLOW}2.${RESET} Start this node:" echo -e " ${CYAN}flymq --config /etc/flymq/flymq.json${RESET}" echo "" echo -e " ${YELLOW}3.${RESET} Verify this node joined successfully:" echo -e " ${CYAN}flymq-cli cluster status${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" } # ============================================================================= # Service Discovery Functions # ============================================================================= # Check if flymq-discover tool is available has_discovery_tool() { command -v flymq-discover &> /dev/null || [[ -x "${SCRIPT_DIR}/flymq-discover" ]] } # Discover FlyMQ nodes on the network using mDNS discover_nodes_mdns() { local timeout="${1:-5}" local discover_cmd="" if command -v flymq-discover &> /dev/null; then discover_cmd="flymq-discover" elif [[ -x "${SCRIPT_DIR}/flymq-discover" ]]; then discover_cmd="${SCRIPT_DIR}/flymq-discover" else return 1 fi "$discover_cmd" --timeout "$timeout" --quiet 2>/dev/null } # Discover nodes using network scanning (fallback) discover_nodes_scan() { local subnet="${1:-}" local port="${2:-9093}" if [[ -z "$subnet" ]]; then # Try to detect local subnet local local_ip=$(get_local_ip) if [[ "$local_ip" =~ ^([0-9]+\.[0-9]+\.[0-9]+)\.[0-9]+$ ]]; then subnet="${BASH_REMATCH[1]}" else return 1 fi fi local found_nodes="" # Quick scan of common IPs in subnet for i in {1..254}; do local target="${subnet}.${i}:${port}" # Use timeout to quickly check if port is open if (echo >/dev/tcp/${subnet}.${i}/${port}) 2>/dev/null; then if [[ -n "$found_nodes" ]]; then found_nodes+="," fi found_nodes+="$target" fi done & # Wait with timeout local pid=$! sleep 3 kill $pid 2>/dev/null || true echo "$found_nodes" } # Interactive node discovery discover_cluster_nodes() { print_section "Cluster Node Discovery" echo -e " ${DIM}FlyMQ can automatically discover existing cluster nodes on your network.${RESET}" echo "" local discovered_nodes="" local discovery_method="" # Try mDNS discovery first if has_discovery_tool; then echo -e " ${CYAN}Scanning for FlyMQ nodes using mDNS...${RESET}" discovered_nodes=$(discover_nodes_mdns 5) if [[ -n "$discovered_nodes" ]]; then discovery_method="mdns" fi fi # Show results if [[ -n "$discovered_nodes" ]]; then echo "" echo -e " ${GREEN}${BOLD}✓ Found existing FlyMQ nodes:${RESET}" echo "" local i=1 IFS=',' read -ra node_array <<< "$discovered_nodes" for node in "${node_array[@]}"; do echo -e " ${CYAN}[$i]${RESET} $node" ((i++)) done echo "" if prompt_yes_no "Use discovered nodes as cluster peers" "y"; then CFG_CLUSTER_PEERS="$discovered_nodes" print_success "Using discovered nodes: $discovered_nodes" return 0 fi else echo -e " ${YELLOW}No FlyMQ nodes found on the network.${RESET}" echo "" echo -e " ${DIM}This could mean:${RESET}" echo -e " ${DIM}- No cluster exists yet (you're setting up the first node)${RESET}" echo -e " ${DIM}- Existing nodes are on a different network${RESET}" echo -e " ${DIM}- mDNS/Bonjour is blocked by firewall${RESET}" echo "" fi # Manual entry option echo -e " ${BOLD}Enter peer addresses manually:${RESET}" echo -e " ${DIM}Format: host1:port,host2:port (e.g., 192.168.1.10:9093,192.168.1.11:9093)${RESET}" echo -e " ${DIM}Leave empty if this is the first node in the cluster.${RESET}" echo "" CFG_CLUSTER_PEERS=$(prompt_value "Peer addresses" "") return 0 } # Test connectivity to a peer node test_peer_connectivity() { local peer="$1" local host="${peer%:*}" local port="${peer##*:}" # Try to connect with timeout if command -v nc &> /dev/null; then nc -z -w 2 "$host" "$port" 2>/dev/null return $? elif command -v timeout &> /dev/null; then timeout 2 bash -c "echo >/dev/tcp/$host/$port" 2>/dev/null return $? else # Fallback: just try to connect (echo >/dev/tcp/$host/$port) 2>/dev/null return $? fi } # Validate connectivity to all configured peers validate_peer_connectivity() { if [[ -z "$CFG_CLUSTER_PEERS" ]]; then return 0 fi echo "" echo -e " ${BOLD}Testing connectivity to peers...${RESET}" local all_ok=true IFS=',' read -ra peer_array <<< "$CFG_CLUSTER_PEERS" for peer in "${peer_array[@]}"; do peer=$(echo "$peer" | xargs) # trim whitespace if [[ -z "$peer" ]]; then continue fi echo -n " Testing $peer... " if test_peer_connectivity "$peer"; then echo -e "${GREEN}✓ OK${RESET}" else echo -e "${RED}✗ UNREACHABLE${RESET}" all_ok=false fi done if [[ "$all_ok" == false ]]; then echo "" print_warning "Some peers are unreachable. This may cause issues when starting the cluster." echo -e " ${DIM}Possible causes:${RESET}" echo -e " ${DIM}- Peer nodes are not running yet${RESET}" echo -e " ${DIM}- Firewall blocking port 9093${RESET}" echo -e " ${DIM}- Incorrect peer addresses${RESET}" echo "" if ! prompt_yes_no "Continue anyway" "y"; then return 1 fi else echo "" print_success "All peers are reachable" fi return 0 } # ============================================================================= # JSON Configuration Generation # ============================================================================= # Format peers array as JSON array format_peers_json() { if [[ -z "$CFG_CLUSTER_PEERS" ]]; then echo "[]" return fi local result="[" local first=true IFS=',' read -ra peer_array <<< "$CFG_CLUSTER_PEERS" for peer in "${peer_array[@]}"; do peer=$(echo "$peer" | xargs) # trim whitespace if [[ -n "$peer" ]]; then if [[ "$first" == "true" ]]; then first=false else result+=", " fi result+="\"$peer\"" fi done result+="]" echo "$result" } generate_config() { local config_dir="$1" local config_file="$config_dir/flymq.json" print_step "Generating configuration" echo "" mkdir -p "$config_dir" # Build peers array and cluster settings based on deployment mode local peers_json local cluster_addr_value local advertise_cluster_value if [[ "$CFG_DEPLOYMENT_MODE" == "cluster" ]]; then peers_json=$(format_peers_json) cluster_addr_value="${CFG_CLUSTER_ADDR}" advertise_cluster_value="${CFG_ADVERTISE_CLUSTER}" else # Standalone mode: no cluster configuration peers_json="[]" cluster_addr_value="" advertise_cluster_value="" fi cat > "$config_file" << EOF { "_comment": "FlyMQ Configuration - Generated by install.sh on $(date -u +"%Y-%m-%dT%H:%M:%SZ")", "_mode": "${CFG_DEPLOYMENT_MODE}", "bind_addr": "${CFG_BIND_ADDR}", "cluster_addr": "${cluster_addr_value}", "advertise_cluster": "${advertise_cluster_value}", "peers": ${peers_json}, "node_id": "${CFG_NODE_ID}", "data_dir": "${CFG_DATA_DIR}", "retention_bytes": ${CFG_RETENTION_BYTES}, "segment_bytes": ${CFG_SEGMENT_BYTES}, "log_level": "${CFG_LOG_LEVEL}", "security": { "tls_enabled": ${CFG_TLS_ENABLED}, "tls_cert_file": "${CFG_TLS_CERT_FILE}", "tls_key_file": "${CFG_TLS_KEY_FILE}", "encryption_enabled": ${CFG_ENCRYPTION_ENABLED} }, "auth": { "enabled": ${CFG_AUTH_ENABLED}, "allow_anonymous": ${CFG_AUTH_ALLOW_ANONYMOUS}, "default_public": ${CFG_AUTH_DEFAULT_PUBLIC}, "admin_username": "${CFG_AUTH_ADMIN_USER}", "admin_password": "${CFG_AUTH_ADMIN_PASS}" }, "performance": { "acks": "${CFG_ACKS}", "default_serde": "${CFG_DEFAULT_SERDE}", "sync_interval_ms": 5 }, "schema": { "enabled": ${CFG_SCHEMA_ENABLED}, "validation": "${CFG_SCHEMA_VALIDATION}" }, "dlq": { "enabled": ${CFG_DLQ_ENABLED}, "max_retries": ${CFG_DLQ_MAX_RETRIES}, "retry_delay": ${CFG_DLQ_RETRY_DELAY}, "topic_suffix": "${CFG_DLQ_TOPIC_SUFFIX}" }, "ttl": { "default_ttl": ${CFG_TTL_DEFAULT}, "cleanup_interval": ${CFG_TTL_CLEANUP_INTERVAL} }, "delayed": { "enabled": ${CFG_DELAYED_ENABLED}, "max_delay": ${CFG_DELAYED_MAX_DELAY} }, "transaction": { "enabled": ${CFG_TXN_ENABLED}, "timeout": ${CFG_TXN_TIMEOUT} }, "grpc": { "enabled": ${CFG_GRPC_ENABLED}, "addr": "${CFG_GRPC_ADDR}", "tls_enabled": ${CFG_GRPC_TLS_ENABLED}, "tls_use_global": ${CFG_GRPC_TLS_USE_GLOBAL}, "tls_cert_file": "${CFG_GRPC_TLS_CERT}", "tls_key_file": "${CFG_GRPC_TLS_KEY}" }, "ws": { "enabled": ${CFG_WS_ENABLED}, "addr": "${CFG_WS_ADDR}", "tls_enabled": ${CFG_WS_TLS_ENABLED}, "tls_use_global": ${CFG_WS_TLS_USE_GLOBAL}, "tls_cert_file": "${CFG_WS_TLS_CERT}", "tls_key_file": "${CFG_WS_TLS_KEY}" }, "mqtt": { "enabled": ${CFG_MQTT_ENABLED}, "addr": "${CFG_MQTT_ADDR}", "tls_enabled": ${CFG_MQTT_TLS_ENABLED}, "tls_use_global": ${CFG_MQTT_TLS_USE_GLOBAL}, "tls_cert_file": "${CFG_MQTT_TLS_CERT}", "tls_key_file": "${CFG_MQTT_TLS_KEY}" }, "partition": { "distribution_strategy": "${CFG_PARTITION_DISTRIBUTION_STRATEGY}", "default_replication_factor": ${CFG_PARTITION_DEFAULT_REPLICATION_FACTOR}, "default_partitions": ${CFG_PARTITION_DEFAULT_PARTITIONS}, "auto_rebalance_enabled": ${CFG_PARTITION_AUTO_REBALANCE_ENABLED}, "auto_rebalance_interval": ${CFG_PARTITION_AUTO_REBALANCE_INTERVAL}, "rebalance_threshold": ${CFG_PARTITION_REBALANCE_THRESHOLD} }, "discovery": { "enabled": ${CFG_DISCOVERY_ENABLED:-false}, "cluster_id": "${CFG_DISCOVERY_CLUSTER_ID:-}" }, "observability": { "metrics": { "enabled": ${CFG_METRICS_ENABLED}, "addr": "${CFG_METRICS_ADDR}" }, "tracing": { "enabled": ${CFG_TRACING_ENABLED}, "endpoint": "${CFG_TRACING_ENDPOINT}", "sample_rate": ${CFG_TRACING_SAMPLE_RATE} }, "health": { "enabled": ${CFG_HEALTH_ENABLED}, "addr": "${CFG_HEALTH_ADDR}", "tls_enabled": ${CFG_HEALTH_TLS_ENABLED}, "tls_cert_file": "${CFG_HEALTH_TLS_CERT_FILE}", "tls_key_file": "${CFG_HEALTH_TLS_KEY_FILE}", "tls_auto_generate": ${CFG_HEALTH_TLS_AUTO_GENERATE}, "tls_use_admin_cert": ${CFG_HEALTH_TLS_USE_ADMIN_CERT} }, "admin": { "enabled": ${CFG_ADMIN_ENABLED}, "addr": "${CFG_ADMIN_ADDR}", "auth_enabled": ${CFG_AUTH_ENABLED}, "tls_enabled": ${CFG_ADMIN_TLS_ENABLED}, "tls_cert_file": "${CFG_ADMIN_TLS_CERT_FILE}", "tls_key_file": "${CFG_ADMIN_TLS_KEY_FILE}", "tls_auto_generate": ${CFG_ADMIN_TLS_AUTO_GENERATE} } }, "audit": { "enabled": ${CFG_AUDIT_ENABLED}, "log_dir": "${CFG_AUDIT_LOG_DIR}", "max_file_size": ${CFG_AUDIT_MAX_FILE_SIZE}, "retention_days": ${CFG_AUDIT_RETENTION_DAYS} } } EOF print_success "Generated: ${CYAN}$config_file${RESET}" # Generate secrets file when encryption is enabled (always, for security) if [[ "$CFG_ENCRYPTION_ENABLED" == "true" ]]; then generate_secrets_file "$config_dir" fi # Generate environment file for cluster deployments if [[ "$CFG_DEPLOYMENT_MODE" == "cluster" ]]; then generate_env_file "$config_dir" fi } # Generate environment variables file for systemd/container deployments generate_env_file() { local config_dir="$1" local env_file="$config_dir/flymq.env" cat > "$env_file" << EOF # FlyMQ Environment Variables # Generated by install.sh on $(date -u +"%Y-%m-%dT%H:%M:%SZ") # Source this file or use with systemd EnvironmentFile= # Network Configuration FLYMQ_BIND_ADDR=${CFG_BIND_ADDR} FLYMQ_CLUSTER_ADDR=${CFG_CLUSTER_ADDR} FLYMQ_ADVERTISE_CLUSTER=${CFG_ADVERTISE_CLUSTER} FLYMQ_NODE_ID=${CFG_NODE_ID} FLYMQ_PEERS=${CFG_CLUSTER_PEERS} # Storage Configuration FLYMQ_DATA_DIR=${CFG_DATA_DIR} FLYMQ_SEGMENT_BYTES=${CFG_SEGMENT_BYTES} FLYMQ_RETENTION_BYTES=${CFG_RETENTION_BYTES} # Logging FLYMQ_LOG_LEVEL=${CFG_LOG_LEVEL} # Performance FLYMQ_ACKS=${CFG_ACKS} FLYMQ_DEFAULT_SERDE=${CFG_DEFAULT_SERDE} # Security (non-sensitive) FLYMQ_TLS_ENABLED=${CFG_TLS_ENABLED} FLYMQ_TLS_CERT_FILE=${CFG_TLS_CERT_FILE} FLYMQ_TLS_KEY_FILE=${CFG_TLS_KEY_FILE} FLYMQ_ENCRYPTION_ENABLED=${CFG_ENCRYPTION_ENABLED} # NOTE: FLYMQ_ENCRYPTION_KEY is stored separately in flymq.secrets for security # Observability FLYMQ_METRICS_ENABLED=${CFG_METRICS_ENABLED} FLYMQ_METRICS_ADDR=${CFG_METRICS_ADDR} FLYMQ_HEALTH_ENABLED=${CFG_HEALTH_ENABLED} FLYMQ_HEALTH_ADDR=${CFG_HEALTH_ADDR} FLYMQ_ADMIN_ENABLED=${CFG_ADMIN_ENABLED} FLYMQ_ADMIN_ADDR=${CFG_ADMIN_ADDR} # gRPC API FLYMQ_GRPC_ENABLED=${CFG_GRPC_ENABLED} FLYMQ_GRPC_ADDR=${CFG_GRPC_ADDR} FLYMQ_GRPC_TLS_ALL=${CFG_GRPC_TLS_ENABLED} FLYMQ_GRPC_TLS_USE_GLOBAL=${CFG_GRPC_TLS_USE_GLOBAL} FLYMQ_GRPC_TLS_CERT_FILE=${CFG_GRPC_TLS_CERT} FLYMQ_GRPC_TLS_KEY_FILE=${CFG_GRPC_TLS_KEY} # WebSocket Gateway FLYMQ_WS_ENABLED=${CFG_WS_ENABLED} FLYMQ_WS_ADDR=${CFG_WS_ADDR} FLYMQ_WS_TLS_ALL=${CFG_WS_TLS_ENABLED} FLYMQ_WS_TLS_USE_GLOBAL=${CFG_WS_TLS_USE_GLOBAL} FLYMQ_WS_TLS_CERT_FILE=${CFG_WS_TLS_CERT} FLYMQ_WS_TLS_KEY_FILE=${CFG_WS_TLS_KEY} # MQTT Bridge FLYMQ_MQTT_ENABLED=${CFG_MQTT_ENABLED} FLYMQ_MQTT_ADDR=${CFG_MQTT_ADDR} FLYMQ_MQTT_TLS_ALL=${CFG_MQTT_TLS_ENABLED} FLYMQ_MQTT_TLS_USE_GLOBAL=${CFG_MQTT_TLS_USE_GLOBAL} FLYMQ_MQTT_TLS_CERT_FILE=${CFG_MQTT_TLS_CERT} FLYMQ_MQTT_TLS_KEY_FILE=${CFG_MQTT_TLS_KEY} EOF print_success "Generated: ${CYAN}$env_file${RESET}" } # Generate secrets file with restricted permissions (encryption key, passwords) generate_secrets_file() { local config_dir="$1" local secrets_file="$config_dir/flymq.secrets" # Create secrets file with restricted permissions (owner read/write only) touch "$secrets_file" chmod 600 "$secrets_file" cat > "$secrets_file" << EOF # FlyMQ Secrets - KEEP THIS FILE SECURE! # Generated by install.sh on $(date -u +"%Y-%m-%dT%H:%M:%SZ") # Permissions: 600 (owner read/write only) # # SECURITY WARNING: # - This file contains sensitive cryptographic keys # - Never commit this file to version control # - Back up securely (encrypted backup recommended) # - Use a secrets manager in production (HashiCorp Vault, AWS Secrets Manager, etc.) # # CLUSTER REQUIREMENT: # - ALL nodes in a cluster MUST use the SAME encryption key # - Nodes with different keys will be rejected from joining # - Copy this file to all cluster nodes or use a secrets manager # # Usage: # source $secrets_file && flymq --config $config_dir/flymq.json # OR # systemd: EnvironmentFile=$secrets_file # Encryption Key (AES-256, 64 hex characters) # Required when encryption_enabled=true in config # IMPORTANT: Must be identical across all cluster nodes! FLYMQ_ENCRYPTION_KEY=${CFG_ENCRYPTION_KEY} EOF # Ensure permissions are set correctly chmod 600 "$secrets_file" print_success "Generated: ${CYAN}$secrets_file${RESET} ${DIM}(mode 600)${RESET}" } run_uninstaller() { print_step "Running uninstaller" echo "" local uninstall_script="${SCRIPT_DIR}/uninstall.sh" if [[ -f "$uninstall_script" ]]; then print_info "Using local uninstaller: ${CYAN}$uninstall_script${RESET}" if [[ "$AUTO_CONFIRM" == "true" ]]; then "$uninstall_script" --yes else "$uninstall_script" fi else print_warning "Uninstaller not found, performing manual cleanup" manual_uninstall fi } manual_uninstall() { print_info "Performing manual uninstall" local bin_dir="$PREFIX/bin" # Remove binaries for binary in flymq flymq-cli flymq-discover; do local bin_path="$bin_dir/$binary" [[ "$OS" == "windows" ]] && bin_path="${bin_path}.exe" if [[ -f "$bin_path" ]]; then rm -f "$bin_path" print_success "Removed $bin_path" fi done print_success "Manual uninstall complete" } # ============================================================================= # Installation Detection and Upgrade # ============================================================================= detect_existing_installation() { print_step "Checking for existing installation" echo "" local found=false local detected_prefix="" local detected_config_dir="" local detected_data_dir="" # Platform-specific detection (same logic as uninstall.sh) if [[ "$OS" == "windows" ]]; then # Windows paths local win_prefix="$HOME/AppData/Local/FlyMQ" if [[ -f "$win_prefix/bin/flymq.exe" ]]; then detected_prefix="$win_prefix" detected_config_dir="$win_prefix/config" detected_data_dir="$win_prefix/data" found=true fi else # Unix-like systems (Linux/macOS) # Check /usr/local/bin (root install) if [[ -f "/usr/local/bin/flymq" ]]; then detected_prefix="/usr/local" found=true fi # Check ~/.local/bin (user install) if [[ -f "$HOME/.local/bin/flymq" ]]; then detected_prefix="$HOME/.local" found=true fi # Detect config directory if [[ -d "/etc/flymq" ]]; then detected_config_dir="/etc/flymq" elif [[ -d "$HOME/.config/flymq" ]]; then detected_config_dir="$HOME/.config/flymq" fi # Detect data directory if [[ -d "/var/lib/flymq" ]]; then detected_data_dir="/var/lib/flymq" elif [[ -d "$HOME/.local/share/flymq" ]]; then detected_data_dir="$HOME/.local/share/flymq" fi fi if [[ "$found" == true ]]; then # Update global variables with detected paths PREFIX="$detected_prefix" [[ -n "$detected_config_dir" ]] && CFG_CONFIG_DIR="$detected_config_dir" [[ -n "$detected_data_dir" ]] && CFG_DATA_DIR="$detected_data_dir" return 0 else return 1 fi } get_installed_version() { local prefix="${PREFIX:-$(get_default_prefix)}" local bin_dir="$prefix/bin" local flymq_bin="$bin_dir/flymq" if [[ "$OS" == "windows" ]]; then flymq_bin="$bin_dir/flymq.exe" fi if [[ -x "$flymq_bin" ]]; then # Extract version, removing 'v' prefix if present local version version=$("$flymq_bin" --version 2>/dev/null | grep -oE 'v?[0-9]+\.[0-9]+\.[0-9]+' | head -1 | sed 's/^v//') echo "${version:-unknown}" else echo "not-installed" fi } check_upgrade_needed() { local installed_version="$(get_installed_version)" if [[ "$installed_version" == "not-installed" ]]; then return 1 # No upgrade needed, fresh install fi if [[ "$FORCE_REINSTALL" == "true" ]]; then return 0 # Force upgrade fi # Simple version comparison - in production you'd want proper semver comparison if [[ "$installed_version" != "$FLYMQ_VERSION" ]]; then return 0 # Upgrade needed fi return 1 # Same version, no upgrade needed } handle_existing_installation() { local installed_version="$(get_installed_version)" local config_dir="${CFG_CONFIG_DIR:-$(get_default_config_dir)}" echo "" print_info "Current version: ${CYAN}${installed_version}${RESET}" print_info "New version: ${CYAN}${FLYMQ_VERSION}${RESET}" print_info "Install location: ${CYAN}${PREFIX}${RESET}" if [[ -f "$config_dir/flymq.json" ]]; then print_info "Configuration: ${CYAN}${config_dir}/flymq.json${RESET} (will be preserved)" fi echo "" # Handle command line flags first if [[ "$UPGRADE" == "true" ]] || [[ "$FORCE_REINSTALL" == "true" ]]; then if [[ "$FORCE_REINSTALL" == "true" ]]; then print_info "Force reinstall requested" else print_info "Upgrade mode enabled" fi return 0 fi # Interactive mode - show options based on version comparison if [[ "$installed_version" == "$FLYMQ_VERSION" ]]; then print_success "FlyMQ ${FLYMQ_VERSION} is already installed" echo "" echo -e " ${BOLD}What would you like to do?${RESET}" echo -e " ${CYAN}1${RESET}) Keep current installation (exit)" echo -e " ${CYAN}2${RESET}) Reinstall binaries (rebuild from source)" echo -e " ${CYAN}3${RESET}) Reconfigure only (keep binaries, update config)" echo -e " ${CYAN}4${RESET}) Fresh install (uninstall first, then reinstall)" echo "" local choice if [[ "$AUTO_CONFIRM" == "true" ]]; then choice="1" else choice=$(prompt_choice "Select option" "1" "1" "2" "3" "4") fi case "$choice" in 1) print_info "Keeping current installation" exit 0 ;; 2) print_info "Reinstalling FlyMQ ${FLYMQ_VERSION}" FORCE_REINSTALL=true return 0 ;; 3) print_info "Reconfiguring FlyMQ ${FLYMQ_VERSION}" return 2 # Config-only mode ;; 4) print_info "Performing fresh installation" if [[ "$AUTO_CONFIRM" == "true" ]] || prompt_yes_no "This will remove the current installation. Continue" "n"; then run_uninstaller return 0 else print_info "Installation cancelled" exit 0 fi ;; esac else # Different version - offer upgrade options echo -e " ${BOLD}Available options:${RESET}" echo -e " ${CYAN}1${RESET}) Upgrade to ${FLYMQ_VERSION} (recommended)" echo -e " ${CYAN}2${RESET}) Keep current version (exit)" echo -e " ${CYAN}3${RESET}) Fresh install (uninstall first, then reinstall)" echo "" local choice if [[ "$AUTO_CONFIRM" == "true" ]]; then choice="1" else choice=$(prompt_choice "Select option" "1" "1" "2" "3") fi case "$choice" in 1) print_info "Upgrading to FlyMQ ${FLYMQ_VERSION}" UPGRADE=true return 0 ;; 2) print_info "Keeping current installation" exit 0 ;; 3) print_info "Performing fresh installation" if [[ "$AUTO_CONFIRM" == "true" ]] || prompt_yes_no "This will remove the current installation. Continue" "n"; then run_uninstaller return 0 else print_info "Installation cancelled" exit 0 fi ;; esac fi } preserve_existing_config() { local config_dir="$(get_default_config_dir)" local config_file="$config_dir/flymq.json" local backup_file="$config_dir/flymq.json.backup.$(date +%s)" if [[ -f "$config_file" ]]; then print_info "Backing up existing configuration" cp "$config_file" "$backup_file" print_success "Config backed up to: ${CYAN}${backup_file}${RESET}" # Load existing config values if possible if command -v jq &> /dev/null; then load_config_from_file "$config_file" else print_warning "jq not found - using default configuration" fi fi } load_config_from_file() { local config_file="$1" # Extract key values from existing config using jq CFG_BIND_ADDR=$(jq -r '.bind_addr // ":9092"' "$config_file" 2>/dev/null || echo ":9092") CFG_DATA_DIR=$(jq -r '.data_dir // ""' "$config_file" 2>/dev/null || echo "") CFG_NODE_ID=$(jq -r '.node_id // ""' "$config_file" 2>/dev/null || echo "") CFG_LOG_LEVEL=$(jq -r '.log_level // "info"' "$config_file" 2>/dev/null || echo "info") # Security settings CFG_TLS_ENABLED=$(jq -r '.security.tls_enabled // false' "$config_file" 2>/dev/null || echo "false") CFG_ENCRYPTION_ENABLED=$(jq -r '.security.encryption_enabled // false' "$config_file" 2>/dev/null || echo "false") CFG_AUTH_ENABLED=$(jq -r '.auth.enabled // false' "$config_file" 2>/dev/null || echo "false") # Protocol bridges CFG_GRPC_ENABLED=$(jq -r '.grpc.enabled // false' "$config_file" 2>/dev/null || echo "false") CFG_WS_ENABLED=$(jq -r '.ws.enabled // false' "$config_file" 2>/dev/null || echo "false") CFG_MQTT_ENABLED=$(jq -r '.mqtt.enabled // false' "$config_file" 2>/dev/null || echo "false") print_success "Loaded configuration from existing file" } # Track if we need to clone the repo (detected early) CLONED_REPO_DIR="" NEEDS_REMOTE_CLONE=false # Cleanup function for cloned repo - called on exit/error cleanup_cloned_repo() { if [[ -n "${CLONED_REPO_DIR}" ]] && [[ -d "${CLONED_REPO_DIR}" ]]; then echo "" print_info "Cleaning up temporary files..." rm -rf "${CLONED_REPO_DIR}" CLONED_REPO_DIR="" fi } # Detect if we're in the repo or need to clone detect_source_mode() { # Check if we're already in the FlyMQ repository if [[ -f "go.mod" ]] && grep -q "module flymq" go.mod 2>/dev/null; then NEEDS_REMOTE_CLONE=false return 0 fi # Check if we're in a subdirectory of the repo if [[ -f "${SCRIPT_DIR}/go.mod" ]] && grep -q "module flymq" "${SCRIPT_DIR}/go.mod" 2>/dev/null; then NEEDS_REMOTE_CLONE=false return 0 fi # Not in repo - will need to clone NEEDS_REMOTE_CLONE=true } check_dependencies() { print_step "Checking dependencies" echo "" # Always need Go 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" # Need Git if running via curl (remote install) if [[ "$NEEDS_REMOTE_CLONE" == true ]]; then if ! command -v git &> /dev/null; then print_error "Git is not installed. Required for remote installation." print_info "Install git or clone manually: ${CYAN}git clone https://github.com/fireflyresearch/flymq.git${RESET}" exit 1 fi print_success "Git found" fi } ensure_source_code() { # Check if we're already in the FlyMQ repository if [[ -f "go.mod" ]] && grep -q "module flymq" go.mod 2>/dev/null; then print_success "Running from FlyMQ repository" return 0 fi # Check if we're in a subdirectory of the repo if [[ -f "${SCRIPT_DIR}/go.mod" ]] && grep -q "module flymq" "${SCRIPT_DIR}/go.mod" 2>/dev/null; then cd "${SCRIPT_DIR}" print_success "Running from FlyMQ repository" return 0 fi # Not in repo - clone from GitHub print_step "Downloading FlyMQ source code" echo "" # Create temp directory for clone CLONED_REPO_DIR=$(mktemp -d "${TMPDIR:-/tmp}/flymq-install.XXXXXX") print_info "Cloning to: ${CYAN}${CLONED_REPO_DIR}${RESET}" # Clone main branch echo -n " " if ! git clone --depth 1 https://github.com/fireflyresearch/flymq.git "${CLONED_REPO_DIR}" 2>&1; then print_error "Failed to clone FlyMQ repository" rm -rf "${CLONED_REPO_DIR}" CLONED_REPO_DIR="" exit 1 fi print_success "Cloned FlyMQ repository" # Verify the clone was successful if [[ ! -f "${CLONED_REPO_DIR}/go.mod" ]]; then print_error "Clone verification failed: go.mod not found" rm -rf "${CLONED_REPO_DIR}" CLONED_REPO_DIR="" exit 1 fi # Change to cloned directory cd "${CLONED_REPO_DIR}" } 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 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 2>&1; then print_error "Failed to build flymq-cli" exit 1 fi print_success "Built flymq-cli" # Build discovery tool local discover_bin="bin/flymq-discover" if [[ "$OS" == "windows" ]]; then discover_bin="bin/flymq-discover.exe" fi echo -n " " if ! GOOS="${OS}" GOARCH="${ARCH}" go build -o "$discover_bin" ./cmd/flymq-discover 2>&1; then print_warning "Failed to build flymq-discover (optional)" else print_success "Built flymq-discover" fi } 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" # Install discovery tool if it was built local discover_src="bin/flymq-discover" local discover_dst="$bin_dir/flymq-discover" if [[ "$OS" == "windows" ]]; then discover_src="bin/flymq-discover.exe" discover_dst="$bin_dir/flymq-discover.exe" fi if [[ -f "$discover_src" ]]; then cp "$discover_src" "$discover_dst" [[ "$OS" != "windows" ]] && chmod +x "$discover_dst" print_success "Installed flymq-discover" fi # 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.json 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_binary_reinstall_complete() { local prefix="$1" local config_dir="$2" local bin_dir="$prefix/bin" echo "" echo "" echo -e " ${GREEN}${BOLD}✓ BINARY REINSTALL COMPLETE${RESET}" echo "" echo -e " ${BOLD}FlyMQ v${FLYMQ_VERSION}${RESET} binaries have been rebuilt and installed!" echo "" # Installation paths echo -e " ${CYAN}${BOLD}UPDATED${RESET}" echo -e " ${DIM}Binaries${RESET} ${CYAN}$bin_dir${RESET}" echo "" # Preserved paths echo -e " ${CYAN}${BOLD}PRESERVED${RESET}" echo -e " ${DIM}Config${RESET} ${CYAN}$config_dir/flymq.json${RESET}" echo -e " ${DIM}Data${RESET} ${CYAN}$CFG_DATA_DIR${RESET}" echo -e " ${DIM}Credentials${RESET} ${GREEN}Unchanged${RESET}" echo -e " ${DIM}Encryption${RESET} ${GREEN}Unchanged${RESET}" echo "" echo -e " ${CYAN}${BOLD}QUICK START${RESET}" echo "" echo -e " ${DIM}# Start server with existing config${RESET}" echo -e " flymq --config $config_dir/flymq.json" echo "" echo -e " ${DIM}# Check version${RESET}" echo -e " flymq --version" echo "" echo -e " ${GREEN}${BOLD}Note:${RESET} ${DIM}Use your existing credentials and encryption key${RESET}" echo "" } # Verify installation by checking binaries exist and are executable verify_installation() { local prefix="$1" local bin_dir="$prefix/bin" local all_ok=true print_step "Verifying installation" echo "" # Check server binary local server_bin="$bin_dir/flymq" [[ "$OS" == "windows" ]] && server_bin="$bin_dir/flymq.exe" if [[ -x "$server_bin" ]] || [[ "$OS" == "windows" && -f "$server_bin" ]]; then local version version=$("$server_bin" --version 2>/dev/null | head -1 || echo "unknown") print_success "flymq: ${CYAN}$version${RESET}" else print_error "flymq binary not found or not executable" all_ok=false fi # Check CLI binary local cli_bin="$bin_dir/flymq-cli" [[ "$OS" == "windows" ]] && cli_bin="$bin_dir/flymq-cli.exe" if [[ -x "$cli_bin" ]] || [[ "$OS" == "windows" && -f "$cli_bin" ]]; then local version version=$("$cli_bin" --version 2>/dev/null | head -1 || echo "unknown") print_success "flymq-cli: ${CYAN}$version${RESET}" else print_error "flymq-cli binary not found or not executable" all_ok=false fi # Check discover binary (optional) local discover_bin="$bin_dir/flymq-discover" [[ "$OS" == "windows" ]] && discover_bin="$bin_dir/flymq-discover.exe" if [[ -x "$discover_bin" ]] || [[ "$OS" == "windows" && -f "$discover_bin" ]]; then print_success "flymq-discover: ${CYAN}installed${RESET}" else print_info "flymq-discover: ${DIM}not installed (optional)${RESET}" fi echo "" if [[ "$all_ok" == true ]]; then print_success "All core binaries verified" else print_warning "Some binaries could not be verified" fi return 0 } print_post_install() { local prefix="$1" local config_dir="$2" local bin_dir="$prefix/bin" echo "" echo "" echo -e " ${GREEN}${BOLD}✓ INSTALLATION COMPLETE${RESET}" echo "" echo -e " ${BOLD}FlyMQ v${FLYMQ_VERSION}${RESET} is ready to use!" echo "" # Installation paths echo -e " ${CYAN}${BOLD}PATHS${RESET}" echo -e " ${DIM}Binaries${RESET} ${CYAN}$bin_dir${RESET}" echo -e " ${DIM}Config${RESET} ${CYAN}$config_dir/flymq.json${RESET}" echo -e " ${DIM}Data${RESET} ${CYAN}$CFG_DATA_DIR${RESET}" echo "" # Features - Grouped echo -e " ${CYAN}${BOLD}ENABLED FEATURES${RESET}" local messaging="" [[ "$CFG_SCHEMA_ENABLED" == "true" ]] && messaging+="Schema(Validation) " [[ "$CFG_DLQ_ENABLED" == "true" ]] && messaging+="DLQ " [[ "$CFG_DELAYED_ENABLED" == "true" ]] && messaging+="Delayed " [[ "$CFG_TXN_ENABLED" == "true" ]] && messaging+="Transactions " local observability="" [[ "$CFG_METRICS_ENABLED" == "true" ]] && observability+="Metrics(Prometheus) " [[ "$CFG_HEALTH_ENABLED" == "true" ]] && observability+="Health " [[ "$CFG_ADMIN_ENABLED" == "true" ]] && observability+="Admin(API) " [[ "$CFG_TRACING_ENABLED" == "true" ]] && observability+="Tracing " [[ "$CFG_AUDIT_ENABLED" == "true" ]] && observability+="Audit " local security="" [[ "$CFG_TLS_ENABLED" == "true" ]] && security+="TLS(Encrypted) " [[ "$CFG_ENCRYPTION_ENABLED" == "true" ]] && security+="Encryption(At-Rest) " [[ "$CFG_AUTH_ENABLED" == "true" ]] && security+="Auth(RBAC) " local discovery="" [[ "$CFG_DISCOVERY_ENABLED" == "true" ]] && discovery+="mDNS(Discovery) " local bridges="" [[ "$CFG_GRPC_ENABLED" == "true" ]] && bridges+="gRPC " [[ "$CFG_WS_ENABLED" == "true" ]] && bridges+="WebSocket " [[ "$CFG_MQTT_ENABLED" == "true" ]] && bridges+="MQTT " [[ -n "$messaging" ]] && echo -e " ${DIM}Messaging${RESET} ${GREEN}${messaging}${RESET}" [[ -n "$observability" ]] && echo -e " ${DIM}Observability${RESET} ${GREEN}${observability}${RESET}" [[ -n "$security" ]] && echo -e " ${DIM}Security${RESET} ${GREEN}${security}${RESET}" [[ -n "$discovery" ]] && echo -e " ${DIM}Discovery${RESET} ${GREEN}${discovery}${RESET}" [[ -n "$bridges" ]] && echo -e " ${DIM}Bridges${RESET} ${GREEN}${bridges}${RESET}" echo "" # Quick Start echo -e " ${CYAN}${BOLD}QUICK START${RESET}" echo "" # Show start command with instructions to export encryption key if [[ "$CFG_ENCRYPTION_ENABLED" == "true" ]]; then echo -e " ${YELLOW}${BOLD}⚠ ENCRYPTION KEY REQUIRED${RESET}" echo -e " ${DIM}Export the key before starting FlyMQ:${RESET}" echo -e " ${CYAN}export FLYMQ_ENCRYPTION_KEY=${CFG_ENCRYPTION_KEY}${RESET}" echo "" fi if [[ "$CFG_DEPLOYMENT_MODE" == "cluster" ]]; then if [[ -z "$CFG_CLUSTER_PEERS" ]]; then echo -e " ${DIM}# Start bootstrap node (JSON logs by default)${RESET}" echo -e " flymq --config $config_dir/flymq.json" echo "" echo -e " ${DIM}# Other nodes join with:${RESET} ${CYAN}${CFG_ADVERTISE_CLUSTER}${RESET}" else echo -e " ${DIM}# Start this node (will auto-join cluster)${RESET}" echo -e " flymq --config $config_dir/flymq.json" fi else echo -e " ${DIM}# Start server (JSON logs by default)${RESET}" echo -e " flymq --config $config_dir/flymq.json" fi echo "" echo -e " ${DIM}# Start with human-readable logs (for development)${RESET}" echo -e " flymq --config $config_dir/flymq.json -human-readable" echo "" echo -e " ${DIM}# Start in quiet mode (logs only, no banner)${RESET}" echo -e " flymq --config $config_dir/flymq.json -quiet" echo "" echo -e " ${DIM}# Send a message${RESET}" if [[ "$CFG_TLS_ENABLED" == "true" ]]; then if [[ "$CFG_TLS_CERT_FILE" == *"/server.crt" ]] && [[ -f "$CFG_TLS_CERT_FILE" ]]; then # Self-signed certificate - show insecure flag echo -e " ${DIM}# With self-signed cert (testing only):${RESET}" echo -e " flymq-cli --tls --insecure produce my-topic \"Hello World\"" echo "" echo -e " ${DIM}# Or with CA cert verification:${RESET}" echo -e " flymq-cli --tls --ca-cert ${CFG_TLS_CERT_FILE} produce my-topic \"Hello World\"" else echo -e " flymq-cli --tls --ca-cert ${CFG_TLS_CERT_FILE} produce my-topic \"Hello World\"" fi else echo -e " flymq-cli produce my-topic \"Hello World\"" fi echo "" echo -e " ${DIM}# Subscribe to messages${RESET}" if [[ "$CFG_TLS_ENABLED" == "true" ]]; then if [[ "$CFG_TLS_CERT_FILE" == *"/server.crt" ]] && [[ -f "$CFG_TLS_CERT_FILE" ]]; then echo -e " flymq-cli --tls --insecure subscribe my-topic" else echo -e " flymq-cli --tls --ca-cert ${CFG_TLS_CERT_FILE} subscribe my-topic" fi else echo -e " flymq-cli subscribe my-topic" fi echo "" # Endpoints if [[ "$CFG_METRICS_ENABLED" == "true" ]] || [[ "$CFG_HEALTH_ENABLED" == "true" ]] || [[ "$CFG_ADMIN_ENABLED" == "true" ]] || \ [[ "$CFG_GRPC_ENABLED" == "true" ]] || [[ "$CFG_WS_ENABLED" == "true" ]] || [[ "$CFG_MQTT_ENABLED" == "true" ]]; then echo -e " ${CYAN}${BOLD}ENDPOINTS${RESET}" echo "" echo -e " ${DIM}Client${RESET} localhost${CFG_BIND_ADDR}" [[ "$CFG_METRICS_ENABLED" == "true" ]] && echo -e " ${DIM}Metrics${RESET} http://localhost${CFG_METRICS_ADDR}/metrics" [[ "$CFG_HEALTH_ENABLED" == "true" ]] && echo -e " ${DIM}Health${RESET} http://localhost${CFG_HEALTH_ADDR}/health" [[ "$CFG_ADMIN_ENABLED" == "true" ]] && echo -e " ${DIM}Admin API${RESET} http://localhost${CFG_ADMIN_ADDR}/api/v1" [[ "$CFG_GRPC_ENABLED" == "true" ]] && echo -e " ${DIM}gRPC${RESET} localhost${CFG_GRPC_ADDR}" [[ "$CFG_WS_ENABLED" == "true" ]] && echo -e " ${DIM}WebSocket${RESET} localhost${CFG_WS_ADDR}" [[ "$CFG_MQTT_ENABLED" == "true" ]] && echo -e " ${DIM}MQTT${RESET} localhost${CFG_MQTT_ADDR}" echo "" fi # Credentials if [[ "$CFG_AUTH_ENABLED" == "true" ]]; then echo -e " ${YELLOW}${BOLD}⚠ SAVE THESE CREDENTIALS${RESET}" echo "" echo -e " ${DIM}Username${RESET} ${CYAN}${CFG_AUTH_ADMIN_USER}${RESET}" echo -e " ${DIM}Password${RESET} ${CYAN}${CFG_AUTH_ADMIN_PASS}${RESET}" echo "" echo -e " ${DIM}# Use with CLI${RESET}" if [[ "$CFG_TLS_ENABLED" == "true" ]]; then if [[ "$CFG_TLS_CERT_FILE" == *"/server.crt" ]] && [[ -f "$CFG_TLS_CERT_FILE" ]]; then echo -e " ${DIM}# With self-signed cert (testing):${RESET}" echo -e " flymq-cli --tls --insecure -u ${CFG_AUTH_ADMIN_USER} -P '***' produce my-topic \"Hello\"" else echo -e " flymq-cli --tls --ca-cert ${CFG_TLS_CERT_FILE} -u ${CFG_AUTH_ADMIN_USER} -P '***' produce my-topic \"Hello\"" fi else echo -e " flymq-cli -u ${CFG_AUTH_ADMIN_USER} -P '***' produce my-topic \"Hello\"" fi echo "" fi if [[ "$CFG_ENCRYPTION_ENABLED" == "true" ]]; then echo -e " ${YELLOW}${BOLD}⚠ ENCRYPTION KEY - KEEP SECURE${RESET}" echo "" echo -e " ${DIM}Key:${RESET} ${DIM}${CFG_ENCRYPTION_KEY}${RESET}" echo "" echo -e " ${DIM}IMPORTANT: You must export this key as an environment variable before starting FlyMQ:${RESET}" echo -e " ${CYAN}export FLYMQ_ENCRYPTION_KEY=${CFG_ENCRYPTION_KEY}${RESET}" echo "" if [[ "$CFG_DEPLOYMENT_MODE" == "cluster" ]]; then echo -e " ${YELLOW}${BOLD}CLUSTER REQUIREMENT:${RESET}" echo -e " ${DIM}All nodes in the cluster MUST use the SAME encryption key.${RESET}" echo "" fi echo -e " ${RED}${BOLD}WARNING:${RESET} ${DIM}Back up this key securely. Data cannot be recovered without it.${RESET}" echo "" fi # PATH warning if [[ ":$PATH:" != *":$bin_dir:"* ]]; then echo -e " ${DIM}Add to PATH:${RESET} export PATH=\"$bin_dir:\$PATH\"" echo "" fi echo -e " ${DIM}Docs: https://github.com/fireflyresearch/flymq${RESET}" echo "" # Prompt for actions prompt_post_install_actions "$prefix" "$config_dir" } # ============================================================================= # Post-Install Actions # ============================================================================= save_installation_summary() { local prefix="$1" local config_dir="$2" local summary_file="$config_dir/INSTALLATION_SUMMARY.txt" print_step "Saving installation summary" echo "" cat > "$summary_file" << EOF FlyMQ Installation Summary ========================== Generated: $(date) Version: ${FLYMQ_VERSION} INSTALLATION PATHS ------------------ Binaries: $prefix/bin Configuration: $config_dir/flymq.json Data Directory: ${CFG_DATA_DIR} DEPLOYMENT ---------- Mode: ${CFG_DEPLOYMENT_MODE} Bind Address: ${CFG_BIND_ADDR} Node ID: ${CFG_NODE_ID} EOF if [[ "$CFG_DEPLOYMENT_MODE" == "cluster" ]]; then cat >> "$summary_file" << EOF Cluster Address: ${CFG_ADVERTISE_CLUSTER} Peers: ${CFG_CLUSTER_PEERS:-none (bootstrap mode)} EOF fi cat >> "$summary_file" << EOF SECURITY -------- TLS: ${CFG_TLS_ENABLED} EOF if [[ "$CFG_TLS_ENABLED" == "true" ]]; then cat >> "$summary_file" << EOF TLS Cert: ${CFG_TLS_CERT_FILE} TLS Key: ${CFG_TLS_KEY_FILE} EOF fi cat >> "$summary_file" << EOF Encryption: ${CFG_ENCRYPTION_ENABLED} Authentication: ${CFG_AUTH_ENABLED} EOF if [[ "$CFG_AUTH_ENABLED" == "true" ]]; then cat >> "$summary_file" << EOF CREDENTIALS (SAVE SECURELY!) ----------------------------- Admin Username: ${CFG_AUTH_ADMIN_USER} Admin Password: ${CFG_AUTH_ADMIN_PASS} EOF fi if [[ "$CFG_ENCRYPTION_ENABLED" == "true" ]]; then cat >> "$summary_file" << EOF ENCRYPTION KEY (SAVE SECURELY!) -------------------------------- ${CFG_ENCRYPTION_KEY} IMPORTANT: Export this key before starting FlyMQ: export FLYMQ_ENCRYPTION_KEY=${CFG_ENCRYPTION_KEY} EOF fi cat >> "$summary_file" << EOF ENDPOINTS --------- Client: localhost${CFG_BIND_ADDR} EOF [[ "$CFG_METRICS_ENABLED" == "true" ]] && echo "Metrics: http://localhost${CFG_METRICS_ADDR}/metrics" >> "$summary_file" [[ "$CFG_HEALTH_ENABLED" == "true" ]] && echo "Health: http://localhost${CFG_HEALTH_ADDR}/health" >> "$summary_file" [[ "$CFG_ADMIN_ENABLED" == "true" ]] && echo "Admin API: http://localhost${CFG_ADMIN_ADDR}/api/v1" >> "$summary_file" [[ "$CFG_GRPC_ENABLED" == "true" ]] && echo "gRPC: localhost${CFG_GRPC_ADDR}" >> "$summary_file" [[ "$CFG_WS_ENABLED" == "true" ]] && echo "WebSocket: localhost${CFG_WS_ADDR}" >> "$summary_file" [[ "$CFG_MQTT_ENABLED" == "true" ]] && echo "MQTT: localhost${CFG_MQTT_ADDR}" >> "$summary_file" cat >> "$summary_file" << EOF QUICK START COMMANDS -------------------- EOF if [[ "$CFG_ENCRYPTION_ENABLED" == "true" ]]; then cat >> "$summary_file" << EOF # Export encryption key (REQUIRED) export FLYMQ_ENCRYPTION_KEY=${CFG_ENCRYPTION_KEY} EOF fi cat >> "$summary_file" << EOF # Start server flymq --config $config_dir/flymq.json # Send a message EOF if [[ "$CFG_TLS_ENABLED" == "true" ]]; then if [[ "$CFG_TLS_CERT_FILE" == *"/server.crt" ]]; then echo "flymq-cli --tls --insecure produce my-topic \"Hello World\"" >> "$summary_file" else echo "flymq-cli --tls --ca-cert ${CFG_TLS_CERT_FILE} produce my-topic \"Hello World\"" >> "$summary_file" fi else echo "flymq-cli produce my-topic \"Hello World\"" >> "$summary_file" fi cat >> "$summary_file" << EOF # Subscribe to messages EOF if [[ "$CFG_TLS_ENABLED" == "true" ]]; then if [[ "$CFG_TLS_CERT_FILE" == *"/server.crt" ]]; then echo "flymq-cli --tls --insecure subscribe my-topic" >> "$summary_file" else echo "flymq-cli --tls --ca-cert ${CFG_TLS_CERT_FILE} subscribe my-topic" >> "$summary_file" fi else echo "flymq-cli subscribe my-topic" >> "$summary_file" fi chmod 600 "$summary_file" print_success "Saved installation summary: ${CYAN}$summary_file${RESET}" echo "" } prompt_post_install_actions() { local prefix="$1" local config_dir="$2" echo -e " ${CYAN}${BOLD}NEXT STEPS${RESET}" echo "" echo -e " ${BOLD}Would you like to:${RESET}" echo "" if prompt_yes_no "1. Save installation summary to file" "y"; then save_installation_summary "$prefix" "$config_dir" fi echo "" if prompt_yes_no "2. Generate environment variables file" "y"; then save_env_file "$config_dir" fi echo "" if prompt_yes_no "3. Start FlyMQ now" "n"; then start_flymq "$config_dir" else echo "" print_info "To start FlyMQ later, run:" if [[ "$CFG_ENCRYPTION_ENABLED" == "true" ]]; then echo -e " ${CYAN}export FLYMQ_ENCRYPTION_KEY=${CFG_ENCRYPTION_KEY}${RESET}" fi echo -e " ${CYAN}flymq --config $config_dir/flymq.json${RESET}" fi } save_env_file() { local config_dir="$1" local env_file="$config_dir/flymq-env.sh" print_step "Generating environment file" echo "" cat > "$env_file" << 'EOF' #!/bin/bash # FlyMQ Environment Variables # Source this file before starting FlyMQ: source flymq-env.sh EOF if [[ "$CFG_ENCRYPTION_ENABLED" == "true" ]]; then cat >> "$env_file" << EOF # Encryption Key (REQUIRED) export FLYMQ_ENCRYPTION_KEY="${CFG_ENCRYPTION_KEY}" EOF fi cat >> "$env_file" << EOF # Optional: Override config file settings # export FLYMQ_BIND_ADDR="${CFG_BIND_ADDR}" # export FLYMQ_DATA_DIR="${CFG_DATA_DIR}" # export FLYMQ_LOG_LEVEL="${CFG_LOG_LEVEL}" EOF if [[ "$CFG_TLS_ENABLED" == "true" ]]; then cat >> "$env_file" << EOF # TLS Configuration # export FLYMQ_TLS_ENABLED="true" # export FLYMQ_TLS_CERT_FILE="${CFG_TLS_CERT_FILE}" # export FLYMQ_TLS_KEY_FILE="${CFG_TLS_KEY_FILE}" EOF fi cat >> "$env_file" << EOF echo "FlyMQ environment variables loaded" EOF chmod 600 "$env_file" print_success "Saved environment file: ${CYAN}$env_file${RESET}" echo "" echo -e " ${BOLD}Usage:${RESET}" echo -e " ${CYAN}source $env_file${RESET}" echo -e " ${CYAN}flymq --config $config_dir/flymq.json${RESET}" echo "" } start_flymq() { local config_dir="$1" print_step "Starting FlyMQ" echo "" # Export encryption key if needed if [[ "$CFG_ENCRYPTION_ENABLED" == "true" ]]; then export FLYMQ_ENCRYPTION_KEY="${CFG_ENCRYPTION_KEY}" print_info "Encryption key exported" fi # Check if flymq is in PATH local flymq_bin="$PREFIX/bin/flymq" if [[ ! -x "$flymq_bin" ]]; then print_error "flymq binary not found at $flymq_bin" return 1 fi print_info "Starting FlyMQ..." echo "" # Start in background "$flymq_bin" --config "$config_dir/flymq.json" > "$config_dir/flymq.log" 2>&1 & local pid=$! # Save PID immediately echo $pid > "$config_dir/flymq.pid" # Show spinner while waiting for startup local max_wait=10 local elapsed=0 while [[ $elapsed -lt $max_wait ]]; do if ! kill -0 $pid 2>/dev/null; then echo "" print_error "FlyMQ process died. Check logs: $config_dir/flymq.log" tail -20 "$config_dir/flymq.log" return 1 fi # Check if server is responding if command -v flymq-cli &> /dev/null; then if flymq-cli health live &> /dev/null 2>&1; then echo "" print_success "FlyMQ started successfully (PID: $pid)" echo "" echo -e " ${BOLD}Status:${RESET} ${GREEN}Running${RESET}" echo -e " ${BOLD}PID:${RESET} ${CYAN}$pid${RESET}" echo -e " ${BOLD}Logs:${RESET} ${CYAN}$config_dir/flymq.log${RESET}" echo -e " ${BOLD}Stop:${RESET} ${CYAN}kill $pid${RESET}" echo "" # Quick health check sleep 1 if flymq-cli health ready &> /dev/null 2>&1; then print_success "Health check passed - FlyMQ is ready!" echo "" print_info "Try it out:" # Build CLI command based on TLS and Auth settings local cli_flags="" # Add TLS flags if [[ "$CFG_TLS_ENABLED" == "true" ]]; then if [[ "$CFG_TLS_CERT_FILE" == *"/server.crt" ]]; then cli_flags="--tls --insecure" else cli_flags="--tls --ca-cert ${CFG_TLS_CERT_FILE}" fi fi # Add auth flags if [[ "$CFG_AUTH_ENABLED" == "true" ]]; then cli_flags="${cli_flags} -u ${CFG_AUTH_ADMIN_USER} -P '${CFG_AUTH_ADMIN_PASS}'" fi # Show commands echo -e " ${CYAN}flymq-cli ${cli_flags} produce test \"Hello FlyMQ!\"${RESET}" echo -e " ${CYAN}flymq-cli ${cli_flags} subscribe test${RESET}" else print_warning "Server started but not fully ready yet" fi return 0 fi fi # Show spinner local spinner_chars="⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏" local i=$((elapsed % 10)) printf "\r ${CYAN}${spinner_chars:$i:1}${RESET} Waiting for FlyMQ to start... ${elapsed}s" sleep 1 ((elapsed++)) done echo "" print_warning "FlyMQ started but not responding after ${max_wait}s" echo "" echo -e " ${BOLD}PID:${RESET} ${CYAN}$pid${RESET}" echo -e " ${BOLD}Logs:${RESET} ${CYAN}$config_dir/flymq.log${RESET}" echo "" print_info "Check logs for details:" echo -e " ${CYAN}tail -f $config_dir/flymq.log${RESET}" return 0 } # ============================================================================= # 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 if [[ -f "$bin_dir/flymq-discover" ]]; then rm -f "$bin_dir/flymq-discover" print_success "Removed flymq-discover" fi # Also check for Windows executables if [[ -f "$bin_dir/flymq.exe" ]]; then rm -f "$bin_dir/flymq.exe" print_success "Removed flymq.exe" fi if [[ -f "$bin_dir/flymq-cli.exe" ]]; then rm -f "$bin_dir/flymq-cli.exe" print_success "Removed flymq-cli.exe" fi if [[ -f "$bin_dir/flymq-discover.exe" ]]; then rm -f "$bin_dir/flymq-discover.exe" print_success "Removed flymq-discover.exe" 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" cleanup_cloned_repo exit 130 } trap cleanup SIGINT SIGTERM trap cleanup_cloned_repo EXIT # ============================================================================= # 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 ;; --upgrade) UPGRADE=true shift ;; --force) FORCE_REINSTALL=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 " --upgrade Upgrade existing installation to latest version" echo " --force Force reinstall even if same version" 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 --upgrade # Upgrade to latest version" 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 } # ============================================================================= # New Configuration Flow - Show Defaults First, Customize by Section # ============================================================================= show_default_configuration() { local config_dir="$1" echo -e " ${GREEN}${BOLD}DEFAULT CONFIGURATION${RESET}" echo -e " ${DIM}FlyMQ comes with production-ready defaults. Review below:${RESET}" echo "" # Section 1: Deployment echo -e " ${WHITE}${BOLD}[${CYAN}1${WHITE}]${RESET} ${BOLD}DEPLOYMENT${RESET}" echo -e " Mode: ${GREEN}Standalone${RESET} ${DIM}(single node)${RESET}" echo -e " Bind Address: ${CYAN}${CFG_BIND_ADDR}${RESET}" echo -e " Data Directory: ${CYAN}${CFG_DATA_DIR}${RESET}" echo -e " Node ID: ${CYAN}${CFG_NODE_ID}${RESET}" echo "" # Section 2: Security echo -e " ${WHITE}${BOLD}[${CYAN}2${WHITE}]${RESET} ${BOLD}SECURITY${RESET}" echo -e " TLS/SSL: ${GREEN}Enabled${RESET} ${DIM}(auto-generated self-signed cert)${RESET}" echo -e " Data Encryption: ${GREEN}Enabled${RESET} ${DIM}(AES-256-GCM, auto-generated key)${RESET}" echo -e " Authentication: ${GREEN}Enabled${RESET} ${DIM}(auto-generated credentials)${RESET}" echo -e " Admin User: ${CYAN}${CFG_AUTH_ADMIN_USER}${RESET}" echo "" # Section 3: Advanced Features echo -e " ${WHITE}${BOLD}[${CYAN}3${WHITE}]${RESET} ${BOLD}ADVANCED FEATURES${RESET}" echo -e " Schema Validation: ${GREEN}Enabled${RESET} ${DIM}(strict mode)${RESET}" echo -e " Dead Letter Queue: ${GREEN}Enabled${RESET} ${DIM}(3 retries)${RESET}" echo -e " Message TTL: ${GREEN}Enabled${RESET} ${DIM}(7 days default)${RESET}" echo -e " Delayed Delivery: ${GREEN}Enabled${RESET} ${DIM}(up to 7 days)${RESET}" echo -e " Transactions: ${GREEN}Enabled${RESET} ${DIM}(60s timeout)${RESET}" echo "" # Section 4: Observability echo -e " ${WHITE}${BOLD}[${CYAN}4${WHITE}]${RESET} ${BOLD}OBSERVABILITY${RESET}" echo -e " Prometheus Metrics: ${GREEN}Enabled${RESET} ${DIM}(${CFG_METRICS_ADDR})${RESET}" echo -e " OpenTelemetry: ${GREEN}Enabled${RESET} ${DIM}(localhost:4317)${RESET}" echo -e " Health Checks: ${GREEN}Enabled${RESET} ${DIM}(${CFG_HEALTH_ADDR})${RESET}" echo -e " Admin API: ${GREEN}Enabled${RESET} ${DIM}(${CFG_ADMIN_ADDR})${RESET}" echo -e " Admin API TLS: ${YELLOW}Disabled${RESET} ${DIM}(HTTP only)${RESET}" echo "" # Section 5: Performance echo -e " ${WHITE}${BOLD}[${CYAN}5${WHITE}]${RESET} ${BOLD}PERFORMANCE${RESET}" echo -e " Acks Mode: ${CYAN}leader${RESET} ${DIM}(balanced durability/latency)${RESET}" echo -e " Segment Size: ${CYAN}64 MB${RESET}" echo -e " Log Level: ${CYAN}info${RESET}" echo "" # Section 6: Protocol Bridges echo -e " ${WHITE}${BOLD}[${CYAN}6${WHITE}]${RESET} ${BOLD}PROTOCOL BRIDGES${RESET}" if [[ "$CFG_GRPC_ENABLED" == "true" ]]; then echo -e " gRPC Server: ${GREEN}Enabled${RESET} ${DIM}(${CFG_GRPC_ADDR})${RESET}" else echo -e " gRPC Server: ${YELLOW}Disabled${RESET}" fi if [[ "$CFG_WS_ENABLED" == "true" ]]; then echo -e " WebSocket Gateway: ${GREEN}Enabled${RESET} ${DIM}(${CFG_WS_ADDR})${RESET}" else echo -e " WebSocket Gateway: ${YELLOW}Disabled${RESET}" fi if [[ "$CFG_MQTT_ENABLED" == "true" ]]; then echo -e " MQTT Bridge: ${GREEN}Enabled${RESET} ${DIM}(${CFG_MQTT_ADDR})${RESET}" else echo -e " MQTT Bridge: ${YELLOW}Disabled${RESET}" fi echo "" } configure_by_sections() { echo "" echo -e " ${BOLD}Select sections to customize:${RESET}" echo -e " ${CYAN}1${RESET} Deployment ${CYAN}2${RESET} Security ${CYAN}3${RESET} Advanced Features" echo -e " ${CYAN}4${RESET} Observability ${CYAN}5${RESET} Performance ${CYAN}6${RESET} Protocol Bridges" echo "" echo -e " ${DIM}Enter numbers separated by commas, 'all', or 'none' to skip${RESET}" echo "" local sections sections=$(prompt_value "Sections" "none") if [[ "$sections" == "none" ]] || [[ -z "$sections" ]]; then return fi # Parse sections if [[ "$sections" == "all" ]]; then sections="1,2,3,4,5,6" fi IFS=',' read -ra section_array <<< "$sections" for section in "${section_array[@]}"; do section=$(echo "$section" | xargs) # trim whitespace case "$section" in 1) configure_section_deployment ;; 2) configure_section_security ;; 3) configure_section_advanced ;; 4) configure_section_observability ;; 5) configure_section_performance ;; 6) configure_section_bridges ;; *) print_warning "Unknown section: $section" ;; esac done } configure_section_deployment() { print_section "Deployment Configuration" # Allow changing deployment mode echo -e " ${BOLD}Current Mode:${RESET} ${CYAN}${CFG_DEPLOYMENT_MODE}${RESET}" echo "" if prompt_yes_no "Change deployment mode" "n"; then echo "" echo -e " ${BOLD}Deployment Mode${RESET}" echo -e " ${CYAN}1${RESET}) Standalone - Single node ${DIM}(development/testing/small production)${RESET}" echo -e " ${CYAN}2${RESET}) Cluster - Multi-node with Raft consensus ${DIM}(high availability)${RESET}" 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_deployment else CFG_DEPLOYMENT_MODE="standalone" configure_standalone_deployment fi fi # Common settings for both modes echo "" echo -e " ${BOLD}Network Settings${RESET}" CFG_BIND_ADDR=$(prompt_value "Client bind address" "${CFG_BIND_ADDR}") CFG_NODE_ID=$(prompt_value "Node ID" "${CFG_NODE_ID}") # Cluster-specific settings if [[ "$CFG_DEPLOYMENT_MODE" == "cluster" ]]; then echo "" echo -e " ${BOLD}Cluster Settings${RESET}" CFG_CLUSTER_ADDR=$(prompt_value "Cluster bind address" "${CFG_CLUSTER_ADDR}") echo "" if prompt_yes_no "Modify peer nodes" "n"; then discover_cluster_nodes if [[ -n "$CFG_CLUSTER_PEERS" ]]; then validate_peer_connectivity fi fi CFG_REPLICATION_FACTOR=$(prompt_value "Replication factor" "${CFG_REPLICATION_FACTOR}") fi echo "" echo -e " ${BOLD}Storage Settings${RESET}" CFG_DATA_DIR=$(prompt_value "Data directory" "${CFG_DATA_DIR}") } configure_section_bridges() { print_section "Protocol Bridges Configuration" echo -e " ${BOLD}gRPC Server${RESET}" echo -e " ${DIM}High-performance protocol for multi-language clients and streaming.${RESET}" if prompt_yes_no "Enable gRPC server" "y"; then CFG_GRPC_ENABLED="true" CFG_GRPC_ADDR=$(prompt_value "gRPC bind address" "${CFG_GRPC_ADDR}") if [[ "$CFG_TLS_ENABLED" == "true" ]]; then if prompt_yes_no "Use global TLS settings for gRPC" "y"; then CFG_GRPC_TLS_ENABLED="true" CFG_GRPC_TLS_USE_GLOBAL="true" else if prompt_yes_no "Enable separate TLS for gRPC" "n"; then CFG_GRPC_TLS_ENABLED="true" CFG_GRPC_TLS_USE_GLOBAL="false" CFG_GRPC_TLS_CERT=$(prompt_value "gRPC TLS certificate file" "") CFG_GRPC_TLS_KEY=$(prompt_value "gRPC TLS key file" "") else CFG_GRPC_TLS_ENABLED="false" fi fi fi else CFG_GRPC_ENABLED="false" fi echo "" echo -e " ${BOLD}WebSocket Gateway${RESET}" echo -e " ${DIM}Allows web applications to produce/consume messages directly via JSON-RPC.${RESET}" if prompt_yes_no "Enable WebSocket gateway" "y"; then CFG_WS_ENABLED="true" CFG_WS_ADDR=$(prompt_value "WebSocket bind address" "${CFG_WS_ADDR}") if [[ "$CFG_TLS_ENABLED" == "true" ]]; then if prompt_yes_no "Use global TLS settings for WebSocket" "y"; then CFG_WS_TLS_ENABLED="true" CFG_WS_TLS_USE_GLOBAL="true" else if prompt_yes_no "Enable separate TLS (WSS) for WebSocket" "n"; then CFG_WS_TLS_ENABLED="true" CFG_WS_TLS_USE_GLOBAL="false" CFG_WS_TLS_CERT=$(prompt_value "WebSocket TLS certificate file" "") CFG_WS_TLS_KEY=$(prompt_value "WebSocket TLS key file" "") else CFG_WS_TLS_ENABLED="false" fi fi fi else CFG_WS_ENABLED="false" fi echo "" echo -e " ${BOLD}MQTT Bridge${RESET}" echo -e " ${DIM}Allows IoT devices to connect using the MQTT v3.1.1 protocol.${RESET}" if prompt_yes_no "Enable MQTT bridge" "y"; then CFG_MQTT_ENABLED="true" CFG_MQTT_ADDR=$(prompt_value "MQTT bind address" "${CFG_MQTT_ADDR}") if [[ "$CFG_TLS_ENABLED" == "true" ]]; then if prompt_yes_no "Use global TLS settings for MQTT" "y"; then CFG_MQTT_TLS_ENABLED="true" CFG_MQTT_TLS_USE_GLOBAL="true" else if prompt_yes_no "Enable separate TLS for MQTT" "n"; then CFG_MQTT_TLS_ENABLED="true" CFG_MQTT_TLS_USE_GLOBAL="false" CFG_MQTT_TLS_CERT=$(prompt_value "MQTT TLS certificate file" "") CFG_MQTT_TLS_KEY=$(prompt_value "MQTT TLS key file" "") else CFG_MQTT_TLS_ENABLED="false" fi fi fi else CFG_MQTT_ENABLED="false" fi echo "" } configure_section_security() { print_section "Security Configuration" echo -e " ${BOLD}TLS/SSL Encryption (Client Connections)${RESET}" echo -e " ${DIM}Enable TLS for encrypted client-server communication.${RESET}" echo -e " ${DIM}Enabled by default with auto-generated self-signed certificate.${RESET}" echo "" if prompt_yes_no "Enable TLS encryption" "y"; then CFG_TLS_ENABLED="true" echo "" echo -e " ${BOLD}TLS Certificate Options:${RESET}" echo -e " ${CYAN}1${RESET}) Auto-generate self-signed certificate ${DIM}(testing/development)${RESET}" echo -e " ${CYAN}2${RESET}) Provide existing certificate files ${DIM}(production)${RESET}" echo "" local cert_choice cert_choice=$(prompt_choice "Certificate option" "1" "1" "2") if [[ "$cert_choice" == "1" ]]; then # Auto-generate self-signed certificate local cert_dir="${CFG_CONFIG_DIR:-$(get_default_config_dir)}" mkdir -p "$cert_dir" print_info "Generating self-signed TLS certificate..." # Generate certificate openssl req -x509 -newkey rsa:4096 -nodes \ -keyout "$cert_dir/server.key" \ -out "$cert_dir/server.crt" \ -days 365 \ -subj "/CN=localhost/O=FlyMQ/C=US" 2>/dev/null if [[ $? -eq 0 ]]; then CFG_TLS_CERT_FILE="$cert_dir/server.crt" CFG_TLS_KEY_FILE="$cert_dir/server.key" chmod 600 "$cert_dir/server.key" print_success "Generated certificate: ${CYAN}$cert_dir/server.crt${RESET}" print_warning "Self-signed certificate is for testing only. Use a proper CA certificate in production." echo "" echo -e " ${YELLOW}${BOLD}TLS CLI Usage:${RESET}" echo -e " ${DIM}Use ${CYAN}--tls --insecure${RESET} ${DIM}to skip certificate verification (testing only)${RESET}" echo -e " ${DIM}Or use ${CYAN}--tls --ca-cert $cert_dir/server.crt${RESET} ${DIM}to verify with the self-signed cert${RESET}" else print_error "Failed to generate certificate. Please provide existing certificates." CFG_TLS_CERT_FILE=$(prompt_value "TLS certificate file" "") CFG_TLS_KEY_FILE=$(prompt_value "TLS key file" "") fi else # Use existing certificates CFG_TLS_CERT_FILE=$(prompt_value "TLS certificate file" "") CFG_TLS_KEY_FILE=$(prompt_value "TLS key file" "") fi else CFG_TLS_ENABLED="false" fi echo "" echo -e " ${BOLD}Data-at-Rest Encryption${RESET}" echo -e " ${DIM}Encrypt all data stored on disk using AES-256-GCM.${RESET}" echo "" if prompt_yes_no "Enable data-at-rest encryption" "y"; then CFG_ENCRYPTION_ENABLED="true" local custom_key custom_key=$(prompt_value "Encryption key (Enter for auto-generate)" "") if [[ -n "$custom_key" ]]; then CFG_ENCRYPTION_KEY="$custom_key" USER_PROVIDED_ENCRYPTION_KEY=true fi else CFG_ENCRYPTION_ENABLED="false" fi echo "" echo -e " ${BOLD}Authentication${RESET}" echo -e " ${DIM}Enable username/password authentication with RBAC.${RESET}" echo "" if prompt_yes_no "Enable authentication" "y"; then CFG_AUTH_ENABLED="true" CFG_AUTH_ADMIN_USER=$(prompt_value "Admin username" "${CFG_AUTH_ADMIN_USER}") local custom_pass="" echo -e " ${DIM}Leave empty to use auto-generated password${RESET}" # Use || true to prevent exit on empty input (read returns 1 on EOF/empty with -s) read -sp " Admin password: " custom_pass /dev/null; then tput clear 2>/dev/null || true else echo -e "\033[2J\033[H" 2>/dev/null || true fi fi } iterative_configuration_loop() { local config_dir="$1" while true; do show_configuration_summary "$config_dir" echo -e " ${BOLD}Options:${RESET}" echo -e " ${CYAN}1-6${RESET} Modify a section" echo -e " ${CYAN}c${RESET} Confirm and proceed with installation" echo -e " ${CYAN}q${RESET} Cancel installation" echo "" local choice choice=$(prompt_value "Enter choice" "c") case "$choice" in 1) configure_section_deployment ;; 2) configure_section_security ;; 3) configure_section_advanced ;; 4) configure_section_observability ;; 5) configure_section_performance ;; 6) configure_section_bridges ;; c|C|confirm) echo "" if prompt_yes_no "Proceed with installation" "y"; then return 0 fi ;; q|Q|quit|cancel) print_info "Installation cancelled" exit 0 ;; *) print_warning "Invalid choice: $choice" sleep 1 ;; esac done } # ============================================================================= # Main Entry Point # ============================================================================= main() { parse_args "$@" print_banner detect_system if [[ "$UNINSTALL" == true ]]; then uninstall exit 0 fi print_info "Detected: ${CYAN}$OS/$ARCH${RESET}" # Detect source mode early and inform user detect_source_mode if [[ "$NEEDS_REMOTE_CLONE" == true ]]; then print_info "Source: ${YELLOW}Will download from GitHub${RESET}" else print_info "Source: ${GREEN}Using local repository${RESET}" fi echo "" # Set default prefix if not specified if [[ -z "$PREFIX" ]]; then PREFIX=$(get_default_prefix) fi # Set default data dir and config dir CFG_DATA_DIR=$(get_default_data_dir) local config_dir=$(get_default_config_dir) CFG_CONFIG_DIR="$config_dir" # Store for later use CFG_NODE_ID=$(hostname) CFG_SCHEMA_REGISTRY_DIR="${CFG_DATA_DIR}/schemas" # Check for existing installation and show clear status local existing_install=false local config_only=false local binary_only=false if detect_existing_installation; then existing_install=true # Show clear status with visual separation echo -e " ${WHITE}${BOLD}EXISTING INSTALLATION FOUND${RESET}" echo -e " ${DIM}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}" echo -e " ${BOLD}Location:${RESET} ${CYAN}$PREFIX${RESET}" [[ -n "$CFG_CONFIG_DIR" ]] && echo -e " ${BOLD}Configuration:${RESET} ${CYAN}$CFG_CONFIG_DIR${RESET}" [[ -n "$CFG_DATA_DIR" ]] && echo -e " ${BOLD}Data:${RESET} ${CYAN}$CFG_DATA_DIR${RESET}" echo -e " ${DIM}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}" echo "" handle_existing_installation local result=$? if [[ $result -eq 2 ]]; then config_only=true elif [[ "$FORCE_REINSTALL" == "true" ]]; then binary_only=true fi # Update config_dir if it was detected config_dir="${CFG_CONFIG_DIR:-$config_dir}" else # Show clear status for fresh install echo -e " ${WHITE}${BOLD}FRESH INSTALLATION${RESET}" echo -e " ${DIM}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}" echo -e " ${DIM}No existing FlyMQ installation detected${RESET}" echo -e " ${DIM}Will install to: ${CYAN}$PREFIX${RESET}" echo -e " ${DIM}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}" echo "" fi # Show main menu if not in auto-confirm mode and not doing binary-only reinstall or config-only if [[ "$AUTO_CONFIRM" != true ]] && [[ "$config_only" != true ]] && [[ "$binary_only" != true ]]; then if [[ "$existing_install" == true ]]; then echo -e " ${BOLD}Installation Mode:${RESET}" else echo -e " ${BOLD}Select Installation Type:${RESET}" fi echo -e " ${CYAN}1${RESET}) Use default configuration ${DIM}(recommended)${RESET}" echo -e " ${CYAN}2${RESET}) Custom configuration ${DIM}(advanced users)${RESET}" echo -e " ${CYAN}3${RESET}) Cancel installation" echo "" local menu_choice menu_choice=$(prompt_choice "Select option" "1" "1" "2" "3") case "$menu_choice" in 1) print_info "Using default configuration" ;; 2) print_info "Starting custom configuration" ;; 3) print_info "Installation cancelled" exit 0 ;; esac fi # Auto-generate encryption key and admin password for defaults 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) fi if [[ -z "$CFG_AUTH_ADMIN_PASS" ]]; then CFG_AUTH_ADMIN_PASS=$(openssl rand -base64 16 2>/dev/null || head -c 16 /dev/urandom | base64) fi # Preserve existing config if upgrading if [[ "$existing_install" == true ]] && [[ "$UPGRADE" == true ]] || [[ "$FORCE_REINSTALL" == true ]]; then preserve_existing_config fi # Interactive configuration or use defaults if [[ "$AUTO_CONFIRM" != true ]] && [[ "$config_only" != true ]] && [[ "$menu_choice" == "2" ]]; then # STEP 1: Ask deployment mode first (most important decision) select_deployment_mode # STEP 2: Show configuration summary and allow iterative modification iterative_configuration_loop "$config_dir" fi echo "" check_dependencies # Skip build and install if config-only mode if [[ "$config_only" != true ]]; then ensure_source_code build_binaries install_binaries "$PREFIX" else print_step "Skipping binary build (config-only mode)" echo "" fi # Skip config generation for binary-only reinstalls if [[ "$binary_only" != true ]]; then create_data_dir # Auto-generate TLS certificates if TLS is enabled and no cert file specified if [[ "$CFG_TLS_ENABLED" == "true" ]] && [[ -z "$CFG_TLS_CERT_FILE" ]]; then print_step "Generating TLS certificate" echo "" mkdir -p "$config_dir" if openssl req -x509 -newkey rsa:4096 -nodes \ -keyout "$config_dir/server.key" \ -out "$config_dir/server.crt" \ -days 365 \ -subj "/CN=localhost/O=FlyMQ/C=US" 2>/dev/null; then CFG_TLS_CERT_FILE="$config_dir/server.crt" CFG_TLS_KEY_FILE="$config_dir/server.key" chmod 600 "$config_dir/server.key" print_success "Generated self-signed certificate: ${CYAN}$config_dir/server.crt${RESET}" print_info "Use ${CYAN}--tls --insecure${RESET} flag with flymq-cli for testing" else print_error "Failed to generate TLS certificate" CFG_TLS_ENABLED="false" fi echo "" fi generate_config "$config_dir" else print_step "Skipping config generation (binary-only reinstall)" echo "" fi if [[ "$config_only" != true ]]; then install_system_service "$config_dir" "$PREFIX" # Verify installation verify_installation "$PREFIX" fi # Show appropriate completion message based on install type if [[ "$binary_only" == true ]]; then verify_installation "$PREFIX" print_binary_reinstall_complete "$PREFIX" "$config_dir" else print_post_install "$PREFIX" "$config_dir" fi # Note: cleanup_cloned_repo is called automatically via EXIT trap } main "$@"