diff --git a/deployment/deploy.sh b/deployment/deploy.sh new file mode 100644 index 0000000..108067d --- /dev/null +++ b/deployment/deploy.sh @@ -0,0 +1,514 @@ +#!/bin/bash +# +# SCRIPT: envoy-deploy.sh +# DESCRIPTION: Sets up .env, docker-compose.yaml, and config files for +# an Envoy Control Plane (xDS) and Envoy Proxy deployment. +# The Control Plane config files (CDS/LDS) are placed under +# ${ENVOY_DATA_PATH}/data/config. +# Designed to be run repeatedly with resource cleanup. +# +# USAGE: bash envoy-deploy.sh + +# --- Configuration Defaults --- +# Path to the directory where all configuration files and persistent data will be stored +DEFAULT_DATA_PATH="." +# Ports +DEFAULT_DASHBOARD_PORT="8090" # For the Control Plane web dashboard +DEFAULT_HTTP_PORT="10000" # Envoy HTTP Listener +DEFAULT_HTTPS_PORT="10001" # Envoy HTTPS Listener +DEFAULT_ADMIN_PORT="11111" # Envoy Admin Interface +# Envoy Node ID +DEFAULT_NODE_ID="home" + +# --- Global Variables for File Paths (Set in generate_env_file) --- +# DEPLOY_BASE_PATH: The chosen path for config/data storage, derived from ENVOY_DATA_PATH +DEPLOY_BASE_PATH="" +# ENV_FILE_PATH: Full path to the .env file +ENV_FILE_PATH="" +# HOST_PATH_PREFIX: Retained for the check_path_type logic, but volumes now rely on absolute ENVOY_DATA_PATH. +HOST_PATH_PREFIX="" + + +# --- Functions --- + +# Function to handle Docker login and repository access check +check_docker_access() { + echo "" + echo "################################################" + echo "## πŸ”’ Checking Docker Repository Access" + echo "################################################" + echo "You need to log in to the private Docker registry: docker.jerxie.com" + + # Run docker login + docker login docker.jerxie.com + + # Check the exit status of the docker login command + if [ $? -ne 0 ]; then + echo "" + echo "## ⚠️ DOCKER LOGIN FAILED" + echo "It appears the login to docker.jerxie.com was unsuccessful." + echo "If you do not have credentials to access the repo 'docker.jerxie.com/xds-server:latest'," + echo "please contact the author **axieyangb@gmail.com** for assistance." + echo "Exiting script." + exit 1 + fi + echo " -> Docker login successful." +} + + +# Function to prompt the user for an environment variable +prompt_for_env_var() { + local var_name=$1 + local default_value=$2 + local prompt_msg=$3 + read -rp "Enter ${prompt_msg} (Default: ${default_value}): " user_input + # Use the input or the default value + eval "${var_name}='${user_input:-${default_value}}'" +} + +# Function to prompt the user for LETSENCRYPT_STAGING +prompt_for_letsencrypt_staging() { + local var_name=$1 + local prompt_msg=$2 + + # Loop until a valid choice is made + while true; do + read -rp "${prompt_msg} (y/N): " user_choice + + # FIX: Use 'tr' for cross-shell compatibility to convert to lowercase + user_choice=$(echo "${user_choice}" | tr '[:upper:]' '[:lower:]') + + if [[ "${user_choice}" == "y" || "${user_choice}" == "yes" ]]; then + eval "${var_name}='true'" + break + elif [[ "${user_choice}" == "n" || "${user_choice}" == "no" || -z "${user_choice}" ]]; then + # An empty input defaults to "N" (no) + eval "${var_name}='false'" + break + else + echo "Invalid input. Please enter 'y' for Yes or 'n' for No." + fi + done +} + +# Function to clean up previous deployment resources +cleanup_resources() { + echo "" + echo "## 🧹 Cleaning up previous Docker resources in '${DEPLOY_BASE_PATH}'..." + + # NOTE: Check if DEPLOY_BASE_PATH/docker-compose.yaml exists, not just "docker-compose.yaml" + local compose_file="${DEPLOY_BASE_PATH}/docker-compose.yaml" + + # If $ENVOY_DATA_PATH was not set, DEPLOY_BASE_PATH may not be set yet on first run + if [ -z "${DEPLOY_BASE_PATH}" ]; then + # Use the default path for cleanup if DEPLOY_BASE_PATH is not yet set + compose_file="${DEFAULT_DATA_PATH}/docker-compose.yaml" + fi + + if [ -f "${compose_file}" ]; then + # Load the .env variables for cleanup since they define volume paths + export $(grep -v '^#' "${DEPLOY_BASE_PATH}/.env" 2>/dev/null | xargs) + + docker compose -f "${compose_file}" down -v --remove-orphans 2>/dev/null + echo " -> Previous containers and networks stopped and removed." + else + echo " -> No previous docker-compose.yaml found, skipping cleanup." + fi # <--- This was the missing 'fi' +} + +# Function to detect if ENVOY_DATA_PATH is absolute or relative +# This is mainly for informational output now that we enforce absolute paths. +check_path_type() { + # Check if the path starts with a / (Unix absolute path) + if [[ "${ENVOY_DATA_PATH}" == /* ]]; then + HOST_PATH_PREFIX="" + echo " -> Path '${ENVOY_DATA_PATH}' detected as **Absolute**." + # Check if the path starts with ~ (home directory) - treat as absolute for Docker bind mounts + elif [[ "${ENVOY_DATA_PATH}" == \~* ]]; then + HOST_PATH_PREFIX="" + echo " -> Path '${ENVOY_DATA_PATH}' detected as **Absolute** (using ~)." + else + HOST_PATH_PREFIX="./" + echo " -> Path '${ENVOY_DATA_PATH}' detected as **Relative**." + fi +} + +# Function to generate the .env file +generate_env_file() { + echo "" + echo "## βš™οΈ Gathering Configuration for .env file" + echo "---" + + prompt_for_env_var ENVOY_DATA_PATH "$DEFAULT_DATA_PATH" "Data Path (e.g., '.')" + prompt_for_env_var CONTROL_PLANE_DASHBOARD_PORT "$DEFAULT_DASHBOARD_PORT" "Control Plane Dashboard Port" + prompt_for_env_var ENVOY_HTTP_PORT "$DEFAULT_HTTP_PORT" "Envoy HTTP Port" + prompt_for_env_var ENVOY_HTTPS_PORT "$DEFAULT_HTTPS_PORT" "Envoy HTTPS Port" + prompt_for_env_var ENVOY_ADMIN_PORT "$DEFAULT_ADMIN_PORT" "Envoy Admin Port" + prompt_for_env_var ENVOY_NODE_ID "$DEFAULT_NODE_ID" "Envoy Node ID" + + # NEW PROMPT for Let's Encrypt Staging + prompt_for_letsencrypt_staging LETSENCRYPT_STAGING "Use Let's Encrypt **Staging** Environment? (Recommended for testing)" + + # --- FIX: Ensure ENVOY_DATA_PATH is always an absolute path --- + echo "" + echo "## πŸ—ΊοΈ Resolving Data Path to Absolute Path" + + # Change to the user-specified directory, resolve symbolic links, and get the absolute path + if pushd "${ENVOY_DATA_PATH}" > /dev/null; then + ENVOY_DATA_PATH=$(pwd -P) + popd > /dev/null # Go back to the original directory + echo " -> Resolved to: **${ENVOY_DATA_PATH}**" + else + echo " -> ERROR: Could not resolve path '${ENVOY_DATA_PATH}'. Exiting." + exit 1 + fi + + # Determine path type (for info/legacy prefix logic) + check_path_type + + # Set the deployment base path now that ENVOY_DATA_PATH is defined + # This ensures all config files are created in the chosen directory (which is now absolute) + DEPLOY_BASE_PATH="${ENVOY_DATA_PATH}" + ENV_FILE_PATH="${DEPLOY_BASE_PATH}/.env" + + # Create the base directory if it doesn't exist + mkdir -p "${DEPLOY_BASE_PATH}" + + echo "" + echo "## πŸ“ Generating .env file..." + + cat << EOF > "${ENV_FILE_PATH}" +# .env file content +# This sets the default base path to the directory containing this file. +# NOTE: This path is now guaranteed to be absolute. +ENVOY_DATA_PATH=${ENVOY_DATA_PATH} + +# External Ports +CONTROL_PLANE_DASHBOARD_PORT=${CONTROL_PLANE_DASHBOARD_PORT} +ENVOY_HTTP_PORT=${ENVOY_HTTP_PORT} +ENVOY_HTTPS_PORT=${ENVOY_HTTPS_PORT} +ENVOY_ADMIN_PORT=${ENVOY_ADMIN_PORT} + +# Node ID parameter +ENVOY_NODE_ID=${ENVOY_NODE_ID} + +# Let's Encrypt Staging Flag +# Set to 'true' to use the staging environment for testing (avoids rate limits). +LETSENCRYPT_STAGING=${LETSENCRYPT_STAGING} +EOF + + echo " -> .env file created at '${ENV_FILE_PATH}' with your configuration." + echo "" + echo "## πŸ“„ Contents of the generated .env file:" + cat "${ENV_FILE_PATH}" + echo "---" +} + +# Function to create the directory structure +create_config_dirs() { + # The control plane needs ${ENVOY_DATA_PATH}/data/config for CDS/LDS + # The proxy needs ${ENVOY_DATA_PATH}/data/envoy_config for envoy.yaml + + # Define the target directory for Control Plane configs + local cp_config_dir="${ENVOY_DATA_PATH}/data/config" + # Define the target directory for Proxy configs + local proxy_config_dir="${ENVOY_DATA_PATH}/data/envoy_config" + + echo "" + echo "## πŸ“ Creating data and config directory structure..." + mkdir -p "${cp_config_dir}" + mkdir -p "${proxy_config_dir}" + echo " -> Directory '${cp_config_dir}' (for Control Plane) created/ensured." + echo " -> Directory '${proxy_config_dir}' (for Envoy Proxy) created/ensured." + + # FIX: Ensure the mounted directories have the correct permissions for the container user + # This is vital for Docker bind mounts where the container runs as a non-root user. + chmod -R 777 "${ENVOY_DATA_PATH}/data" + echo " -> Permissions set to 777 recursively on '${ENVOY_DATA_PATH}/data'." + + # Set the variable for file generation functions + CONTROL_PLANE_CONFIG_PATH="${cp_config_dir}" +} + +# Function to generate the docker-compose.yaml file +generate_docker_compose() { + echo "" + + local compose_file="${DEPLOY_BASE_PATH}/docker-compose.yaml" + echo "## 🐳 Generating ${compose_file}..." + + # NOTE: We use 'EOF' to allow environment variable substitution for the variables + # passed to docker-compose (like ${ENVOY_DATA_PATH}), and we rely on ENVOY_DATA_PATH + # being an absolute path now. + cat << EOF > "${compose_file}" +version: "3.9" + +services: + # 1. The Envoy Control Plane + envoy-control-plane: + image: docker.jerxie.com/xds-server:latest + container_name: envoy-control-plane + restart: unless-stopped + ports: + # Exposes the gRPC XDS service port (18000) for the Envoy proxy to connect to + - "${CONTROL_PLANE_DASHBOARD_PORT}:8080" + volumes: + # Maps the host's ${ENVOY_DATA_PATH}/data (now absolute) to the container's /app/data + - ${ENVOY_DATA_PATH}/data:/app/data:rw + command: ["--node-id", "${ENVOY_NODE_ID}", "--config-dir", "/app/data/config","--db","file:/app/data/data.db?_foreign_keys=on", "--enable-cert-issuance", "webroot-path=/app/data/acme"] + environment: + # Pass the staging flag to the control plane (it will handle using it) + - LETSENCRYPT_STAGING=${LETSENCRYPT_STAGING} + # Add a network to ensure both services can communicate + networks: + - envoy_network + + + # 2. The Envoy Proxy + envoy-proxy: + # Use the official Envoy Docker image + image: envoyproxy/envoy:v1.33.12 # Use a specific, stable version + container_name: envoy-proxy + restart: unless-stopped + # Expose a port where the proxy will listen for client traffic (e.g., 11111 for admin, 10000,10001 for listener) + ports: + - "${ENVOY_HTTP_PORT}:10000" + - "${ENVOY_HTTPS_PORT}:10001" + - "${ENVOY_ADMIN_PORT}:11111" + volumes: + # Maps the host's ${ENVOY_DATA_PATH}/data/envoy_config (now absolute) to the container's /etc/config + - ${ENVOY_DATA_PATH}/data/envoy_config:/etc/config:rw + # The starting command you provided + command: + - "envoy" + - "-c" + - "/etc/config/envoy.yaml" + # Ensure this service waits for the control plane to be up + depends_on: + - envoy-control-plane + # Connect to the same network as the control plane + networks: + - envoy_network + +# Define a custom network for inter-service communication +networks: + envoy_network: + driver: bridge + +EOF + echo " -> ${compose_file} created." +} + +# Function to generate config/cds.yaml +generate_cds_yaml() { + echo "" + echo "## βš™οΈ Generating ${CONTROL_PLANE_CONFIG_PATH}/cds.yaml..." + + # The CONTROL_PLANE_CONFIG_PATH variable is set in create_config_dirs + cat << 'EOF' > "${CONTROL_PLANE_CONFIG_PATH}/cds.yaml" +resources: +- "@type": type.googleapis.com/envoy.config.cluster.v3.Cluster + name: _acme_renewer + connect_timeout: 0.2s + type: STRICT_DNS + lb_policy: ROUND_ROBIN + load_assignment: + cluster_name: acme_renewer + endpoints: + - lb_endpoints: + - endpoint: + health_check_config: + port_value: 8080 + address: + socket_address: + # Targets the control plane service on the internal Docker network + address: envoy-control-plane + port_value: 8080 +EOF + + echo " -> ${CONTROL_PLANE_CONFIG_PATH}/cds.yaml created." +} + +# Function to generate config/lds.yaml +generate_lds_yaml() { + echo "" + echo "## βš™οΈ Generating ${CONTROL_PLANE_CONFIG_PATH}/lds.yaml..." + + # The CONTROL_PLANE_CONFIG_PATH variable is set in create_config_dirs + cat << 'EOF' > "${CONTROL_PLANE_CONFIG_PATH}/lds.yaml" +resources: +- "@type": type.googleapis.com/envoy.config.listener.v3.Listener + name: http_listener + address: + socket_address: { address: 0.0.0.0, port_value: 10000 } + filter_chains: + - filters: + - name: envoy.filters.network.http_connection_manager + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + stat_prefix: ingress_http + codec_type: AUTO + route_config: + name: ingress_generic_insecure + virtual_hosts: + - name: http_to_https + domains: ["*"] + routes: + # ACME Challenge handling for certificate renewal + - match: { prefix : "/.well-known/acme-challenge"} + route: { cluster: _acme_renewer } + # Redirect all other traffic to HTTPS + - match: { prefix: "/" } + redirect: { https_redirect: true } + http_filters: + - name: envoy.filters.http.router + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + +## --- EMPTY HTTPS LISTENER --- +#- "@type": type.googleapis.com/envoy.config.listener.v3.Listener +# name: https_listener +# address: +# socket_address: { address: 0.0.0.0, port_value: 10001 } +# listener_filters: +# # This filter is necessary to inspect the connection for TLS/SNI +# # so you can later match specific filter chains based on hostname. +# - name: "envoy.filters.listener.tls_inspector" +# typed_config: +# "@type": type.googleapis.com/envoy.extensions.filters.listener.tls_inspector.v3.TlsInspector +# filter_chains: +# # FIX: Add a dummy/placeholder filter chain to satisfy Envoy's requirement +# - filter_chain_match: {} +# filters: +# - name: envoy.filters.network.http_connection_manager +# typed_config: +# "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager +# stat_prefix: placeholder_https +# http_filters: +# - name: envoy.filters.http.router +# typed_config: +# "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router +# # NOTE: route_config is omitted/empty, so no traffic will be routed by default +# route_config: +# name: placeholder_route +# virtual_hosts: [] +EOF + + echo " -> ${CONTROL_PLANE_CONFIG_PATH}/lds.yaml created." +} + +# Function to generate config/envoy.yaml (Envoy Bootstrap Config) +generate_envoy_yaml() { + # The proxy config directory is derived from ENVOY_DATA_PATH in create_config_dirs + local proxy_config_dir="${ENVOY_DATA_PATH}/data/envoy_config" + local config_file="${proxy_config_dir}/envoy.yaml" + + echo "" + echo "## βš™οΈ Generating ${config_file} (Envoy Bootstrap)..." + + # Note: Using 'EOF' ensures shell variables like ${ENVOY_NODE_ID} are substituted + cat << EOF > "${config_file}" +node: + id: ${ENVOY_NODE_ID} + cluster: home-cluster + +dynamic_resources: + ads_config: + api_type: GRPC + transport_api_version: V3 + grpc_services: + - envoy_grpc: + cluster_name: xds_cluster + + cds_config: + ads: {} + + lds_config: + ads: {} + +admin: + access_log_path: /dev/null + address: + socket_address: + address: 0.0.0.0 + port_value: ${ENVOY_ADMIN_PORT} + +static_resources: + clusters: + # xDS management server (used for CDS, LDS, and SDS) + - name: xds_cluster + connect_timeout: 1s + type: STRICT_DNS + lb_policy: ROUND_ROBIN + typed_extension_protocol_options: + envoy.extensions.upstreams.http.v3.HttpProtocolOptions: + "@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions + explicit_http_config: + http2_protocol_options: {} + load_assignment: + cluster_name: xds_cluster + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: envoy-control-plane + port_value: 18000 +EOF + + echo " -> ${config_file} created." +} + +# Function to deploy the services +deploy_services() { + echo "" + echo "################################################" + echo "## πŸš€ Starting Docker Deployment..." + echo "################################################" + + local compose_file="${DEPLOY_BASE_PATH}/docker-compose.yaml" + + # Use -d for detached mode, --build to ensure latest images are used + # We explicitly pass the .env file path using the --env-file option + docker compose -f "${compose_file}" --env-file "${DEPLOY_BASE_PATH}/.env" up -d + + # Display the final status + echo "" + echo "Deployment Status:" + docker compose -f "${compose_file}" --env-file "${DEPLOY_BASE_PATH}/.env" ps + + echo "" + echo "βœ… **Deployment Complete!**" + echo "You can access the Control Plane Dashboard at: http://localhost:${CONTROL_PLANE_DASHBOARD_PORT}" + echo "You can access the Envoy Admin Interface at: http://localhost:${ENVOY_ADMIN_PORT}/stats" +} + +# --- Main Script Execution --- + +# 1. Check Docker Access before doing anything else +check_docker_access + +# 2. Get user input and generate .env file, and establish DEPLOY_BASE_PATH +# This step now ensures ENVOY_DATA_PATH is an absolute path. +generate_env_file + +# Load the new environment variables for the current script session +# DEPLOY_BASE_PATH is now set, and contains the .env file +if [ -f "${DEPLOY_BASE_PATH}/.env" ]; then + export $(grep -v '^#' "${DEPLOY_BASE_PATH}/.env" | xargs) +fi + +# 3. Clean up old resources (essential for robust repeat running) +cleanup_resources + +# 4. Create necessary folders (and set permissions) +create_config_dirs + +# 5. Generate other configuration files +generate_docker_compose +generate_cds_yaml +generate_lds_yaml +generate_envoy_yaml + +# 6. Deploy the services +deploy_services \ No newline at end of file