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.
 
 

780 lines
22 KiB

# -*- mode: shell-script -*-
get_domain() {
local cfg="$1" cache_file="$state_tmpdir/$FUNCNAME.cache.$(H "$SERVICE_NAME" "$MASTER_BASE_SERVICE_NAME" "$@")" \
domain
if [ -e "$cache_file" ]; then
cat "$cache_file"
return 0
fi
domain=$(e "$cfg" | cfg-get-value domain 2>/dev/null) || true
if [ "$domain" ]; then
echo "$domain" | tee "$cache_file"
elif [[ "$BASE_SERVICE_NAME" =~ ^([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}$ ]]; then
echo "$BASE_SERVICE_NAME" | tee "$cache_file"
else
err "You must specify a ${WHITE}domain$NORMAL option in relation. (${FUNCNAME[@]})"
return 1
fi
}
##
## Master entrypoints
##
apache_proxy_dir() {
local cfg="$1" domain
apache_vhost_create web_proxy "$cfg" || return 1
}
export -f apache_proxy_dir
apache_publish_dir() {
local cfg="$1" domain
apache_vhost_create publish_dir "$cfg" || return 1
apache_code_dir "$cfg" || return 1
apache_data_dirs "$cfg"
}
export -f apache_publish_dir
##
## Simple functions
##
apache_vhost_create() {
local type="$1" cfg="$2" protocols="$3" dest="$4" custom_rules vhost_statement creds
export APACHE_CONFIG_LOCATION="$SERVICE_CONFIGSTORE/etc/apache2/sites-enabled"
if [ -z "$protocols" ]; then
protocols=$(__vhost_cfg_normalize_protocol "$cfg") || return 1
fi
domain=$(get_domain "$cfg") && relation-set domain "$domain"
if is_protocol_enabled https "$protocols"; then
if [ -z "$domain" ]; then
err "You must specify a domain for ssl to work."
return 1
fi
read-0 ssl_plugin_fun ssl_cfg_value ssl_cfg_options < <(ssl_get_plugin_fun "$cfg") || return 1
"$ssl_plugin_fun"_vars "$cfg" "$ssl_cfg_options" "$ssl_cfg_value" "$domain" || return 1
redirect=$(e "$cfg" | cfg-get-value 'redirect-to-ssl' 2>/dev/null) || true
if is_protocol_enabled http "$protocols"; then
redirect=${redirect:-true}
else
redirect=false
fi
if [ "$redirect" == "true" ]; then
custom_rules=$(_get_custom_rules "$cfg") || return 1
if [[ "$custom_rules" != *"## Auto-redirection from http to https"* ]]; then
redirect_rule="- |
## Auto-redirection from http to https
RewriteEngine On
RewriteCond %{HTTPS} off
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [R=302,L,QSA]
"
relation-set apache-custom-rules "$redirect_rule
$(if [ "$custom_rules" ]; then
echo "- |"$'\n'"$(echo "$custom_rules" | prefix " ")"
fi)"
cfg=$(merge_yaml_str "$cfg" "$(yaml_key_val_str "apache-custom-rules" "$redirect_rule
$(if [ "$custom_rules" ]; then
echo "- |"$'\n'"$(echo "$custom_rules" | prefix " ")"
fi)")")
fi
relation-set protocol https
else
## Both services are available and different, don't do anything then ?
#relation-set protocol https
:
fi
else
relation-set protocol http
fi
vhost_statement=$(apache_vhost_statement "$type" "$protocols" "$cfg" "$domain") || {
err "Failed to get vhost statement for type $type on ${protocols:1:-1}"
return 1
}
dest=${dest:-$domain}
if [ -z "$dest" ]; then
err "Please set either a domain or set a destination file."
return 1
fi
echo "$vhost_statement" | file_put "$APACHE_CONFIG_LOCATION/$dest.conf" || return 1
creds=$(e "$cfg" | cfg-get-value creds 2>/dev/null) || true
if [ "$creds" ]; then
apache_passwd_file "$cfg" "$dest"|| return 1
fi
if is_protocol_enabled https "$protocols"; then
"$ssl_plugin_fun"_prepare "$cfg" "$ssl_cfg_options" "$ssl_cfg_value" || return 1
fi
}
is_protocol_enabled() {
local protocol="$1" protocols="$2"
[[ "$protocols" == *",$protocol,"* ]]
}
export -f is_protocol_enabled
_get_ssl_option_value() {
local cfg="$1" cache_file="$state_tmpdir/$FUNCNAME.cache.$(H "$SERVICE_NAME" "$MASTER_BASE_SERVICE_NAME" "$@")" \
target_relation rn ts rc td
if [ -e "$cache_file" ]; then
cat "$cache_file"
return 0
fi
if ssl_cfg=$(e "$cfg" | cfg-get-value ssl 2>/dev/null); then
if [[ "$ssl_cfg" =~ ^False|None$ ]]; then
ssl_cfg=""
fi
echo "$ssl_cfg" | tee "$cache_file"
return 0
fi
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" | tee "$cache_file"
return 0
done < <(get_service_relations "$SERVICE_NAME")
return 1
}
__vhost_cfg_normalize_protocol() {
local cfg="$1" protocol ssl
## XXXvlab: can't cache if libcharm is not able to give me some checksums
## indeed, ``_get_ssl_option_value`` depends on relations calculations...
# local cfg="$1" cache_file="$CACHEDIR/$FUNCNAME.cache.$(p0 "$@" | md5_compat)" \
# protocol
# if [ -e "$cache_file" ]; then
# #debug "$FUNCNAME: STATIC cache hit $1"
# cat "$cache_file" &&
# touch "$cache_file" || return 1
# return 0
# fi
if protocol=$(e "$cfg" | cfg-get-value protocol 2>/dev/null); then
protocol=${protocol:-auto}
else
protocol=auto
fi
case "$protocol" in
auto)
ssl=$(_get_ssl_option_value "$cfg" 2>/dev/null)
if [ "$ssl" ] ; then
protocol="http,https"
else
protocol="http"
fi
;;
both)
protocol="http,https"
;;
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 -n ",$protocol,"
#| tee "$cache_file"
}
## 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 master_cfg="$1" cfg type keys
cfg=$(_get_ssl_option_value "$master_cfg") || return 1
local cache_file="$state_tmpdir/$FUNCNAME.cache.$(H "$SERVICE_NAME" "$cfg")"
if [ -e "$cache_file" ]; then
cat "$cache_file"
return 0
fi
[ "$cfg" ] || {
touch "$cache_file"
return 0
}
type="$(echo "$cfg" | shyaml -y get-type 2>/dev/null)" || return 1
if [[ "$type" == "bool" ]]; then
printf "%s\0" "ssl_fallback" "" "$cfg" | tee "$cache_file"
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" | tee "$cache_file"
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" | tee "$cache_file"
}
ssl_fallback_vars() {
local cfg="$1" ssl_cfg="$2" value="$3" domain="$4" cert key ca_cert domain
if __vhost_cfg_ssl_cert=$(echo "$ssl_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 "$ssl_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 "$ssl_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() {
local cfg="$1" ssl_cfg="$2" value="$3" domain="$4"
__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" ssl_cfg="$2" service="$3" options domain server_aliases
domain=$(get_domain "$cfg") || return 1
options=$(yaml_key_val_str "options" "$ssl_cfg") || return 1
service_config=$(yaml_key_val_str "$service" "$options")
server_aliases=$(e "$cfg" | cfg-get-value server-aliases 2>/dev/null) || true
[ "$server_aliases" == None ] && server_aliases=""
if [ "$server_aliases" ]; then
server_aliases=($(echo "$server_aliases" | shyaml get-values)) || return 1
else
server_aliases=()
fi
compose --debug --add-compose-content "$service_config" run --rm --service-ports "$service" \
crt create "$domain" "${server_aliases[@]}" || {
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() {
local cfg="$1" dest="$2" creds
include parse || true
## XXXvlab: called twice... no better way to do this ?
creds=$(e "$cfg" | cfg-get-value creds 2>/dev/null) || true
password_path=$(password-path-get "$dest")
first=
if ! [ -e "$CONFIGSTORE/$MASTER_TARGET_SERVICE_NAME$password_path" ]; then
debug "No file $CONFIGSTORE/$MASTER_TARGET_SERVICE_NAME$password_path, creating password file." || true
first=c
fi
while read-0 login password; do
debug "htpasswd -b$first '${password_path}' '$login' '$password'"
echo "htpasswd -b$first '${password_path}' '$login' '$password'"
if [ "$first" ]; then
first=
fi
done < <(e "$creds" | 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 type="$1" protocols="$2" cfg="$3" domain="$4" \
vhost_statement
if is_protocol_enabled http "$protocols"; then
__vhost_full_vhost_statement "$type" http "$cfg" "$domain" || return 1
fi
if is_protocol_enabled https "$protocols"; then
read-0 ssl_plugin_fun ssl_cfg_value ssl_cfg_options < <(ssl_get_plugin_fun "$cfg") || return 1
"$ssl_plugin_fun"_vars "$cfg" "$ssl_cfg_options" "$ssl_cfg_value" "$domain" || return 1
vhost_statement=$(__vhost_full_vhost_statement "$type" https "$cfg" "$domain") || return 1
cat <<EOF
<IfModule mod_ssl.c>
$(echo "$vhost_statement" | prefix " ")
</IfModule>
EOF
fi
}
export -f apache_vhost_statement
apache_code_dir() {
local cfg="$1" www_data_gid local_path
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
}
domain=$(get_domain "$cfg") || return 1
local_path="/var/www/${domain}"
host_path=$(e "$cfg" | cfg-get-value location 2>/dev/null) ||
host_path="$DATASTORE/$BASE_SERVICE_NAME${local_path}"
mkdir -p "$host_path" || return 1
setfacl -R -m g:"$www_data_gid":rx "$host_path"
info "Set permission for read and traversal on '$host_path'."
config-add "
$MASTER_BASE_SERVICE_NAME:
volumes:
- $host_path:$local_path
"
}
apache_data_dirs() {
local cfg="$1" data_dirs dst data dirs
data_dirs=$(e "$cfg" | cfg-get-value data-dirs 2>/dev/null | shyaml get-values 2>/dev/null) || true
if [ -z "$data_dirs" ]; then
return 0
fi
domain=$(get_domain "$cfg") || return 1
local_path="/var/www/${domain}"
dst=$DATASTORE/$BASE_SERVICE_NAME$local_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:$local_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
}
password-path-get() {
local dest="$1"
echo "/etc/apache2/sites-enabled/${dest}.passwd"
}
__vhost_creds_statement() {
local cfg="$1" dest="$2" password_path
password_path=$(password-path-get "$dest") || return 1
if ! e "$cfg" | cfg-get-value creds >/dev/null 2>&1; then
echo "Allow from all"
return 0
fi
cat <<EOF
AuthType basic
AuthName "private"
AuthUserFile ${password_path}
Require valid-user
EOF
}
__vhost_head_statement() {
local cfg="$1" protocol="$2" domain="$3" server_aliases admin_mail prefix
admin_mail=$(e "$1" | cfg-get-value "admin-mail" 2>/dev/null) || true
server_aliases=$(e "$cfg" | cfg-get-value server-aliases 2>/dev/null) || true
[ "$server_aliases" == None ] && server_aliases=""
if [ "$server_aliases" ]; then
server_aliases=($(e "$server_aliases" | shyaml get-values)) || return 1
if [ -z "$domain" ]; then
err "You can't specify server aliases if you don't have a domain."
return 1
fi
else
server_aliases=()
fi
if [ "$protocol" == "https" ]; then
prefix="s-"
else
prefix=
fi
cat <<EOF
ServerAdmin ${admin_mail:-contact@$domain}
ServerName ${domain}
$(
for alias in "${server_aliases[@]}"; do
[ "$alias" ] || continue
echo "ServerAlias $alias"
done
)
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 cfg="$1" custom_rules type elt value first
custom_rules=$(e "$cfg" | cfg-get-value 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 cfg="$1" custom_rules
custom_rules=$(_get_custom_rules "$cfg") || return 1
if [ "$custom_rules" ]; then
cat <<EOF
##
## Custom rules
##
$custom_rules
EOF
fi
}
__vhost_content_statement() {
local type="$1"
shift
case "$type" in
"web_proxy")
__vhost_proxy_statement "$@" || return 1
;;
"publish_dir")
__vhost_publish_dir_statement "$@" || return 1
;;
esac
}
target-get() {
local cfg="$1" target first_exposed_port base_image
target=$(e "$cfg" | cfg-get-value 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" >&2
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
echo "$target"
}
__vhost_proxy_statement() {
local protocol="$1" cfg="$2" dest="$3"
target=$(target-get "$cfg") || return 1
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 "$cfg" "$dest" | 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 type="$1" protocol="$2" cfg="$3" domain="$4" head_statement custom_rules content_statement
head_statement=$(__vhost_head_statement "$cfg" "$protocol" "$domain") || return 1
custom_rules=$(__vhost_custom_rules "$cfg") || return 1
content_statement=$(__vhost_content_statement "$type" "$protocol" "$cfg" "${domain:-html}") || 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() {
local protocol="$1" cfg="$2" dest="$3" dest
local_path="/var/www/${dest}"
cat <<EOF
##
## Publish directory $local_path
##
DocumentRoot $local_path
<Directory />
Options FollowSymLinks
AllowOverride None
</Directory>
<Directory $local_path>
Options Indexes FollowSymLinks MultiViews
AllowOverride all
$(__vhost_creds_statement "$cfg" "$dest" | 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
"
}