- stop using cid files
[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. able to attach stderr / out / tty
125 test=`$docker_path run $user_args -i --rm -a stdout -a stderr $image echo working`
126 if [[ "$test" =~ "working" ]] ; then : ; else
127 echo "Your Docker installation is not working correctly"
128 echo
129 echo "See: https://meta.discourse.org/t/docker-error-on-bootstrap/13657/18?u=sam"
130 exit 1
131 fi
132 }
133
134 if [ "$opt" != "--skip-prereqs" ] ; then
135 prereqs
136 fi
137
138 if [ "$opt" == "--docker-args" ] ; then
139 user_args=$4
140 else
141 user_args=""
142 fi
143
144 get_ssh_pub_key() {
145 local ${ssh_key_locations}
146 ssh_key_locations=(
147 ~/.ssh/id_ed25519.pub
148 ~/.ssh/id_ecdsa.pub
149 ~/.ssh/id_rsa.pub
150 ~/.ssh/id_dsa.pub
151 ~core/.ssh/authorized_keys
152 )
153
154 local $keyfile
155 for keyfile in "${ssh_key_locations[@]}"; do
156 if [[ -e ${keyfile} ]] ; then
157 ssh_pub_key="$(cat ${keyfile})"
158 return 0
159 fi
160 done
161
162 if tty -s ; then
163 echo "This user has no SSH key, but a SSH key is required to access the Discourse Docker container."
164 read -p "Generate a SSH key? (Y/n) " -n 1 -r
165 if [[ $REPLY =~ ^[Nn]$ ]] ; then
166 echo
167 echo WARNING: You may not be able to log in to your container.
168 echo
169 else
170 echo
171 echo Generating SSH key
172 mkdir -p ~/.ssh && ssh-keygen -f ~/.ssh/id_rsa -t rsa -N ''
173 echo
174 ssh_pub_key="$(cat ~/.ssh/id_rsa.pub)"
175 return 0
176 fi
177 fi
178
179 return 1
180 }
181
182
183 install_docker() {
184
185 echo "Docker is not installed, make sure you are running on the 3.8 kernel"
186 echo "The best supported Docker release is Ubuntu 12.04.03 for it run the following"
187 echo
188 echo "sudo apt-get update"
189 echo "sudo apt-get install linux-image-generic-lts-raring linux-headers-generic-lts-raring"
190 echo "sudo reboot"
191 echo
192
193 echo "sudo sh -c \"wget -qO- https://get.docker.io/gpg | apt-key add -\""
194 echo "sudo sh -c \"echo deb http://get.docker.io/ubuntu docker main > /etc/apt/sources.list.d/docker.list\""
195 echo "sudo apt-get update"
196 echo "sudo apt-get install lxc-docker"
197
198 exit 1
199 }
200
201 host_run() {
202 read -r -d '' env_ruby << 'RUBY'
203 require 'yaml'
204
205 input = STDIN.readlines.join
206 yaml = YAML.load(input)
207
208 if host_run = yaml['host_run']
209 params = yaml['params'] || {}
210 host_run.each do |run|
211 params.each do |k,v|
212 run = run.gsub("$#{k}", v)
213 end
214 STDOUT.write "#{run}--SEP--"
215 end
216 end
217 RUBY
218
219 host_run=`cat $config_file | $docker_path run $user_args --rm -i -a stdout -a stdin $image ruby -e "$env_ruby"`
220
221 while [ "$host_run" ] ; do
222 iter=${host_run%%--SEP--*}
223 echo
224 echo "Host run: $iter"
225 $iter || exit 1
226 echo
227 host_run="${host_run#*--SEP--}"
228 done
229 }
230
231
232 set_volumes() {
233 volumes=`cat $config_file | $docker_path run $user_args --rm -i -a stdout -a stdin $image ruby -e \
234 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['volumes'].map{|v| '-v ' << v['volume']['host'] << ':' << v['volume']['guest'] << ' '}.join"`
235 }
236
237 set_links() {
238 links=`cat $config_file | $docker_path run $user_args --rm -i -a stdout -a stdin $image ruby -e \
239 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['links'].map{|l| '--link ' << l['link']['name'] << ':' << l['link']['alias'] << ' '}.join"`
240 }
241
242 set_template_info() {
243
244 templates=`cat $config_file | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
245 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['templates']"`
246
247
248 arrTemplates=(${templates// / })
249 config_data=$(cat $config_file)
250
251 input="hack: true"
252
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 /var/discourse/containers/*.yml config files."
300 exit 1
301 fi
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 space=$(df /var/lib/docker | awk '{ print $4 }' | grep -v Available)
318
319 echo "Starting Cleanup"
320
321 if [[ ! -z `docker ps -aq` ]]; then
322 docker inspect -f '{{.Id}},{{.State.Running}},{{.State.FinishedAt}}' $(docker ps -qa) | \
323 awk -F, 'BEGIN { TIME=strftime("%FT%T.000000000Z",systime()-60*60*24); } $2=="false" && $3 < TIME {print $1;}' | \
324 xargs --no-run-if-empty docker rm >/dev/null 2>/dev/null
325 fi
326
327 docker rmi `docker images -a | grep '<none>' | awk '{print $3}'` 2> /dev/null
328
329 let freed=$space-$(df /var/lib/docker | awk '{ print $4 }' | grep -v Available)
330
331 echo $space
332 echo $(df /var/lib/docker | awk '{ print $4 }' | grep -v Available)
333
334
335 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 }}}'
336 [ -z "$output" ] && {
337 [[ $freed > 0 ]] && { echo "./launcher cleanup cleared up $freed of disk space."; } || { echo "./launcher cleanup has finished, no files were removed."; }
338 } || { echo "./launcher cleanup cleared up $freed of disk space."; }
339 else
340 exit 1
341 fi
342 exit 0
343 }
344
345 [ $# -lt 2 ] && {
346 usage
347 }
348
349 if [ ! -e $config_file ]
350 then
351 echo "Config file was not found, ensure $config_file exists"
352 echo ""
353 echo "Available configs ( `cd containers && ls -dm *.yml | tr -s '\n' ' ' | awk '{ gsub(/\.yml/, ""); print }'`)"
354 exit 1
355 fi
356
357
358 docker_version=($($docker_path --version))
359 docker_version=${test[2]//,/}
360
361 if compare_version "1.2.0" "$docker_version"; then
362 echo "We recommend you upgrade docker, the version you are running has no restart policies, on reboot your container may not start up"
363 restart_policy=""
364 else
365 restart_policy=${restart_policy:---restart=always}
366 fi
367
368
369 run_mailtest(){
370 if [ ! -e $config_file ]; then
371 echo "Config does not exist: $config_file" >&2
372 exit 1
373 fi
374 exec scripts/mailtest $config_file
375 }
376
377 set_existing_container(){
378 existing=`$docker_path ps -a | awk '{ print $1, $(NF) }' | grep "$config$" | awk '{ print $1 }'`
379 }
380
381 run_stop(){
382
383 set_existing_container
384
385 if [ ! -z $existing ]
386 then
387 (
388 set -x
389 $docker_path stop -t 10 $config
390 )
391 else
392 echo "$config was not started !"
393 exit 1
394 fi
395 }
396
397 run_start(){
398
399 existing=`$docker_path ps | awk '{ print $1, $(NF) }' | grep "$config$" | awk '{ print $1 }'`
400 if [ ! -z $existing ]
401 then
402 echo "Nothing to do, your container has already started!"
403 exit 1
404 fi
405
406 existing=`$docker_path ps -a | awk '{ print $1, $(NF) }' | grep "$config$" | awk '{ print $1 }'`
407 if [ ! -z $existing ]
408 then
409 echo "starting up existing container"
410 (
411 set -x
412 $docker_path start $config
413 )
414 exit 0
415 fi
416
417 host_run
418 ports=`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)['expose'].map{|p| '-p ' << p.to_s << ' '}.join"`
420
421 set_template_info
422 set_volumes
423 set_links
424
425 (
426 hostname=`hostname`
427 set -x
428 $docker_path run $user_args $links $attach_on_run $restart_policy "${env[@]}" -h "$hostname-$config" \
429 -e DOCKER_HOST_IP=$docker_ip --name $config -t $ports $volumes $local_discourse/$config /sbin/boot
430
431 )
432 exit 0
433
434 }
435
436 run_bootstrap(){
437
438 host_run
439
440 get_ssh_pub_key
441
442 # Is the image available?
443 # If not, pull it here so the user is aware what's happening.
444 $docker_path history $image >/dev/null 2>&1 || $docker_path pull $image
445
446 set_template_info
447
448 base_image=`cat $config_file | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
449 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['base_image']"`
450
451 update_pups=`cat $config_file | $docker_path run $user_args --rm -i -a stdin -a stdout $image ruby -e \
452 "require 'yaml'; puts YAML.load(STDIN.readlines.join)['update_pups']"`
453
454 if [[ ! X"" = X"$base_image" ]]; then
455 image=$base_image
456 fi
457
458 set_volumes
459 set_links
460
461 rm -f $cidbootstrap
462
463 run_command="cd /pups &&"
464 if [[ ! "false" = $update_pups ]]; then
465 run_command="$run_command git pull &&"
466 fi
467 run_command="$run_command /pups/bin/pups --stdin"
468
469 echo $run_command
470
471 env=("${env[@]}" "-e" "SSH_PUB_KEY=$ssh_pub_key")
472
473 (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 \
474 /bin/bash -c "$run_command") \
475 || ($docker_path rm `cat $cidbootstrap` && rm $cidbootstrap)
476
477 [ ! -e $cidbootstrap ] && echo "FAILED TO BOOTSTRAP" && exit 1
478
479 sleep 5
480
481 $docker_path commit `cat $cidbootstrap` $local_discourse/$config || echo 'FAILED TO COMMIT'
482 $docker_path rm `cat $cidbootstrap` && rm $cidbootstrap
483 }
484
485 case "$command" in
486 bootstrap)
487 run_bootstrap
488 echo "Successfully bootstrapped, to startup use ./launcher start $config"
489 exit 0
490 ;;
491
492 mailtest)
493 run_mailtest
494 exit 0
495 ;;
496
497 enter)
498 exec $docker_path exec -it $config /bin/bash
499 ;;
500
501 ssh)
502 existing=`$docker_path ps | awk '{ print $1, $(NF) }' | grep "$config$" | awk '{ print $1 }'`
503
504 if [[ ! -z $existing ]]; then
505 address="`$docker_path port $config 22`"
506 split=(${address//:/ })
507 exec ssh -o StrictHostKeyChecking=no root@${split[0]} -p ${split[1]}
508 else
509 echo "$config is not running!"
510 exit 1
511 fi
512 ;;
513
514 stop)
515 run_stop
516 exit 0
517 ;;
518
519 logs)
520
521 $docker_path logs $config
522 exit 0
523 ;;
524
525 restart)
526 run_stop
527 run_start
528 exit 0
529 ;;
530
531 start)
532 run_start
533 exit 0
534 ;;
535
536 rebuild)
537 if [ "$(git symbolic-ref --short HEAD)" == "master" ]; then
538 echo "Ensuring discourse docker is up to date"
539
540 git remote update
541
542 LOCAL=$(git rev-parse @)
543 REMOTE=$(git rev-parse @{u})
544 BASE=$(git merge-base @ @{u})
545
546 if [ $LOCAL = $REMOTE ]; then
547 echo "Discourse Docker is up-to-date"
548
549 elif [ $LOCAL = $BASE ]; then
550 echo "Updating Discourse Docker"
551 git pull || (echo 'failed to update' && exit 1)
552 exec /bin/bash $0 $@
553
554 elif [ $REMOTE = $BASE ]; then
555 echo "Your version of Discourse Docker is ahead of origin"
556
557 else
558 echo "Discourse Docker has diverged source, this is only expected in Dev mode"
559 fi
560
561 fi
562
563 set_existing_container
564
565 if [ ! -z $existing ]
566 then
567 echo "Stopping old container"
568 (
569 set -x
570 $docker_path stop -t 10 $config
571 )
572 fi
573
574 run_bootstrap
575
576 if [ ! -z $existing ]
577 then
578 echo "Removing old container"
579 (
580 set -x
581 $docker_path rm $config
582 )
583 fi
584
585 run_start
586 exit 0
587 ;;
588
589
590 destroy)
591 (set -x; $docker_path stop -t 10 $config && $docker_path rm $config) || (echo "$config was not found" && exit 0)
592 exit 0
593 ;;
594 esac
595
596 usage