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.

1553 lines
48 KiB

9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
  1. #!/bin/bash
  2. #:-
  3. . /etc/shlib
  4. #:-
  5. include pretty
  6. include parse
  7. depends shyaml docker
  8. [[ "${BASH_SOURCE[0]}" != "${0}" ]] && SOURCED=true
  9. export VARDIR=/var/lib/compose
  10. usage="$exname CHARM"'
  11. Deploy and manage a swarm of containers to provide services based on
  12. a ``compose.yml`` definition and charms from a ``charm-store``.
  13. '
  14. export DEFAULT_COMPOSE_FILE
  15. ##
  16. ## Merge YAML files
  17. ##
  18. _merge_yaml_common_code="
  19. import sys
  20. import yaml
  21. def fc(filename):
  22. with open(filename) as f:
  23. return f.read()
  24. def merge(*args):
  25. # sys.stderr.write('%r\n' % (args, ))
  26. args = [arg for arg in args if arg is not None]
  27. if len(args) == 0:
  28. return None
  29. if len(args) == 1:
  30. return args[0]
  31. if all(isinstance(arg, (int, basestring, bool)) for arg in args):
  32. return args[-1]
  33. elif all(isinstance(arg, list) for arg in args):
  34. res = []
  35. for arg in args:
  36. res.extend(arg)
  37. return res
  38. elif all(isinstance(arg, dict) for arg in args):
  39. keys = set()
  40. for arg in args:
  41. keys |= set(arg.keys())
  42. dct = {}
  43. for key in keys:
  44. sub_args = []
  45. for arg in args:
  46. if key in arg:
  47. sub_args.append(arg)
  48. try:
  49. dct[key] = merge(*(a[key] for a in sub_args))
  50. except NotImplementedError as e:
  51. raise NotImplementedError(
  52. e.args[0],
  53. '%s.%s' % (key, e.args[1]) if e.args[1] else key,
  54. e.args[2])
  55. if dct[key] is None:
  56. del dct[key]
  57. return dct
  58. else:
  59. raise NotImplementedError(
  60. 'Unsupported types: %s'
  61. % (', '.join(list(set(arg.__class__.__name__ for arg in args)))), '', args)
  62. return None
  63. def merge_cli(*args):
  64. try:
  65. c = merge(*args)
  66. except NotImplementedError as e:
  67. sys.stderr.write('%s. Conflicting key is %r. Values are:\n%s\n' % (e.args[0], e.args[1], e.args[2]))
  68. exit(1)
  69. if c is not None:
  70. print '%s' % yaml.dump(c, default_flow_style=False)
  71. "
  72. merge_yaml() {
  73. if ! [ -r "$state_tmpdir/merge_yaml.py" ]; then
  74. cat <<EOF > "$state_tmpdir/merge_yaml.py"
  75. $_merge_yaml_common_code
  76. merge_cli(*(yaml.load(fc(f)) for f in sys.argv[1:]))
  77. EOF
  78. fi
  79. python "$state_tmpdir/merge_yaml.py" "$@"
  80. }
  81. export -f merge_yaml
  82. merge_yaml_str() {
  83. local entries="$@"
  84. if ! [ -r "$state_tmpdir/merge_yaml_str.py" ]; then
  85. cat <<EOF > "$state_tmpdir/merge_yaml_str.py"
  86. $_merge_yaml_common_code
  87. merge_cli(*(yaml.load(f) for f in sys.argv[1:]))
  88. EOF
  89. fi
  90. python "$state_tmpdir/merge_yaml_str.py" "$@"
  91. }
  92. export -f merge_yaml_str
  93. ##
  94. ## Functions
  95. ##
  96. gen_password() {
  97. python -c 'import random; \
  98. xx = "azertyuiopqsdfghjklmwxcvbn1234567890AZERTYUIOPQSDFGHJKLMWXCVBN+_-"; \
  99. print "".join([xx[random.randint(0, len(xx)-1)] for x in range(0, 14)])'
  100. }
  101. export -f gen_password
  102. file_put() {
  103. local TARGET="$1"
  104. mkdir -p "$(dirname "$TARGET")" &&
  105. cat - > "$TARGET"
  106. }
  107. export -f file_put
  108. _get_docker_compose_links() {
  109. local service="$1" cache_file="$state_tmpdir/$FUNCNAME.cache.$(echo "$*" | md5_compat)" links charm charm_part
  110. if [ -z "$service" ]; then
  111. print_syntax_error "$FUNCNAME: Please specify a service as first argument."
  112. return 1
  113. fi
  114. if [ -e "$cache_file" ]; then
  115. # debug "$FUNCNAME: cache hit ($*)"
  116. cat "$cache_file"
  117. return 0
  118. fi
  119. master_charm=$(_get_master_charm_for_service "$service") || return 1
  120. deps=()
  121. while read-0 relation_name target_service relation_config reverse; do
  122. [ "$master_charm" == "$target_service" ] && continue
  123. if [ "$reverse" ]; then
  124. deps+=("$(echo -en "$target_service:\n links:\n - $master_charm")")
  125. else
  126. deps+=("$(echo -en "$master_charm:\n links:\n - $target_service")")
  127. fi
  128. done < <(get_compose_relations "$service") || return 1
  129. merge_yaml_str "${deps[@]}" | tee "$cache_file"
  130. }
  131. ##
  132. ## By Reading the metadata.yml, we create a docker-compose.yml mixin.
  133. ## Some metadata.yml (of subordinates) will indeed modify other
  134. ## services than themselves.
  135. _get_docker_compose_service_mixin() {
  136. local service="$1" cache_file="$state_tmpdir/$FUNCNAME.cache.$(echo "$*" | md5_compat)" links charm charm_part
  137. if [ -z "$service" ]; then
  138. print_syntax_error "$FUNCNAME: Please specify a service as first argument."
  139. return 1
  140. fi
  141. if [ -e "$cache_file" ]; then
  142. # debug "$FUNCNAME: cache hit ($*)"
  143. cat "$cache_file"
  144. return 0
  145. fi
  146. master_charm=$(_get_master_charm_for_service "$service") || return 1
  147. ## The compose part
  148. links_yaml=$(_get_docker_compose_links "$service") || return 1
  149. ## the charm part
  150. #debug "Get charm name from service name $DARKYELLOW$service$NORMAL."
  151. charm=$(get_service_charm "$service") || return 1
  152. charm_part=$(get_docker_compose_mixin_from_metadata "$charm") || return 1
  153. ## Merge results
  154. if [ "$charm_part" ]; then
  155. charm_yaml="$(echo -en "${master_charm}:\n$(echo "$charm_part" | prefix " ")")"
  156. merge_yaml_str "$links_yaml" "$charm_yaml"
  157. else
  158. echo "$links_yaml"
  159. fi > "$cache_file"
  160. cat "$cache_file"
  161. }
  162. export -f _get_docker_compose_service_mixin
  163. ##
  164. ## Get full `docker-compose.yml` format for all listed services (and
  165. ## their deps)
  166. ##
  167. get_docker_compose() {
  168. local cache_file="$state_tmpdir/$FUNCNAME.cache.$(echo "$*" | md5_compat)" entries services
  169. if [ -e "$cache_file" ]; then
  170. # debug "$FUNCNAME: cache hit ($*)"
  171. cat "$cache_file"
  172. return 0
  173. fi
  174. ##
  175. ## Adding sub services configurations
  176. ##
  177. declare -A entries
  178. for target_service in "$@"; do
  179. services=$(get_ordered_service_dependencies "$target_service") || return 1
  180. for service in $services; do
  181. if [ "${entries[$service]}" ]; then
  182. ## Prevent double inclusion of same service if this
  183. ## service is deps of two or more of your
  184. ## requirements.
  185. continue
  186. fi
  187. ## mark the service as "loaded" as well as it's containers
  188. ## if this is a subordinate service
  189. entries[$service]=$(_get_docker_compose_service_mixin "$service") || return 1
  190. done
  191. done
  192. merge_yaml_str "${entries[@]}" > "$cache_file"
  193. export _current_docker_compose="$(cat "$cache_file")"
  194. echo "$_current_docker_compose"
  195. }
  196. export -f get_docker_compose
  197. ## XXXvlab: a lot to be done to cache the results
  198. get_compose_service_def () {
  199. local service="$1" cache_file="$state_tmpdir/$FUNCNAME.cache.$(echo "$*" | md5_compat)"
  200. if [ -e "$cache_file" ]; then
  201. # debug "$FUNCNAME: cache hit ($*)"
  202. cat "$cache_file"
  203. return 0
  204. fi
  205. [ -z "$service" ] && print_syntax_error "Missing service as first argument."
  206. service_def_base=
  207. if [ -d "$CHARM_STORE/$service" ]; then
  208. service_def_base="charm: $service"
  209. fi
  210. value=
  211. if [ -r "$COMPOSE_YML_FILE" ]; then
  212. value=$(shyaml get-value "$service" 2>/dev/null < "$COMPOSE_YML_FILE")
  213. fi
  214. if [ -z "$service_def_base" -a -z "$value" ]; then
  215. err "Invalid service $DARKYELLOW$service$NORMAL: no definition in" \
  216. "compose file nor charm with same name."
  217. return 1
  218. fi
  219. merge_yaml <(echo "$service_def_base") <(echo "$value") > "$cache_file"
  220. cat "$cache_file"
  221. }
  222. export -f get_compose_service_def
  223. ## XXXvlab: MUST CACHE
  224. get_service_charm () {
  225. local service="$1"
  226. if [ -z "$service" ]; then
  227. print_syntax_error "$FUNCNAME: Please specify a service as first argument."
  228. return 1
  229. fi
  230. service_def=$(get_compose_service_def "$service") || return 1
  231. charm=$(echo "$service_def" | shyaml get-value charm 2>/dev/null)
  232. if [ -z "$charm" ]; then
  233. err "Missing charm in service $DARKYELLOW$service$NORMAL definition."
  234. return 1
  235. fi
  236. echo "$charm"
  237. }
  238. export -f get_service_charm
  239. ## built above the docker-compose abstraction, so it relies on the
  240. ## full docker-compose.yml to be already built.
  241. get_service_def() {
  242. local service="$1" def
  243. if [ -z "$_current_docker_compose" ]; then
  244. print_syntax_error "$FUNCNAME is meant to be called after"\
  245. "\$_current_docker_compose has been calculated."
  246. fi
  247. def=$(echo "$_current_docker_compose" | shyaml get-value "$service" 2>/dev/null)
  248. if [ -z "$def" ]; then
  249. err "No definition for service $DARKYELLOW$service$NORMAL."
  250. return 1
  251. fi
  252. echo "$def"
  253. }
  254. export -f get_service_def
  255. ## Return the base docker image name of a service
  256. service_base_docker_image() {
  257. local service="$1"
  258. service_def="$(get_service_def "$service")" || return 1
  259. service_image=$(echo "$service_def" | shyaml get-value image 2>/dev/null)
  260. if [ "$?" != 0 ]; then
  261. service_build=$(echo "$service_def" | shyaml get-value build 2>/dev/null)
  262. if [ "$?" != 0 ]; then
  263. err "Service $DARKYELLOW$service$NORMAL has no ${WHITE}image${NORMAL} nor ${WHITE}build${NORMAL} parameter."
  264. echo "$service_def" >&2
  265. return 1
  266. fi
  267. service_dockerfile="$CHARM_STORE/${service_build}/Dockerfile"
  268. if ! [ -e "$service_dockerfile" ]; then
  269. err "No Dockerfile found in '$service_dockerfile' location."
  270. return 1
  271. fi
  272. grep '^FROM' "$service_dockerfile" | xargs echo | cut -f 2 -d " "
  273. else
  274. echo "$service_image"
  275. fi
  276. }
  277. export -f service_base_docker_image
  278. ## XXXvlab: provided in shlib/common
  279. # read-0() {
  280. # local eof
  281. # eof=
  282. # while [ "$1" ]; do
  283. # IFS='' read -r -d '' "$1" || eof=true
  284. # shift
  285. # done
  286. # test "$eof" != true
  287. # }
  288. # export -f read-0
  289. fetch_file() {
  290. local src="$1"
  291. case "$src" in
  292. *"://"*)
  293. err "Unsupported target scheme."
  294. return 1
  295. ;;
  296. *)
  297. ## Try direct
  298. if ! [ -r "$src" ]; then
  299. err "File '$src' not found/readable."
  300. return 1
  301. fi
  302. cat "$src" || return 1
  303. ;;
  304. esac
  305. }
  306. export -f fetch_file
  307. ## receives stdin content to decompress on stdout
  308. ## stdout content should be tar format.
  309. uncompress_file() {
  310. local filename="$1"
  311. ## Warning, the content of the file is already as stdin, the filename
  312. ## is there to hint for correct decompression.
  313. case "$filename" in
  314. *".gz")
  315. gunzip
  316. ;;
  317. *".bz2")
  318. bunzip2
  319. ;;
  320. *)
  321. cat
  322. ;;
  323. esac
  324. }
  325. export -f uncompress_file
  326. get_file() {
  327. local src="$1"
  328. fetch_file "$src" | uncompress_file "$src"
  329. }
  330. export -f get_file
  331. cmd_on_base_image() {
  332. local charm="$1"
  333. shift
  334. base_image=$(service_base_docker_image "$charm") || return 1
  335. docker run -i --entrypoint /bin/bash "$base_image" -c "$*"
  336. }
  337. export -f cmd_on_base_image
  338. cached_cmd_on_base_image() {
  339. local charm="$1" cache_file="$state_tmpdir/$FUNCNAME.cache.$(echo "$*" | md5_compat)"
  340. shift
  341. if [ -e "$cache_file" ]; then
  342. # debug "$FUNCNAME: cache hit ($*)"
  343. cat "$cache_file"
  344. return 0
  345. fi
  346. base_image=$(service_base_docker_image "$charm") || return 1
  347. result=$(cmd_on_base_image "$charm" "$@") || return 1
  348. echo "$result" | tee "$cache_file"
  349. }
  350. export -f cached_cmd_on_base_image
  351. array_values_to_stdin() {
  352. local e
  353. if [ "$#" -ne "1" ]; then
  354. print_syntax_warning "$FUNCNAME: need one argument."
  355. return 1
  356. fi
  357. var="$1"
  358. eval "for e in \"\${$var[@]}\"; do echo -en \"\$e\\0\"; done"
  359. }
  360. array_keys_to_stdin() {
  361. local e
  362. if [ "$#" -ne "1" ]; then
  363. print_syntax_warning "$FUNCNAME: need one argument."
  364. return 1
  365. fi
  366. var="$1"
  367. eval "for e in \"\${!$var[@]}\"; do echo -en \"\$e\\0\"; done"
  368. }
  369. array_kv_to_stdin() {
  370. local e
  371. if [ "$#" -ne "1" ]; then
  372. print_syntax_warning "$FUNCNAME: need one argument."
  373. return 1
  374. fi
  375. var="$1"
  376. eval "for e in \"\${!$var[@]}\"; do echo -n \"\$e\"; echo -en '\0'; echo -n \"\${$var[\$e]}\"; echo -en '\0'; done"
  377. }
  378. array_pop() {
  379. local narr="$1" nres="$2"
  380. for key in $(eval "echo \${!$narr[@]}"); do
  381. eval "$nres=\${$narr[\"\$key\"]}"
  382. eval "unset $narr[\"\$key\"]"
  383. return 0
  384. done
  385. }
  386. export -f array_pop
  387. array_member() {
  388. local src elt
  389. src="$1"
  390. elt="$2"
  391. while read-0 key; do
  392. if [ "$(eval "echo -n \"\${$src[\$key]}\"")" == "$elt" ]; then
  393. return 0
  394. fi
  395. done < <(array_keys_to_stdin "$src")
  396. return 1
  397. }
  398. export -f array_member
  399. get_charm_relation_def () {
  400. local charm="$1" relation_name="$2" cache_file="$state_tmpdir/$FUNCNAME.cache.$(echo "$*" | md5_compat)" \
  401. relation_def metadata
  402. if [ -e "$cache_file" ]; then
  403. # debug "$FUNCNAME: cache hit ($*)"
  404. cat "$cache_file"
  405. return 0
  406. fi
  407. metadata="$(get_charm_metadata "$charm")" || return 1
  408. relation_def="$(echo "$metadata" | shyaml get-value "provides.${relation_name}" 2>/dev/null)"
  409. echo "$relation_def" | tee "$cache_file"
  410. }
  411. export -f get_charm_relation_def
  412. get_charm_reverse_tech_dep_relation() {
  413. local charm="$1" relation_name="$2" cache_file="$state_tmpdir/$FUNCNAME.cache.$(echo "$*" | md5_compat)" \
  414. relation_def metadata
  415. if [ -e "$cache_file" ]; then
  416. # debug "$FUNCNAME: cache hit ($*)"
  417. cat "$cache_file"
  418. return 0
  419. fi
  420. relation_def=$(get_charm_relation_def "$charm" "$relation_name" 2>/dev/null)
  421. value=$(echo "$relation_def" | shyaml get-value 'reverse-tech-dep' 2>/dev/null)
  422. echo "$value" | tee "$cache_file"
  423. }
  424. export -f get_charm_reverse_tech_dep_relation
  425. ##
  426. ## Use compose file to get deps, and relation definition in metadata.yml
  427. ## for reverse-tech-dep attribute.
  428. get_service_deps() {
  429. local service="$1" cache_file="$state_tmpdir/$FUNCNAME.cache.$(echo "$*" | md5_compat)"
  430. if [ -e "$cache_file" ]; then
  431. # debug "$FUNCNAME: cache hit ($*)"
  432. cat "$cache_file"
  433. return 0
  434. fi
  435. (
  436. set -o pipefail
  437. get_compose_relations "$service" | \
  438. while read-0 relation_name target_service _relation_config reverse; do
  439. echo "$target_service"
  440. done | tee "$cache_file"
  441. ) || return 1
  442. }
  443. export -f get_service_deps
  444. _rec_get_depth() {
  445. local elt=$1
  446. #debug "Asking for $DARKYELLOW$elt$NORMAL dependencies"
  447. if [ "${depths[$elt]}" ]; then
  448. return 0
  449. fi
  450. deps=$(get_service_deps "$elt") || return 1
  451. if [ -z "$deps" ]; then
  452. depths[$elt]=0
  453. fi
  454. max=0
  455. for dep in $deps; do
  456. _rec_get_depth "$dep" || return 1
  457. if (( "${depths[$dep]}" > "$max" )); then
  458. max="${depths[$dep]}"
  459. fi
  460. done
  461. depths[$elt]=$((max + 1))
  462. }
  463. get_ordered_service_dependencies() {
  464. local services=("$@") cache_file="$state_tmpdir/$FUNCNAME.cache.$(echo "$*" | md5_compat)"
  465. if [ -e "$cache_file" ]; then
  466. # debug "$FUNCNAME: cache hit ($*)"
  467. cat "$cache_file"
  468. return 0
  469. fi
  470. #debug "Figuring ordered deps of $DARKYELLOW$services$NORMAL"
  471. if [ -z "${services[*]}" ]; then
  472. print_syntax_error "$FUNCNAME: no arguments"
  473. return 1
  474. fi
  475. declare -A depths
  476. visited=()
  477. heads=("${services[@]}")
  478. while [ "${#heads[@]}" != 0 ]; do
  479. array_pop heads head
  480. visited+=("$head")
  481. _rec_get_depth "$head" || return 1
  482. done
  483. i=0
  484. while [ "${#depths[@]}" != 0 ]; do
  485. for key in "${!depths[@]}"; do
  486. value="${depths[$key]}"
  487. if [ "$value" == "$i" ]; then
  488. echo "$key"
  489. unset depths[$key]
  490. fi
  491. done
  492. i=$((i + 1))
  493. done > "$cache_file"
  494. cat "$cache_file"
  495. }
  496. run_service_hook () {
  497. local services="$1" action="$2" loaded
  498. declare -A loaded
  499. for service in $services; do
  500. for subservice in $(get_ordered_service_dependencies "$service"); do
  501. if [ "${loaded[$subservice]}" ]; then
  502. ## Prevent double inclusion of same service if this
  503. ## service is deps of two or more of your
  504. ## requirements.
  505. continue
  506. fi
  507. charm=$(get_service_charm "$subservice") || return 1
  508. TARGET_SCRIPT="$CHARM_STORE/$charm/hooks/$action"
  509. [ -e "$TARGET_SCRIPT" ] || continue
  510. Wrap -d "$YELLOW$action$NORMAL hook of charm $DARKYELLOW$charm$NORMAL" <<EOF || return 1
  511. cd "$CHARM_STORE/$charm"
  512. SERVICE_NAME=$subservice \
  513. DOCKER_BASE_IMAGE=$(service_base_docker_image "$charm") \
  514. SERVICE_DATASTORE="$DATASTORE/$charm" \
  515. SERVICE_CONFIGSTORE="$CONFIGSTORE/$charm" \
  516. "$TARGET_SCRIPT"
  517. EOF
  518. loaded[$subservice]=1
  519. done
  520. done
  521. return 0
  522. }
  523. relation-get () {
  524. local key="$1"
  525. cat "$RELATION_DATA_FILE" | shyaml get-value "$key" 2>/dev/null
  526. if [ "$?" != 0 ]; then
  527. err "The key $WHITE$key$NORMAL was not found in relation's data."
  528. return 1
  529. fi
  530. }
  531. export -f relation-get
  532. relation-base-compose-get () {
  533. local key="$1"
  534. echo "$RELATION_BASE_COMPOSE_DEF" | shyaml get-value "options.$key" 2>/dev/null
  535. if [ "$?" != 0 ]; then
  536. err "The key $WHITE$key$NORMAL was not found in base service compose definition.."
  537. return 1
  538. fi
  539. }
  540. export -f relation-base-compose-get
  541. relation-target-compose-get () {
  542. local key="$1"
  543. echo "$RELATION_BASE_COMPOSE_DEF" | shyaml get-value "options.$key" 2>/dev/null
  544. if [ "$?" != 0 ]; then
  545. err "The key $WHITE$key$NORMAL was not found in base service compose definition.."
  546. return 1
  547. fi
  548. }
  549. export -f relation-base-compose-get
  550. relation-set () {
  551. local key="$1" value="$2"
  552. if [ -z "$RELATION_DATA_FILE" ]; then
  553. err "$FUNCNAME: relation does not seems to be correctly setup."
  554. return 1
  555. fi
  556. if ! [ -r "$RELATION_DATA_FILE" ]; then
  557. err "$FUNCNAME: can't read relation's data." >&2
  558. return 1
  559. fi
  560. _config_merge "$RELATION_DATA_FILE" <(echo "$key: $value")
  561. }
  562. export -f relation-set
  563. _config_merge() {
  564. local config_filename="$1" merge_to_file="$2"
  565. touch "$config_filename" &&
  566. merge_yaml "$config_filename" "$merge_to_file" > "$config_filename.tmp" || return 1
  567. mv "$config_filename.tmp" "$config_filename"
  568. }
  569. export -f _config_merge
  570. config-add() {
  571. local metadata="$1"
  572. _config_merge "$RELATION_CONFIG" <(echo "$metadata")
  573. }
  574. export -f config-add
  575. logstdout() {
  576. local name="$1"
  577. sed -r 's%^%'"${name}"'> %g'
  578. }
  579. export -f logstdout
  580. logstderr() {
  581. local name="$1"
  582. sed -r 's%^(.*)$%'"${RED}${name}>${NORMAL} \1"'%g'
  583. }
  584. export -f logstderr
  585. _run_service_relation () {
  586. local relation_name="$1" service="$2" target_service="$3" relation_config="$4" relation_dir services
  587. charm=$(get_service_charm "$service") || return 1
  588. target_charm=$(get_service_charm "$target_service") || return 1
  589. base_script_name=$(echo "$relation_name" | tr "-" "_" )-relation-joined
  590. script_name="hooks/${base_script_name}"
  591. relation_dir=$(get_relation_data_dir "$charm" "$target_charm" "$relation_name") || return 1
  592. RELATION_DATA_FILE=$(get_relation_data_file "$charm" "$target_charm" "$relation_name" "$relation_config") || return 1
  593. RELATION_BASE_COMPOSE_DEF=$(get_compose_service_def "$service") || return 1
  594. RELATION_TARGET_COMPOSE_DEF=$(get_compose_service_def "$target_service") || return 1
  595. export BASE_SERVICE_NAME=$service
  596. export TARGET_SERVICE_NAME=$target_service
  597. export BASE_CHARM_NAME=$charm
  598. export TARGET_CHARM_NAME=$target_charm
  599. export RELATION_DATA_FILE RELATION_BASE_COMPOSE_DEF RELATION_TARGET_COMPOSE_DEF
  600. target_errlvl=0
  601. if ! [ -e "$CHARM_STORE/$target_charm/$script_name" ]; then
  602. verb "Missing relation script '$script_name' in $DARKYELLOW$target_charm$NORMAL."
  603. else
  604. verb "Building ${DARKBLUE}$relation_name${NORMAL} relation-joined script" \
  605. "for target charm $DARKYELLOW$target_charm$NORMAL"
  606. export RELATION_CONFIG="$relation_dir/config_provider"
  607. {
  608. (
  609. cd "$CHARM_STORE/$target_charm"
  610. export SERVICE_NAME=$target_service
  611. export DOCKER_BASE_IMAGE=$(service_base_docker_image "$target_service")
  612. export SERVICE_DATASTORE="$DATASTORE/$target_charm"
  613. export SERVICE_CONFIGSTORE="$CONFIGSTORE/$target_charm"
  614. "$script_name"
  615. echo "$?" > "$relation_dir/target_errlvl"
  616. ) | logstdout "$DARKYELLOW$target_charm$NORMAL/$base_script_name ${GREEN}@${NORMAL}"
  617. } 3>&1 1>&2 2>&3 | logstderr "$DARKYELLOW$target_charm$NORMAL/$base_script_name ${RED}@${NORMAL}" 3>&1 1>&2 2>&3
  618. target_errlvl="$(cat "$relation_dir/target_errlvl")"
  619. if [ -e "$RELATION_CONFIG" ]; then
  620. debug "Merging some new config info in $DARKYELLOW$target_service$NORMAL"
  621. _config_merge "$state_tmpdir/to-merge-in-docker-compose.yml" "$RELATION_CONFIG"
  622. rm "$RELATION_CONFIG"
  623. ((target_errlvl |= "$?"))
  624. fi
  625. fi
  626. if [ "$target_errlvl" == 0 ]; then
  627. errlvl=0
  628. if [ -e "$CHARM_STORE/$charm/$script_name" ]; then
  629. verb "Running ${DARKBLUE}$relation_name${NORMAL} relation-joined script" \
  630. "for charm $DARKYELLOW$charm$NORMAL"
  631. export RELATION_CONFIG="$relation_dir/config_providee"
  632. export RELATION_DATA="$(cat "$RELATION_DATA_FILE")"
  633. {
  634. (
  635. cd "$CHARM_STORE/$charm"
  636. export SERVICE_NAME=$service
  637. export DOCKER_BASE_IMAGE=$(service_base_docker_image "$service")
  638. export SERVICE_DATASTORE="$DATASTORE/$charm"
  639. export SERVICE_CONFIGSTORE="$CONFIGSTORE/$charm"
  640. "$script_name"
  641. echo "$?" > "$relation_dir/errlvl"
  642. ) | logstdout "$DARKYELLOW$charm$NORMAL/$base_script_name ${GREEN}@${NORMAL}"
  643. } 3>&1 1>&2 2>&3 | logstderr "$DARKYELLOW$charm$NORMAL/$base_script_name ${RED}@$NORMAL" 3>&1 1>&2 2>&3
  644. errlvl="$(cat "$relation_dir/errlvl")"
  645. if [ -e "$RELATION_CONFIG" ]; then
  646. _config_merge "$state_tmpdir/to-merge-in-docker-compose.yml" "$RELATION_CONFIG"
  647. rm "$RELATION_CONFIG"
  648. fi
  649. if [ "$errlvl" != 0 ]; then
  650. err "Relation $DARKBLUE$relation_name$NORMAL on $DARKYELLOW$target_charm$NORMAL failed to run properly."
  651. fi
  652. else
  653. verb "No relation script '$script_name' in charm $DARKYELLOW$charm$NORMAL. Ignoring."
  654. fi
  655. else
  656. err "Relation $DARKBLUE$relation_name$NORMAL on $DARKYELLOW$target_charm$NORMAL failed to run properly."
  657. fi
  658. if [ "$target_errlvl" == 0 -a "$errlvl" == 0 ]; then
  659. debug "Relation $DARKBLUE$relation_name$NORMAL is established" \
  660. "between $DARKYELLOW$service$NORMAL and $DARKYELLOW$target_service$NORMAL."
  661. return 0
  662. else
  663. return 1
  664. fi
  665. }
  666. export -f _run_service_relation
  667. get_compose_relations() {
  668. local service="$1" cache_file="$state_tmpdir/$FUNCNAME.cache.$(echo "$*" | md5_compat)"
  669. if [ -e "$cache_file" ]; then
  670. # debug "$FUNCNAME: cache hit ($*)"
  671. cat "$cache_file"
  672. return 0
  673. fi
  674. compose_def="$(get_compose_service_def "$service")" || return 1
  675. (
  676. set -o pipefail
  677. if [ "$compose_def" ]; then
  678. while read-0 relation_name relation_def; do
  679. case "$(echo "$relation_def" | shyaml get-type 2>/dev/null)" in
  680. "str")
  681. target_service="$(echo "$relation_def" | shyaml get-value 2>/dev/null)"
  682. reverse="$(get_charm_reverse_tech_dep_relation "$target_service" "$relation_name")"
  683. echo -en "$relation_name\0$target_service\0\0$reverse\0"
  684. ;;
  685. "sequence")
  686. while read-0 target_service; do
  687. reverse="$(get_charm_reverse_tech_dep_relation "$target_service" "$relation_name")"
  688. echo -en "$relation_name\0$target_service\0\0$reverse\0"
  689. done < <(echo "$relation_def" | shyaml get-values-0 2>/dev/null)
  690. ;;
  691. "struct")
  692. while read-0 target_service relation_config; do
  693. reverse="$(get_charm_reverse_tech_dep_relation "$target_service" "$relation_name")"
  694. echo -en "$relation_name\0$target_service\0$relation_config\0$reverse\0"
  695. done < <(echo "$relation_def" | shyaml key-values-0 2>/dev/null)
  696. ;;
  697. esac
  698. done < <(echo "$compose_def" | shyaml key-values-0 relations 2>/dev/null)
  699. fi | tee "$cache_file"
  700. )
  701. if [ "$?" != 0 ]; then
  702. rm -f "$cache_file" ## no cache
  703. return 1
  704. fi
  705. return 0
  706. }
  707. run_service_relations () {
  708. local services="$1"
  709. for service in $services; do
  710. for subservice in $(get_ordered_service_dependencies "$service"); do
  711. while read-0 relation_name target_service relation_config reverse; do
  712. export relation_config
  713. Wrap -d "Building $DARKYELLOW$service$NORMAL --$DARKBLUE$relation_name$NORMAL--> $DARKYELLOW$target_service$NORMAL" <<EOF || return 1
  714. _run_service_relation "$relation_name" "$subservice" "$target_service" "\$relation_config"
  715. EOF
  716. done < <(get_compose_relations "$subservice") || return 1
  717. done
  718. done
  719. }
  720. export -f run_service_relations
  721. _run_service_action_direct() {
  722. local service="$1" action="$2" charm script _dummy
  723. shift; shift
  724. read-0 charm script || true ## against 'set -e' that could be setup in parent scripts
  725. if read-0 _dummy || [ "$_dummy" ]; then
  726. print_syntax_error "$FUNCNAME: too many arguments in action descriptor"
  727. return 1
  728. fi
  729. compose_file=$(get_compose_yml_location) || return 1
  730. export action_errlvl_file="$state_tmpdir/action-$service-$charm-$action-errlvl"
  731. export state_tmpdir
  732. {
  733. (
  734. set +e ## Prevents unwanted leaks from parent shell
  735. cd "$CHARM_STORE/$charm"
  736. export COMPOSE_CONFIG=$(cat "$compose_file")
  737. export METADATA_CONFIG=$(cat "$CHARM_STORE/$charm/metadata.yml")
  738. export SERVICE_NAME=$service
  739. export ACTION_NAME=$action
  740. export CONTAINER_NAME=$(_get_master_charm_for_service "$service")
  741. export DOCKER_BASE_IMAGE=$(service_base_docker_image "$CONTAINER_NAME")
  742. export SERVICE_DATASTORE="$DATASTORE/$service"
  743. export SERVICE_CONFIGSTORE="$CONFIGSTORE/$service"
  744. exname="$exname $ACTION_NAME $SERVICE_NAME" \
  745. stdbuf -oL -eL "$script" "$@"
  746. echo "$?" > "$action_errlvl_file"
  747. ) | logstdout "$DARKYELLOW$charm$NORMAL/${DARKCYAN}$action${NORMAL} ${GREEN}@${NORMAL}"
  748. } 3>&1 1>&2 2>&3 | logstderr "$DARKYELLOW$charm$NORMAL/${DARKCYAN}$action${NORMAL} ${RED}@$NORMAL" 3>&1 1>&2 2>&3
  749. if ! [ -e "$action_errlvl_file" ]; then
  750. err "Action $DARKYELLOW$service$NORMAL:$DARKCYAN$action$NORMAL has failed without having time" \
  751. "to output an errlvl"
  752. return 1
  753. fi
  754. return "$(cat "$action_errlvl_file")"
  755. }
  756. export -f _run_service_action_direct
  757. _run_service_action_relation() {
  758. local service="$1" action="$2" charm target_charm relation_name target_script relation_config _dummy
  759. shift; shift
  760. read-0 charm target_charm relation_name target_script relation_config || true
  761. if read-0 _dummy || [ "$_dummy" ]; then
  762. print_syntax_error "$FUNCNAME: too many arguments in action descriptor"
  763. return 1
  764. fi
  765. export RELATION_DATA_FILE=$(get_relation_data_file "$charm" "$target_charm" "$relation_name" "$relation_config")
  766. compose_file=$(get_compose_yml_location) || return 1
  767. export action_errlvl_file="$state_tmpdir/action-$service-$charm-$action-errlvl"
  768. export state_tmpdir
  769. {
  770. (
  771. set +e ## Prevents unwanted leaks from parent shell
  772. cd "$CHARM_STORE/$charm"
  773. export METADATA_CONFIG=$(cat "$CHARM_STORE/$charm/metadata.yml")
  774. export SERVICE_NAME=$service
  775. export RELATION_TARGET_CHARM="$target_charm"
  776. export RELATION_CHARM="$charm"
  777. export ACTION_NAME=$action
  778. export CONTAINER_NAME=$(_get_master_charm_for_service "$service")
  779. export DOCKER_BASE_IMAGE=$(service_base_docker_image "$CONTAINER_NAME")
  780. export SERVICE_DATASTORE="$DATASTORE/$service"
  781. export SERVICE_CONFIGSTORE="$CONFIGSTORE/$service"
  782. exname="$exname $ACTION_NAME $SERVICE_NAME" \
  783. stdbuf -oL -eL "$target_script" "$@"
  784. echo "$?" > "$action_errlvl_file"
  785. ) | logstdout "$DARKYELLOW$charm$NORMAL/${DARKCYAN}$action${NORMAL} ${GREEN}@${NORMAL}"
  786. } 3>&1 1>&2 2>&3 | logstderr "$DARKYELLOW$charm$NORMAL/${DARKCYAN}$action${NORMAL} ${RED}@$NORMAL" 3>&1 1>&2 2>&3
  787. if ! [ -e "$action_errlvl_file" ]; then
  788. err "Action $DARKYELLOW$service$NORMAL:$DARKCYAN$action$NORMAL has failed without having time" \
  789. "to output an errlvl"
  790. return 1
  791. fi
  792. return "$(cat "$action_errlvl_file")"
  793. }
  794. export -f _run_service_action_relation
  795. get_relation_data_dir() {
  796. local charm="$1" target_charm="$2" relation_name="$3" cache_file="$state_tmpdir/$FUNCNAME.cache.$(echo "$*" | md5_compat)"
  797. if [ -e "$cache_file" ]; then
  798. # debug "$FUNCNAME: cache hit ($*)"
  799. cat "$cache_file"
  800. return 0
  801. fi
  802. project=$(get_default_project_name) || return 1
  803. relation_dir="$VARDIR/relations/$project/$charm-$target_charm/$relation_name"
  804. if ! [ -d "$relation_dir" ]; then
  805. mkdir -p "$relation_dir" || return 1
  806. chmod go-rwx "$relation_dir" || return 1 ## protecting this directory
  807. fi
  808. echo "$relation_dir" || tee "$cache_file"
  809. }
  810. export -f get_relation_data_dir
  811. get_relation_data_file() {
  812. local charm="$1" target_charm="$2" relation_name="$3" relation_config="$4"
  813. relation_dir=$(get_relation_data_dir "$charm" "$target_charm" "$relation_name") || return 1
  814. relation_data_file="$relation_dir/data"
  815. if ! [ -e "$relation_data_file" ]; then
  816. echo "$relation_config" > "$relation_data_file"
  817. chmod go-rwx "$relation_data_file" ## protecting this file
  818. fi
  819. echo "$relation_data_file"
  820. }
  821. export -f get_relation_data_file
  822. has_service_action () {
  823. local service="$1" action="$2" cache_file="$state_tmpdir/$FUNCNAME.cache.$(echo "$*" | md5_compat)" \
  824. target_script charm target_charm
  825. if [ -e "$cache_file" ]; then
  826. # debug "$FUNCNAME: cache hit ($*)"
  827. cat "$cache_file"
  828. return 0
  829. fi
  830. charm=$(get_service_charm "$service") || return 1
  831. ## Action directly provided ?
  832. target_script="$CHARM_STORE/$charm/actions/$action"
  833. if [ -x "$target_script" ]; then
  834. echo -en "direct\0$charm\0$target_script" | tee "$cache_file"
  835. return 0
  836. fi
  837. ## Action provided by relation ?
  838. while read-0 relation_name target_service relation_config reverse; do
  839. target_charm=$(get_service_charm "$target_service") || return 1
  840. target_script="$CHARM_STORE/$target_charm/actions/relations/$relation_name/$action"
  841. if [ -x "$target_script" ]; then
  842. echo -en "relation\0$charm\0$target_charm\0$relation_name\0$target_script\0$relation_config" | tee "$cache_file"
  843. return 0
  844. fi
  845. done < <(get_compose_relations "$service")
  846. return 1
  847. }
  848. export -f has_service_action
  849. run_service_action () {
  850. local service="$1" action="$2"
  851. shift ; shift
  852. {
  853. if ! read-0 action_type; then
  854. info "Service $DARKYELLOW$service$NORMAL does not have any action $DARKCYAN$action$NORMAL defined."
  855. info " Add an executable script to '$CHARM_STORE/$charm/actions/$action' to implement action."
  856. return 1
  857. fi
  858. Section "running $DARKYELLOW$service$NORMAL/$DARKCYAN$action$NORMAL ($action_type)"; Feed
  859. "_run_service_action_${action_type}" "$service" "$action" "$@"
  860. } < <(has_service_action "$service" "$action")
  861. }
  862. export -f run_service_action
  863. get_compose_relation_config() {
  864. local service=$1 relation_config cache_file="$state_tmpdir/$FUNCNAME.cache.$(echo "$*" | md5_compat)"
  865. if [ -e "$cache_file" ]; then
  866. # debug "$FUNCNAME: cache hit ($*)"
  867. cat "$cache_file"
  868. return 0
  869. fi
  870. compose_service_def=$(get_compose_service_def "$service") || return 1
  871. echo "$compose_service_def" | shyaml get-value "relations" 2>/dev/null | tee "$cache_file"
  872. }
  873. export -f get_compose_relation_config
  874. ## Return key-values-0
  875. get_compose_relation_config_for_service() {
  876. local service=$1 relation_name=$2 relation_config
  877. compose_service_relations=$(get_compose_relation_config "$service") || return 1
  878. if ! relation_config=$(
  879. echo "$compose_service_relations" |
  880. shyaml get-value "${relation_name}" 2>/dev/null); then
  881. err "Couldn't find $DARKYELLOW${service}$NORMAL/${WHITE}${relation_name}$NORMAL" \
  882. "relation config in compose configuration."
  883. return 1
  884. fi
  885. if [ -z "$relation_config" ]; then
  886. err "Relation ${WHITE}mysql-database$NORMAL is empty in compose configuration."
  887. return 1
  888. fi
  889. if ! echo "$relation_config" | shyaml key-values-0 2>/dev/null; then
  890. err "No key/values in ${DARKBLUE}mysql-database$NORMAL of compose config."
  891. return 1
  892. fi
  893. }
  894. export -f get_compose_relation_config_for_service
  895. _get_master_charm_for_service() {
  896. local service="$1" cache_file="$state_tmpdir/$FUNCNAME.cache.$(echo "$*" | md5_compat)" \
  897. charm metadata requires master_charm target_charm target_service service_def
  898. if [ -e "$cache_file" ]; then
  899. # debug "$FUNCNAME: cache hit ($*)"
  900. cat "$cache_file"
  901. return 0
  902. fi
  903. charm=$(get_service_charm "$service") || return 1
  904. metadata=$(get_charm_metadata "$charm") || return 1
  905. if [ "$(echo "$metadata" | shyaml get-value "subordinate" 2>/dev/null)" != "True" ]; then
  906. ## just return charm name
  907. echo "$charm" > "$cache_file"
  908. echo "$charm"
  909. return 0
  910. fi
  911. ## fetch the container relation
  912. requires="$(echo "$metadata" | shyaml get-value "requires" 2>/dev/null)"
  913. if [ -z "$requires" ]; then
  914. die "Charm $DARKYELLOW$charm$NORMAL is a subordinate but does not have any 'requires' " \
  915. "section."
  916. fi
  917. master_charm=
  918. while read-0 relation_name relation; do
  919. if [ "$(echo "$relation" | shyaml get-value "scope" 2>/dev/null)" == "container" ]; then
  920. # debug "$DARKYELLOW$service$NORMAL's relation" \
  921. # "$DARKBLUE${relation_name}$NORMAL is a container."
  922. interface="$(echo "$relation" | shyaml get-value "interface" 2>/dev/null)"
  923. if [ -z "$interface" ]; then
  924. err "No ${WHITE}$interface${NORMAL} set for relation $relation_name."
  925. return 1
  926. fi
  927. service_def=$(get_compose_service_def "$service") || return 1
  928. target_service=$(echo "$service_def" |
  929. shyaml keys "relations.${interface}" 2>/dev/null |
  930. head -n 1)
  931. if [ -z "$target_service" ]; then
  932. err "Couldn't find ${WHITE}relations.$interface${NORMAL} in" \
  933. "${DARKYELLOW}$service$NORMAL compose definition."
  934. return 1
  935. fi
  936. target_charm=$(get_service_charm "$target_service") || return 1
  937. master_charm="$(_get_master_charm_for_service "$target_service")"
  938. break
  939. fi
  940. done < <(echo "$requires" | shyaml key-values-0 2>/dev/null)
  941. if [ -z "$master_charm" ]; then
  942. die "Charm $DARKYELLOW$charm$NORMAL is a subordinate but does not have any relation with" \
  943. " ${WHITE}scope${NORMAL} set to 'container'."
  944. fi
  945. echo "$master_charm" > "$cache_file"
  946. echo "$master_charm"
  947. return 0
  948. }
  949. export -f _get_master_charm_for_service
  950. get_charm_metadata() {
  951. local charm="$1"
  952. metadata_file="$CHARM_STORE/$charm/metadata.yml"
  953. if ! [ -e "$metadata_file" ]; then
  954. return 0 ## No metadata file is as if metadata was empty
  955. fi
  956. cat "$metadata_file"
  957. }
  958. ##
  959. ## The result is a mixin that is not allways a complete valid
  960. ## docker-compose entry (thinkinf of subordinates). The result
  961. ## will be merge with master charms.
  962. get_docker_compose_mixin_from_metadata() {
  963. local charm="$1" cache_file="$state_tmpdir/$FUNCNAME.cache.$(echo "$*" | md5_compat)" \
  964. metadata_file metadata volumes docker_compose subordinate image
  965. if [ -e "$cache_file" ]; then
  966. debug "$FUNCNAME: cache hit ($*)"
  967. cat "$cache_file"
  968. return 0
  969. fi
  970. mixin=
  971. metadata="$(get_charm_metadata "$charm")"
  972. if [ "$metadata" ]; then
  973. ## resources to volumes
  974. volumes=$(
  975. for resource_type in data config; do
  976. while read-0 resource; do
  977. eval "echo \" - \$${resource_type^^}STORE/\$charm\$resource:\$resource:rw\""
  978. done < <(echo "$metadata" | shyaml get-values-0 "${resource_type}-resources" 2>/dev/null)
  979. done
  980. while read-0 resource; do
  981. if [[ "$resource" == *:* ]]; then
  982. echo " - $resource:rw"
  983. else
  984. echo " - $resource:$resource:rw"
  985. fi
  986. done < <(echo "$metadata" | shyaml get-values-0 "host-resources" 2>/dev/null)
  987. )
  988. if [ "$volumes" ]; then
  989. mixin=$(echo -en "volumes:\n$volumes")
  990. fi
  991. docker_compose=$(echo "$metadata" | shyaml get-value "docker-compose" 2>/dev/null)
  992. if [ "$docker_compose" ]; then
  993. mixin=$(merge_yaml_str "$mixin" "$docker_compose")
  994. fi
  995. if [ "$(echo "$metadata" | shyaml get-value "subordinate" 2>/dev/null)" == "True" ]; then
  996. subordinate=true
  997. fi
  998. fi
  999. image=$(echo "$metadata" | shyaml get-value "docker-image" 2>/dev/null)
  1000. image_or_build_statement=
  1001. if [ "$image" ]; then
  1002. if [ "$subordinate" ]; then
  1003. err "Subordinate charm can not have a ${WHITE}docker-image${NORMAL} value."
  1004. return 1
  1005. fi
  1006. image_or_build_statement="image: $image"
  1007. elif [ -d "$CHARM_STORE/$charm/build" ]; then
  1008. if [ "$subordinate" ]; then
  1009. err "Subordinate charm can not have a 'build' sub directory."
  1010. return 1
  1011. fi
  1012. image_or_build_statement="build: $charm/build"
  1013. fi
  1014. if [ "$image_or_build_statement" ]; then
  1015. mixin=$(merge_yaml_str "$mixin" "$image_or_build_statement")
  1016. fi
  1017. echo "$mixin" > "$cache_file"
  1018. echo "$mixin"
  1019. }
  1020. export -f get_docker_compose_mixin_from_metadata
  1021. _save() {
  1022. local name="$1"
  1023. cat - | tee -a "$docker_compose_dir/.data/$name"
  1024. }
  1025. get_default_project_name() {
  1026. if [ "$DEFAULT_PROJECT_NAME" ]; then
  1027. echo "$DEFAULT_PROJECT_NAME"
  1028. return 0
  1029. fi
  1030. compose_yml_location="$(get_compose_yml_location)"
  1031. if [ "$compose_yml_location" ]; then
  1032. if normalized_path=$(readlink -e "$compose_yml_location"); then
  1033. echo "$(basename "$(dirname "$normalized_path")")"
  1034. return 0
  1035. fi
  1036. fi
  1037. echo "project"
  1038. return 0
  1039. }
  1040. export -f get_default_project_name
  1041. launch_docker_compose() {
  1042. docker_compose_tmpdir=$(mktemp -d -t tmp.XXXXXXXXXX)
  1043. #debug "Creating temporary docker-compose directory in '$docker_compose_tmpdir'."
  1044. # trap_add EXIT "debug \"Removing temporary docker-compose directory in $docker_compose_tmpdir.\";\
  1045. # rm -rf \"$docker_compose_tmpdir\""
  1046. trap_add EXIT "rm -rf \"$docker_compose_tmpdir\""
  1047. project=$(get_default_project_name)
  1048. mkdir -p "$docker_compose_tmpdir/$project"
  1049. docker_compose_dir="$docker_compose_tmpdir/$project"
  1050. if [ -z "$services" ]; then
  1051. services=$(get_default_target_services $services)
  1052. fi
  1053. get_docker_compose $services > "$docker_compose_dir/docker-compose.yml" || return 1
  1054. if [ -e "$state_tmpdir/to-merge-in-docker-compose.yml" ]; then
  1055. # debug "Merging some config data in docker-compose.yml:"
  1056. # debug "$(cat $state_tmpdir/to-merge-in-docker-compose.yml)"
  1057. _config_merge "$docker_compose_dir/docker-compose.yml" "$state_tmpdir/to-merge-in-docker-compose.yml" || return 1
  1058. fi
  1059. ## XXXvlab: could be more specific and only link the needed charms
  1060. ln -sf "$CHARM_STORE/"* "$docker_compose_dir/"
  1061. mkdir "$docker_compose_dir/.data"
  1062. {
  1063. {
  1064. cd "$docker_compose_dir"
  1065. debug "${WHITE}docker-compose.yml:$NORMAL"
  1066. debug "$(cat "$docker_compose_dir/docker-compose.yml")"
  1067. debug "${WHITE}Launching$NORMAL: docker-compose $@"
  1068. docker-compose "$@"
  1069. echo "$?" > "$docker_compose_dir/.data/errlvl"
  1070. } | _save stdout
  1071. } 3>&1 1>&2 2>&3 | _save stderr 3>&1 1>&2 2>&3
  1072. if tail -n 1 "$docker_compose_dir/.data/stderr" | egrep "Service .+ failed to build: Error getting container [0-9a-f]+ from driver devicemapper: (open|Error mounting) /dev/mapper/docker-.*: no such file or directory$" >/dev/null 2>&1; then
  1073. debug NASTY ERROR DETECTED
  1074. fi
  1075. docker_compose_errlvl="$(cat "$docker_compose_dir/.data/errlvl")"
  1076. if [ -z "docker_compose_dir" ]; then
  1077. err "Something went wrong before you could gather docker-compose errorlevel."
  1078. return 1
  1079. fi
  1080. return "$docker_compose_errlvl"
  1081. }
  1082. get_compose_yml_location() {
  1083. parent=$(while ! [ -e "./compose.yml" ]; do
  1084. [ "$PWD" == "/" ] && exit 0
  1085. cd ..
  1086. done; echo "$PWD"
  1087. )
  1088. if [ "$parent" ]; then
  1089. echo "$parent/compose.yml"
  1090. return 0
  1091. fi
  1092. if [ "$DEFAULT_COMPOSE_FILE" ]; then
  1093. if ! [ -e "$DEFAULT_COMPOSE_FILE" ]; then
  1094. err "No 'compose.yml' was found in current or parent dirs," \
  1095. "and \$DEFAULT_COMPOSE_FILE points to an unexistent file."
  1096. die "Please provide a 'compose.yml' file."
  1097. fi
  1098. echo "$DEFAULT_COMPOSE_FILE"
  1099. return 0
  1100. fi
  1101. err "No 'compose.yml' was found in current or parent dirs, and no \$DEFAULT_COMPOSE_FILE was set."
  1102. die "Please provide a 'compose.yml' file."
  1103. return 1
  1104. }
  1105. export -f get_compose_yml_location
  1106. get_default_target_services() {
  1107. local services=("$@")
  1108. if [ -z "${services[*]}" ]; then
  1109. if [ "$DEFAULT_SERVICES" ]; then
  1110. info "No service provided, using \$DEFAULT_SERVICES variable. Target services: $DEFAULT_SERVICES"
  1111. services="$DEFAULT_SERVICES"
  1112. else
  1113. err "No service provided."
  1114. return 1
  1115. fi
  1116. fi
  1117. echo "${services[*]}"
  1118. }
  1119. get_master_services() {
  1120. local loaded master_service
  1121. declare -A loaded
  1122. for service in "$@"; do
  1123. master_service=$(_get_master_charm_for_service "$service") || return 1
  1124. if [ "${loaded[$master_service]}" ]; then
  1125. continue
  1126. fi
  1127. echo "$master_service"
  1128. loaded["$master_service"]=1
  1129. done | xargs echo
  1130. }
  1131. _setup_state_dir() {
  1132. export state_tmpdir=$(mktemp -d -t tmp.XXXXXXXXXX)
  1133. #debug "Creating temporary state directory in '$state_tmpdir'."
  1134. # trap_add EXIT "debug \"Removing temporary state directory in $state_tmpdir.\";\
  1135. # rm -rf \"$state_tmpdir\""
  1136. trap_add EXIT "rm -rf \"$state_tmpdir\""
  1137. }
  1138. [ "$SOURCED" ] && return 0
  1139. ##
  1140. ## Argument parsing
  1141. ##
  1142. fullargs=()
  1143. opts=()
  1144. posargs=()
  1145. no_hooks=
  1146. no_init=
  1147. while [ "$#" != 0 ]; do
  1148. case "$1" in
  1149. # --help|-h)
  1150. # print_help
  1151. # exit 0
  1152. # ;;
  1153. --verbose|-v)
  1154. fullargs+=("$1")
  1155. export VERBOSE=true
  1156. ;;
  1157. -f)
  1158. if ! [ -e "$2" ]; then
  1159. die "File $2 doesn't exists"
  1160. fi
  1161. export DEFAULT_COMPOSE_FILE="$2"
  1162. shift
  1163. ;;
  1164. -p)
  1165. fullargs+=("$1" "$2")
  1166. opts=("${opts[@]}" "$1" "$2")
  1167. export DEFAULT_PROJECT_NAME="$2"
  1168. shift
  1169. ;;
  1170. --no-relations)
  1171. export no_relations=true
  1172. ;;
  1173. --no-hooks)
  1174. export no_hooks=true
  1175. ;;
  1176. --no-init)
  1177. export no_init=true
  1178. ;;
  1179. --debug)
  1180. export DEBUG=true
  1181. export VERBOSE=true
  1182. ;;
  1183. --)
  1184. fullargs+=("$1")
  1185. shift
  1186. opts=("${opts[@]}" "$@")
  1187. break 2
  1188. ;;
  1189. -*)
  1190. fullargs+=("$1" "$2")
  1191. opts=("${opts[@]}" "$1" "$2")
  1192. shift
  1193. ;;
  1194. *)
  1195. fullargs+=("$1")
  1196. posargs=("${posargs[@]}" "$1")
  1197. ;;
  1198. esac
  1199. shift
  1200. done
  1201. if [ -z "$DISABLE_SYSTEM_CONFIG_FILE" ]; then
  1202. if [ -r /etc/default/charm ]; then
  1203. . /etc/default/charm
  1204. fi
  1205. if [ -r "/etc/default/$exname" ]; then
  1206. . "/etc/default/$exname"
  1207. fi
  1208. ## XXXvlab: should provide YML config opportunities in possible parent dirs ?
  1209. ## userdir ? and global /etc/compose.yml ?
  1210. . /etc/compose.conf
  1211. . /etc/compose.local.conf
  1212. fi
  1213. ##
  1214. ## Actual code
  1215. ##
  1216. export CHARM_STORE=${CHARM_STORE:-/srv/charm-store}
  1217. export DOCKER_DATASTORE=${DOCKER_DATASTORE:-/srv/docker-datastore}
  1218. export COMPOSE_YML_FILE=$(get_compose_yml_location) || exit 1
  1219. debug "Found 'compose.yml' file in '$COMPOSE_YML_FILE'."
  1220. if ! [ -d "$CHARM_STORE" ]; then
  1221. err "Charm store path $YELLOW$CHARM_STORE$NORMAL does not exists. "
  1222. err "Please check your $YELLOW\$CHARM_STORE$NORMAL variable value."
  1223. exit 1
  1224. fi
  1225. if [ -z "$(cd "$CHARM_STORE"; ls)" ]; then
  1226. err "no available charms in charm store $YELLOW$CHARM_STORE$NORMAL. Either:"
  1227. err " - check $YELLOW\$CHARM_STORE$NORMAL variable value"
  1228. err " - download charms in $CHARM_STORE"
  1229. print_error "Charm store is empty. Cannot continue."
  1230. fi
  1231. _setup_state_dir
  1232. ##
  1233. ## Get services in command line.
  1234. ##
  1235. action="${posargs[0]}"
  1236. is_service_action=
  1237. case "$action" in
  1238. up|build|start|stop|config)
  1239. services="$(get_default_target_services "${posargs[@]:1}")" || exit 1
  1240. orig_services="${posargs[@]:1}"
  1241. ;;
  1242. run)
  1243. services="${posargs[1]}"
  1244. ;;
  1245. *)
  1246. if is_service_action=$(has_service_action "${posargs[1]}" "$action"); then
  1247. debug "Found action $DARKCYAN$action$NORMAL in service $DARKYELLOW${posargs[1]}$NORMAL"
  1248. services="${posargs[1]}"
  1249. else
  1250. services="$(get_default_target_services)"
  1251. fi
  1252. ;;
  1253. esac
  1254. get_docker_compose $services >/dev/null || exit 1 ## precalculate variable \$_currnet_docker_compose
  1255. ##
  1256. ## Pre-action
  1257. ##
  1258. full_init=
  1259. case "$action" in
  1260. up|run)
  1261. full_init=true
  1262. ;;
  1263. *)
  1264. if [ "$is_service_action" ]; then
  1265. full_init=true
  1266. fi
  1267. ;;
  1268. esac
  1269. if [ "$full_init" ]; then
  1270. ## init in order
  1271. Section initialisation
  1272. if [ -z "$no_init" ]; then
  1273. run_service_hook "$services" init || exit 1
  1274. fi
  1275. ## Get relations
  1276. if [ -z "$no_relations" ]; then
  1277. run_service_relations "$services" || exit 1
  1278. fi
  1279. ## XXXvlab: to be removed when all relation and service stuff is resolved
  1280. if [ -z "$no_hooks" ]; then
  1281. ordered_services=$(get_ordered_service_dependencies $services) || exit 1
  1282. for service in $ordered_services; do
  1283. charm=$(get_service_charm "$service") || exit 1
  1284. for script in "$CHARM_STORE/$charm/hooks.d/"*.sh; do
  1285. [ -e "$script" ] || continue
  1286. [ -x "$script" ] || { echo "compose: script $script is not executable." >&2; exit 1; }
  1287. (
  1288. debug "Launching '$script'."
  1289. cd "$(dirname "$script)")";
  1290. "$script" "$@"
  1291. ) || { echo "compose: hook $script failed. Stopping." >&2; exit 1; }
  1292. done
  1293. done
  1294. fi
  1295. fi
  1296. ##
  1297. ## Docker-compose
  1298. ##
  1299. case "$action" in
  1300. up|start|stop|build)
  1301. master_services=$(get_master_services $services) || exit 1
  1302. launch_docker_compose "$action" "${opts[@]}" $master_services
  1303. ;;
  1304. run)
  1305. master_service=$(get_master_services $services) || exit 1
  1306. launch_docker_compose "$action" "$master_service" "${opts[@]}" "${posargs[@]:2}"
  1307. ;;
  1308. config)
  1309. ## removing the services
  1310. launch_docker_compose config "${opts[@]}"
  1311. warn "Runtime configuration modification (from relations) are not included here."
  1312. ;;
  1313. *)
  1314. if [ "$is_service_action" ]; then
  1315. run_service_action "$services" "$action" "${opts[@]}" "${posargs[@]:2}"
  1316. else
  1317. launch_docker_compose "${fullargs[@]}"
  1318. fi
  1319. ;;
  1320. esac