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.

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