split Discourse specific code into 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 docker_args=`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)['docker_args']"`
417
418 set_template_info
419 set_volumes
420 set_links
421 set_run_image
422 set_boot_command
423
424 # get hostname and settings from container configuration
425 for envar in "${env[@]}"
426 do
427 if [[ $envar == DOCKER_USE_HOSTNAME* ]] || [[ $envar == DISCOURSE_HOSTNAME* ]]
428 then
429 # use as environment variable
430 eval $envar
431 fi
432 done
433
434 (
435 hostname=`hostname -s`
436 # overwrite hostname
437 if [ "$DOCKER_USE_HOSTNAME" = "true" ]
438 then
439 hostname=$DISCOURSE_HOSTNAME
440 else
441 hostname=$hostname-$config
442 fi
443
444 # we got to normalize so we only have allowed strings, this is more comprehensive but lets see how bash does first
445 # hostname=`$docker_path run $user_args --rm $image ruby -e 'print ARGV[0].gsub(/[^a-zA-Z-]/, "-")' $hostname`
446 # docker added more hostname rules
447 hostname=${hostname//_/-}
448
449 set -x
450 $docker_path run $user_args $links $attach_on_run $restart_policy "${env[@]}" -h "$hostname" \
451 -e DOCKER_HOST_IP=$docker_ip --name $config -t $ports $volumes $docker_args $run_image $boot_command
452
453 )
454 exit 0
455
456 }
457
458
459 run_bootstrap() {
460
461 # I got no frigging clue what this does, ask Sam Saffron. It RUNS STUFF ON THE HOST I GUESS?
462 host_run
463
464 # Is the image available?
465 # If not, pull it here so the user is aware what's happening.
466 $docker_path history $image >/dev/null 2>&1 || $docker_path pull $image
467
468 set_template_info
469
470 base_image=`cat $config_file | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
471 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['base_image']"`
472
473 update_pups=`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)['update_pups']"`
475
476 if [[ ! X"" = X"$base_image" ]]; then
477 image=$base_image
478 fi
479
480 set_volumes
481 set_links
482
483 rm -f $cidbootstrap
484
485 run_command="cd /pups &&"
486 if [[ ! "false" = $update_pups ]]; then
487 run_command="$run_command git pull &&"
488 fi
489 run_command="$run_command /pups/bin/pups --stdin"
490
491 echo $run_command
492
493 (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 \
494 /bin/bash -c "$run_command") \
495 || ($docker_path rm `cat $cidbootstrap` && rm $cidbootstrap)
496
497 [ ! -e $cidbootstrap ] && echo "** FAILED TO BOOTSTRAP ** please scroll up and look for earlier error messages, there may be more than one" && exit 1
498
499 sleep 5
500
501 $docker_path commit `cat $cidbootstrap` $local_discourse/$config || echo 'FAILED TO COMMIT'
502 $docker_path rm `cat $cidbootstrap` && rm $cidbootstrap
503 }
504
505
506
507 case "$command" in
508 bootstrap)
509 run_bootstrap
510 echo "Successfully bootstrapped, to startup use ./launcher start $config"
511 exit 0
512 ;;
513
514 enter)
515 exec $docker_path exec -it $config /bin/bash --login
516 ;;
517
518 stop)
519 run_stop
520 exit 0
521 ;;
522
523 logs)
524
525 $docker_path logs $config
526 exit 0
527 ;;
528
529 restart)
530 run_stop
531 run_start
532 exit 0
533 ;;
534
535 start)
536 run_start
537 exit 0
538 ;;
539
540 rebuild)
541 if [ "$(git symbolic-ref --short HEAD)" == "master" ]; then
542 echo "Ensuring launcher is up to date"
543
544 git remote update
545
546 LOCAL=$(git rev-parse @)
547 REMOTE=$(git rev-parse @{u})
548 BASE=$(git merge-base @ @{u})
549
550 if [ $LOCAL = $REMOTE ]; then
551 echo "Launcher is up-to-date"
552
553 elif [ $LOCAL = $BASE ]; then
554 echo "Updating Launcher"
555 git pull || (echo 'failed to update' && exit 1)
556 exec /bin/bash $0 $@
557
558 elif [ $REMOTE = $BASE ]; then
559 echo "Your version of Launcher is ahead of origin"
560
561 else
562 echo "Launcher has diverged source, this is only expected in Dev mode"
563 fi
564
565 fi
566
567 set_existing_container
568
569 if [ ! -z $existing ]
570 then
571 echo "Stopping old container"
572 (
573 set -x
574 $docker_path stop -t 10 $config
575 )
576 fi
577
578 run_bootstrap
579
580 if [ ! -z $existing ]
581 then
582 echo "Removing old container"
583 (
584 set -x
585 $docker_path rm $config
586 )
587 fi
588
589 run_start
590 exit 0
591 ;;
592
593
594 destroy)
595 (set -x; $docker_path stop -t 10 $config && $docker_path rm $config) || (echo "$config was not found" && exit 0)
596 exit 0
597 ;;
598 esac
599
600 usage