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