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.

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