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.

530 lines
14 KiB

  1. # -*- mode: shell-script -*-
  2. config_hash=
  3. apache_proxy_dir () {
  4. DOMAIN=$(relation-get domain) || {
  5. err "You must specify a ${WHITE}domain$NORMAL option in relation."
  6. return 1
  7. }
  8. proxy=yes apache_vhost_create || return 1
  9. info "Added $DOMAIN as a proxy to $TARGET."
  10. }
  11. export -f apache_proxy_dir
  12. apache_publish_dir () {
  13. DOMAIN=$(relation-get domain) || {
  14. err "You must specify a ${WHITE}domain$NORMAL option in relation."
  15. return 1
  16. }
  17. DOCKER_SITE_PATH="/var/www/${DOMAIN}"
  18. LOCATION=$(relation-get location 2>/dev/null) ||
  19. LOCATION="$DATASTORE/$BASE_SERVICE_NAME$DOCKER_SITE_PATH"
  20. apache_vhost_create || return 1
  21. info "Added $DOMAIN apache config."
  22. apache_code_dir || return 1
  23. apache_data_dirs
  24. }
  25. export -f apache_publish_dir
  26. apache_vhost_create () {
  27. export APACHE_CONFIG_LOCATION="$SERVICE_CONFIGSTORE/etc/apache2/sites-enabled"
  28. export SERVER_ALIAS=$(relation-get server-aliases 2>/dev/null) || true
  29. export PROTOCOLS=$(__vhost_cfg_normalize_protocol) || return 1
  30. export SSL_PLUGIN_FUN=$(ssl_get_plugin_fun) || return 1
  31. if is_protocol_enabled https; then
  32. "$SSL_PLUGIN_FUN"_vars "$(relation-get ssl)" || return 1
  33. fi
  34. apache_vhost_statement "$PROTOCOLS" |
  35. file_put "$APACHE_CONFIG_LOCATION/$prefix$DOMAIN.conf" || return 1
  36. __vhost_cfg_creds_enabled=$(relation-get creds 2>/dev/null) || true
  37. if [ "$__vhost_cfg_creds_enabled" ]; then
  38. apache_passwd_file || return 1
  39. fi
  40. "$SSL_PLUGIN_FUN"_prepare "$(relation-get ssl)" || return 1
  41. }
  42. is_protocol_enabled() {
  43. local protocol=$1
  44. [[ "$PROTOCOLS" == *",$protocol,"* ]]
  45. }
  46. export -f is_protocol_enabled
  47. __vhost_cfg_normalize_protocol() {
  48. local protocol
  49. if ! protocol=$(relation-get protocol 2>/dev/null); then
  50. protocol=auto
  51. else
  52. protocol=${protocol:-auto}
  53. fi
  54. case "$protocol" in
  55. auto)
  56. if __vhost_cfg_ssl="$(relation-get ssl 2>/dev/null)"; then
  57. protocol="https"
  58. export __vhost_cfg_ssl
  59. else
  60. protocol="http"
  61. fi
  62. ;;
  63. both)
  64. protocol="https,http"
  65. ;;
  66. ssl|https)
  67. protocol="https"
  68. ;;
  69. http)
  70. protocol="http"
  71. ;;
  72. *)
  73. err "Invalid value '$protocol' for ${WHITE}protocol$NORMAL option (use one of: http, https, both, auto)."
  74. return 1
  75. esac
  76. echo ",$protocol,"
  77. }
  78. ## ssl_plugin_* and ssl_fallback should :
  79. ## - do anything to ensure that
  80. ## - issue config-add to add volumes if necessary
  81. ## - output 3 vars of where to find the 3 files from within the docker apache
  82. ssl_get_plugin_fun() {
  83. local cfg="$(relation-get ssl 2>/dev/null)"
  84. if [[ "$(echo "$cfg" | shyaml get-type 2>/dev/null)" == "str" ]]; then
  85. target_relation=
  86. while read-0 relation_name target_service relation_config tech_dep; do
  87. [ "$target_service" == "$cfg" ] || continue
  88. verb "service ${DARKYELLOW}$target_service${NORMAL} matches" \
  89. "${WHITE}ssl${NORMAL} value: candidate relation is ${DARKBLUE}$relation_name${NORMAL}"
  90. fun="ssl_plugin_${relation_name}"
  91. if declare -F "${fun}_vars" >/dev/null 2>&1 && declare -F "${fun}_prepare" >/dev/null 2>&1; then
  92. verb "Corresponding plugin ${DARKGREEN}found${NORMAL} for relation ${DARKBLUE}$relation_name${NORMAL}"
  93. echo "$fun"
  94. return 0
  95. else
  96. verb "Corresponding plugin ${DARKRED}not found${NORMAL} for relation ${DARKBLUE}$relation_name${NORMAL}"
  97. fi
  98. done < <(get_compose_relations "$SERVICE_NAME") || return 1
  99. err "Invalid ${WHITE}ssl${NORMAL} value: '$cfg' is not a valid linked service through a support relation."
  100. return 1
  101. else
  102. echo ssl_fallback
  103. fi
  104. }
  105. ssl_fallback_vars() {
  106. local cfg="$1" cert key ca_cert
  107. if __vhost_cfg_ssl_cert=$(echo "$cfg" | shyaml get-value cert 2>/dev/null); then
  108. __vhost_cfg_SSL_CERT_LOCATION=/etc/ssl/certs/${DOMAIN}.pem
  109. fi
  110. if __vhost_cfg_ssl_key=$(echo "$cfg" | shyaml get-value key 2>/dev/null); then
  111. __vhost_cfg_SSL_KEY_LOCATION=/etc/ssl/private/${DOMAIN}.key
  112. fi
  113. if __vhost_cfg_ssl_ca_cert=$(echo "$cfg" | shyaml get-value ca-cert 2>/dev/null); then
  114. __vhost_cfg_SSL_CA_CERT_LOCATION=/etc/ssl/certs/${DOMAIN}-ca.pem
  115. fi
  116. }
  117. ssl_fallback_prepare() {
  118. local cfg="$1" cert key ca_cert
  119. dst="$CONFIGSTORE/$BASE_SERVICE_NAME"
  120. volumes=""
  121. for label in cert key ca_cert; do
  122. content="$(eval echo "\"\$__vhost_cfg_ssl_$label\"")"
  123. if [ "$content" ]; then
  124. location="$(eval echo "\$__vhost_cfg_SSL_${label^^}_LOCATION")"
  125. echo "$content" | file_put "$dst$location"
  126. config_hash=$(printf "%s\0" "$config_hash" "$label" "$content" | md5_compat)
  127. volumes="$volumes
  128. - $dst$location:$location:ro"
  129. fi
  130. done
  131. if [ "$volumes" ]; then
  132. config-add "\
  133. services:
  134. $MASTER_TARGET_SERVICE_NAME:
  135. volumes:
  136. $volumes
  137. "
  138. fi
  139. }
  140. ssl_plugin_letsencrypt-dns_vars() {
  141. __vhost_cfg_SSL_CERT_LOCATION=/etc/letsencrypt/live/${DOMAIN}/cert.pem
  142. __vhost_cfg_SSL_KEY_LOCATION=/etc/letsencrypt/live/${DOMAIN}/privkey.pem
  143. __vhost_cfg_SSL_CHAIN=/etc/letsencrypt/live/${DOMAIN}/chain.pem
  144. }
  145. ssl_plugin_letsencrypt-dns_prepare() {
  146. local service="$1" letsencrypt_charm
  147. shift
  148. export DEFAULT_COMPOSE_FILE="$COMPOSE_YML_FILE"
  149. run_service_action "$service" add "$DOMAIN" $(echo "$SERVER_ALIAS" | shyaml get-values 2>/dev/null) || return 1
  150. letsencrypt_charm=$(get_service_charm "$service") || return 1
  151. config-add "\
  152. services:
  153. $MASTER_TARGET_SERVICE_NAME:
  154. volumes:
  155. - $DATASTORE/${letsencrypt_charm}/etc/letsencrypt:/etc/letsencrypt:ro
  156. " || return 1
  157. }
  158. apache_passwd_file() {
  159. include parse || true
  160. ## XXXvlab: called twice... no better way to do this ?
  161. __vhost_creds_statement >/dev/null
  162. first=
  163. if ! [ -e "$CONFIGSTORE/$MASTER_TARGET_SERVICE_NAME$password_file" ]; then
  164. debug "No file $CONFIGSTORE/$MASTER_TARGET_SERVICE_NAME$password_file, creating password file." || true
  165. first=c
  166. fi
  167. while read-0 login password; do
  168. debug "htpasswd -b$first '${password_file}' '$login' '$password'"
  169. echo "htpasswd -b$first '${password_file}' '$login' '$password'"
  170. if [ "$first" ]; then
  171. first=
  172. fi
  173. done < <(echo "$__vhost_cfg_creds_enabled" | shyaml key-values-0 2>/dev/null) |
  174. docker run -i --entrypoint "/bin/bash" \
  175. -v "$APACHE_CONFIG_LOCATION:/etc/apache2/sites-enabled" \
  176. "$DOCKER_BASE_IMAGE" || return 1
  177. }
  178. ## Produce the full statements depending on relation-get informations
  179. apache_vhost_statement() {
  180. local vhost_statement
  181. export SERVER_ALIAS=$(relation-get server-aliases 2>/dev/null) || true
  182. export PROTOCOLS="$1"
  183. if is_protocol_enabled http; then
  184. __vhost_full_vhost_statement http
  185. fi
  186. if is_protocol_enabled https; then
  187. export SSL_PLUGIN_FUN=$(ssl_get_plugin_fun) || return 1
  188. "$SSL_PLUGIN_FUN"_vars "$(relation-get ssl 2>/dev/null)"
  189. cat <<EOF
  190. <IfModule mod_ssl.c>
  191. $(__vhost_full_vhost_statement https | prefix " ")
  192. </IfModule>
  193. EOF
  194. fi
  195. }
  196. export -f apache_vhost_statement
  197. apache_code_dir() {
  198. local www_data_gid
  199. www_data_gid=$(cached_cmd_on_base_image apache 'id -g www-data') || {
  200. debug "Failed to query for www-data gid in ${DARKYELLOW}apache${NORMAL} base image."
  201. return 1
  202. }
  203. mkdir -p "$LOCATION" || return 1
  204. setfacl -R -m g:"$www_data_gid":rx "$LOCATION"
  205. info "Set permission for read and traversal on '$LOCATION'."
  206. config-add "
  207. $MASTER_BASE_SERVICE_NAME:
  208. volumes:
  209. - $LOCATION:$DOCKER_SITE_PATH
  210. "
  211. }
  212. apache_data_dirs() {
  213. DATA_DIRS=$(relation-get data-dirs 2>/dev/null | shyaml get-values 2>/dev/null) || true
  214. if [ -z "$DATA_DIRS" ]; then
  215. return 0
  216. fi
  217. DST=$DATASTORE/$BASE_SERVICE_NAME$DOCKER_SITE_PATH
  218. DATA=()
  219. while IFS="," read -ra ADDR; do
  220. for dir in "${ADDR[@]}"; do
  221. DATA+=($dir)
  222. done
  223. done <<< "$DATA_DIRS"
  224. www_data_gid=$(cached_cmd_on_base_image apache 'id -g www-data') || {
  225. debug "Failed to query for www-data gid in ${DARKYELLOW}apache${NORMAL} base image."
  226. return 1
  227. }
  228. info "www-data gid from ${DARKYELLOW}apache${NORMAL} is '$www_data_gid'"
  229. dirs=()
  230. for d in "${DATA[@]}"; do
  231. dirs+=("$DST/$d")
  232. done
  233. mkdir -p "${dirs[@]}"
  234. setfacl -R -m g:"$www_data_gid":rwx "${dirs[@]}"
  235. setfacl -R -d -m g:"$www_data_gid":rwx "${dirs[@]}"
  236. config-add "
  237. $MASTER_BASE_SERVICE_NAME:
  238. volumes:
  239. $(
  240. for d in "${DATA[@]}"; do
  241. echo " - $DST/$d:$DOCKER_SITE_PATH/$d"
  242. done
  243. )"
  244. }
  245. deploy_files() {
  246. local src="$1" dst="$2"
  247. if ! [ -d "$dst" ]; then
  248. err "Destination '$dst' does not exist or is not a directory"
  249. return 1
  250. fi
  251. (
  252. cd "$dst" && info "In $dst:" &&
  253. get_file "$src" | tar xv
  254. )
  255. }
  256. export -f deploy_files
  257. apache_core_rules_add() {
  258. local conf="$1" dst="/etc/apache2/conf-enabled/$BASE_SERVICE_NAME.conf"
  259. debug "Adding core rule."
  260. echo "$conf" | file_put "$CONFIGSTORE/$BASE_SERVICE_NAME$dst"
  261. config_hash=$(printf "%s\0" "$config_hash" "$conf" | md5_compat)
  262. config-add "
  263. $MASTER_BASE_SERVICE_NAME:
  264. volumes:
  265. - $CONFIGSTORE/$BASE_SERVICE_NAME$dst:$dst:ro
  266. "
  267. }
  268. __vhost_ssl_statement() {
  269. ## defaults
  270. __vhost_cfg_SSL_CERT_LOCATION=${__vhost_cfg_SSL_CERT_LOCATION:-/etc/ssl/certs/ssl-cert-snakeoil.pem}
  271. __vhost_cfg_SSL_KEY_LOCATION=${__vhost_cfg_SSL_KEY_LOCATION:-/etc/ssl/private/ssl-cert-snakeoil.key}
  272. cat <<EOF
  273. ##
  274. ## SSL Configuration
  275. ##
  276. SSLEngine On
  277. SSLCertificateFile $__vhost_cfg_SSL_CERT_LOCATION
  278. SSLCertificateKeyFile $__vhost_cfg_SSL_KEY_LOCATION
  279. $([ -z "$__vhost_cfg_SSL_CA_CERT_LOCATION" ] || echo "SSLCACertificateFile $__vhost_cfg_SSL_CA_CERT_LOCATION")
  280. $([ -z "$__vhost_cfg_SSL_CHAIN" ] || echo "SSLCertificateChainFile $__vhost_cfg_SSL_CHAIN")
  281. SSLVerifyClient None
  282. EOF
  283. }
  284. __vhost_creds_statement() {
  285. if ! __vhost_cfg_creds_enabled=$(relation-get creds 2>/dev/null); then
  286. echo "Allow from all"
  287. return 0
  288. fi
  289. password_file=/etc/apache2/sites-enabled/${DOMAIN}.passwd
  290. cat <<EOF
  291. AuthType basic
  292. AuthName "private"
  293. AuthUserFile ${password_file}
  294. Require valid-user
  295. EOF
  296. }
  297. __vhost_head_statement() {
  298. local protocol="$1"
  299. if [ "$protocol" == "https" ]; then
  300. prefix="s-"
  301. else
  302. prefix=
  303. fi
  304. cat <<EOF
  305. ServerAdmin ${ADMIN_MAIL:-contact@$DOMAIN}
  306. ServerName ${DOMAIN}
  307. $(
  308. while read-0 alias; do
  309. echo "ServerAlias $alias"
  310. done < <(echo "$SERVER_ALIAS" | shyaml get-values-0 2>/dev/null)
  311. )
  312. ServerSignature Off
  313. CustomLog /var/log/apache2/${prefix}${DOMAIN}_access.log combined
  314. ErrorLog /var/log/apache2/${prefix}${DOMAIN}_error.log
  315. ErrorLog syslog:local2
  316. EOF
  317. }
  318. __vhost_custom_rules() {
  319. local custom_rules
  320. if custom_rules=$(relation-get apache-custom-rules 2>/dev/null); then
  321. cat <<EOF
  322. ##
  323. ## Custom rules
  324. ##
  325. $custom_rules
  326. EOF
  327. fi
  328. }
  329. __vhost_content_statement() {
  330. if [ "$proxy" ]; then
  331. __vhost_proxy_statement "$@"
  332. else
  333. __vhost_publish_dir_statement "$@"
  334. fi
  335. }
  336. __vhost_proxy_statement() {
  337. local protocol="$1"
  338. TARGET=$(relation-get target 2>/dev/null) || true
  339. if [ -z "$TARGET" ]; then
  340. ## First exposed port:
  341. base_image=$(service_base_docker_image "$BASE_SERVICE_NAME") || return 1
  342. first_exposed_port=$(image_exposed_ports_0 "$base_image" | tr '\0' '\n' | head -n 1 | cut -f 1 -d /) || return 1
  343. TARGET=$MASTER_BASE_SERVICE_NAME:$first_exposed_port
  344. info "No target was specified, introspection found: $TARGET"
  345. fi
  346. cat <<EOF
  347. ##
  348. ## Proxy declaration towards $TARGET
  349. ##
  350. <IfModule mod_proxy.c>
  351. ProxyRequests Off
  352. <Proxy *>
  353. Order deny,allow
  354. Allow from all
  355. </Proxy>
  356. ProxyVia On
  357. ProxyPass / http://$TARGET/ retry=0
  358. <Location / >
  359. $(__vhost_creds_statement | prefix " ")
  360. ProxyPassReverse /
  361. </Location>
  362. $([ "$protocol" == "https" ] && echo " SSLProxyEngine On")
  363. </IfModule>
  364. RequestHeader set "X-Forwarded-Proto" "$protocol"
  365. ## Fix IE problem (httpapache proxy dav error 408/409)
  366. SetEnv proxy-nokeepalive 1
  367. EOF
  368. }
  369. __vhost_full_vhost_statement() {
  370. local protocol="$1"
  371. case "$protocol" in
  372. https)
  373. PORT=443
  374. ;;
  375. http)
  376. PORT=80
  377. ;;
  378. esac
  379. cat <<EOF
  380. <VirtualHost *:$PORT>
  381. $(__vhost_head_statement "$protocol" | prefix " " && echo)
  382. $(__vhost_custom_rules | prefix " " && echo)
  383. $(__vhost_content_statement "$protocol" | prefix " ")
  384. ## Forbid any cache, this is only usefull on dev server.
  385. #Header set Cache-Control "no-cache"
  386. #Header set Access-Control-Allow-Origin "*"
  387. #Header set Access-Control-Allow-Methods "POST, GET, OPTIONS"
  388. #Header set Access-Control-Allow-Headers "origin, content-type, accept"
  389. $([ "$protocol" == "https" ] && __vhost_ssl_statement | prefix " " && echo )
  390. </VirtualHost>
  391. EOF
  392. }
  393. __vhost_publish_dir_statement() {
  394. cat <<EOF
  395. ##
  396. ## Publish directory $DOCKER_SITE_PATH
  397. ##
  398. DocumentRoot $DOCKER_SITE_PATH
  399. <Directory />
  400. Options FollowSymLinks
  401. AllowOverride None
  402. </Directory>
  403. <Directory $DOCKER_SITE_PATH>
  404. Options Indexes FollowSymLinks MultiViews
  405. AllowOverride all
  406. $(__vhost_creds_statement | prefix " ")
  407. </Directory>
  408. EOF
  409. }
  410. apache_config_hash() {
  411. debug "Adding config hash to enable recreating upon config change."
  412. config_hash=$({
  413. printf "%s\0" "$config_hash"
  414. find "$SERVICE_CONFIGSTORE/etc/apache2/sites-enabled" \
  415. -name \*.conf -exec md5sum {} \;
  416. } | md5_compat) || exit 1
  417. init-config-add "
  418. $MASTER_BASE_SERVICE_NAME:
  419. labels:
  420. - compose.config_hash=$config_hash
  421. "
  422. }