#!/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