You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
382 lines
12 KiB
382 lines
12 KiB
# -*- mode: shell-script -*-
|
|
|
|
yaml_opt_bash_env() {
|
|
local prefix="$1" key value
|
|
while read-0 key value; do
|
|
new_prefix="${prefix}_${key^^}"
|
|
if [[ "$(echo "$value" | shyaml get-type)" == "struct" ]]; then
|
|
echo "$value" | yaml_opt_bash_env "${new_prefix}"
|
|
else
|
|
printf "%s\0%s\0" "${new_prefix/-/_}" "$value"
|
|
fi
|
|
done < <(shyaml key-values-0)
|
|
}
|
|
|
|
|
|
yaml_opt_bash_env_ignore_first_level() {
|
|
local prefix="$1" key value
|
|
while read-0 key value; do
|
|
new_prefix="${prefix}_${key^^}"
|
|
if [[ "$(echo "$value" | shyaml get-type)" == "struct" ]]; then
|
|
echo "$value" | yaml_opt_bash_env "${new_prefix}"
|
|
fi
|
|
done < <(shyaml key-values-0)
|
|
}
|
|
|
|
|
|
get_dc_env() {
|
|
local cfg="$1" action="$2" domain="$3"
|
|
|
|
config="\
|
|
$SERVICE_NAME:
|
|
docker-compose:
|
|
environment:"
|
|
if USER_EMAIL=$(echo "$cfg" | shyaml get-value email 2>/dev/null); then
|
|
config+=$'\n'" LETSENCRYPT_USER_MAIL: $USER_EMAIL"
|
|
fi
|
|
|
|
if environment_def="$(printf "%s" "$cfg" | shyaml -y get-value env 2>/dev/null)"; then
|
|
while read-0 key value; do
|
|
config+="$(printf "\n %s: %s" "$key" "$value")"
|
|
done < <(e "$environment_def" | yaml_opt_bash_env_ignore_first_level LEXICON)
|
|
|
|
if ! provider=$(e "$environment_def" | shyaml get-value provider 2>/dev/null); then
|
|
provider=
|
|
## If no provider is given, we fallback on the first found
|
|
while read-0 key value; do
|
|
[[ "$(echo "$value" | shyaml get-type)" == "struct" ]] && {
|
|
provider="$key"
|
|
break
|
|
}
|
|
done < <(e "$environment_def" | shyaml key-values-0)
|
|
warn "No ${WHITE}provider${NORMAL} key given, had to infer it, chose '$key'."
|
|
fi
|
|
if [ "$provider" ]; then
|
|
config+=$(echo -en "\n LEXICON_PROVIDER: $provider")
|
|
fi
|
|
fi
|
|
|
|
challenge_type=$(get_challenge_type "$cfg" "$action" "$domain")
|
|
|
|
config+=$(echo -en "\n CHALLENGE_TYPE: $challenge_type")
|
|
info "Challenge type is $challenge_type"
|
|
|
|
echo "$config"
|
|
}
|
|
|
|
compose_get_challenge_type() {
|
|
local cfg="$1"
|
|
e "$cfg" | shyaml get-value "challenge-type" 2>/dev/null
|
|
}
|
|
|
|
letsencrypt_get_challenge_type() {
|
|
local domain="$1" renewal_file
|
|
renewal_file="$SERVICE_DATASTORE"/etc/letsencrypt/renewal/"$domain".conf
|
|
[ -e "$renewal_file" ] || return 1
|
|
grep '^pref_challs' "$renewal_file" | cut -f 2 -d "=" | nspc
|
|
}
|
|
|
|
letsencrypt_set_renew_before_expiry() {
|
|
local domain="$1" days="$2" renewal_file
|
|
domain="${domain#\*.}" ## remove '*.' from wildcard domain
|
|
renewal_file="$SERVICE_DATASTORE"/etc/letsencrypt/renewal/"$domain".conf
|
|
[ -e "$renewal_file" ] || return 1
|
|
sed -ri "s/^(#\s+)?(renew_before_expiry\s*=)\s*[0-9]+(\s+days)$/\2 $days\3/g" "$renewal_file"
|
|
}
|
|
|
|
letsencrypt_get_renew_before_expiry() {
|
|
local domain="$1" renewal_file
|
|
renewal_file="$SERVICE_DATASTORE"/etc/letsencrypt/renewal/"$domain".conf
|
|
[ -e "$renewal_file" ] || return 1
|
|
if out=$(egrep "^renew_before_expiry\s*=\s*[0-9]+\s+days$" "$renewal_file" 2>/dev/null); then
|
|
e "$out" | sed -r "s/^renew_before_expiry\s*=\s*([0-9]+)\s+days$/\1/g"
|
|
else
|
|
err "Couldn't find 'renew_before_expiry' in letsencrypt renewal" \
|
|
"configuration for domain '$domain'."
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
|
|
get_challenge_type() {
|
|
local cfg="$1" action="$2" domain="$3" challenge_type renewal_file challenge
|
|
case "$action" in
|
|
create)
|
|
if ! challenge_type=$(compose_get_challenge_type "$cfg"); then
|
|
warn "No ${WHITE}challenge-type${NORMAL} provided, defaulting to 'http'."
|
|
challenge_type=http
|
|
fi
|
|
echo "$challenge_type"
|
|
;;
|
|
renew)
|
|
challenge=$(letsencrypt_get_challenge_type "$domain")
|
|
if [[ "$challenge" =~ ^http ]]; then
|
|
echo "http"
|
|
else
|
|
echo "$challenge"
|
|
fi
|
|
;;
|
|
*)
|
|
err "Invalid action '$action'."
|
|
;;
|
|
esac
|
|
}
|
|
|
|
|
|
will_need_http_access() {
|
|
local cfg="$1" action="$2" domain="$3" domains args_domains remaining
|
|
|
|
challenge_type=$(get_challenge_type "$cfg" "$action" "$domain")
|
|
[ "$challenge_type" == "http" ] || return 1
|
|
|
|
|
|
}
|
|
|
|
|
|
has_existing_cert() {
|
|
local domain="$1"
|
|
[ -d "$SERVICE_DATASTORE/etc/letsencrypt/live/$domain" ] || return 1
|
|
}
|
|
|
|
letsencrypt_cert_info() {
|
|
local domain="$1"
|
|
compose -q --no-hooks run -T --rm "$SERVICE_NAME" \
|
|
crt info "$domain"
|
|
}
|
|
|
|
|
|
letsencrypt_cert_delete() {
|
|
local domain="$1"
|
|
compose --debug --no-hooks run --rm "$SERVICE_NAME" \
|
|
certbot delete --cert-name "$domain"
|
|
}
|
|
|
|
|
|
valid_existing_cert() {
|
|
local renew_before_expiry="$1" domain="$2" args_domains domains remaining
|
|
shift
|
|
args_domains=("$@")
|
|
has_existing_cert "$domain" || return 1
|
|
|
|
info "Querying $domain for previous info..."
|
|
out=$(letsencrypt_cert_info "$domain") || return 1
|
|
|
|
## check if output is valid yaml
|
|
err=$(e "$out" | shyaml get-value 2>&1 >/dev/null) || {
|
|
err "Cert info on '$domain' output do not seem to be valid YAML:"
|
|
echo " cert info content:" >&2
|
|
e "$out" | prefix " ${GRAY}|$NORMAL " >&2
|
|
echo >&2
|
|
echo " parsing error:" >&2
|
|
e "$err" | prefix " ${RED}!$NORMAL " >&2
|
|
echo >&2
|
|
return 3
|
|
}
|
|
|
|
domains=$(e "$out" | shyaml get-value domains) || return 1
|
|
|
|
domains=$(printf "%s " $domains | tr " " "\n" | sort)
|
|
args_domains=$(printf "%s " "${args_domains[@]}" | tr " " "\n" | sort)
|
|
# info domains: "$domains"
|
|
# info args_domain: "$args_domains"
|
|
remaining=$(e "$out" | shyaml get-value remaining) || return 1
|
|
if [ "$domains" != "$args_domains" ]; then
|
|
info "Domains mismatch:"
|
|
info " old: $domains"
|
|
info " new: $args_domains"
|
|
return 2
|
|
fi
|
|
|
|
if [ "$remaining" == EXPIRED ]; then
|
|
info "Existing certificate expired."
|
|
return 1
|
|
fi
|
|
|
|
if [ "$remaining" -lt "$renew_before_expiry" ]; then
|
|
info "Existing certificate in renew period" \
|
|
"($remaining remaining days of validity)."
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
|
|
get_domain_list() {
|
|
compose -q --no-hooks run --rm "$SERVICE_NAME" crt list
|
|
}
|
|
|
|
|
|
crt() {
|
|
local cfg="$1" action="$2" domain="$3" config \
|
|
stopped_containers container_ids
|
|
shift
|
|
shift
|
|
## expiry was checked, launch the action on the real charm, but take care of
|
|
## correctly running it.
|
|
## - provide env
|
|
## - declare proper ports
|
|
## - stop containers and restart them if necessary
|
|
|
|
config=$(get_dc_env "$cfg" "$action" "$domain") || return 1
|
|
stopped_containers=()
|
|
if will_need_http_access "$cfg" "$action" "$domain"; then
|
|
container_ids=($(docker ps \
|
|
--filter label="compose.project=$PROJECT_NAME" \
|
|
--filter publish=80 \
|
|
--format "{{.ID}}"
|
|
)) || exit 1
|
|
|
|
for container_id in "${container_ids[@]}"; do
|
|
info "Attempting to clear port 80 by stopping $container_id"
|
|
docker stop -t 5 "$container_id"
|
|
stopped_containers+=("$container_id")
|
|
done
|
|
config+=$(echo -en "\n ports:
|
|
- \"0.0.0.0:80:80\"")
|
|
fi
|
|
|
|
compose_opts=()
|
|
if [ "$DEBUG" ]; then
|
|
compose_opts+=("--debug")
|
|
else
|
|
compose_opts+=("--quiet")
|
|
fi
|
|
compose "${compose_opts[@]}" --no-init --no-relations --add-compose-content "$config" \
|
|
run --service-ports --rm "$SERVICE_NAME" crt "$action" "$@"
|
|
errlvl="$?"
|
|
|
|
for container_id in "${stopped_containers[@]}"; do
|
|
info "Attempting restart $container_id"
|
|
docker start "$container_id"
|
|
done
|
|
|
|
return "$errlvl"
|
|
}
|
|
|
|
|
|
crt_create() {
|
|
local force service_def cfg renew_before_expiry msg domains
|
|
usage="
|
|
$exname [-h|--help]
|
|
$exname MAIN_DOMAIN [ALT_DOMAINS...]"
|
|
|
|
force=
|
|
domains=()
|
|
while [ "$1" ]; do
|
|
case "$1" in
|
|
"--help"|"-h")
|
|
print_usage
|
|
return 0
|
|
;;
|
|
"--force"|"-f") force=1;;
|
|
*) domains+=("$1");;
|
|
esac
|
|
shift
|
|
done
|
|
|
|
if [ "${#domains[@]}" == 0 ]; then
|
|
err "At least one domain should be provided as argument."
|
|
print_usage >&2
|
|
return 1
|
|
fi
|
|
|
|
service_def=$(get_compose_service_def "$SERVICE_NAME") || return 1
|
|
cfg=$(e "$service_def" | shyaml get-value "options" 2>/dev/null)
|
|
renew_before_expiry=$(e "$cfg" | shyaml get-value "renew-before-expiry" 30 2>/dev/null)
|
|
renew_before_expiry=${renew_before_expiry:-30}
|
|
valid_existing_cert "$renew_before_expiry" "${domains[@]}"
|
|
valid_existing_cert="$?"
|
|
if [ -z "$force" ] && [ "$valid_existing_cert" == 0 ]; then
|
|
if [ "${#domains[@]}" -gt 1 ]; then
|
|
msg=" (with ${domains[*]:1})"
|
|
fi
|
|
info "A valid cert already exists for domain ${domains[0]}$msg."
|
|
return 0
|
|
fi
|
|
|
|
if [ "$valid_existing_cert" == 2 ]; then
|
|
err "Domain mismatch detected, lets delete previous cert."
|
|
letsencrypt_cert_delete "${domains[0]}" || return 1
|
|
err "Previous cert for ${domains[0]} deleted."
|
|
fi
|
|
|
|
if [ "$valid_existing_cert" == 3 ]; then
|
|
err "Unexpected failure while checking previous cert info"
|
|
return 1
|
|
fi
|
|
|
|
crt "$cfg" create "${domains[@]}" || {
|
|
err "Certificate creation/renew failed for domain '${domains[0]}'."
|
|
return 1
|
|
}
|
|
|
|
letsencrypt_set_renew_before_expiry "${domains[0]}" "$renew_before_expiry" || {
|
|
err "Setting renew-before-expiry on '${domains[0]}' failed."
|
|
return 1
|
|
}
|
|
}
|
|
|
|
|
|
crt_renew() {
|
|
local service_def cfg renew_before_expiry msg start domains_yml \
|
|
domain domain_cfg
|
|
usage="$
|
|
$exname [-h|--help]
|
|
"
|
|
|
|
while [ "$1" ]; do
|
|
case "$1" in
|
|
"--help"|"-h")
|
|
print_usage
|
|
return 0
|
|
;;
|
|
*)
|
|
err "No argument required"
|
|
print_usage >&2
|
|
return 1
|
|
;;
|
|
esac
|
|
shift
|
|
done
|
|
|
|
service_def=$(get_compose_service_def "$SERVICE_NAME") || return 1
|
|
cfg=$(e "$service_def" | shyaml get-value "options" 2>/dev/null)
|
|
default_renew_before_expiry=$(e "$cfg" | shyaml get-value "renew-before-expiry" 2>/dev/null)
|
|
default_renew_before_expiry=${renew_before_expiry:-30}
|
|
if ! renew_before_expiry=$(letsencrypt_get_renew_before_expiry "$domain") || \
|
|
[ -z "$renew_before_expiry" ]; then
|
|
renew_before_expiry=$default_renew_before_expiry
|
|
fi
|
|
start="$SECONDS"
|
|
info "Get domain list.."
|
|
domains_yml=$(get_domain_list) || return 1
|
|
info " .. Done ${GRAY}($((SECONDS - start))s)${NORMAL}"
|
|
|
|
[ "$domains_yml" ] || {
|
|
info "No domain founds"
|
|
return 0
|
|
}
|
|
|
|
failed=()
|
|
while read-0 domain domain_cfg; do
|
|
remaining=$(e "$domain_cfg" | shyaml get-value "remaining") || return 1
|
|
if [ "$remaining" == EXPIRED ] || [ "$remaining" -lt "$renew_before_expiry" ]; then
|
|
if [ "$remaining" == EXPIRED ]; then
|
|
info "Renewing domain $domain (expired)."
|
|
else
|
|
info "Renewing domain $domain ($remaining days left)."
|
|
fi
|
|
crt "$cfg" renew "$domain" </dev/null
|
|
if [ "$?" != "0" ]; then
|
|
failed+=("$domain")
|
|
err "Certificate renew of '$domain' failed."
|
|
fi
|
|
else
|
|
info "Domain $domain does not need renewing ($remaining days left)."
|
|
fi
|
|
done < <(e "$domains_yml" | shyaml key-values-0)
|
|
|
|
if [ "${#failed[@]}" -gt 0 ]; then
|
|
err "At least one domain failed to be renewed: ${failed[@]}"
|
|
return 1
|
|
fi
|
|
}
|