|
| 1 | +#!/usr/bin/env sh |
| 2 | + |
| 3 | +################################################################################ |
| 4 | +# ACME.sh 3rd party deploy plugin for multiple (same) services |
| 5 | +################################################################################ |
| 6 | +# Authors: tomo2403 (creator), https://github.com/tomo2403 |
| 7 | +# Updated: 2025-03-01 |
| 8 | +# Issues: https://github.com/acmesh-official/acme.sh/issues and mention @tomo2403 |
| 9 | +################################################################################ |
| 10 | +# Usage (shown values are the examples): |
| 11 | +# 1. Set optional environment variables |
| 12 | +# - export MULTIDEPLOY_FILENAME="multideploy.yaml" - "multideploy.yml" will be automatically used if not set" |
| 13 | +# |
| 14 | +# 2. Run command: |
| 15 | +# acme.sh --deploy --deploy-hook multideploy -d example.com |
| 16 | +################################################################################ |
| 17 | +# Dependencies: |
| 18 | +# - yq |
| 19 | +################################################################################ |
| 20 | +# Return value: |
| 21 | +# 0 means success, otherwise error. |
| 22 | +################################################################################ |
| 23 | + |
| 24 | +MULTIDEPLOY_VERSION="1.0" |
| 25 | + |
| 26 | +# Description: This function handles the deployment of certificates to multiple services. |
| 27 | +# It processes the provided certificate files and deploys them according to the |
| 28 | +# configuration specified in the multideploy file. |
| 29 | +# |
| 30 | +# Parameters: |
| 31 | +# _cdomain - The domain name for which the certificate is issued. |
| 32 | +# _ckey - The private key file for the certificate. |
| 33 | +# _ccert - The certificate file. |
| 34 | +# _cca - The CA (Certificate Authority) file. |
| 35 | +# _cfullchain - The full chain certificate file. |
| 36 | +# _cpfx - The PFX (Personal Information Exchange) file. |
| 37 | +multideploy_deploy() { |
| 38 | + _cdomain="$1" |
| 39 | + _ckey="$2" |
| 40 | + _ccert="$3" |
| 41 | + _cca="$4" |
| 42 | + _cfullchain="$5" |
| 43 | + _cpfx="$6" |
| 44 | + |
| 45 | + _debug _cdomain "$_cdomain" |
| 46 | + _debug _ckey "$_ckey" |
| 47 | + _debug _ccert "$_ccert" |
| 48 | + _debug _cca "$_cca" |
| 49 | + _debug _cfullchain "$_cfullchain" |
| 50 | + _debug _cpfx "$_cpfx" |
| 51 | + |
| 52 | + MULTIDEPLOY_FILENAME="${MULTIDEPLOY_FILENAME:-$(_getdeployconf MULTIDEPLOY_FILENAME)}" |
| 53 | + if [ -z "$MULTIDEPLOY_FILENAME" ]; then |
| 54 | + MULTIDEPLOY_FILENAME="multideploy.yml" |
| 55 | + _info "MULTIDEPLOY_FILENAME is not set, so I will use 'multideploy.yml'." |
| 56 | + else |
| 57 | + _savedeployconf "MULTIDEPLOY_FILENAME" "$MULTIDEPLOY_FILENAME" |
| 58 | + _debug2 "MULTIDEPLOY_FILENAME" "$MULTIDEPLOY_FILENAME" |
| 59 | + fi |
| 60 | + |
| 61 | + if ! file=$(_preprocess_deployfile "$MULTIDEPLOY_FILENAME"); then |
| 62 | + _err "Failed to preprocess deploy file." |
| 63 | + return 1 |
| 64 | + fi |
| 65 | + _debug3 "File" "$file" |
| 66 | + |
| 67 | + # Deploy to services |
| 68 | + _deploy_services "$file" |
| 69 | + _exitCode="$?" |
| 70 | + |
| 71 | + return "$_exitCode" |
| 72 | +} |
| 73 | + |
| 74 | +# Description: |
| 75 | +# This function preprocesses the deploy file by checking if 'yq' is installed, |
| 76 | +# verifying the existence of the deploy file, and ensuring only one deploy file is present. |
| 77 | +# Arguments: |
| 78 | +# $@ - Posible deploy file names. |
| 79 | +# Usage: |
| 80 | +# _preprocess_deployfile "<deploy_file1>" "<deploy_file2>?" |
| 81 | +_preprocess_deployfile() { |
| 82 | + # Check if yq is installed |
| 83 | + if ! command -v yq >/dev/null 2>&1; then |
| 84 | + _err "yq is not installed! Please install yq and try again." |
| 85 | + return 1 |
| 86 | + fi |
| 87 | + _debug3 "yq is installed." |
| 88 | + |
| 89 | + # Check if deploy file exists |
| 90 | + for file in "$@"; do |
| 91 | + _debug3 "Checking file" "$DOMAIN_PATH/$file" |
| 92 | + if [ -f "$DOMAIN_PATH/$file" ]; then |
| 93 | + _debug3 "File found" |
| 94 | + if [ -n "$found_file" ]; then |
| 95 | + _err "Multiple deploy files found. Please keep only one deploy file." |
| 96 | + return 1 |
| 97 | + fi |
| 98 | + found_file="$file" |
| 99 | + else |
| 100 | + _debug3 "File not found" |
| 101 | + fi |
| 102 | + done |
| 103 | + |
| 104 | + if [ -z "$found_file" ]; then |
| 105 | + _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." |
| 106 | + return 1 |
| 107 | + fi |
| 108 | + if ! _check_deployfile "$DOMAIN_PATH/$found_file"; then |
| 109 | + _err "Deploy file is not valid: $DOMAIN_PATH/$found_file" |
| 110 | + return 1 |
| 111 | + fi |
| 112 | + |
| 113 | + echo "$DOMAIN_PATH/$found_file" |
| 114 | +} |
| 115 | + |
| 116 | +# Description: |
| 117 | +# This function checks the deploy file for version compatibility and the existence of the specified configuration and services. |
| 118 | +# Arguments: |
| 119 | +# $1 - The path to the deploy configuration file. |
| 120 | +# $2 - The name of the deploy configuration to use. |
| 121 | +# Usage: |
| 122 | +# _check_deployfile "<deploy_file_path>" |
| 123 | +_check_deployfile() { |
| 124 | + _deploy_file="$1" |
| 125 | + _debug2 "check: Deploy file" "$_deploy_file" |
| 126 | + |
| 127 | + # Check version |
| 128 | + _deploy_file_version=$(yq -r '.version' "$_deploy_file") |
| 129 | + if [ "$MULTIDEPLOY_VERSION" != "$_deploy_file_version" ]; then |
| 130 | + _err "As of $PROJECT_NAME $VER, the deploy file needs version $MULTIDEPLOY_VERSION! Your current deploy file is of version $_deploy_file_version." |
| 131 | + return 1 |
| 132 | + fi |
| 133 | + _debug2 "check: Deploy file version is compatible: $_deploy_file_version" |
| 134 | + |
| 135 | + # Extract all services from config |
| 136 | + _services=$(yq -r '.services[].name' "$_deploy_file") |
| 137 | + |
| 138 | + if [ -z "$_services" ]; then |
| 139 | + _err "Config does not have any services to deploy to." |
| 140 | + return 1 |
| 141 | + fi |
| 142 | + _debug2 "check: Config has services." |
| 143 | + echo "$_services" | while read -r _service; do |
| 144 | + _debug3 " - $_service" |
| 145 | + done |
| 146 | + |
| 147 | + # Check if extracted services exist in services list |
| 148 | + echo "$_services" | while read -r _service; do |
| 149 | + _debug2 "check: Checking service: $_service" |
| 150 | + # Check if service exists |
| 151 | + _service_config=$(yq -r ".services[] | select(.name == \"$_service\")" "$_deploy_file") |
| 152 | + if [ -z "$_service_config" ] || [ "$_service_config" = "null" ]; then |
| 153 | + _err "Service '$_service' not found." |
| 154 | + return 1 |
| 155 | + fi |
| 156 | + |
| 157 | + _service_hook=$(echo "$_service_config" | yq -r ".hook" -) |
| 158 | + if [ -z "$_service_hook" ] || [ "$_service_hook" = "null" ]; then |
| 159 | + _err "Service '$_service' does not have a hook." |
| 160 | + return 1 |
| 161 | + fi |
| 162 | + |
| 163 | + _service_environment=$(echo "$_service_config" | yq -r ".environment" -) |
| 164 | + if [ -z "$_service_environment" ] || [ "$_service_environment" = "null" ]; then |
| 165 | + _err "Service '$_service' does not have an environment." |
| 166 | + return 1 |
| 167 | + fi |
| 168 | + done |
| 169 | +} |
| 170 | + |
| 171 | +# Description: This function takes a list of environment variables in YAML format, |
| 172 | +# parses them, and exports each key-value pair as environment variables. |
| 173 | +# Arguments: |
| 174 | +# $1 - A string containing the list of environment variables in YAML format. |
| 175 | +# Usage: |
| 176 | +# _export_envs "$env_list" |
| 177 | +_export_envs() { |
| 178 | + _env_list="$1" |
| 179 | + |
| 180 | + _secure_debug3 "Exporting envs" "$_env_list" |
| 181 | + |
| 182 | + echo "$_env_list" | yq -r 'to_entries | .[] | .key + "=" + .value' | while IFS='=' read -r _key _value; do |
| 183 | + # Using eval to expand nested variables in the configuration file |
| 184 | + _value=$(eval 'echo "'"$_value"'"') |
| 185 | + _savedeployconf "$_key" "$_value" |
| 186 | + _secure_debug3 "Saved $_key" "$_value" |
| 187 | + done |
| 188 | +} |
| 189 | + |
| 190 | +# Description: |
| 191 | +# This function takes a YAML formatted string of environment variables, parses it, |
| 192 | +# and clears each environment variable. It logs the process of clearing each variable. |
| 193 | +# |
| 194 | +# Note: Environment variables for a hook may be optional and differ between |
| 195 | +# services using the same hook. |
| 196 | +# If one service sets optional environment variables and another does not, the |
| 197 | +# variables may persist and affect subsequent deployments. |
| 198 | +# Clearing these variables after each service ensures that only the |
| 199 | +# environment variables explicitly specified for each service in the deploy |
| 200 | +# file are used. |
| 201 | +# Arguments: |
| 202 | +# $1 - A YAML formatted string containing environment variable key-value pairs. |
| 203 | +# Usage: |
| 204 | +# _clear_envs "<yaml_string>" |
| 205 | +_clear_envs() { |
| 206 | + _env_list="$1" |
| 207 | + |
| 208 | + _secure_debug3 "Clearing envs" "$_env_list" |
| 209 | + env_pairs=$(echo "$_env_list" | yq -r 'to_entries | .[] | .key + "=" + .value') |
| 210 | + |
| 211 | + echo "$env_pairs" | while IFS='=' read -r _key _value; do |
| 212 | + _debug3 "Deleting key" "$_key" |
| 213 | + _cleardomainconf "SAVED_$_key" |
| 214 | + unset -v "$_key" |
| 215 | + done |
| 216 | +} |
| 217 | + |
| 218 | +# Description: |
| 219 | +# This function deploys services listed in the deploy configuration file. |
| 220 | +# Arguments: |
| 221 | +# $1 - The path to the deploy configuration file. |
| 222 | +# $2 - The list of services to deploy. |
| 223 | +# Usage: |
| 224 | +# _deploy_services "<deploy_file_path>" "<services_list>" |
| 225 | +_deploy_services() { |
| 226 | + _deploy_file="$1" |
| 227 | + _debug3 "Deploy file" "$_deploy_file" |
| 228 | + |
| 229 | + _tempfile=$(mktemp) |
| 230 | + trap 'rm -f $_tempfile' EXIT |
| 231 | + |
| 232 | + yq -r '.services[].name' "$_deploy_file" >"$_tempfile" |
| 233 | + _debug3 "Services" "$(cat "$_tempfile")" |
| 234 | + |
| 235 | + _failedServices="" |
| 236 | + _failedCount=0 |
| 237 | + while read -r _service <&3; do |
| 238 | + _debug2 "Service" "$_service" |
| 239 | + _hook=$(yq -r ".services[] | select(.name == \"$_service\").hook" "$_deploy_file") |
| 240 | + _envs=$(yq -r ".services[] | select(.name == \"$_service\").environment" "$_deploy_file") |
| 241 | + |
| 242 | + _export_envs "$_envs" |
| 243 | + if ! _deploy_service "$_service" "$_hook"; then |
| 244 | + _failedServices="$_service, $_failedServices" |
| 245 | + _failedCount=$((_failedCount + 1)) |
| 246 | + fi |
| 247 | + _clear_envs "$_envs" |
| 248 | + done 3<"$_tempfile" |
| 249 | + |
| 250 | + _debug3 "Failed services" "$_failedServices" |
| 251 | + _debug2 "Failed count" "$_failedCount" |
| 252 | + if [ -n "$_failedServices" ]; then |
| 253 | + _info "$(__red "Deployment failed") for services: $_failedServices" |
| 254 | + else |
| 255 | + _debug "All services deployed successfully." |
| 256 | + fi |
| 257 | + |
| 258 | + return "$_failedCount" |
| 259 | +} |
| 260 | + |
| 261 | +# Description: Deploys a service using the specified hook. |
| 262 | +# Arguments: |
| 263 | +# $1 - The name of the service to deploy. |
| 264 | +# $2 - The hook to use for deployment. |
| 265 | +# Usage: |
| 266 | +# _deploy_service <service_name> <hook> |
| 267 | +_deploy_service() { |
| 268 | + _name="$1" |
| 269 | + _hook="$2" |
| 270 | + |
| 271 | + _debug2 "SERVICE" "$_name" |
| 272 | + _debug2 "HOOK" "$_hook" |
| 273 | + |
| 274 | + _info "$(__green "Deploying") to '$_name' using '$_hook'" |
| 275 | + _deploy "$_cdomain" "$_hook" |
| 276 | +} |
0 commit comments