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.
 
 

667 lines
18 KiB

# -*- 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
relation-set domain "$DOMAIN"
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
relation-set domain "$DOMAIN"
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 () {
local custom_rules
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
redirect=$(relation-get 'redirect-to-ssl' 2>/dev/null) || true
if is_protocol_enabled http; then
redirect=${redirect:-true}
else
redirect=false
fi
if [ "$redirect" == "true" ]; then
custom_rules=$(_get_custom_rules) || return 1
if [[ "$custom_rules" != *"## Auto-redirection from http to https"* ]]; then
relation-set apache-custom-rules "- |
## Auto-redirection from http to https
RewriteEngine On
RewriteCond %{HTTPS} off
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [R=302,L,QSA]
$(if [ "$custom_rules" ]; then
echo "- |"$'\n'"$(echo "$custom_rules" | prefix " ")"
fi)"
fi
fi
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="http,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
if [ -z "$first_exposed_port" ]; then
err "Failed to get first exposed port of image '$base_image'."
return 1
fi
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
"
}