1
0

multideploy.sh 9.2 KB

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