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