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