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