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