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.

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