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