Skip to content

Commit 260df00

Browse files
authored
Merge pull request #6241 from tomo2403/multideploy-yaml
Multideploy: Deploy to multiple hooks of the same type
2 parents cbd5dae + 11cae37 commit 260df00

2 files changed

Lines changed: 277 additions & 0 deletions

File tree

Dockerfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ RUN apk --no-cache add -f \
1313
tar \
1414
libidn \
1515
jq \
16+
yq-go \
1617
cronie
1718

1819
ENV LE_WORKING_DIR=/acmebin

deploy/multideploy.sh

Lines changed: 276 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,276 @@
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

Comments
 (0)