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