diff --git a/Dockerfile b/Dockerfile index 3f400283..422001c4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,6 +13,7 @@ RUN apk --no-cache add -f \ tar \ libidn \ jq \ + yq \ cronie ENV LE_CONFIG_HOME=/acme.sh diff --git a/deploy/multideploy.sh b/deploy/multideploy.sh new file mode 100644 index 00000000..b002f068 --- /dev/null +++ b/deploy/multideploy.sh @@ -0,0 +1,300 @@ +#!/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 +}