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.

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