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.

635 lines
16 KiB

9 years ago
  1. #!/bin/bash
  2. . /etc/shlib
  3. include pretty
  4. depends shyaml docker
  5. if [ -r /etc/default/charm ]; then
  6. . /etc/default/charm
  7. fi
  8. if [ -r /etc/default/$exname ]; then
  9. . /etc/default/$exname
  10. fi
  11. usage="$exname CHARM"'
  12. Deploy and manage a swarm of containers to provide services based on
  13. a ``compose.yml`` definition and charms from a ``charm-store``.
  14. '
  15. export DEFAULT_COMPOSE_FILE
  16. ##
  17. ## Functions
  18. ##
  19. export APACHE_CONFIG_LOCATION=$CONFIGSTORE/apache/etc/apache2/sites-enabled
  20. apache_ssl_proxy_config () {
  21. local DOMAIN=$1 TARGET=$2
  22. cat <<EOF
  23. <IfModule mod_ssl.c>
  24. <VirtualHost *:443>
  25. ServerAdmin ${ADMIN_MAIL:-contact@$DOMAIN}
  26. ServerName ${DOMAIN}
  27. ServerSignature Off
  28. CustomLog /var/log/apache2/s-${DOMAIN}_access.log combined
  29. ErrorLog /var/log/apache2/s-${DOMAIN}_error.log
  30. ErrorLog syslog:local2
  31. <IfModule mod_proxy.c>
  32. ProxyRequests Off
  33. <Proxy *>
  34. Order deny,allow
  35. Allow from all
  36. </Proxy>
  37. ProxyVia On
  38. ProxyPass / http://$TARGET/ retry=0
  39. <Location / >
  40. ProxyPassReverse /
  41. </Location>
  42. </IfModule>
  43. ## Forbid any cache, this is only usefull on dev server.
  44. #Header set Cache-Control "no-cache"
  45. #Header set Access-Control-Allow-Origin "*"
  46. #Header set Access-Control-Allow-Methods "POST, GET, OPTIONS"
  47. #Header set Access-Control-Allow-Headers "origin, content-type, accept"
  48. RequestHeader set "X-Forwarded-Proto" "https"
  49. ## Fix IE problem (httpapache proxy dav error 408/409)
  50. SetEnv proxy-nokeepalive 1
  51. #ServerSignature On
  52. SSLProxyEngine On
  53. SSLEngine On
  54. ## Full stance
  55. SSLCertificateFile /etc/ssl/certs/ssl-cert-snakeoil.pem
  56. SSLCertificateKeyFile /etc/ssl/private/ssl-cert-snakeoil.key
  57. SSLVerifyClient None
  58. </VirtualHost>
  59. </IfModule>
  60. EOF
  61. }
  62. export -f apache_ssl_proxy_config
  63. apache_ssl_config() {
  64. local DOMAIN=$1
  65. cat <<EOF
  66. <IfModule mod_ssl.c>
  67. <VirtualHost *:443>
  68. ServerAdmin ${ADMIN_MAIL:-contact@$DOMAIN}
  69. ServerName ${DOMAIN}
  70. ServerSignature Off
  71. CustomLog /var/log/apache2/s-${DOMAIN}_access.log combined
  72. ErrorLog /var/log/apache2/s-${DOMAIN}_error.log
  73. ErrorLog syslog:local2
  74. DocumentRoot /var/www/${DOMAIN}
  75. <Directory />
  76. Options FollowSymLinks
  77. AllowOverride None
  78. </Directory>
  79. <Directory /var/www/${DOMAIN}>
  80. Options Indexes FollowSymLinks MultiViews
  81. AllowOverride all
  82. Order allow,deny
  83. allow from all
  84. </Directory>
  85. SSLEngine On
  86. ## Full stance
  87. SSLCertificateFile /etc/ssl/certs/ssl-cert-snakeoil.pem
  88. SSLCertificateKeyFile /etc/ssl/private/ssl-cert-snakeoil.key
  89. SSLVerifyClient None
  90. </VirtualHost>
  91. </IfModule>
  92. EOF
  93. }
  94. export -f apache_ssl_config
  95. apache_ssl_add () {
  96. local DOMAIN=$1
  97. [ -e "$APACHE_CONFIG_LOCATION/$DOMAIN.conf" ] && return 0
  98. mkdir -p "$APACHE_CONFIG_LOCATION"
  99. apache_ssl_config $DOMAIN > $APACHE_CONFIG_LOCATION/$DOMAIN.conf
  100. echo "Added $DOMAIN apache config." >&2
  101. }
  102. export -f apache_ssl_add
  103. apache_ssl_proxy_add () {
  104. local DOMAIN=$1 TARGET=$2
  105. [ -e "$APACHE_CONFIG_LOCATION/$DOMAIN.conf" ] && return 0
  106. mkdir -p "$APACHE_CONFIG_LOCATION"
  107. apache_ssl_proxy_config $DOMAIN $TARGET > $APACHE_CONFIG_LOCATION/$DOMAIN.conf
  108. echo "Added $DOMAIN as a proxy to $TARGET." >&2
  109. }
  110. export -f apache_ssl_proxy_add
  111. gen_password() {
  112. python -c 'import random; \
  113. xx = "azertyuiopqsdfghjklmwxcvbn1234567890AZERTYUIOPQSDFGHJKLMWXCVBN+_-"; \
  114. print "".join([xx[random.randint(0, len(xx)-1)] for x in range(0, 14)])'
  115. }
  116. export -f gen_password
  117. file_put() {
  118. local TARGET="$1"
  119. mkdir -p "$(dirname "$TARGET")" &&
  120. cat - > "$TARGET"
  121. }
  122. export -f file_put
  123. apache_data_dir() {
  124. local DOMAIN=$1 DATA_COMMA_SEPARATED=$2
  125. export APACHE_DOCKER_IMAGE=$(service_base_docker_image apache)
  126. DOCKER_SITE_PATH=/var/www/$DOMAIN
  127. BASE=$DATASTORE/apache
  128. DST=$BASE/$DOCKER_SITE_PATH
  129. DATA=()
  130. while IFS="," read -ra ADDR; do
  131. for dir in "${ADDR[@]}"; do
  132. mkdir -p "$DST/$dir"
  133. DATA+=($dir)
  134. done
  135. done <<< "$DATA_COMMA_SEPARATED"
  136. if [ -z "$APACHE_DOCKER_GID" ] &&
  137. ! grep "^export APACHE_DOCKER_GID=" /etc/compose.local.conf >/dev/null 2>&1; then
  138. echo "Adding APACHE_DOCKER_GID to '/etc/compose.local.conf'."
  139. export APACHE_DOCKER_GID=$(docker run "$APACHE_DOCKER_IMAGE" id -g www-data)
  140. cat <<EOF >> /etc/compose.local.conf
  141. export APACHE_DOCKER_GID=$APACHE_DOCKER_GID
  142. EOF
  143. fi
  144. dirs=()
  145. for d in "${DATA[@]}"; do
  146. dirs+=("$DST/$d")
  147. done
  148. chgrp www-data "${dirs[@]}" -R && chmod 775 "${dirs[@]}" -R
  149. }
  150. export -f apache_data_dir
  151. export _DOCKER_COMPOSE_DEF=""
  152. get_compose_def() {
  153. local local_compose
  154. if [ "$_DOCKER_COMPOSE_DEF" ]; then
  155. echo "$_DOCKER_COMPOSE_DEF"
  156. return 0
  157. fi
  158. ##
  159. ## Adding sub services configurations
  160. ##
  161. additional_services=
  162. if [ -z "$*" ]; then
  163. info "No service provided, using \$DEFAULT_SERVICES variable. Target services: $DEFAULT_SERVICES"
  164. additional_services=$DEFAULT_SERVICES
  165. fi
  166. declare -A loaded
  167. for target_service in "$@" $additional_services; do
  168. services=$(get_ordered_service_dependencies "$target_service") || return 1
  169. for service in $services; do
  170. if [ "${loaded[$service]}" ]; then
  171. continue
  172. fi
  173. loaded[$service]=1
  174. export _DOCKER_COMPOSE_DEF="$_DOCKER_COMPOSE_DEF
  175. $service:
  176. $(get_service_def "$service" | sed -r 's/^/ /g')"
  177. done
  178. done
  179. echo "$_DOCKER_COMPOSE_DEF"
  180. }
  181. export -f get_compose_def
  182. get_service_def() {
  183. local service="$1"
  184. if [ -z "$service" ]; then
  185. echo "Please specify a service." >&2
  186. return 1
  187. fi
  188. if [ -d "$CHARM_STORE/$service" ]; then
  189. compose_file="$CHARM_STORE/$service/compose.yml"
  190. local_compose=""
  191. if [ -e "$compose_file" ]; then
  192. debug "Found compose.yml in $service directory. Including in 'docker-compose.conf'."
  193. local_compose="$(cat "$compose_file")"
  194. fi
  195. metadata_file="$CHARM_STORE/$service/metadata.yml"
  196. if [ -e "$metadata_file" ]; then
  197. debug "Found metadata.yml in $service directory. Including in 'docker-compose.conf'."
  198. docker_compose_entry=$(get_docker_compose_entry_from_metadata "$service" < "$metadata_file") || return 1
  199. local_compose="$local_compose
  200. $docker_compose_entry"
  201. fi
  202. echo "$local_compose"
  203. return 0
  204. fi
  205. err "service '$DARKYELLOW$service$NORMAL' not found." >&2
  206. return 1
  207. }
  208. export -f get_service_def
  209. service_base_docker_image() {
  210. local service="$1"
  211. service_def="$(get_service_def "$service")" || return 1
  212. service_image=$(echo "$service_def" | shyaml get-value image 2>/dev/null)
  213. if [ "$?" != 0 ]; then
  214. service_build=$(echo "$service_def" | shyaml get-value build)
  215. if [ "$?" != 0 ]; then
  216. echo "Service '$service' has no 'image' nor 'build' parameter." >&2
  217. return 1
  218. fi
  219. service_dockerfile="$COMPOSE_YML_PATH/$service_build/Dockerfile"
  220. if ! [ -e "$service_dockerfile" ]; then
  221. echo "No Dockerfile found in '$service_dockerfile' location." >&2
  222. return 1
  223. fi
  224. grep '^FROM' "$service_dockerfile" | xargs echo | cut -f 2 -d " "
  225. else
  226. echo "$service_image"
  227. fi
  228. }
  229. export -f service_base_docker_image
  230. read-0() {
  231. local eof
  232. eof=
  233. while [ "$1" ]; do
  234. IFS='' read -r -d '' "$1" || eof=true
  235. shift
  236. done
  237. test "$eof" != true
  238. }
  239. export -f read-0
  240. array_values_to_stdin() {
  241. local e
  242. if [ "$#" -ne "1" ]; then
  243. print_syntax_warning "$FUNCNAME: need one argument."
  244. return 1
  245. fi
  246. var="$1"
  247. eval "for e in \"\${$var[@]}\"; do echo -en \"\$e\\0\"; done"
  248. }
  249. array_keys_to_stdin() {
  250. local e
  251. if [ "$#" -ne "1" ]; then
  252. print_syntax_warning "$FUNCNAME: need one argument."
  253. return 1
  254. fi
  255. var="$1"
  256. eval "for e in \"\${!$var[@]}\"; do echo -en \"\$e\\0\"; done"
  257. }
  258. array_kv_to_stdin() {
  259. local e
  260. if [ "$#" -ne "1" ]; then
  261. print_syntax_warning "$FUNCNAME: need one argument."
  262. return 1
  263. fi
  264. var="$1"
  265. eval "for e in \"\${!$var[@]}\"; do echo -n \"\$e\"; echo -en '\0'; echo -n \"\${$var[\$e]}\"; echo -en '\0'; done"
  266. }
  267. array_pop() {
  268. local narr="$1" nres="$2"
  269. for key in $(eval "echo \${!$narr[@]}"); do
  270. eval "$nres=\${$narr[\"\$key\"]}"
  271. eval "unset $narr[\"\$key\"]"
  272. return 0
  273. done
  274. }
  275. export -f array_pop
  276. array_member() {
  277. local src elt
  278. src="$1"
  279. elt="$2"
  280. while read-0 key; do
  281. if [ "$(eval "echo -n \"\${$src[\$key]}\"")" == "$elt" ]; then
  282. return 0
  283. fi
  284. done < <(array_keys_to_stdin "$src")
  285. return 1
  286. }
  287. export -f array_member
  288. get_service_deps() {
  289. local service="$1"
  290. service_def=$(get_service_def "$service") || return 1
  291. echo "$service_def" | shyaml get-values links 2>/dev/null
  292. return 0
  293. }
  294. export -f get_service_deps
  295. ## a service is not always a container.
  296. ## XXXvlab: a service name should not be a container name neither... see this later.
  297. # get_container_name() {
  298. # local service="$1"
  299. # get_service_def "$service" | shyaml get-values links 2>/dev/null
  300. # if [ "$(get_md_service_def "$service" | shyaml get-value subordinate 2>/dev/null)" != "true" ]; then
  301. # echo "$service"
  302. # return 0
  303. # fi
  304. # }
  305. _rec_get_depth() {
  306. local elt=$1
  307. if [ "${depths[$elt]}" ]; then
  308. return 0
  309. fi
  310. deps=$(get_service_deps "$elt") || return 1
  311. if [ -z "$deps" ]; then
  312. depths[$elt]=0
  313. fi
  314. max=0
  315. for dep in $deps; do
  316. _rec_get_depth "$dep" || return 1
  317. if (( "${depths[$dep]}" > "$max" )); then
  318. max="${depths[$dep]}"
  319. fi
  320. done
  321. depths[$elt]=$((max + 1))
  322. }
  323. get_ordered_service_dependencies() {
  324. local services=("$@")
  325. if [ -z "${services[*]}" ]; then
  326. print_syntax_error "$FUNCNAME: no arguments"
  327. fi
  328. declare -A depths
  329. visited=()
  330. heads=("${services[@]}")
  331. while [ "${#heads[@]}" != 0 ]; do
  332. array_pop heads head
  333. visited+=("$head")
  334. _rec_get_depth "$head" || return 1
  335. done
  336. i=0
  337. while [ "${#depths[@]}" != 0 ]; do
  338. for key in "${!depths[@]}"; do
  339. value="${depths[$key]}"
  340. if [ "$value" == "$i" ]; then
  341. echo "$key"
  342. unset depths[$key]
  343. fi
  344. done
  345. i=$((i + 1))
  346. done
  347. }
  348. run_service_hook () {
  349. local service="$1" action="$2"
  350. services=$(get_ordered_service_dependencies "$service") || return 1
  351. ## init in order
  352. for service in $services; do
  353. TARGET_SCRIPT="$COMPOSE_YML_PATH/service/$service/hooks/$2"
  354. [ -e "$TARGET_SCRIPT" ] && {
  355. [ "$verbose" ] && echo "Init $service"
  356. SERVICE_NAME=$service \
  357. DOCKER_BASE_IMAGE=$(service_base_docker_image "$service") \
  358. SERVICE_DATASTORE="$DATASTORE/$service" \
  359. SERVICE_CONFIGSTORE="$CONFIGSTORE/$service" \
  360. "$TARGET_SCRIPT"
  361. }
  362. done
  363. }
  364. run_service_action () {
  365. local service="$1" action="$2"
  366. shift shift
  367. run_service_hook "$service" init
  368. services=$(get_ordered_service_dependencies "$service") || return 1
  369. for service in $services; do
  370. TARGET_SCRIPT="$COMPOSE_YML_PATH/service/$service/actions/$2"
  371. if [ -e "$TARGET_SCRIPT" ]; then
  372. [ "$verbose" ] && echo "Init $service"
  373. SERVICE_NAME=$service \
  374. CONTAINER_NAME=$(get_container_name "$service") \
  375. DOCKER_BASE_IMAGE=$(service_base_docker_image "$service") \
  376. SERVICE_DATASTORE="$DATASTORE/$service" \
  377. SERVICE_CONFIGSTORE="$CONFIGSTORE/$service" \
  378. echo "$TARGET_SCRIPT" "$@"
  379. else
  380. echo "Service '$service' does not have any action '$action' defined." >&2
  381. return 1
  382. fi
  383. done
  384. }
  385. get_docker_compose_entry_from_metadata() {
  386. local service="$1"
  387. metadata="$(cat -)"
  388. export DATASTORE CONFIGSTORE
  389. ## resources to volumes
  390. volumes=$(
  391. for resource_type in data config; do
  392. while read-0 resource; do
  393. eval "echo \" - \$${resource_type^^}STORE/\$service\$resource:\$resource:rw\""
  394. done < <(echo "$metadata" | shyaml get-values-0 "${resource_type}-resources" 2>/dev/null)
  395. done
  396. while read-0 resource; do
  397. if [[ "$resource" == *:* ]]; then
  398. echo " - $resource:rw"
  399. else
  400. echo " - $resource:$resource:rw"
  401. fi
  402. done < <(echo "$metadata" | shyaml get-values-0 "host-resources" 2>/dev/null)
  403. )
  404. if [ "$volumes" ]; then
  405. echo "volumes:"
  406. echo "$volumes"
  407. fi
  408. ## resources to volumes
  409. image=$(echo "$metadata" | shyaml get-values "docker-image" 2>/dev/null)
  410. if [ "$image" ]; then
  411. echo "image: $image"
  412. else
  413. if ! [ -d "$CHARM_STORE/$service/build" ]; then
  414. die "No 'docker-image' value set in 'metadata.yml' nor 'build/' directory found in charm $DARKYELLOW$service$NORMAL."
  415. fi
  416. echo "build: $service/build"
  417. fi
  418. }
  419. export -f get_docker_compose_entry_from_metadata
  420. launch_docker_compose() {
  421. debug "Creating temporary docker-compose directory in '$tmpdir'."
  422. tmpdir=$(mktemp -d -t tmp.XXXXXXXXXX)
  423. function finish {
  424. debug "Removing temporary docker-compose directory in '$tmpdir'."
  425. rm -rf "$tmpdir"
  426. }
  427. trap finish EXIT
  428. get_compose_def > "$tmpdir/docker-compose.yml" || return 1
  429. ## XXXvlab: could be more specific and only link the needed charms
  430. ln -sf "$CHARM_STORE/"* "$tmpdir/"
  431. cd "$tmpdir" && docker-compose "$@"
  432. }
  433. ##
  434. ## Argument parsing
  435. ##
  436. fullargs=()
  437. opts=()
  438. posargs=()
  439. no_hooks=
  440. no_init=
  441. while [ "$#" != 0 ]; do
  442. case "$1" in
  443. # --help|-h)
  444. # print_help
  445. # exit 0
  446. # ;;
  447. --verbose|-v)
  448. fullargs+=("$1")
  449. export VERBOSE=true
  450. ;;
  451. --no-hooks)
  452. export no_hooks=true
  453. ;;
  454. --no-init)
  455. export no_init=true
  456. ;;
  457. --debug)
  458. export DEBUG=true
  459. export VERBOSE=true
  460. ;;
  461. --)
  462. fullargs+=("$1")
  463. shift
  464. opts=("${opts[@]}" "$@")
  465. break 2
  466. ;;
  467. -*)
  468. fullargs+=("$1")
  469. opts=("${opts[@]}" "$1" "$2")
  470. shift
  471. ;;
  472. *)
  473. fullargs+=("$1")
  474. posargs=("${posargs[@]}" "$1")
  475. ;;
  476. esac
  477. shift
  478. done
  479. ##
  480. ## Actual code
  481. ##
  482. export CHARM_STORE=${CHARM_STORE:-/srv/charm-store}
  483. export DOCKER_DATASTORE=${DOCKER_DATASTORE:-/srv/docker-datastore}
  484. ## XXXvlab: should provide YML config opportunities in possible parent dirs ?
  485. ## userdir ? and global /etc/compose.yml ?
  486. . /etc/compose.conf
  487. . /etc/compose.local.conf
  488. if ! [ -d "$CHARM_STORE" ]; then
  489. err "Charm store path $YELLOW$CHARM_STORE$NORMAL does not exists. "
  490. err "Please check your $YELLOW\$CHARM_STORE$NORMAL variable value."
  491. exit 1
  492. fi
  493. if [ -z "$(cd "$CHARM_STORE"; ls)" ]; then
  494. err "no available charms in charm store $YELLOW$CHARM_STORE$NORMAL. Either:"
  495. err " - check $YELLOW\$CHARM_STORE$NORMAL variable value"
  496. err " - download charms in $CHARM_STORE"
  497. print_error "Charm store is empty. Cannot continue."
  498. fi
  499. action="${posargs[0]}"
  500. case "$action" in
  501. load|save)
  502. service="${posargs[1]}"
  503. run_service_action "$service" "$action" "${opts[@]}" "${posargs[@]:2}"
  504. ;;
  505. up)
  506. service="${posargs[1]}"
  507. ## init in order
  508. [ "$no_init" ] || run_service_hook "$service" init
  509. ## XXXvlab: to be removed when all relation and service stuff is resolved
  510. if [ -z "$no_hooks" ]; then
  511. for script in "$CHARM_STORE/"*/hooks.d/*.sh; do
  512. [ -e "$script" ] || continue
  513. [ -x "$script" ] || { echo "compose: script $script is not executable." >&2; exit 1; }
  514. (
  515. cd "$(dirname "$script/..")";
  516. "$script" "$@"
  517. ) || { echo "compose: hook $script failed. Stopping." >&2; exit 1; }
  518. done
  519. fi
  520. launch_docker_compose "${fullargs[@]}"
  521. ;;
  522. *) launch_docker_compose "${fullargs[@]}";;
  523. esac