#!/usr/bin/env sh ################################################################################ # ACME.sh 3rd party deploy plugin for multiple (same) services ################################################################################ # Authors: tomo2403 (creator), https://github.com/tomo2403 # Updated: 2025-03-01 # Issues: https://github.com/acmesh-official/acme.sh/issues and mention @tomo2403 ################################################################################ # Usage (shown values are the examples): # 1. Set optional environment variables # - export MULTIDEPLOY_CONFIG="default" - "default" will be automatically used if not set" # # 2. Run command: # acme.sh --deploy --deploy-hook multideploy -d example.com ################################################################################ # Dependencies: # - yq ################################################################################ # Return value: # 0 means success, otherwise error. ################################################################################ MULTIDEPLOY_VERSION="1.0" MULTIDEPLOY_FILENAME="multideploy.yml" MULTIDEPLOY_FILENAME2="multideploy.yaml" # Description: This function handles the deployment of certificates to multiple services. # It processes the provided certificate files and deploys them according to the # configuration specified in the MULTIDEPLOY_CONFIG. # # Parameters: # _cdomain - The domain name for which the certificate is issued. # _ckey - The private key file for the certificate. # _ccert - The certificate file. # _cca - The CA (Certificate Authority) file. # _cfullchain - The full chain certificate file. # _cpfx - The PFX (Personal Information Exchange) file. multideploy_deploy() { _cdomain="$1" _ckey="$2" _ccert="$3" _cca="$4" _cfullchain="$5" _cpfx="$6" _debug _cdomain "$_cdomain" _debug _ckey "$_ckey" _debug _ccert "$_ccert" _debug _cca "$_cca" _debug _cfullchain "$_cfullchain" _debug _cpfx "$_cpfx" DOMAIN_DIR=$_cdomain if echo "$DOMAIN_PATH" | grep -q "$ECC_SUFFIX"; then DOMAIN_DIR="$DOMAIN_DIR"_ecc fi _debug2 "DOMAIN_DIR" "$DOMAIN_DIR" MULTIDEPLOY_CONFIG="${MULTIDEPLOY_CONFIG:-$(_getdeployconf MULTIDEPLOY_CONFIG)}" if [ -z "$MULTIDEPLOY_CONFIG" ]; then MULTIDEPLOY_CONFIG="default" _info "MULTIDEPLOY_CONFIG is not set, so I will use 'default'." else _savedeployconf "MULTIDEPLOY_CONFIG" "$MULTIDEPLOY_CONFIG" _debug2 "MULTIDEPLOY_CONFIG" "$MULTIDEPLOY_CONFIG" fi OLDIFS=$IFS if ! file=$(_preprocess_deployfile "$MULTIDEPLOY_FILENAME" "$MULTIDEPLOY_FILENAME2"); then _err "Failed to preprocess deploy file." return 1 fi _debug3 "File" "$file" # Deploy to services _services=$(_get_services_list "$file" "$MULTIDEPLOY_CONFIG") _deploy_services "$file" "$_services" # Save deployhook for renewals _debug2 "Setting Le_DeployHook" _savedomainconf "Le_DeployHook" "multideploy" return 0 } # Description: # This function preprocesses the deploy file by checking if 'yq' is installed, # verifying the existence of the deploy file, and ensuring only one deploy file is present. # Arguments: # $@ - Posible deploy file names. # Usage: # _preprocess_deployfile "" "" _preprocess_deployfile() { # Check if yq is installed if ! command -v yq >/dev/null 2>&1; then _err "yq is not installed! Please install yq and try again." return 1 fi _debug3 "yq is installed." # Check if deploy file exists IFS=$(printf '\n') for file in "$@"; do _debug3 "Checking file" "$DOMAIN_PATH/$file" if [ -f "$DOMAIN_PATH/$file" ]; then _debug3 "File found" if [ -n "$found_file" ]; then _err "Multiple deploy files found. Please keep only one deploy file." return 1 fi found_file="$file" else _debug3 "File not found" fi done IFS=$OLDIFS if [ -n "$found_file" ]; then _check_deployfile "$DOMAIN_PATH/$found_file" "$MULTIDEPLOY_CONFIG" else _err "Deploy file not found. Go to https://github.com/acmesh-official/acme.sh/wiki/deployhooks#36-deploying-to-multiple-services-with-the-same-hooks to see how to create one." return 1 fi echo "$DOMAIN_PATH/$found_file" } # Description: # This function checks the deploy file for version compatibility and the existence of the specified configuration and services. # Arguments: # $1 - The path to the deploy configuration file. # $2 - The name of the deploy configuration to use. # Usage: # _check_deployfile "" "" _check_deployfile() { _deploy_file="$1" _deploy_config="$2" _debug2 "Deploy file" "$_deploy_file" _debug2 "Deploy config" "$_deploy_config" # Check version _deploy_file_version=$(yq '.version' "$_deploy_file") if [ "$MULTIDEPLOY_VERSION" != "$_deploy_file_version" ]; then _err "As of $PROJECT_NAME $VER, the deploy file needs version $MULTIDEPLOY_VERSION! Your current deploy file is of version $_deploy_file_version." return 1 fi _debug2 "Deploy file version is compatible: $_deploy_file_version" # Check if config exists if ! yq e ".configs[] | select(.name == \"$_deploy_config\")" "$_deploy_file" >/dev/null; then _err "Config '$_deploy_config' not found." return 1 fi _debug2 "Config found: $_deploy_config" # Extract all services from config _services=$(_get_services_list "$_deploy_file" "$_deploy_config") _debug2 "Services" "$_services" if [ -z "$_services" ]; then _err "Config '$_deploy_config' does not have any services to deploy to." return 1 fi _debug2 "Config has services." IFS=$(printf '\n') # Check if extracted services exist in services list for _service in $_services; do _debug2 "Checking service" "$_service" # Check if service exists if ! yq e ".services[] | select(.name == \"$_service\")" "$_deploy_file" >/dev/null; then _err "Service '$_service' not found." return 1 fi # Check if service has hook if ! yq e ".services[] | select(.name == \"$_service\").hook" "$_deploy_file" >/dev/null; then _err "Service '$_service' does not have a hook." return 1 fi # Check if service has environment if ! yq e ".services[] | select(.name == \"$_service\").environment" "$_deploy_file" >/dev/null; then _err "Service '$_service' does not have an environment." return 1 fi done IFS=$OLDIFS } # Description: # This function retrieves a list of services from the deploy configuration file. # Arguments: # $1 - The path to the deploy configuration file. # $2 - The name of the deploy configuration to use. # Usage: # _get_services_list "" "" _get_services_list() { _deploy_file="$1" _deploy_config="$2" _debug2 "Getting services list" _debug3 "Deploy file" "$_deploy_file" _debug3 "Deploy config" "$_deploy_config" _services=$(yq e ".configs[] | select(.name == \"$_deploy_config\").services[]" "$_deploy_file") echo "$_services" } # Description: This function takes a list of environment variables in YAML format, # parses them, and exports each key-value pair as environment variables. # Arguments: # $1 - A string containing the list of environment variables in YAML format. # Usage: # _export_envs "$env_list" _export_envs() { _env_list="$1" _secure_debug3 "Exporting envs" "$_env_list" IFS=$(printf '\n') echo "$_env_list" | yq e -r 'to_entries | .[] | .key + "=" + .value' | while IFS='=' read -r _key _value; do _value=$(eval echo "$_value") _savedomainconf "$_key" "$_value" _secure_debug3 "Saved $_key" "$_value" done IFS=$OLDIFS } # Description: # This function takes a YAML formatted string of environment variables, parses it, # and clears each environment variable. It logs the process of clearing each variable. # Arguments: # $1 - A YAML formatted string containing environment variable key-value pairs. # Usage: # _clear_envs "" _clear_envs() { _env_list="$1" _secure_debug3 "Clearing envs" "$_env_list" env_pairs=$(echo "$_env_list" | yq e -r 'to_entries | .[] | .key + "=" + .value') IFS=$(printf '\n') echo "$env_pairs" | while IFS='=' read -r _key _value; do _debug3 "Deleting key" "$_key" _cleardomainconf "SAVED_$_key" unset -v "$_key" done IFS="$OLDIFS" } # Description: # This function deploys services listed in the deploy configuration file. # Arguments: # $1 - The path to the deploy configuration file. # $2 - The list of services to deploy. # Usage: # _deploy_services "" "" _deploy_services() { _deploy_file="$1" shift _services="$*" _debug3 "Deploy file" "$_deploy_file" _debug3 "Services" "$_services" printf '%s\n' "$_services" | while IFS= read -r _service; do _debug2 "Service" "$_service" _hook=$(yq e ".services[] | select(.name == \"$_service\").hook" "$_deploy_file") _envs=$(yq e ".services[] | select(.name == \"$_service\").environment[]" "$_deploy_file") _export_envs "$_envs" _deploy_service "$_service" "$_hook" _clear_envs "$_envs" done } # Description: Deploys a service using the specified hook. # Arguments: # $1 - The name of the service to deploy. # $2 - The hook to use for deployment. # Usage: # _deploy_service _deploy_service() { _name="$1" _hook="$2" _debug2 "SERVICE" "$_name" _debug2 "HOOK" "$_hook" _info "$(__green "Deploying") to '$_name' using '$_hook'" if echo "$DOMAIN_PATH" | grep -q "$ECC_SUFFIX"; then _debug2 "User wants to use ECC." deploy "$_cdomain" "$_hook" "isEcc" else deploy "$_cdomain" "$_hook" fi }