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.

674 lines
18 KiB

  1. # -*- mode: shell-script -*-
  2. config_hash=
  3. get_domain () {
  4. relation-get domain 2>/dev/null && return 0
  5. ## is service name a regex ?
  6. if [[ "$BASE_SERVICE_NAME" =~ ^([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}$ ]]; then
  7. echo "$BASE_SERVICE_NAME"
  8. return 0
  9. fi
  10. err "You must specify a ${WHITE}domain$NORMAL option in relation."
  11. return 1
  12. }
  13. apache_proxy_dir () {
  14. DOMAIN=$(get_domain) || return 1
  15. relation-set domain "$DOMAIN"
  16. proxy=yes apache_vhost_create || return 1
  17. info "Added $DOMAIN as a proxy to $TARGET."
  18. }
  19. export -f apache_proxy_dir
  20. apache_publish_dir () {
  21. DOMAIN=$(get_domain) || return 1
  22. relation-set domain "$DOMAIN"
  23. DOCKER_SITE_PATH="/var/www/${DOMAIN}"
  24. LOCATION=$(relation-get location 2>/dev/null) ||
  25. LOCATION="$DATASTORE/$BASE_SERVICE_NAME$DOCKER_SITE_PATH"
  26. apache_vhost_create || return 1
  27. info "Added $DOMAIN apache config."
  28. apache_code_dir || return 1
  29. apache_data_dirs
  30. }
  31. export -f apache_publish_dir
  32. apache_vhost_create () {
  33. local custom_rules
  34. export APACHE_CONFIG_LOCATION="$SERVICE_CONFIGSTORE/etc/apache2/sites-enabled" vhost_statement
  35. SERVER_ALIAS=$(relation-get server-aliases 2>/dev/null) || true
  36. PROTOCOLS=$(__vhost_cfg_normalize_protocol) || return 1
  37. export SERVER_ALIAS PROTOCOLS SSL_PLUGIN_FUN SSL_CFG_{VALUE,OPTION}
  38. if is_protocol_enabled https; then
  39. read-0 SSL_PLUGIN_FUN SSL_CFG_VALUE SSL_CFG_OPTIONS < <(ssl_get_plugin_fun) || return 1
  40. "$SSL_PLUGIN_FUN"_vars "$SSL_CFG_OPTIONS" "$SSL_CFG_VALUE" || return 1
  41. redirect=$(relation-get 'redirect-to-ssl' 2>/dev/null) || true
  42. if is_protocol_enabled http; then
  43. redirect=${redirect:-true}
  44. else
  45. redirect=false
  46. fi
  47. if [ "$redirect" == "true" ]; then
  48. custom_rules=$(_get_custom_rules) || return 1
  49. if [[ "$custom_rules" != *"## Auto-redirection from http to https"* ]]; then
  50. relation-set apache-custom-rules "- |
  51. ## Auto-redirection from http to https
  52. RewriteEngine On
  53. RewriteCond %{HTTPS} off
  54. RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [R=302,L,QSA]
  55. $(if [ "$custom_rules" ]; then
  56. echo "- |"$'\n'"$(echo "$custom_rules" | prefix " ")"
  57. fi)"
  58. fi
  59. relation-set protocol https
  60. else
  61. ## Both services are available and different, don't do anything then ?
  62. #relation-set protocol https
  63. :
  64. fi
  65. else
  66. relation-set protocol http
  67. fi
  68. vhost_statement=$(apache_vhost_statement "$PROTOCOLS") || return 1
  69. echo "$vhost_statement"| file_put "$APACHE_CONFIG_LOCATION/$prefix$DOMAIN.conf" || return 1
  70. __vhost_cfg_creds_enabled=$(relation-get creds 2>/dev/null) || true
  71. if [ "$__vhost_cfg_creds_enabled" ]; then
  72. apache_passwd_file || return 1
  73. fi
  74. if is_protocol_enabled https; then
  75. "$SSL_PLUGIN_FUN"_prepare "$SSL_CFG_OPTIONS" "$SSL_CFG_VALUE" || return 1
  76. fi
  77. }
  78. is_protocol_enabled() {
  79. local protocol=$1
  80. [[ "$PROTOCOLS" == *",$protocol,"* ]]
  81. }
  82. export -f is_protocol_enabled
  83. _get_ssl_option_value() {
  84. local target_relation rn ts rc td
  85. relation-get ssl 2>/dev/null && return 0
  86. target_relation="cert-provider"
  87. while read-0 rn ts rc td; do
  88. [ "$rn" == "${target_relation}" ] || continue
  89. info "A cert-provider '$ts' declared as 'ssl' value"
  90. echo "$ts"
  91. return 0
  92. done < <(get_service_relations "$SERVICE_NAME")
  93. return 1
  94. }
  95. __vhost_cfg_normalize_protocol() {
  96. local protocol
  97. if ! protocol=$(relation-get protocol 2>/dev/null); then
  98. protocol=auto
  99. else
  100. protocol=${protocol:-auto}
  101. fi
  102. case "$protocol" in
  103. auto)
  104. if __vhost_cfg_ssl="$(_get_ssl_option_value)"; then
  105. protocol="http,https"
  106. export __vhost_cfg_ssl
  107. else
  108. protocol="http"
  109. fi
  110. ;;
  111. both)
  112. protocol="https,http"
  113. ;;
  114. ssl|https)
  115. protocol="https"
  116. ;;
  117. http)
  118. protocol="http"
  119. ;;
  120. *)
  121. err "Invalid value '$protocol' for ${WHITE}protocol$NORMAL option (use one of: http, https, both, auto)."
  122. return 1
  123. esac
  124. echo ",$protocol,"
  125. }
  126. ## ssl_plugin_* and ssl_fallback should :
  127. ## - do anything to ensure that
  128. ## - issue config-add to add volumes if necessary
  129. ## - output 3 vars of where to find the 3 files from within the docker apache
  130. ssl_get_plugin_fun() {
  131. # from ssl conf, return the function that should manage SSL code creation
  132. local cfg type keys
  133. cfg=$(_get_ssl_option_value)
  134. if [ -z "$cfg" ]; then
  135. return 0
  136. else
  137. type="$(echo "$cfg" | shyaml -y get-type 2>/dev/null)" || return 1
  138. fi
  139. if [[ "$type" == "bool" ]]; then
  140. printf "%s\0" "ssl_fallback" "" "$cfg"
  141. echo ssl_fallback
  142. return 0
  143. fi
  144. if ! [[ "$type" == "str" || "$type" == "struct" ]]; then
  145. err "Invalid ${WHITE}ssl${NORMAL} value type '$type': please provide a string or a struct."
  146. return 1
  147. fi
  148. if [ -z "$NO_CERT_PROVIDER" ]; then
  149. if [[ "$type" == "str" ]]; then
  150. keys=("$cfg")
  151. else
  152. keys=($(echo "$cfg" | shyaml keys 2>/dev/null))
  153. fi
  154. for key in "${keys[@]}"; do
  155. target_relation="cert-provider"
  156. fun="ssl_plugin_${target_relation}"
  157. while read-0 relation_name target_service relation_config tech_dep; do
  158. [ "$relation_name" == "${target_relation}" ] || continue
  159. [ "$target_service" == "$key" ] || continue
  160. verb "Corresponding plugin ${DARKGREEN}found${NORMAL}" \
  161. "in ${DARKBLUE}$relation_name${NORMAL}/${DARKYELLOW}$key${NORMAL}"
  162. ssl_cfg=$(printf "%s" "$cfg" | shyaml get-value "$key" 2>/dev/null) || true
  163. merged_config=$(merge_yaml_str "$relation_config" "$ssl_cfg") || return 1
  164. printf "%s\0" "$fun" "$key" "$merged_config"
  165. return 0
  166. done < <(get_service_relations "$SERVICE_NAME") || return 1
  167. case "$key" in
  168. cert|ca-cert|key)
  169. :
  170. ;;
  171. *)
  172. err "Invalid key '$key' in ${WHITE}ssl${NORMAL}:" \
  173. "no corresponding services declared in ${DARKBLUE}${target_relation}$NORMAL"
  174. return 1
  175. ;;
  176. esac
  177. done
  178. fi
  179. ## No key of the struct seem to be declared cert-provider, so fallback
  180. printf "%s\0" "ssl_fallback" "" "$cfg"
  181. echo ssl_fallback
  182. }
  183. ssl_fallback_vars() {
  184. local cfg="$1" cert key ca_cert
  185. if __vhost_cfg_ssl_cert=$(echo "$cfg" | shyaml get-value cert 2>/dev/null); then
  186. __vhost_cfg_SSL_CERT_LOCATION=/etc/ssl/certs/${DOMAIN}.pem
  187. fi
  188. if __vhost_cfg_ssl_key=$(echo "$cfg" | shyaml get-value key 2>/dev/null); then
  189. __vhost_cfg_SSL_KEY_LOCATION=/etc/ssl/private/${DOMAIN}.key
  190. fi
  191. if __vhost_cfg_ssl_ca_cert=$(echo "$cfg" | shyaml get-value ca-cert 2>/dev/null); then
  192. __vhost_cfg_SSL_CA_CERT_LOCATION=/etc/ssl/certs/${DOMAIN}-ca.pem
  193. fi
  194. }
  195. ssl_fallback_prepare() {
  196. local cfg="$1" cert key ca_cert
  197. dst="$CONFIGSTORE/$BASE_SERVICE_NAME"
  198. volumes=""
  199. for label in cert key ca_cert; do
  200. content="$(eval echo "\"\$__vhost_cfg_ssl_$label\"")"
  201. if [ "$content" ]; then
  202. location="$(eval echo "\$__vhost_cfg_SSL_${label^^}_LOCATION")"
  203. echo "$content" | file_put "$dst$location"
  204. config_hash=$(printf "%s\0" "$config_hash" "$label" "$content" | md5_compat)
  205. volumes="$volumes
  206. - $dst$location:$location:ro"
  207. fi
  208. done
  209. if [ "$volumes" ]; then
  210. config-add "\
  211. services:
  212. $MASTER_TARGET_SERVICE_NAME:
  213. volumes:
  214. $volumes
  215. "
  216. fi
  217. }
  218. ssl_plugin_cert-provider_vars() {
  219. __vhost_cfg_SSL_CERT_LOCATION=/etc/letsencrypt/live/${DOMAIN}/cert.pem
  220. __vhost_cfg_SSL_KEY_LOCATION=/etc/letsencrypt/live/${DOMAIN}/privkey.pem
  221. __vhost_cfg_SSL_CHAIN=/etc/letsencrypt/live/${DOMAIN}/chain.pem
  222. }
  223. ssl_plugin_cert-provider_prepare() {
  224. local cfg="$1" service="$2" options
  225. options=$(yaml_key_val_str "options" "$cfg") || return 1
  226. service_config=$(yaml_key_val_str "$service" "$options")
  227. compose --debug --add-compose-content "$service_config" run --rm --service-ports "$service" \
  228. crt create "$DOMAIN" $(echo "$SERVER_ALIAS" | shyaml get-values 2>/dev/null) || {
  229. err "Failed to launch letsencrypt for certificate creation."
  230. return 1
  231. }
  232. config-add "\
  233. services:
  234. $MASTER_TARGET_SERVICE_NAME:
  235. volumes:
  236. - $DATASTORE/$service/etc/letsencrypt:/etc/letsencrypt:ro
  237. " || return 1
  238. }
  239. apache_passwd_file() {
  240. include parse || true
  241. ## XXXvlab: called twice... no better way to do this ?
  242. __vhost_creds_statement >/dev/null
  243. first=
  244. if ! [ -e "$CONFIGSTORE/$MASTER_TARGET_SERVICE_NAME$password_file" ]; then
  245. debug "No file $CONFIGSTORE/$MASTER_TARGET_SERVICE_NAME$password_file, creating password file." || true
  246. first=c
  247. fi
  248. while read-0 login password; do
  249. debug "htpasswd -b$first '${password_file}' '$login' '$password'"
  250. echo "htpasswd -b$first '${password_file}' '$login' '$password'"
  251. if [ "$first" ]; then
  252. first=
  253. fi
  254. done < <(echo "$__vhost_cfg_creds_enabled" | shyaml key-values-0 2>/dev/null) |
  255. docker run -i --entrypoint "/bin/bash" \
  256. -v "$APACHE_CONFIG_LOCATION:/etc/apache2/sites-enabled" \
  257. "$DOCKER_BASE_IMAGE" || return 1
  258. }
  259. ## Produce the full statements depending on relation-get informations
  260. apache_vhost_statement() {
  261. local vhost_statement
  262. export SERVER_ALIAS=$(relation-get server-aliases 2>/dev/null) || true
  263. export PROTOCOLS="$1"
  264. if is_protocol_enabled http; then
  265. __vhost_full_vhost_statement http || return 1
  266. fi
  267. if is_protocol_enabled https; then
  268. "$SSL_PLUGIN_FUN"_vars "$(_get_ssl_option_value 2>/dev/null)" || return 1
  269. vhost_statement=$(__vhost_full_vhost_statement https) || return 1
  270. cat <<EOF
  271. <IfModule mod_ssl.c>
  272. $(echo "$vhost_statement" | prefix " ")
  273. </IfModule>
  274. EOF
  275. fi
  276. }
  277. export -f apache_vhost_statement
  278. apache_code_dir() {
  279. local www_data_gid
  280. www_data_gid=$(cached_cmd_on_base_image apache 'id -g www-data') || {
  281. debug "Failed to query for www-data gid in ${DARKYELLOW}apache${NORMAL} base image."
  282. return 1
  283. }
  284. mkdir -p "$LOCATION" || return 1
  285. setfacl -R -m g:"$www_data_gid":rx "$LOCATION"
  286. info "Set permission for read and traversal on '$LOCATION'."
  287. config-add "
  288. $MASTER_BASE_SERVICE_NAME:
  289. volumes:
  290. - $LOCATION:$DOCKER_SITE_PATH
  291. "
  292. }
  293. apache_data_dirs() {
  294. DATA_DIRS=$(relation-get data-dirs 2>/dev/null | shyaml get-values 2>/dev/null) || true
  295. if [ -z "$DATA_DIRS" ]; then
  296. return 0
  297. fi
  298. DST=$DATASTORE/$BASE_SERVICE_NAME$DOCKER_SITE_PATH
  299. DATA=()
  300. while IFS="," read -ra ADDR; do
  301. for dir in "${ADDR[@]}"; do
  302. DATA+=($dir)
  303. done
  304. done <<< "$DATA_DIRS"
  305. www_data_gid=$(cached_cmd_on_base_image apache 'id -g www-data') || {
  306. debug "Failed to query for www-data gid in ${DARKYELLOW}apache${NORMAL} base image."
  307. return 1
  308. }
  309. info "www-data gid from ${DARKYELLOW}apache${NORMAL} is '$www_data_gid'"
  310. dirs=()
  311. for d in "${DATA[@]}"; do
  312. dirs+=("$DST/$d")
  313. done
  314. mkdir -p "${dirs[@]}"
  315. setfacl -R -m g:"$www_data_gid":rwx "${dirs[@]}"
  316. setfacl -R -d -m g:"$www_data_gid":rwx "${dirs[@]}"
  317. config-add "
  318. $MASTER_BASE_SERVICE_NAME:
  319. volumes:
  320. $(
  321. for d in "${DATA[@]}"; do
  322. echo " - $DST/$d:$DOCKER_SITE_PATH/$d"
  323. done
  324. )"
  325. }
  326. deploy_files() {
  327. local src="$1" dst="$2"
  328. if ! [ -d "$dst" ]; then
  329. err "Destination '$dst' does not exist or is not a directory"
  330. return 1
  331. fi
  332. (
  333. cd "$dst" && info "In $dst:" &&
  334. get_file "$src" | tar xv
  335. )
  336. }
  337. export -f deploy_files
  338. apache_core_rules_add() {
  339. local conf="$1" dst="/etc/apache2/conf-enabled/$BASE_SERVICE_NAME.conf"
  340. debug "Adding core rule."
  341. echo "$conf" | file_put "$CONFIGSTORE/$BASE_SERVICE_NAME$dst"
  342. config_hash=$(printf "%s\0" "$config_hash" "$conf" | md5_compat)
  343. config-add "
  344. $MASTER_BASE_SERVICE_NAME:
  345. volumes:
  346. - $CONFIGSTORE/$BASE_SERVICE_NAME$dst:$dst:ro
  347. "
  348. }
  349. __vhost_ssl_statement() {
  350. ## defaults
  351. __vhost_cfg_SSL_CERT_LOCATION=${__vhost_cfg_SSL_CERT_LOCATION:-/etc/ssl/certs/ssl-cert-snakeoil.pem}
  352. __vhost_cfg_SSL_KEY_LOCATION=${__vhost_cfg_SSL_KEY_LOCATION:-/etc/ssl/private/ssl-cert-snakeoil.key}
  353. cat <<EOF
  354. ##
  355. ## SSL Configuration
  356. ##
  357. SSLEngine On
  358. SSLCertificateFile $__vhost_cfg_SSL_CERT_LOCATION
  359. SSLCertificateKeyFile $__vhost_cfg_SSL_KEY_LOCATION
  360. $([ -z "$__vhost_cfg_SSL_CA_CERT_LOCATION" ] || echo "SSLCACertificateFile $__vhost_cfg_SSL_CA_CERT_LOCATION")
  361. $([ -z "$__vhost_cfg_SSL_CHAIN" ] || echo "SSLCertificateChainFile $__vhost_cfg_SSL_CHAIN")
  362. SSLVerifyClient None
  363. EOF
  364. }
  365. __vhost_creds_statement() {
  366. if ! __vhost_cfg_creds_enabled=$(relation-get creds 2>/dev/null); then
  367. echo "Allow from all"
  368. return 0
  369. fi
  370. password_file=/etc/apache2/sites-enabled/${DOMAIN}.passwd
  371. cat <<EOF
  372. AuthType basic
  373. AuthName "private"
  374. AuthUserFile ${password_file}
  375. Require valid-user
  376. EOF
  377. }
  378. __vhost_head_statement() {
  379. local protocol="$1"
  380. if [ "$protocol" == "https" ]; then
  381. prefix="s-"
  382. else
  383. prefix=
  384. fi
  385. cat <<EOF
  386. ServerAdmin ${ADMIN_MAIL:-contact@$DOMAIN}
  387. ServerName ${DOMAIN}
  388. $(
  389. while read-0 alias; do
  390. echo "ServerAlias $alias"
  391. done < <(echo "$SERVER_ALIAS" | shyaml get-values-0 2>/dev/null)
  392. )
  393. ServerSignature Off
  394. CustomLog /var/log/apache2/${prefix}${DOMAIN}_access.log combined
  395. ErrorLog /var/log/apache2/${prefix}${DOMAIN}_error.log
  396. ErrorLog syslog:local2
  397. EOF
  398. }
  399. _get_custom_rules() {
  400. local custom_rules type elt value first
  401. custom_rules=$(relation-get apache-custom-rules 2>/dev/null) || true
  402. if [ -z "$custom_rules" ]; then
  403. return 0
  404. fi
  405. type=$(echo "$custom_rules" | shyaml get-type)
  406. value=
  407. case "$type" in
  408. "sequence")
  409. first=1
  410. while read-0 elt; do
  411. elt="$(echo "$elt" | yaml_get_interpret)" || return 1
  412. [ "$elt" ] || continue
  413. if [ "$first" ]; then
  414. first=
  415. else
  416. value+=$'\n'$'\n'
  417. fi
  418. first=
  419. value+="$elt"
  420. done < <(echo "$custom_rules" | shyaml -y get-values-0)
  421. ;;
  422. "struct")
  423. while read-0 _key val; do
  424. value+=$'\n'"$(echo "$val" | yaml_get_interpret)" || return 1
  425. done < <(echo "$custom_rules" | shyaml -y key-values-0)
  426. ;;
  427. "str")
  428. value+=$(echo "$custom_rules")
  429. ;;
  430. *)
  431. value+=$(echo "$custom_rules")
  432. ;;
  433. esac
  434. printf "%s" "$value"
  435. }
  436. __vhost_custom_rules() {
  437. local custom_rules
  438. custom_rules=$(_get_custom_rules) || return 1
  439. if [ "$custom_rules" ]; then
  440. cat <<EOF
  441. ##
  442. ## Custom rules
  443. ##
  444. $custom_rules
  445. EOF
  446. fi
  447. }
  448. __vhost_content_statement() {
  449. if [ "$proxy" ]; then
  450. __vhost_proxy_statement "$@" || return 1
  451. else
  452. __vhost_publish_dir_statement "$@" || return 1
  453. fi
  454. }
  455. __vhost_proxy_statement() {
  456. local protocol="$1"
  457. TARGET=$(relation-get target 2>/dev/null) || true
  458. if [ -z "$TARGET" ]; then
  459. ## First exposed port:
  460. base_image=$(service_base_docker_image "$BASE_SERVICE_NAME") || return 1
  461. if ! docker_has_image "$base_image"; then
  462. docker pull "$base_image" >&2
  463. fi
  464. first_exposed_port=$(image_exposed_ports_0 "$base_image" | tr '\0' '\n' | head -n 1 | cut -f 1 -d /) || return 1
  465. if [ -z "$first_exposed_port" ]; then
  466. err "Failed to get first exposed port of image '$base_image'."
  467. return 1
  468. fi
  469. TARGET=$MASTER_BASE_SERVICE_NAME:$first_exposed_port
  470. info "No target was specified, introspection found: $TARGET"
  471. fi
  472. cat <<EOF
  473. ##
  474. ## Proxy declaration towards $TARGET
  475. ##
  476. <IfModule mod_proxy.c>
  477. ProxyRequests Off
  478. <Proxy *>
  479. Order deny,allow
  480. Allow from all
  481. </Proxy>
  482. ProxyVia On
  483. ProxyPass / http://$TARGET/ retry=0
  484. <Location / >
  485. $(__vhost_creds_statement | prefix " ")
  486. ProxyPassReverse /
  487. </Location>
  488. $([ "$protocol" == "https" ] && echo " SSLProxyEngine On")
  489. </IfModule>
  490. RequestHeader set "X-Forwarded-Proto" "$protocol"
  491. ## Fix IE problem (httpapache proxy dav error 408/409)
  492. SetEnv proxy-nokeepalive 1
  493. EOF
  494. }
  495. __vhost_full_vhost_statement() {
  496. local protocol="$1" head_statement custom_rules content_statement
  497. head_statement=$(__vhost_head_statement "$protocol") || return 1
  498. custom_rules=$(__vhost_custom_rules) || return 1
  499. content_statement=$(__vhost_content_statement "$protocol") || return 1
  500. case "$protocol" in
  501. https)
  502. PORT=443
  503. ;;
  504. http)
  505. PORT=80
  506. ;;
  507. esac
  508. cat <<EOF
  509. <VirtualHost *:$PORT>
  510. $(echo "$head_statement" | prefix " ")
  511. $(echo "$custom_rules" | prefix " ")
  512. $(echo "$content_statement" | prefix " ")
  513. ## Forbid any cache, this is only usefull on dev server.
  514. #Header set Cache-Control "no-cache"
  515. #Header set Access-Control-Allow-Origin "*"
  516. #Header set Access-Control-Allow-Methods "POST, GET, OPTIONS"
  517. #Header set Access-Control-Allow-Headers "origin, content-type, accept"
  518. $([ "$protocol" == "https" ] && __vhost_ssl_statement | prefix " " && echo )
  519. </VirtualHost>
  520. EOF
  521. }
  522. __vhost_publish_dir_statement() {
  523. cat <<EOF
  524. ##
  525. ## Publish directory $DOCKER_SITE_PATH
  526. ##
  527. DocumentRoot $DOCKER_SITE_PATH
  528. <Directory />
  529. Options FollowSymLinks
  530. AllowOverride None
  531. </Directory>
  532. <Directory $DOCKER_SITE_PATH>
  533. Options Indexes FollowSymLinks MultiViews
  534. AllowOverride all
  535. $(__vhost_creds_statement | prefix " ")
  536. </Directory>
  537. EOF
  538. }
  539. apache_config_hash() {
  540. debug "Adding config hash to enable recreating upon config change."
  541. config_hash=$({
  542. printf "%s\0" "$config_hash"
  543. find "$SERVICE_CONFIGSTORE/etc/apache2/sites-enabled" \
  544. -name \*.conf -exec md5sum {} \;
  545. } | md5_compat) || exit 1
  546. init-config-add "
  547. $MASTER_BASE_SERVICE_NAME:
  548. labels:
  549. - compose.config_hash=$config_hash
  550. "
  551. }