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

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