|
|
# -*- mode: shell-script -*-
config_hash=
get_domain () { relation-get domain 2>/dev/null && return 0
## is service name a regex ? if [[ "$BASE_SERVICE_NAME" =~ ^([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}$ ]]; then echo "$BASE_SERVICE_NAME" return 0 fi
err "You must specify a ${WHITE}domain$NORMAL option in relation." return 1 }
apache_proxy_dir () { DOMAIN=$(get_domain) || return 1 proxy=yes apache_vhost_create || return 1 info "Added $DOMAIN as a proxy to $TARGET." } export -f apache_proxy_dir
apache_publish_dir () { DOMAIN=$(get_domain) || return 1 DOCKER_SITE_PATH="/var/www/${DOMAIN}" LOCATION=$(relation-get location 2>/dev/null) || LOCATION="$DATASTORE/$BASE_SERVICE_NAME$DOCKER_SITE_PATH"
apache_vhost_create || return 1 info "Added $DOMAIN apache config." apache_code_dir || return 1 apache_data_dirs
} export -f apache_publish_dir
apache_vhost_create () { export APACHE_CONFIG_LOCATION="$SERVICE_CONFIGSTORE/etc/apache2/sites-enabled" vhost_statement
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}
if is_protocol_enabled https; then 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 vhost_statement=$(apache_vhost_statement "$PROTOCOLS") || return 1 echo "$vhost_statement"| file_put "$APACHE_CONFIG_LOCATION/$prefix$DOMAIN.conf" || return 1
__vhost_cfg_creds_enabled=$(relation-get creds 2>/dev/null) || true if [ "$__vhost_cfg_creds_enabled" ]; then apache_passwd_file || return 1 fi
if is_protocol_enabled https; then "$SSL_PLUGIN_FUN"_prepare "$SSL_CFG_OPTIONS" "$SSL_CFG_VALUE" || return 1 fi }
is_protocol_enabled() { local protocol=$1 [[ "$PROTOCOLS" == *",$protocol,"* ]] } export -f is_protocol_enabled
_get_ssl_option_value() { local target_relation rn ts rc td relation-get ssl 2>/dev/null && return 0
target_relation="cert-provider" while read-0 rn ts rc td; do [ "$rn" == "${target_relation}" ] || continue info "A cert-provider '$ts' declared as 'ssl' value" echo "$ts" return 0 done < <(get_service_relations "$SERVICE_NAME")
return 1 }
__vhost_cfg_normalize_protocol() { local protocol
if ! protocol=$(relation-get protocol 2>/dev/null); then protocol=auto else protocol=${protocol:-auto} fi
case "$protocol" in auto) if __vhost_cfg_ssl="$(_get_ssl_option_value)"; then protocol="https" export __vhost_cfg_ssl else protocol="http" fi ;; both) protocol="https,http" ;; ssl|https) protocol="https" ;; http) protocol="http" ;; *) err "Invalid value '$protocol' for ${WHITE}protocol$NORMAL option (use one of: http, https, both, auto)." return 1 esac echo ",$protocol," }
## ssl_plugin_* and ssl_fallback should : ## - do anything to ensure that ## - issue config-add to add volumes if necessary ## - output 3 vars of where to find the 3 files from within the docker apache
ssl_get_plugin_fun() { # from ssl conf, return the function that should manage SSL code creation local cfg type keys cfg=$(_get_ssl_option_value) 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_service_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 }
ssl_fallback_vars() { local cfg="$1" cert key ca_cert
if __vhost_cfg_ssl_cert=$(echo "$cfg" | shyaml get-value cert 2>/dev/null); then __vhost_cfg_SSL_CERT_LOCATION=/etc/ssl/certs/${DOMAIN}.pem fi
if __vhost_cfg_ssl_key=$(echo "$cfg" | shyaml get-value key 2>/dev/null); then __vhost_cfg_SSL_KEY_LOCATION=/etc/ssl/private/${DOMAIN}.key fi
if __vhost_cfg_ssl_ca_cert=$(echo "$cfg" | shyaml get-value ca-cert 2>/dev/null); then __vhost_cfg_SSL_CA_CERT_LOCATION=/etc/ssl/certs/${DOMAIN}-ca.pem fi }
ssl_fallback_prepare() { local cfg="$1" cert key ca_cert
dst="$CONFIGSTORE/$BASE_SERVICE_NAME" volumes="" for label in cert key ca_cert; do content="$(eval echo "\"\$__vhost_cfg_ssl_$label\"")" if [ "$content" ]; then location="$(eval echo "\$__vhost_cfg_SSL_${label^^}_LOCATION")" echo "$content" | file_put "$dst$location" config_hash=$(printf "%s\0" "$config_hash" "$label" "$content" | md5_compat) volumes="$volumes - $dst$location:$location:ro" fi done
if [ "$volumes" ]; then config-add "\ services: $MASTER_TARGET_SERVICE_NAME: volumes: $volumes " fi
}
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_cert-provider_prepare() { local cfg="$1" service="$2" options
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 --rm --service-ports "$service" \ crt create "$DOMAIN" $(echo "$SERVER_ALIAS" | shyaml get-values 2>/dev/null) || { err "Failed to launch letsencrypt for certificate creation." return 1 } config-add "\ services: $MASTER_TARGET_SERVICE_NAME: volumes: - $DATASTORE/$service/etc/letsencrypt:/etc/letsencrypt:ro " || return 1
}
apache_passwd_file() { include parse || true
## XXXvlab: called twice... no better way to do this ? __vhost_creds_statement >/dev/null first= if ! [ -e "$CONFIGSTORE/$MASTER_TARGET_SERVICE_NAME$password_file" ]; then debug "No file $CONFIGSTORE/$MASTER_TARGET_SERVICE_NAME$password_file, creating password file." || true first=c fi while read-0 login password; do debug "htpasswd -b$first '${password_file}' '$login' '$password'" echo "htpasswd -b$first '${password_file}' '$login' '$password'" if [ "$first" ]; then first= fi done < <(echo "$__vhost_cfg_creds_enabled" | shyaml key-values-0 2>/dev/null) | docker run -i --entrypoint "/bin/bash" \ -v "$APACHE_CONFIG_LOCATION:/etc/apache2/sites-enabled" \ "$DOCKER_BASE_IMAGE" || return 1 }
## Produce the full statements depending on relation-get informations apache_vhost_statement() { local vhost_statement export SERVER_ALIAS=$(relation-get server-aliases 2>/dev/null) || true export PROTOCOLS="$1"
if is_protocol_enabled http; then __vhost_full_vhost_statement http || return 1 fi if is_protocol_enabled https; then "$SSL_PLUGIN_FUN"_vars "$(_get_ssl_option_value 2>/dev/null)" || return 1 vhost_statement=$(__vhost_full_vhost_statement https) || return 1 cat <<EOF
<IfModule mod_ssl.c> $(echo "$vhost_statement" | prefix " ") </IfModule> EOF fi } export -f apache_vhost_statement
apache_code_dir() { local www_data_gid www_data_gid=$(cached_cmd_on_base_image apache 'id -g www-data') || { debug "Failed to query for www-data gid in ${DARKYELLOW}apache${NORMAL} base image." return 1 }
mkdir -p "$LOCATION" || return 1 setfacl -R -m g:"$www_data_gid":rx "$LOCATION" info "Set permission for read and traversal on '$LOCATION'."
config-add " $MASTER_BASE_SERVICE_NAME: volumes: - $LOCATION:$DOCKER_SITE_PATH " }
apache_data_dirs() {
DATA_DIRS=$(relation-get data-dirs 2>/dev/null | shyaml get-values 2>/dev/null) || true if [ -z "$DATA_DIRS" ]; then return 0 fi
DST=$DATASTORE/$BASE_SERVICE_NAME$DOCKER_SITE_PATH DATA=() while IFS="," read -ra ADDR; do for dir in "${ADDR[@]}"; do DATA+=($dir) done done <<< "$DATA_DIRS"
www_data_gid=$(cached_cmd_on_base_image apache 'id -g www-data') || { debug "Failed to query for www-data gid in ${DARKYELLOW}apache${NORMAL} base image." return 1 } info "www-data gid from ${DARKYELLOW}apache${NORMAL} is '$www_data_gid'"
dirs=() for d in "${DATA[@]}"; do dirs+=("$DST/$d") done
mkdir -p "${dirs[@]}" setfacl -R -m g:"$www_data_gid":rwx "${dirs[@]}" setfacl -R -d -m g:"$www_data_gid":rwx "${dirs[@]}"
config-add " $MASTER_BASE_SERVICE_NAME: volumes: $( for d in "${DATA[@]}"; do echo " - $DST/$d:$DOCKER_SITE_PATH/$d" done )"
}
deploy_files() { local src="$1" dst="$2"
if ! [ -d "$dst" ]; then err "Destination '$dst' does not exist or is not a directory" return 1 fi ( cd "$dst" && info "In $dst:" && get_file "$src" | tar xv ) } export -f deploy_files
apache_core_rules_add() { local conf="$1" dst="/etc/apache2/conf-enabled/$BASE_SERVICE_NAME.conf" debug "Adding core rule." echo "$conf" | file_put "$CONFIGSTORE/$BASE_SERVICE_NAME$dst" config_hash=$(printf "%s\0" "$config_hash" "$conf" | md5_compat) config-add " $MASTER_BASE_SERVICE_NAME: volumes: - $CONFIGSTORE/$BASE_SERVICE_NAME$dst:$dst:ro " }
__vhost_ssl_statement() {
## defaults
__vhost_cfg_SSL_CERT_LOCATION=${__vhost_cfg_SSL_CERT_LOCATION:-/etc/ssl/certs/ssl-cert-snakeoil.pem} __vhost_cfg_SSL_KEY_LOCATION=${__vhost_cfg_SSL_KEY_LOCATION:-/etc/ssl/private/ssl-cert-snakeoil.key}
cat <<EOF
## ## SSL Configuration ##
SSLEngine On
SSLCertificateFile $__vhost_cfg_SSL_CERT_LOCATION SSLCertificateKeyFile $__vhost_cfg_SSL_KEY_LOCATION $([ -z "$__vhost_cfg_SSL_CA_CERT_LOCATION" ] || echo "SSLCACertificateFile $__vhost_cfg_SSL_CA_CERT_LOCATION") $([ -z "$__vhost_cfg_SSL_CHAIN" ] || echo "SSLCertificateChainFile $__vhost_cfg_SSL_CHAIN") SSLVerifyClient None
EOF
}
__vhost_creds_statement() { if ! __vhost_cfg_creds_enabled=$(relation-get creds 2>/dev/null); then echo "Allow from all" return 0 fi
password_file=/etc/apache2/sites-enabled/${DOMAIN}.passwd
cat <<EOF AuthType basic AuthName "private" AuthUserFile ${password_file} Require valid-user EOF
}
__vhost_head_statement() { local protocol="$1"
if [ "$protocol" == "https" ]; then prefix="s-" else prefix= fi
cat <<EOF ServerAdmin ${ADMIN_MAIL:-contact@$DOMAIN} ServerName ${DOMAIN} $( while read-0 alias; do echo "ServerAlias $alias" done < <(echo "$SERVER_ALIAS" | shyaml get-values-0 2>/dev/null) ) ServerSignature Off CustomLog /var/log/apache2/${prefix}${DOMAIN}_access.log combined ErrorLog /var/log/apache2/${prefix}${DOMAIN}_error.log ErrorLog syslog:local2 EOF
}
_get_custom_rules() { local custom_rules type elt value first custom_rules=$(relation-get apache-custom-rules 2>/dev/null) || true if [ -z "$custom_rules" ]; then return 0 fi type=$(echo "$custom_rules" | shyaml get-type) value= case "$type" in "sequence") first=1 while read-0 elt; do elt="$(echo "$elt" | yaml_get_interpret)" || return 1 [ "$elt" ] || continue if [ "$first" ]; then first= else value+=$'\n'$'\n' fi first= value+="$elt" done < <(echo "$custom_rules" | shyaml -y get-values-0) ;; "struct") while read-0 _key val; do value+=$'\n'"$(echo "$val" | yaml_get_interpret)" || return 1 done < <(echo "$custom_rules" | shyaml -y key-values-0) ;; "str") value+=$(echo "$custom_rules") ;; *) value+=$(echo "$custom_rules") ;; esac printf "%s" "$value" }
__vhost_custom_rules() { local custom_rules custom_rules=$(_get_custom_rules) || return 1 if [ "$custom_rules" ]; then cat <<EOF
## ## Custom rules ##
$custom_rules
EOF fi }
__vhost_content_statement() { if [ "$proxy" ]; then __vhost_proxy_statement "$@" || return 1 else __vhost_publish_dir_statement "$@" || return 1 fi }
__vhost_proxy_statement() { local protocol="$1"
TARGET=$(relation-get target 2>/dev/null) || true if [ -z "$TARGET" ]; then ## First exposed port: base_image=$(service_base_docker_image "$BASE_SERVICE_NAME") || return 1 if ! docker_has_image "$base_image"; then docker pull "$base_image" fi first_exposed_port=$(image_exposed_ports_0 "$base_image" | tr '\0' '\n' | head -n 1 | cut -f 1 -d /) || return 1 TARGET=$MASTER_BASE_SERVICE_NAME:$first_exposed_port info "No target was specified, introspection found: $TARGET" fi
cat <<EOF
## ## Proxy declaration towards $TARGET ##
<IfModule mod_proxy.c> ProxyRequests Off <Proxy *> Order deny,allow Allow from all </Proxy> ProxyVia On ProxyPass / http://$TARGET/ retry=0 <Location / > $(__vhost_creds_statement | prefix " ") ProxyPassReverse / </Location> $([ "$protocol" == "https" ] && echo " SSLProxyEngine On") </IfModule>
RequestHeader set "X-Forwarded-Proto" "$protocol"
## Fix IE problem (httpapache proxy dav error 408/409) SetEnv proxy-nokeepalive 1 EOF
}
__vhost_full_vhost_statement() { local protocol="$1" head_statement custom_rules content_statement
head_statement=$(__vhost_head_statement "$protocol") || return 1 custom_rules=$(__vhost_custom_rules) || return 1 content_statement=$(__vhost_content_statement "$protocol") || return 1
case "$protocol" in https) PORT=443 ;; http) PORT=80 ;; esac
cat <<EOF <VirtualHost *:$PORT>
$(echo "$head_statement" | prefix " ") $(echo "$custom_rules" | prefix " ") $(echo "$content_statement" | prefix " ")
## Forbid any cache, this is only usefull on dev server. #Header set Cache-Control "no-cache" #Header set Access-Control-Allow-Origin "*" #Header set Access-Control-Allow-Methods "POST, GET, OPTIONS" #Header set Access-Control-Allow-Headers "origin, content-type, accept" $([ "$protocol" == "https" ] && __vhost_ssl_statement | prefix " " && echo ) </VirtualHost> EOF
}
__vhost_publish_dir_statement() { cat <<EOF ## ## Publish directory $DOCKER_SITE_PATH ##
DocumentRoot $DOCKER_SITE_PATH
<Directory /> Options FollowSymLinks AllowOverride None </Directory>
<Directory $DOCKER_SITE_PATH> Options Indexes FollowSymLinks MultiViews AllowOverride all $(__vhost_creds_statement | prefix " ") </Directory>
EOF }
apache_config_hash() { debug "Adding config hash to enable recreating upon config change." config_hash=$({ printf "%s\0" "$config_hash" find "$SERVICE_CONFIGSTORE/etc/apache2/sites-enabled" \ -name \*.conf -exec md5sum {} \; } | md5_compat) || exit 1 init-config-add " $MASTER_BASE_SERVICE_NAME: labels: - compose.config_hash=$config_hash " }
|