687557b81c7aedc64ebbd7869a66f8fceeca4d82
[discourse_docker.git] / launcher
1 #!/bin/bash
2
3 usage () {
4 echo "Usage: launcher COMMAND CONFIG [--skip-prereqs] [--docker-args STRING]"
5 echo "Commands:"
6 echo " start: Start/initialize a container"
7 echo " stop: Stop a running container"
8 echo " restart: Restart a container"
9 echo " destroy: Stop and remove a container"
10 echo " enter: Open a shell to run commands inside the container"
11 echo " logs: View the Docker logs for a container"
12 echo " bootstrap: Bootstrap a container for the config based on a template"
13 echo " rebuild: Rebuild a container (destroy old, bootstrap, start new)"
14 echo " cleanup: Remove all containers that have stopped for > 24 hours"
15 echo
16 echo "Options:"
17 echo " --skip-prereqs Don't check launcher prerequisites"
18 echo " --docker-args Extra arguments to pass when running docker"
19 echo " --skip-mac-address Don't assign a mac address"
20 exit 1
21 }
22
23 command=$1
24 config=$2
25 user_args=""
26
27 while [ ${#} -gt 0 ]; do
28 case "${1}" in
29 --debug)
30 DEBUG="1"
31 ;;
32 --skip-prereqs)
33 SKIP_PREREQS="1"
34 ;;
35 --skip-mac-address)
36 SKIP_MAC_ADDRESS="1"
37 ;;
38 --docker-args)
39 user_args="$2"
40 shift
41 ;;
42 esac
43
44 shift 1
45 done
46
47 # Docker doesn't like uppercase characters, spaces or special characters, catch it now before we build everything out and then find out
48 re='[A-Z/ !@#$%^&*()+~`=]'
49 if [[ $config =~ $re ]];
50 then
51 echo
52 echo "ERROR: Config name must not contain upper case characters, spaces or special characters. Correct config name and rerun $0."
53 echo
54 exit 1
55 fi
56
57 cd "$(dirname "$0")"
58
59 docker_min_version='1.8.0'
60 docker_rec_version='1.8.0'
61 git_min_version='1.8.0'
62 git_rec_version='1.8.0'
63
64 config_file=containers/"$config".yml
65 cidbootstrap=cids/"$config"_bootstrap.cid
66 local_discourse=local_discourse
67 image=discourse/base:2.0.20170728
68 docker_path=`which docker.io || which docker`
69 git_path=`which git`
70
71 if [ "${SUPERVISED}" = "true" ]; then
72 restart_policy="--restart=no"
73 attach_on_start="-a"
74 attach_on_run="-a stdout -a stderr"
75 else
76 attach_on_run="-d"
77 fi
78
79 if [ -n "$DOCKER_HOST" ]; then
80 docker_ip=`sed -e 's/^tcp:\/\/\(.*\):.*$/\1/' <<< "$DOCKER_HOST"`
81 elif [ -x "$(which ip 2>/dev/null)" ]; then
82 docker_ip=`ip addr show docker0 | \
83 grep 'inet ' | \
84 awk '{ split($2,a,"/"); print a[1] }';`
85 else
86 docker_ip=`ifconfig | \
87 grep -B1 "inet addr" | \
88 awk '{ if ( $1 == "inet" ) { print $2 } else if ( $2 == "Link" ) { printf "%s:" ,$1 } }' | \
89 grep docker0 | \
90 awk -F: '{ print $3 }';`
91 fi
92
93 compare_version() {
94 declare -a ver_a
95 declare -a ver_b
96 IFS=. read -a ver_a <<< "$1"
97 IFS=. read -a ver_b <<< "$2"
98
99 while [[ -n $ver_a ]]; do
100 if (( ver_a > ver_b )); then
101 return 0
102 elif (( ver_b > ver_a )); then
103 return 1
104 else
105 unset ver_a[0]
106 ver_a=("${ver_a[@]}")
107 unset ver_b[0]
108 ver_b=("${ver_b[@]}")
109 fi
110 done
111 return 1 # They are equal
112 }
113
114
115 install_docker() {
116 echo "Docker is not installed, you will need to install Docker in order to run Launcher"
117 echo "See https://docs.docker.com/installation/"
118 exit 1
119 }
120
121 check_prereqs() {
122
123 if [ -z $docker_path ]; then
124 install_docker
125 fi
126
127 # 1. docker daemon running?
128 # we send stderr to /dev/null cause we don't care about warnings,
129 # it usually complains about swap which does not matter
130 test=`$docker_path info 2> /dev/null`
131 if [[ $? -ne 0 ]] ; then
132 echo "Cannot connect to the docker daemon - verify it is running and you have access"
133 exit 1
134 fi
135
136 # 2. running an approved storage driver?
137 if ! $docker_path info 2> /dev/null | egrep -q '^Storage Driver: (aufs|btrfs|zfs|overlay|overlay2)$'; then
138 echo "Your Docker installation is not using a supported storage driver. If we were to proceed you may have a broken install."
139 echo "aufs is the recommended storage driver, although zfs/btrfs/overlay and overlay2 may work as well."
140 echo "Other storage drivers are known to be problematic."
141 echo "You can tell what filesystem you are using by running \"docker info\" and looking at the 'Storage Driver' line."
142 echo
143 echo "If you wish to continue anyway using your existing unsupported storage driver,"
144 echo "read the source code of launcher and figure out how to bypass this check."
145 exit 1
146 fi
147
148 # 3. running recommended docker version
149 test=($($docker_path --version)) # Get docker version string
150 test=${test[2]//,/} # Get version alone and strip comma if exists
151
152 # At least minimum docker version
153 if compare_version "${docker_min_version}" "${test}"; then
154 echo "ERROR: Docker version ${test} not supported, please upgrade to at least ${docker_min_version}, or recommended ${docker_rec_version}"
155 exit 1
156 fi
157
158 # Recommend newer docker version
159 if compare_version "${docker_rec_version}" "${test}"; then
160 echo "WARNING: Docker version ${test} deprecated, recommend upgrade to ${docker_rec_version} or newer."
161 fi
162
163 # 4. discourse docker image is downloaded
164 test=`$docker_path images | awk '{print $1 ":" $2 }' | grep "$image"`
165
166 if [ -z "$test" ]; then
167 echo
168 echo "WARNING: We are about to start downloading the Discourse base image"
169 echo "This process may take anywhere between a few minutes to an hour, depending on your network speed"
170 echo
171 echo "Please be patient"
172 echo
173 fi
174
175 # 5. running recommended git version
176 test=($($git_path --version)) # Get git version string
177 test=${test[2]//,/} # Get version alone and strip comma if exists
178
179 # At least minimum version
180 if compare_version "${git_min_version}" "${test}"; then
181 echo "ERROR: Git version ${test} not supported, please upgrade to at least ${git_min_version}, or recommended ${git_rec_version}"
182 exit 1
183 fi
184
185 # Recommend best version
186 if compare_version "${git_rec_version}" "${test}"; then
187 echo "WARNING: Git version ${test} deprecated, recommend upgrade to ${git_rec_version} or newer."
188 fi
189
190 # 6. able to attach stderr / out / tty
191 test=`$docker_path run $user_args -i --rm -a stdout -a stderr $image echo working`
192 if [[ "$test" =~ "working" ]] ; then : ; else
193 echo "Your Docker installation is not working correctly"
194 echo
195 echo "See: https://meta.discourse.org/t/docker-error-on-bootstrap/13657/18?u=sam"
196 exit 1
197 fi
198
199 # 7. enough space for the bootstrap on docker folder
200 folder=`$docker_path info --format '{{.DockerRootDir}}'`
201 safe_folder=${folder:-/var/lib/docker}
202 test=$(($(stat -f --format="%a*%S" $safe_folder)/1024**3 < 5))
203 if [[ $test -ne 0 ]] ; then
204 echo "You have less than 5GB of free space on the disk where $safe_folder is located. You will need more space to continue"
205 df -h $safe_folder
206 echo
207 read -p "Would you like to attempt to recover space by cleaning docker images and containers in the system?(y/N)" -n 1 -r
208 echo
209 if [[ $REPLY =~ ^[Yy]$ ]]
210 then
211 docker system prune
212 echo "If the cleanup was successful, you may try again now"
213 fi
214 exit 1
215 fi
216 }
217
218
219 if [ -z "$SKIP_PREREQS" ] && [ "$command" != "cleanup" ]; then
220 check_prereqs
221 fi
222
223 host_run() {
224 read -r -d '' env_ruby << 'RUBY'
225 require 'yaml'
226
227 input = STDIN.readlines.join
228 yaml = YAML.load(input)
229
230 if host_run = yaml['host_run']
231 params = yaml['params'] || {}
232 host_run.each do |run|
233 params.each do |k,v|
234 run = run.gsub("$#{k}", v)
235 end
236 STDOUT.write "#{run}--SEP--"
237 end
238 end
239 RUBY
240
241 host_run=`cat $config_file | $docker_path run $user_args --rm -i -a stdout -a stdin $image ruby -e "$env_ruby"`
242
243 while [ "$host_run" ] ; do
244 iter=${host_run%%--SEP--*}
245 echo
246 echo "Host run: $iter"
247 $iter || exit 1
248 echo
249 host_run="${host_run#*--SEP--}"
250 done
251 }
252
253
254 set_volumes() {
255 local volopts=
256 if selinuxenabled; then
257 volopts=":Z"
258 fi
259 volumes=`cat $config_file | $docker_path run $user_args --rm -i -a stdout -a stdin $image ruby -e \
260 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['volumes'].map{|v| '-v ' << v['volume']['host'] << ':' << v['volume']['guest'] << '${volopts} '}.join"`
261 }
262
263 set_links() {
264 links=`cat $config_file | $docker_path run $user_args --rm -i -a stdout -a stdin $image ruby -e \
265 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['links'].map{|l| '--link ' << l['link']['name'] << ':' << l['link']['alias'] << ' '}.join"`
266 }
267
268 find_templates() {
269 local templates=`cat $1 | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
270 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['templates']"`
271
272 local arrTemplates=${templates// / }
273
274 if [ ! -z "$templates" ]; then
275 for template in "${arrTemplates[@]}"
276 do
277 local nested_templates=$(find_templates $template)
278
279 if [ ! -z "$nested_templates" ]; then
280 templates="$templates $nested_templates"
281 fi
282 done
283
284 echo $templates
285 else
286 echo ""
287 fi
288 }
289
290 set_template_info() {
291 templates=$(find_templates $config_file)
292
293 arrTemplates=(${templates// / })
294 config_data=$(cat $config_file)
295
296 input="hack: true"
297
298 for template in "${arrTemplates[@]}"
299 do
300 [ ! -z $template ] && {
301 input="$input _FILE_SEPERATOR_ $(cat $template)"
302 }
303 done
304
305 # we always want our config file last so it takes priority
306 input="$input _FILE_SEPERATOR_ $config_data"
307
308 read -r -d '' env_ruby << 'RUBY'
309 require 'yaml'
310
311 input=STDIN.readlines.join
312 # default to UTF-8 for the dbs sake
313 env = {'LANG' => 'en_US.UTF-8'}
314 input.split('_FILE_SEPERATOR_').each do |yml|
315 yml.strip!
316 begin
317 env.merge!(YAML.load(yml)['env'] || {})
318 rescue Psych::SyntaxError => e
319 puts e
320 puts "*ERROR."
321 rescue => e
322 puts yml
323 p e
324 end
325 end
326 puts env.map{|k,v| "-e\n#{k}=#{v}" }.join("\n")
327 RUBY
328
329 raw=`exec echo "$input" | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e "$env_ruby"`
330
331 env=()
332 ok=1
333 while read i; do
334 if [ "$i" == "*ERROR." ]; then
335 ok=0
336 elif [ -n "$i" ]; then
337 env[${#env[@]}]=$i
338 fi
339 done <<< "$raw"
340
341 if [ "$ok" -ne 1 ]; then
342 echo "${env[@]}"
343 echo "YAML syntax error. Please check your containers/*.yml config files."
344 exit 1
345 fi
346
347 read -r -d '' labels_ruby << 'RUBY'
348 require 'yaml'
349
350 input=STDIN.readlines.join
351 # default to UTF-8 for the dbs sake
352 labels = {}
353 input.split('_FILE_SEPERATOR_').each do |yml|
354 yml.strip!
355 begin
356 labels.merge!(YAML.load(yml)['labels'] || {})
357 rescue Psych::SyntaxError => e
358 puts e
359 puts "*ERROR."
360 rescue => e
361 puts yml
362 p e
363 end
364 end
365 puts labels.map{|k,v| "-l\n#{k}=#{v}" }.join("\n")
366 RUBY
367
368 raw=`exec echo "$input" | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e "$labels_ruby"`
369
370 labels=()
371 ok=1
372 while read i; do
373 if [ "$i" == "*ERROR." ]; then
374 ok=0
375 elif [ -n "$i" ]; then
376 labels[${#labels[@]}]=$i
377 fi
378 done <<< "$raw"
379
380 if [ "$ok" -ne 1 ]; then
381 echo "${labels[@]}"
382 echo "YAML syntax error. Please check your containers/*.yml config files."
383 exit 1
384 fi
385 }
386
387 if [ -z $docker_path ]; then
388 install_docker
389 fi
390
391 [ "$command" == "cleanup" ] && {
392 echo
393 echo "The following command will"
394 echo "- Delete all docker images for old containers"
395 echo "- Delete all stopped and orphan containers"
396 echo
397 read -p "Are you sure (Y/n): " -n 1 -r && echo
398 if [[ $REPLY =~ ^[Yy]$ || ! $REPLY ]]
399 then
400 space=$(df /var/lib/docker | awk '{ print $4 }' | grep -v Available)
401 echo "Starting Cleanup (bytes free $space)"
402
403 STATE_DIR=./.gc-state scripts/docker-gc
404
405 space=$(df /var/lib/docker | awk '{ print $4 }' | grep -v Available)
406 echo "Finished Cleanup (bytes free $space)"
407
408 else
409 exit 1
410 fi
411 exit 0
412 }
413
414 if [ -z "$command" -a -z "$config" ]; then
415 usage
416 fi
417
418 if [ ! "$command" == "setup" ]; then
419 if [[ ! -e $config_file ]]; then
420 echo "Config file was not found, ensure $config_file exists"
421 echo
422 echo "Available configs ( `cd containers && ls -dm *.yml | tr -s '\n' ' ' | awk '{ gsub(/\.yml/, ""); print }'`)"
423 exit 1
424 fi
425 fi
426
427 docker_version=($($docker_path --version))
428 docker_version=${test[2]//,/}
429 restart_policy=${restart_policy:---restart=always}
430
431 set_existing_container(){
432 existing=`$docker_path ps -a | awk '{ print $1, $(NF) }' | grep " $config$" | awk '{ print $1 }'`
433 }
434
435 run_stop() {
436
437 set_existing_container
438
439 if [ ! -z $existing ]
440 then
441 (
442 set -x
443 $docker_path stop -t 10 $config
444 )
445 else
446 echo "$config was not started !"
447 exit 1
448 fi
449 }
450
451 set_run_image() {
452 run_image=`cat $config_file | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
453 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['run_image']"`
454
455 if [ -z "$run_image" ]; then
456 run_image="$local_discourse/$config"
457 fi
458 }
459
460 set_boot_command() {
461 boot_command=`cat $config_file | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
462 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['boot_command']"`
463
464 if [ -z "$boot_command" ]; then
465
466 no_boot_command=`cat $config_file | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
467 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['no_boot_command']"`
468
469 if [ -z "$no_boot_command" ]; then
470 boot_command="/sbin/boot"
471 fi
472 fi
473 }
474
475 run_start() {
476
477 existing=`$docker_path ps | awk '{ print $1, $(NF) }' | grep " $config$" | awk '{ print $1 }'`
478 echo $existing
479 if [ ! -z $existing ]
480 then
481 echo "Nothing to do, your container has already started!"
482 exit 0
483 fi
484
485 existing=`$docker_path ps -a | awk '{ print $1, $(NF) }' | grep " $config$" | awk '{ print $1 }'`
486 if [ ! -z $existing ]
487 then
488 echo "starting up existing container"
489 (
490 set -x
491 $docker_path start $config
492 )
493 exit 0
494 fi
495
496 host_run
497
498 ports=`cat $config_file | $docker_path run $user_args --rm -i -a stdout -a stdin $image ruby -e \
499 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['expose'].map{|p| \"-p #{p}\"}.join(' ')"`
500
501 docker_args=`cat $config_file | $docker_path run $user_args --rm -i -a stdout -a stdin $image ruby -e \
502 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['docker_args']"`
503
504 set_template_info
505 set_volumes
506 set_links
507 set_run_image
508 set_boot_command
509
510 # get hostname and settings from container configuration
511 for envar in "${env[@]}"
512 do
513 if [[ $envar == DOCKER_USE_HOSTNAME* ]] || [[ $envar == DISCOURSE_HOSTNAME* ]]
514 then
515 # use as environment variable
516 eval $envar
517 fi
518 done
519
520 (
521 hostname=`hostname -s`
522 # overwrite hostname
523 if [ "$DOCKER_USE_HOSTNAME" = "true" ]
524 then
525 hostname=$DISCOURSE_HOSTNAME
526 else
527 hostname=$hostname-$config
528 fi
529
530 # we got to normalize so we only have allowed strings, this is more comprehensive but lets see how bash does first
531 # hostname=`$docker_path run $user_args --rm $image ruby -e 'print ARGV[0].gsub(/[^a-zA-Z-]/, "-")' $hostname`
532 # docker added more hostname rules
533 hostname=${hostname//_/-}
534
535
536 if [ -z "$SKIP_MAC_ADDRESS" ] ; then
537 mac_address="--mac-address $($docker_path run $user_args -i --rm -a stdout -a stderr $image /bin/sh -c "echo $hostname | md5sum | sed 's/^\(..\)\(..\)\(..\)\(..\)\(..\).*$/02:\1:\2:\3:\4:\5/'")"
538 fi
539
540 set -x
541 $docker_path run $links $attach_on_run $restart_policy "${env[@]}" "${labels[@]}" -h "$hostname" \
542 -e DOCKER_HOST_IP="$docker_ip" --name $config -t $ports $volumes $mac_address $docker_args $user_args \
543 $run_image $boot_command
544
545 )
546 exit 0
547
548 }
549
550
551 run_bootstrap() {
552
553 # I got no frigging clue what this does, ask Sam Saffron. It RUNS STUFF ON THE HOST I GUESS?
554 host_run
555
556 # Is the image available?
557 # If not, pull it here so the user is aware what's happening.
558 $docker_path history $image >/dev/null 2>&1 || $docker_path pull $image
559
560 set_template_info
561
562 base_image=`cat $config_file | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
563 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['base_image']"`
564
565 update_pups=`cat $config_file | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
566 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['update_pups']"`
567
568 if [[ ! X"" = X"$base_image" ]]; then
569 image=$base_image
570 fi
571
572 set_volumes
573 set_links
574
575 rm -f $cidbootstrap
576
577 run_command="cd /pups &&"
578 if [[ ! "false" = $update_pups ]]; then
579 run_command="$run_command git pull &&"
580 fi
581 run_command="$run_command /pups/bin/pups --stdin"
582
583 echo $run_command
584
585 unset ERR
586 (exec echo "$input" | $docker_path run $user_args $links "${env[@]}" -e DOCKER_HOST_IP="$docker_ip" --cidfile $cidbootstrap -i -a stdin -a stdout -a stderr $volumes $image \
587 /bin/bash -c "$run_command") || ERR=$?
588
589 unset FAILED
590 # magic exit code that indicates a retry
591 if [[ "$ERR" == 77 ]]; then
592 $docker_path rm `cat $cidbootstrap`
593 rm $cidbootstrap
594 exit 77
595 elif [[ "$ERR" > 0 ]]; then
596 FAILED=TRUE
597 fi
598
599 if [[ $FAILED = "TRUE" ]]; then
600 if [[ ! -z "$DEBUG" ]]; then
601 $docker_path commit `cat $cidbootstrap` $local_discourse/$config-debug || echo 'FAILED TO COMMIT'
602 echo "** DEBUG ** Maintaining image for diagnostics $local_discourse/$config-debug"
603 fi
604
605 $docker_path rm `cat $cidbootstrap`
606 rm $cidbootstrap
607 echo "** FAILED TO BOOTSTRAP ** please scroll up and look for earlier error messages, there may be more than one"
608 exit 1
609 fi
610
611 sleep 5
612
613 $docker_path commit `cat $cidbootstrap` $local_discourse/$config || echo 'FAILED TO COMMIT'
614 $docker_path rm `cat $cidbootstrap` && rm $cidbootstrap
615 }
616
617
618
619 case "$command" in
620 bootstrap)
621 run_bootstrap
622 echo "Successfully bootstrapped, to startup use ./launcher start $config"
623 exit 0
624 ;;
625
626 enter)
627 exec $docker_path exec -it $config /bin/bash --login
628 ;;
629
630 stop)
631 run_stop
632 exit 0
633 ;;
634
635 logs)
636
637 $docker_path logs $config
638 exit 0
639 ;;
640
641 restart)
642 run_stop
643 run_start
644 exit 0
645 ;;
646
647 start)
648 run_start
649 exit 0
650 ;;
651
652 rebuild)
653 if [ "$(git symbolic-ref --short HEAD)" == "master" ]; then
654 echo "Ensuring launcher is up to date"
655
656 git remote update
657
658 LOCAL=$(git rev-parse @)
659 REMOTE=$(git rev-parse @{u})
660 BASE=$(git merge-base @ @{u})
661
662 if [ $LOCAL = $REMOTE ]; then
663 echo "Launcher is up-to-date"
664
665 elif [ $LOCAL = $BASE ]; then
666 echo "Updating Launcher"
667 git pull || (echo 'failed to update' && exit 1)
668
669 for (( i=${#BASH_ARGV[@]}-1,j=0; i>=0,j<${#BASH_ARGV[@]}; i--,j++ ))
670 do
671 args[$j]=${BASH_ARGV[$i]}
672 done
673 exec /bin/bash $0 "${args[@]}" # $@ is empty, because of shift at the beginning. Use BASH_ARGV instead.
674
675 elif [ $REMOTE = $BASE ]; then
676 echo "Your version of Launcher is ahead of origin"
677
678 else
679 echo "Launcher has diverged source, this is only expected in Dev mode"
680 fi
681
682 fi
683
684 set_existing_container
685
686 if [ ! -z $existing ]
687 then
688 echo "Stopping old container"
689 (
690 set -x
691 $docker_path stop -t 10 $config
692 )
693 fi
694
695 run_bootstrap
696
697 if [ ! -z $existing ]
698 then
699 echo "Removing old container"
700 (
701 set -x
702 $docker_path rm $config
703 )
704 fi
705
706 run_start
707 exit 0
708 ;;
709
710
711 destroy)
712 (set -x; $docker_path stop -t 10 $config && $docker_path rm $config) || (echo "$config was not found" && exit 0)
713 exit 0
714 ;;
715 esac
716
717 usage