Add config options to skip assignment of a mac address.
[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 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/discourse:1.3.5
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 aufs or btrfs
137 test=`$docker_path info 2> /dev/null | grep 'Driver: '`
138 if [[ "$test" =~ [aufs|btrfs|zfs|overlay] ]] ; then : ; else
139 echo "Your Docker installation is not using a supported filesystem if we were to proceed you may have a broken install."
140 echo "aufs is the recommended filesystem you should be using (zfs/btrfs and overlay may work as well)"
141 echo "You can tell what filesystem you are using by running \"docker info\" and looking at the driver"
142 echo
143 echo "If you wish to continue anyway using your existing unsupported filesystem, "
144 echo "read the source code of launcher and figure out how to bypass this."
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 }
200
201
202 if [ -z "$SKIP_PREREQS" ] ; then
203 check_prereqs
204 fi
205
206 host_run() {
207 read -r -d '' env_ruby << 'RUBY'
208 require 'yaml'
209
210 input = STDIN.readlines.join
211 yaml = YAML.load(input)
212
213 if host_run = yaml['host_run']
214 params = yaml['params'] || {}
215 host_run.each do |run|
216 params.each do |k,v|
217 run = run.gsub("$#{k}", v)
218 end
219 STDOUT.write "#{run}--SEP--"
220 end
221 end
222 RUBY
223
224 host_run=`cat $config_file | $docker_path run $user_args --rm -i -a stdout -a stdin $image ruby -e "$env_ruby"`
225
226 while [ "$host_run" ] ; do
227 iter=${host_run%%--SEP--*}
228 echo
229 echo "Host run: $iter"
230 $iter || exit 1
231 echo
232 host_run="${host_run#*--SEP--}"
233 done
234 }
235
236
237 set_volumes() {
238 volumes=`cat $config_file | $docker_path run $user_args --rm -i -a stdout -a stdin $image ruby -e \
239 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['volumes'].map{|v| '-v ' << v['volume']['host'] << ':' << v['volume']['guest'] << ' '}.join"`
240 }
241
242 set_links() {
243 links=`cat $config_file | $docker_path run $user_args --rm -i -a stdout -a stdin $image ruby -e \
244 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['links'].map{|l| '--link ' << l['link']['name'] << ':' << l['link']['alias'] << ' '}.join"`
245 }
246
247 set_template_info() {
248
249 templates=`cat $config_file | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
250 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['templates']"`
251
252 arrTemplates=(${templates// / })
253 config_data=$(cat $config_file)
254
255 input="hack: true"
256
257 for template in "${arrTemplates[@]}"
258 do
259 [ ! -z $template ] && {
260 input="$input _FILE_SEPERATOR_ $(cat $template)"
261 }
262 done
263
264 # we always want our config file last so it takes priority
265 input="$input _FILE_SEPERATOR_ $config_data"
266
267 read -r -d '' env_ruby << 'RUBY'
268 require 'yaml'
269
270 input=STDIN.readlines.join
271 # default to UTF-8 for the dbs sake
272 env = {'LANG' => 'en_US.UTF-8'}
273 input.split('_FILE_SEPERATOR_').each do |yml|
274 yml.strip!
275 begin
276 env.merge!(YAML.load(yml)['env'] || {})
277 rescue Psych::SyntaxError => e
278 puts e
279 puts "*ERROR."
280 rescue => e
281 puts yml
282 p e
283 end
284 end
285 puts env.map{|k,v| "-e\n#{k}=#{v}" }.join("\n")
286 RUBY
287
288 raw=`exec echo "$input" | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e "$env_ruby"`
289
290 env=()
291 ok=1
292 while read i; do
293 if [ "$i" == "*ERROR." ]; then
294 ok=0
295 elif [ -n "$i" ]; then
296 env[${#env[@]}]=$i
297 fi
298 done <<< "$raw"
299
300 if [ "$ok" -ne 1 ]; then
301 echo "${env[@]}"
302 echo "YAML syntax error. Please check your containers/*.yml config files."
303 exit 1
304 fi
305 }
306
307 if [ -z $docker_path ]; then
308 install_docker
309 fi
310
311 [ "$command" == "cleanup" ] && {
312 echo
313 echo "The following command will"
314 echo "- Delete all docker images for old containers"
315 echo "- Delete all stopped and orphan containers"
316 echo
317 read -p "Are you sure (Y/n): " -n 1 -r && echo
318 if [[ $REPLY =~ ^[Yy]$ || ! $REPLY ]]
319 then
320 space=$(df /var/lib/docker | awk '{ print $4 }' | grep -v Available)
321 echo "Starting Cleanup (bytes free $space)"
322
323 STATE_DIR=./.gc-state scripts/docker-gc
324
325 space=$(df /var/lib/docker | awk '{ print $4 }' | grep -v Available)
326 echo "Finished Cleanup (bytes free $space)"
327
328 else
329 exit 1
330 fi
331 exit 0
332 }
333
334 if [ -z "$command" -a -z "$config" ]; then
335 usage
336 fi
337
338 if [ ! "$command" == "setup" ]; then
339 if [[ ! -e $config_file ]]; then
340 echo "Config file was not found, ensure $config_file exists"
341 echo
342 echo "Available configs ( `cd containers && ls -dm *.yml | tr -s '\n' ' ' | awk '{ gsub(/\.yml/, ""); print }'`)"
343 exit 1
344 fi
345 fi
346
347 docker_version=($($docker_path --version))
348 docker_version=${test[2]//,/}
349 restart_policy=${restart_policy:---restart=always}
350
351 set_existing_container(){
352 existing=`$docker_path ps -a | awk '{ print $1, $(NF) }' | grep " $config$" | awk '{ print $1 }'`
353 }
354
355 run_stop() {
356
357 set_existing_container
358
359 if [ ! -z $existing ]
360 then
361 (
362 set -x
363 $docker_path stop -t 10 $config
364 )
365 else
366 echo "$config was not started !"
367 exit 1
368 fi
369 }
370
371 set_run_image() {
372 run_image=`cat $config_file | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
373 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['run_image']"`
374
375 if [ -z "$run_image" ]; then
376 run_image="$local_discourse/$config"
377 fi
378 }
379
380 set_boot_command() {
381 boot_command=`cat $config_file | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
382 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['boot_command']"`
383
384 if [ -z "$boot_command" ]; then
385
386 no_boot_command=`cat $config_file | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
387 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['no_boot_command']"`
388
389 if [ -z "$no_boot_command" ]; then
390 boot_command="/sbin/boot"
391 fi
392 fi
393 }
394
395 run_start() {
396
397 existing=`$docker_path ps | awk '{ print $1, $(NF) }' | grep " $config$" | awk '{ print $1 }'`
398 echo $existing
399 if [ ! -z $existing ]
400 then
401 echo "Nothing to do, your container has already started!"
402 exit 0
403 fi
404
405 existing=`$docker_path ps -a | awk '{ print $1, $(NF) }' | grep " $config$" | awk '{ print $1 }'`
406 if [ ! -z $existing ]
407 then
408 echo "starting up existing container"
409 (
410 set -x
411 $docker_path start $config
412 )
413 exit 0
414 fi
415
416 host_run
417
418 ports=`cat $config_file | $docker_path run $user_args --rm -i -a stdout -a stdin $image ruby -e \
419 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['expose'].map{|p| \"-p #{p}\"}.join(' ')"`
420
421 docker_args=`cat $config_file | $docker_path run $user_args --rm -i -a stdout -a stdin $image ruby -e \
422 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['docker_args']"`
423
424 set_template_info
425 set_volumes
426 set_links
427 set_run_image
428 set_boot_command
429
430 # get hostname and settings from container configuration
431 for envar in "${env[@]}"
432 do
433 if [[ $envar == DOCKER_USE_HOSTNAME* ]] || [[ $envar == DISCOURSE_HOSTNAME* ]]
434 then
435 # use as environment variable
436 eval $envar
437 fi
438 done
439
440 (
441 hostname=`hostname -s`
442 # overwrite hostname
443 if [ "$DOCKER_USE_HOSTNAME" = "true" ]
444 then
445 hostname=$DISCOURSE_HOSTNAME
446 else
447 hostname=$hostname-$config
448 fi
449
450 # we got to normalize so we only have allowed strings, this is more comprehensive but lets see how bash does first
451 # hostname=`$docker_path run $user_args --rm $image ruby -e 'print ARGV[0].gsub(/[^a-zA-Z-]/, "-")' $hostname`
452 # docker added more hostname rules
453 hostname=${hostname//_/-}
454
455
456 if [ -z "$SKIP_MAC_ADDRESS" ] ; then
457 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/'")"
458 fi
459
460 set -x
461 $docker_path run $user_args $links $attach_on_run $restart_policy "${env[@]}" -h "$hostname" \
462 -e DOCKER_HOST_IP=$docker_ip --name $config -t $ports $volumes $mac_address $docker_args \
463 $run_image $boot_command
464
465 )
466 exit 0
467
468 }
469
470
471 run_bootstrap() {
472
473 # I got no frigging clue what this does, ask Sam Saffron. It RUNS STUFF ON THE HOST I GUESS?
474 host_run
475
476 # Is the image available?
477 # If not, pull it here so the user is aware what's happening.
478 $docker_path history $image >/dev/null 2>&1 || $docker_path pull $image
479
480 set_template_info
481
482 base_image=`cat $config_file | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
483 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['base_image']"`
484
485 update_pups=`cat $config_file | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
486 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['update_pups']"`
487
488 if [[ ! X"" = X"$base_image" ]]; then
489 image=$base_image
490 fi
491
492 set_volumes
493 set_links
494
495 rm -f $cidbootstrap
496
497 run_command="cd /pups &&"
498 if [[ ! "false" = $update_pups ]]; then
499 run_command="$run_command git pull &&"
500 fi
501 run_command="$run_command /pups/bin/pups --stdin"
502
503 echo $run_command
504
505 unset ERR
506 (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 \
507 /bin/bash -c "$run_command") || ERR=$?
508
509 unset FAILED
510 # magic exit code that indicates a retry
511 if [[ "$ERR" == 77 ]]; then
512 $docker_path rm `cat $cidbootstrap`
513 rm $cidbootstrap
514 exit 77
515 elif [[ "$ERR" > 0 ]]; then
516 FAILED=TRUE
517 fi
518
519 if [[ $FAILED = "TRUE" ]]; then
520 if [[ ! -z "$DEBUG" ]]; then
521 $docker_path commit `cat $cidbootstrap` $local_discourse/$config-debug || echo 'FAILED TO COMMIT'
522 echo "** DEBUG ** Maintaining image for diagnostics $local_discourse/$config-debug"
523 fi
524
525 $docker_path rm `cat $cidbootstrap`
526 rm $cidbootstrap
527 echo "** FAILED TO BOOTSTRAP ** please scroll up and look for earlier error messages, there may be more than one"
528 exit 1
529 fi
530
531 sleep 5
532
533 $docker_path commit `cat $cidbootstrap` $local_discourse/$config || echo 'FAILED TO COMMIT'
534 $docker_path rm `cat $cidbootstrap` && rm $cidbootstrap
535 }
536
537
538
539 case "$command" in
540 bootstrap)
541 run_bootstrap
542 echo "Successfully bootstrapped, to startup use ./launcher start $config"
543 exit 0
544 ;;
545
546 enter)
547 exec $docker_path exec -it $config /bin/bash --login
548 ;;
549
550 stop)
551 run_stop
552 exit 0
553 ;;
554
555 logs)
556
557 $docker_path logs $config
558 exit 0
559 ;;
560
561 restart)
562 run_stop
563 run_start
564 exit 0
565 ;;
566
567 start)
568 run_start
569 exit 0
570 ;;
571
572 rebuild)
573 if [ "$(git symbolic-ref --short HEAD)" == "master" ]; then
574 echo "Ensuring launcher is up to date"
575
576 git remote update
577
578 LOCAL=$(git rev-parse @)
579 REMOTE=$(git rev-parse @{u})
580 BASE=$(git merge-base @ @{u})
581
582 if [ $LOCAL = $REMOTE ]; then
583 echo "Launcher is up-to-date"
584
585 elif [ $LOCAL = $BASE ]; then
586 echo "Updating Launcher"
587 git pull || (echo 'failed to update' && exit 1)
588 exec /bin/bash $0 $@
589
590 elif [ $REMOTE = $BASE ]; then
591 echo "Your version of Launcher is ahead of origin"
592
593 else
594 echo "Launcher has diverged source, this is only expected in Dev mode"
595 fi
596
597 fi
598
599 set_existing_container
600
601 if [ ! -z $existing ]
602 then
603 echo "Stopping old container"
604 (
605 set -x
606 $docker_path stop -t 10 $config
607 )
608 fi
609
610 run_bootstrap
611
612 if [ ! -z $existing ]
613 then
614 echo "Removing old container"
615 (
616 set -x
617 $docker_path rm $config
618 )
619 fi
620
621 run_start
622 exit 0
623 ;;
624
625
626 destroy)
627 (set -x; $docker_path stop -t 10 $config && $docker_path rm $config) || (echo "$config was not found" && exit 0)
628 exit 0
629 ;;
630 esac
631
632 usage