diff --git a/apache/lib/common b/apache/lib/common index f074c10..6c6e240 100644 --- a/apache/lib/common +++ b/apache/lib/common @@ -33,13 +33,15 @@ export -f apache_publish_dir apache_vhost_create () { export APACHE_CONFIG_LOCATION="$SERVICE_CONFIGSTORE/etc/apache2/sites-enabled" - export SERVER_ALIAS=$(relation-get server-aliases 2>/dev/null) || true - export PROTOCOLS=$(__vhost_cfg_normalize_protocol) || return 1 + SERVER_ALIAS=$(relation-get server-aliases 2>/dev/null) || true + PROTOCOLS=$(__vhost_cfg_normalize_protocol) || return 1 + + export SERVER_ALIAS PROTOCOLS SSL_PLUGIN_FUN SSL_CFG_{VALUE,OPTION} - export SSL_PLUGIN_FUN=$(ssl_get_plugin_fun) || return 1 if is_protocol_enabled https; then - "$SSL_PLUGIN_FUN"_vars "$(relation-get ssl)" || return 1 + read-0 SSL_PLUGIN_FUN SSL_CFG_VALUE SSL_CFG_OPTIONS < <(ssl_get_plugin_fun) || return 1 + "$SSL_PLUGIN_FUN"_vars "$SSL_CFG_OPTIONS" "$SSL_CFG_VALUE" || return 1 fi apache_vhost_statement "$PROTOCOLS" | file_put "$APACHE_CONFIG_LOCATION/$prefix$DOMAIN.conf" || return 1 @@ -49,7 +51,9 @@ apache_vhost_create () { apache_passwd_file || return 1 fi - "$SSL_PLUGIN_FUN"_prepare "$(relation-get ssl)" || return 1 + if is_protocol_enabled https; then + "$SSL_PLUGIN_FUN"_prepare "$SSL_CFG_OPTIONS" "$SSL_CFG_VALUE" || return 1 + fi } @@ -101,27 +105,59 @@ __vhost_cfg_normalize_protocol() { ## - output 3 vars of where to find the 3 files from within the docker apache ssl_get_plugin_fun() { - local cfg="$(relation-get ssl 2>/dev/null)" - if [[ "$(echo "$cfg" | shyaml get-type 2>/dev/null)" == "str" ]]; then - target_relation= - while read-0 relation_name target_service relation_config tech_dep; do - [ "$target_service" == "$cfg" ] || continue - verb "service ${DARKYELLOW}$target_service${NORMAL} matches" \ - "${WHITE}ssl${NORMAL} value: candidate relation is ${DARKBLUE}$relation_name${NORMAL}" - fun="ssl_plugin_${relation_name}" - if declare -F "${fun}_vars" >/dev/null 2>&1 && declare -F "${fun}_prepare" >/dev/null 2>&1; then - verb "Corresponding plugin ${DARKGREEN}found${NORMAL} for relation ${DARKBLUE}$relation_name${NORMAL}" - echo "$fun" - return 0 - else - verb "Corresponding plugin ${DARKRED}not found${NORMAL} for relation ${DARKBLUE}$relation_name${NORMAL}" - fi - done < <(get_compose_relations "$SERVICE_NAME") || return 1 - err "Invalid ${WHITE}ssl${NORMAL} value: '$cfg' is not a valid linked service through a support relation." - return 1 + # from ssl conf, return the function that should manage SSL code creation + local cfg="$(relation-get ssl 2>/dev/null)" type keys + if [ -z "$cfg" ]; then + return 0 else + type="$(echo "$cfg" | shyaml -y get-type 2>/dev/null)" || return 1 + fi + + if [[ "$type" == "bool" ]]; then + printf "%s\0" "ssl_fallback" "" "$cfg" echo ssl_fallback + return 0 + fi + + if ! [[ "$type" == "str" || "$type" == "struct" ]]; then + err "Invalid ${WHITE}ssl${NORMAL} value type '$type': please provide a string or a struct." + return 1 + fi + + if [ -z "$NO_CERT_PROVIDER" ]; then + if [[ "$type" == "str" ]]; then + keys=("$cfg") + else + keys=($(echo "$cfg" | shyaml keys 2>/dev/null)) + fi + for key in "${keys[@]}"; do + target_relation="cert-provider" + fun="ssl_plugin_${target_relation}" + while read-0 relation_name target_service relation_config tech_dep; do + [ "$relation_name" == "${target_relation}" ] || continue + [ "$target_service" == "$key" ] || continue + verb "Corresponding plugin ${DARKGREEN}found${NORMAL}" \ + "in ${DARKBLUE}$relation_name${NORMAL}/${DARKYELLOW}$key${NORMAL}" + ssl_cfg=$(printf "%s" "$cfg" | shyaml get-value "$key" 2>/dev/null) || true + merged_config=$(merge_yaml_str "$relation_config" "$ssl_cfg") || return 1 + printf "%s\0" "$fun" "$key" "$merged_config" + return 0 + done < <(get_compose_relations "$SERVICE_NAME") || return 1 + case "$key" in + cert|ca-cert|key) + : + ;; + *) + err "Invalid key '$key' in ${WHITE}ssl${NORMAL}:" \ + "no corresponding services declared in ${DARKBLUE}${target_relation}$NORMAL" + return 1 + ;; + esac + done fi + ## No key of the struct seem to be declared cert-provider, so fallback + printf "%s\0" "ssl_fallback" "" "$cfg" + echo ssl_fallback } @@ -168,26 +204,26 @@ $volumes } -ssl_plugin_letsencrypt-dns_vars() { +ssl_plugin_cert-provider_vars() { __vhost_cfg_SSL_CERT_LOCATION=/etc/letsencrypt/live/${DOMAIN}/cert.pem __vhost_cfg_SSL_KEY_LOCATION=/etc/letsencrypt/live/${DOMAIN}/privkey.pem __vhost_cfg_SSL_CHAIN=/etc/letsencrypt/live/${DOMAIN}/chain.pem } -ssl_plugin_letsencrypt-dns_prepare() { - local service="$1" letsencrypt_charm - shift +ssl_plugin_cert-provider_prepare() { + local cfg="$1" service="$2" options - export DEFAULT_COMPOSE_FILE="$COMPOSE_YML_FILE" - run_service_action "$service" add "$DOMAIN" $(echo "$SERVER_ALIAS" | shyaml get-values 2>/dev/null) || return 1 - letsencrypt_charm=$(get_service_charm "$service") || return 1 + options=$(yaml_key_val_str "options" "$cfg") || return 1 + service_config=$(yaml_key_val_str "$service" "$options") + compose --debug --add-compose-content "$service_config" run "$service" \ + crt create "$DOMAIN" $(echo "$SERVER_ALIAS" | shyaml -y get-values 2>/dev/null) || return 1 config-add "\ services: $MASTER_TARGET_SERVICE_NAME: volumes: - - $DATASTORE/${letsencrypt_charm}/etc/letsencrypt:/etc/letsencrypt:ro + - $DATASTORE/$service/etc/letsencrypt:/etc/letsencrypt:ro " || return 1 } @@ -225,7 +261,6 @@ apache_vhost_statement() { __vhost_full_vhost_statement http fi if is_protocol_enabled https; then - export SSL_PLUGIN_FUN=$(ssl_get_plugin_fun) || return 1 "$SSL_PLUGIN_FUN"_vars "$(relation-get ssl 2>/dev/null)" cat <' @@ -178,7 +178,7 @@ ssl: cert: c target: popo:3333 ' -proxy=yes apache_vhost_statement ,https," +SSL_PLUGIN_FUN=ssl_fallback proxy=yes apache_vhost_statement ,https," "ssl providing keys inline" noerror is out reg 'SSLCertificateFile /etc/ssl/certs/www.example.com.pem' is out reg 'SSLCertificateKeyFile /etc/ssl/private/www.example.com.key' @@ -203,7 +203,7 @@ apache-custom-rules: | RewriteRule ^(/web/webclient/home.*)$ $1?skin=formanoo [L,QSA,R=302] target: popo:3333 ' -proxy=yes apache_vhost_statement ,https," +SSL_PLUGIN_FUN=ssl_fallback proxy=yes apache_vhost_statement ,https," "custom rules" noerror is out reg 'RewriteEngine On' @@ -226,7 +226,7 @@ apache-custom-rules: | RewriteRule ^(/web/webclient/home.*)$ $1?skin=formanoo [L,QSA,R=302] target: popo:3333 ' -proxy=yes apache_vhost_statement ,https,http," +SSL_PLUGIN_FUN=ssl_fallback proxy=yes apache_vhost_statement ,https,http," "both http and https" noerror is out ' diff --git a/apache/test/vhost_cert_provider b/apache/test/vhost_cert_provider new file mode 100644 index 0000000..4351bef --- /dev/null +++ b/apache/test/vhost_cert_provider @@ -0,0 +1,146 @@ +#!/bin/bash + +exname=$(basename $0) + +prefix_cmd=" +. /etc/shlib + +include common +include parse + +. ../lib/common + +depends compose + +" + +## +## Mocks +## + +relation-get() { + local key="$1" + echo "$CFG" | shyaml get-value "$key" 2>/dev/null +} +export -f relation-get + +get_compose_relations() { + local service="$1" + printf "%s\0" "${RELATIONS[@]}" +} +export -f get_compose_relations + +merge_yaml_str() { + printf "" +} +export -f merge_yaml_str + +compose() { + printf "Calling: compose " + printf "%s " "$*" + echo +} +export -f compose + +yaml_key_val_str() { + printf "%s: %s" "$1" "$2" +} +export -f yaml_key_val_str + +file_put() { + echo "file_put $1" + cat - | prefix " | " +} +export -f file_put + +docker() { + echo "docker" "$@" + echo stdin: + cat - | prefix " | " +} +export -f docker + +config-add() { + echo "config-add" + echo "$1" | prefix " | " +} +export -f config-add + +mkdir() { + echo "called: $FUNCNAME $@" >&2 +} +export -f mkdir + +setfacl() { + echo "called: $FUNCNAME $@" >&2 +} +export -f setfacl + +chgrp() { + echo "called: $FUNCNAME $@" >&2 +} +export -f chgrp + +chmod() { + echo "called: $FUNCNAME $@" >&2 +} +export -f chmod + + +cached_cmd_on_base_image() { + echo "called: $FUNCNAME $@" >&2 + echo "stdout:" >&2 + echo "" | prefix " | " >&2 + echo "" +} +export -f cached_cmd_on_base_image + + +## +## cert-provider +## + + +try " +export SERVICE_CONFIGSTORE='\$SERVICE_CONFIGSTORE' +export CONFIGSTORE='\$CONFIGSTORE' +export BASE_SERVICE_NAME='\$BASE_SERVICE_NAME' +export MASTER_TARGET_SERVICE_NAME='\$MASTER_TARGET_SERVICE_NAME' +DOMAIN=www.example.com +DOCKER_SITE_PATH=/var/www/\$DOMAIN +CFG=' +ssl: + foo: | + a + b +' +RELATIONS=() +apache_vhost_create" "unknown cert key" +is errlvl 1 +is err reg 'Error: .*cert-provider.*' + + +try " +export SERVICE_CONFIGSTORE='\$SERVICE_CONFIGSTORE' +export CONFIGSTORE='\$CONFIGSTORE' +export DATASTORE='\$DATASTORE' +export BASE_SERVICE_NAME='\$BASE_SERVICE_NAME' +export MASTER_TARGET_SERVICE_NAME='\$MASTER_TARGET_SERVICE_NAME' +DOMAIN=www.example.com +DOCKER_SITE_PATH=/var/www/\$DOMAIN +CFG=' +ssl: + foo: 12 +' +RELATIONS=(cert-provider foo a True) +apache_vhost_create" "known cert key" +noerror +is out reg 'Calling: compose .*foo: options: .*run foo.*' +is out part 'config-add + | services: + | $MASTER_TARGET_SERVICE_NAME: + | volumes: + | - $DATASTORE/foo/etc/letsencrypt:/etc/letsencrypt:ro' + diff --git a/letsencrypt/actions/add b/letsencrypt/actions/add deleted file mode 100755 index 8b56f94..0000000 --- a/letsencrypt/actions/add +++ /dev/null @@ -1,46 +0,0 @@ -#!/bin/bash - -## Load action gets a first argument a DIRECTORY holding the necessary files. -## -## - -if [ -z "$SERVICE_DATASTORE" ]; then - echo "This script is meant to be run through 'compose' to work properly." >&2 - exit 1 -fi - -usage="$exname [-h|--help] DOMAIN [DOMAIN...]" - -domains=() -while [ "$1" ]; do - case "$1" in - "--help"|"-h") - print_usage - exit 0 - ;; - --*|-*) - err "Unexpected optional argument '$1'" - print_usage - exit 1 - ;; - *) - domains+=("$1") - ;; - esac - shift -done - -if [ -z "${domains[*]}" ]; then - err "You must provide at least one domain as positional argument." - print_usage - exit 1 -fi - -set -e - -## XXXvlab: should check that domain can be declared (with whois, check that the -## registrar is a provider that have config values declared in compose.conf) -mkdir -p "$SERVICE_DATASTORE/etc/letsencrypt" -echo "${domains[@]}" >> "$SERVICE_DATASTORE/etc/letsencrypt/domains.conf" - -info "Added '${domains[*]}' domains to letsencrypt domain lists." diff --git a/letsencrypt/hooks/dc-pre-run b/letsencrypt/hooks/dc-pre-run new file mode 100755 index 0000000..b1370c2 --- /dev/null +++ b/letsencrypt/hooks/dc-pre-run @@ -0,0 +1,58 @@ +#!/bin/bash + +## Init is run on host +## For now it is run every time the script is launched, but +## it should be launched only once after build. + +## Accessible variables are: +## - SERVICE_NAME Name of current service +## - DOCKER_BASE_IMAGE Base image from which this service might be built if any +## - SERVICE_DATASTORE Location on host of the DATASTORE of this service +## - SERVICE_CONFIGSTORE Location on host of the CONFIGSTORE of this service + +. lib/common || exit 1 + +set -e + +service_def=$(get_compose_service_def "$SERVICE_NAME") + +USER_EMAIL=$(echo "$service_def" | shyaml get-value options.email 2>/dev/null) || { + err "No ${WHITE}email${NORMAL} value in ${DARKYELLOW}$SERVICE_NAME${NORMAL} compose's ${WHITE}options${NORMAL}." + exit 1 +} + +config=" +$SERVICE_NAME: + environment: + LETSENCRYPT_USER_MAIL: $USER_EMAIL" + +if environment_def="$(printf "%s" "$service_def" | shyaml -y get-value options.env 2>/dev/null)"; then + while read-0 key value; do + config+="$(printf "\n %s: %s" "$key" "$value")" + done < <(printf "%s" "$environment_def" | yaml_opt_bash_env_ignore_first_level LEXICON) + + if ! provider=$(printf "%s" "$environment_def" | shyaml -y 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 < <(echo "$environment_def" | shyaml key-values-0) + warn "No ${WHITE}provider${NORMAL} key given, had to infer it, chose '$key'." + fi + + config+=$(echo -en "\n LEXICON_PROVIDER: $provider") +fi + +if ! challenge_type=$(printf "%s" "$service_def" | shyaml get-value "options.challenge-type" 2>/dev/null); then + warn "No ${WHITE}challenge-type${NORMAL} provided, defaulting to 'http'." + challenge_type=http +fi +config+=$(echo -en "\n CHALLENGE_TYPE: $challenge_type") + +init-config-add "$config" + +mkdir -p "$SERVICE_DATASTORE/etc/letsencrypt" diff --git a/letsencrypt/hooks/init b/letsencrypt/hooks/init deleted file mode 100755 index f129b61..0000000 --- a/letsencrypt/hooks/init +++ /dev/null @@ -1,71 +0,0 @@ -#!/bin/bash - -## Init is run on host -## For now it is run every time the script is launched, but -## it should be launched only once after build. - -## Accessible variables are: -## - SERVICE_NAME Name of current service -## - DOCKER_BASE_IMAGE Base image from which this service might be built if any -## - SERVICE_DATASTORE Location on host of the DATASTORE of this service -## - SERVICE_CONFIGSTORE Location on host of the CONFIGSTORE of this service - -set -e - -service_def=$(get_compose_service_def "$SERVICE_NAME") - -USER_EMAIL=$(echo "$service_def" | shyaml get-value options.email 2>/dev/null) || { - err "No ${WHITE}email${NORMAL} value in ${DARKYELLOW}$SERVICE_NAME${NORMAL} compose's ${WHITE}options${NORMAL}." - exit 1 -} - -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) -} - - -config=" -$SERVICE_NAME: - environment: - LETSENCRYPT_USER_MAIL: $USER_EMAIL" - - -while read-0 key value; do - config+="$(printf "\n %s: %s" "$key" "$value")" -done < <(yaml_opt_bash_env_ignore_first_level LEXICON < <(echo "$service_def" | shyaml -y get-value options)) - -## XXXvlab: this is very temporary, we should change image to support more -## than one provider (cf: https://github.com/adferrand/docker-letsencrypt-dns/issues/24) -first_key= -while read-0 key value; do - [[ "$(echo "$value" | shyaml get-type)" == "struct" ]] && { - first_key="$key" - break - } -done < <(echo "$service_def" | shyaml key-values-0 options) - -config+=$(echo -en "\n LEXICON_PROVIDER: $first_key") - -init-config-add "$config" - -mkdir -p "$SERVICE_DATASTORE/etc/letsencrypt" -touch "$SERVICE_DATASTORE/etc/letsencrypt/domains.conf" - diff --git a/letsencrypt/lib/common b/letsencrypt/lib/common new file mode 100644 index 0000000..772777f --- /dev/null +++ b/letsencrypt/lib/common @@ -0,0 +1,25 @@ + + + + +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) +} diff --git a/letsencrypt/metadata.yml b/letsencrypt/metadata.yml index fff6298..6b225f6 100644 --- a/letsencrypt/metadata.yml +++ b/letsencrypt/metadata.yml @@ -1,7 +1,9 @@ -description: "Let's Encrypt" +description: "Let's Encrypt server" +type: run-once maintainer: "Valentin Lab " ## XXXvlab: docker uses the 'build' directory or the 'image:' option here. -docker-image: adferrand/letsencrypt-dns +docker-image: docker.0k.io/letsencrypt data-resources: - - /etc/letsencrypt - - /var/log/letsencrypt + - /etc/letsencrypt ## yes certificates are stored here, this is data + - /var/log/letsencrypt ## logs + - /var/lib/tldextract ## latest data about TLDs, this is used by lexicon...