Merge pull request #100 from wmark/fix-launcher
[discourse_docker.git] / launcher
old mode 100755 (executable)
new mode 100644 (file)
index a1140d4..3bf9f8d
--- a/launcher
+++ b/launcher
 
 command=$1
 config=$2
+opt=$3
+
+cd "$(dirname "$0")"
+
+docker_min_version='1.2.0'
+docker_rec_version='1.2.0'
+
 config_file=containers/"$config".yml
 cidfile=cids/"$config".cid
 cidbootstrap=cids/"$config"_boostrap.cid
 local_discourse=local_discourse
-image=samsaffron/discourse:0.1.2
-docker_path=`which docker`
+image=samsaffron/discourse:1.0.5
+docker_path=`which docker.io || which docker`
+
+if [ "${SUPERVISED}" = "true" ]; then
+  restart_policy="--restart=no"
+  attach_on_start="-a"
+  attach_on_run="-a stdout -a stderr"
+else
+  attach_on_run="-d"
+fi
 
-docker_ip=`/sbin/ifconfig | \
-                grep -B1 "inet addr" | \
-                awk '{ if ( $1 == "inet" ) { print $2 } else if ( $2 == "Link" ) { printf "%s:" ,$1 } }' | \
-                grep docker0 | \
-                awk -F: '{ print $3 }';`
+if [ -x "$(which ip 2>/dev/null)" ]; then
+  docker_ip=`ip addr show docker0 | \
+                  grep 'inet ' | \
+                  awk '{ split($2,a,"/"); print a[1] }';`
+else
+  docker_ip=`ifconfig | \
+                  grep -B1 "inet addr" | \
+                  awk '{ if ( $1 == "inet" ) { print $2 } else if ( $2 == "Link" ) { printf "%s:" ,$1 } }' | \
+                  grep docker0 | \
+                  awk -F: '{ print $3 }';`
+fi
 
 
 usage () {
-  echo "Usage: launcher COMMAND CONFIG"
+  echo "Usage: launcher COMMAND CONFIG [--skip-prereqs]"
   echo "Commands:"
   echo "    start:      Start/initialize a container"
   echo "    stop:       Stop a running container"
   echo "    restart:    Restart a container"
   echo "    destroy:    Stop and remove a container"
+  echo "    enter:      Use nsenter to enter a container"
   echo "    ssh:        Start a bash shell in a running container"
   echo "    logs:       Docker logs for container"
+  echo "    mailtest:   Test the mail settings in a container"
   echo "    bootstrap:  Bootstrap a container for the config based on a template"
+  echo "    rebuild:    Rebuild a container (destroy old, bootstrap, start new)"
+  echo
+  echo "Options:"
+  echo "    --skip-prereqs   Don't check prerequisites"
   exit 1
 }
 
+compare_version() {
+    declare -a ver_a
+    declare -a ver_b
+    IFS=. read -a ver_a <<< "$1"
+    IFS=. read -a ver_b <<< "$2"
+
+    while [[ -n $ver_a ]]; do
+        if (( ver_a > ver_b )); then
+            return 0
+        elif (( ver_b > ver_a )); then
+            return 1
+        else
+            unset ver_a[0]
+            ver_a=("${ver_a[@]}")
+            unset ver_b[0]
+            ver_b=("${ver_b[@]}")
+        fi
+    done
+    return 1  # They are equal
+}
+
 prereqs() {
 
-  # 1. running aufs
-  test=`docker info 2> /dev/null | grep 'Driver: aufs'`
-  if [[ "$test" =~ "aufs" ]] ; then : ; else
-    echo "Your Docker installation is not using aufs"
-    echo "Device mapper and other experimental drivers are unstable"
-    echo
-    echo "Please ensure your kernel is running linux extras and aufs"
-    echo "Please follow the installation guide for Docker here: http://docs.docker.io/en/latest/installation/ubuntulinux/"
+  # 1. docker daemon running?
+  test=`$docker_path info >/dev/null`
+
+  if [[ $? -ne 0 ]] ; then
+    echo "Cannot connect to the docker daemon - verify it is running and you have access"
     exit 1
   fi
 
-  # 2. running docker 0.9+
-  test=`docker --version | grep 0.9`
+  # 2. running aufs or btrfs
+  test=`$docker_path info 2> /dev/null | grep 'Driver: '`
+  if [[ "$test" =~ [aufs|btrfs] ]] ; then : ; else
+    echo "Your Docker installation is not using the recommended AuFS (union filesystem) and may be unstable."
+    echo "If you are unable to bootstrap / stop your image please report the issue at:"
+    echo "https://meta.discourse.org/t/discourse-docker-installation-without-aufs/15639"
+  fi
+
+  # 3. running recommended docker version
+  test=($($docker_path --version))  # Get docker version string
+  test=${test[2]//,/}  # Get version alone and strip comma if exists
+
+  [[ "$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
 
-  if [[ "$test" =~ "0.9" ]] ; then : ; else
-    echo "Your Docker installation is old, please upgrade to 0.9.0 or up"
+  # At least minimum version
+  if compare_version "${docker_min_version}" "${test}"; then
+    echo "ERROR: Docker version ${test} not supported, please upgrade to at least ${docker_min_version}, or recommended ${docker_rec_version}"
     exit 1
   fi
 
-  # 3. able to attach stderr / out / tty
-  test=`docker run -i --rm -a stdin -a stdout -a stderr $image echo working`
+  # Recommend best version
+  if compare_version "${docker_rec_version}" "${test}"; then
+    echo "WARNING: Docker version ${test} deprecated, recommend upgrade to ${docker_rec_version} or newer."
+  fi
+
+  # 4. able to attach stderr / out / tty
+  test=`$docker_path run -i --rm -a stdout -a stderr $image echo working`
   if [[ "$test" =~ "working" ]] ; then : ; else
     echo "Your Docker installation is not working correctly"
     echo
@@ -60,27 +122,46 @@ prereqs() {
   fi
 }
 
-prereqs
+if [ "$opt" != "--skip-prereqs" ] ; then
+  prereqs
+fi
 
 get_ssh_pub_key() {
+  local ${ssh_key_locations}
+  ssh_key_locations=(
+    ~/.ssh/id_ed25519.pub
+    ~/.ssh/id_ecdsa.pub
+    ~/.ssh/id_rsa.pub
+    ~/.ssh/id_dsa.pub
+    ~core/.ssh/authorized_keys
+  )
+
+  local $keyfile
+  for keyfile in "${ssh_key_locations[@]}"; do
+    if [[ -e ${keyfile} ]] ; then
+      ssh_pub_key="$(cat ${keyfile})"
+      return 0
+    fi
+  done
+
   if tty -s ; then
-    if [[ ! -e ~/.ssh/id_rsa.pub && ! -e ~/.ssh/id_dsa.pub ]] ; then
-      echo You have no SSH key associated to this profile
-      echo "(This will allow you ssh access into your container)"
-      read -p "Generate SSH key at ~/.ssh/id_rsa.pub? (y/N) " -n 1 -r
-      if [[ $REPLY =~ ^[Yy]$ ]] ; then
-        echo
-        echo Generating SSH key
-        mkdir -p ~/.ssh && ssh-keygen -f ~/.ssh/id_rsa -t rsa -N ''
-      else
-        echo
-        echo WARNING: You may not be able to log in to your container.
-        echo
-      fi
+    echo "This user has no SSH key, but a SSH key is required to access the Discourse Docker container."
+    read -p "Generate a SSH key? (Y/n) " -n 1 -r
+    if [[ $REPLY =~ ^[Nn]$ ]] ; then
+      echo
+      echo WARNING: You may not be able to log in to your container.
+      echo
+    else
+      echo
+      echo Generating SSH key
+      mkdir -p ~/.ssh && ssh-keygen -f ~/.ssh/id_rsa -t rsa -N ''
+      echo
+      ssh_pub_key="$(cat ~/.ssh/id_rsa.pub)"
+      return 0
     fi
   fi
 
-  ssh_pub_key="$(cat ~/.ssh/id_rsa.pub 2>/dev/null || cat ~/.ssh/id_dsa.pub)"
+  return 1
 }
 
 
@@ -102,14 +183,50 @@ install_docker() {
   exit 1
 }
 
+host_run() {
+  read -r -d '' env_ruby << 'RUBY'
+  require 'yaml'
+
+  input = STDIN.readlines.join
+  yaml = YAML.load(input)
+
+  if host_run = yaml['host_run']
+    params = yaml['params'] || {}
+    host_run.each do |run|
+      params.each do |k,v|
+        run = run.gsub("$#{k}", v)
+      end
+      STDOUT.write "#{run}--SEP--"
+    end
+  end
+RUBY
+
+  host_run=`cat $config_file | $docker_path run --rm -i -a stdout -a stdin $image ruby -e "$env_ruby"`
+
+  while [ "$host_run" ] ; do
+    iter=${host_run%%--SEP--*}
+    echo
+    echo "Host run: $iter"
+    $iter || exit 1
+    echo
+    host_run="${host_run#*--SEP--}"
+  done
+}
+
+
 set_volumes() {
-  volumes=`cat $config_file | docker run --rm -i -a stdout -a stdin $image ruby -e \
+  volumes=`cat $config_file | $docker_path run --rm -i -a stdout -a stdin $image ruby -e \
         "require 'yaml'; puts YAML.load(STDIN.readlines.join)['volumes'].map{|v| '-v ' << v['volume']['host'] << ':' << v['volume']['guest'] << ' '}.join"`
 }
 
+set_links() {
+    links=`cat $config_file | $docker_path run --rm -i -a stdout -a stdin $image ruby -e \
+        "require 'yaml'; puts YAML.load(STDIN.readlines.join)['links'].map{|l| '--link ' << l['link']['name'] << ':' << l['link']['alias'] << ' '}.join"`
+}
+
 set_template_info() {
 
-    templates=`cat $config_file | docker run --rm -i -a stdin -a stdout $image ruby -e \
+    templates=`cat $config_file | $docker_path run --rm -i -a stdin -a stdout $image ruby -e \
       "require 'yaml'; puts YAML.load(STDIN.readlines.join)['templates']"`
 
 
@@ -133,11 +250,15 @@ set_template_info() {
     require 'yaml'
 
     input=STDIN.readlines.join
-    env = {}
+    # default to UTF-8 for the dbs sake
+    env = {'LANG' => 'en_US.UTF-8'}
     input.split('_FILE_SEPERATOR_').each do |yml|
        yml.strip!
        begin
          env.merge!(YAML.load(yml)['env'] || {})
+       rescue Psych::SyntaxError => e
+        puts e
+        puts "*ERROR."
        rescue => e
         puts yml
         p e
@@ -146,13 +267,23 @@ set_template_info() {
     puts env.map{|k,v| "-e\n#{k}=#{v}" }.join("\n")
 RUBY
 
-    raw=`exec echo "$input" | docker run --rm -i -a stdin -a stdout $image ruby -e "$env_ruby"`
+    raw=`exec echo "$input" | $docker_path run --rm -i -a stdin -a stdout $image ruby -e "$env_ruby"`
 
     env=()
+    ok=1
     while read i; do
-      env[${#env[@]}]=$i
+      if [ "$i" == "*ERROR." ]; then
+        ok=0
+      elif [ -n "$i" ]; then
+        env[${#env[@]}]=$i
+      fi
     done <<< "$raw"
 
+    if [ "$ok" -ne 1 ]; then
+      echo "${env[@]}"
+      echo "YAML syntax error. Please check your /var/docker/containers/*.yml config files."
+      exit 1
+    fi
     echo "Calculated ENV: ${env[@]}"
 }
 
@@ -161,39 +292,63 @@ RUBY
 }
 
 
-[ $# -ne 2 ] && {
+[ $# -lt 2 ] && {
   usage
 }
 
 if [ ! -e $config_file ]
   then
     echo "Config file was not found, ensure $config_file exists"
+    echo ""
+    echo "Available configs ( `cd containers && ls -dm *.yml | tr -s '\n' ' ' | awk '{ gsub(/\.yml/, ""); print }'`)"
     exit 1
 fi
 
 
+docker_version=($($docker_path --version))
+docker_version=${test[2]//,/}
+
+if compare_version "1.2.0" "$docker_version"; then
+  echo "We recommend you upgrade docker, the version you are running has no restart policies, on reboot your container may not start up"
+  restart_policy=""
+else
+  restart_policy=${restart_policy:---restart=always}
+fi
+
+
+run_mailtest(){
+  if [ ! -e $config_file ]; then
+    echo "Config does not exist: $config_file" >&2
+    exit 1
+  fi
+  exec scripts/mailtest $config_file
+}
+
 run_stop(){
   if [ ! -e $cidfile ]
      then
        echo "No cid found"
        exit 1
      else
-       docker stop -t 10 `cat $cidfile`
+       $docker_path stop -t 10 `cat $cidfile`
   fi
 }
 
 run_start(){
 
+  host_run
+
   if [ ! -e $cidfile ]
      then
        echo "No cid found, creating a new container"
-       ports=`cat $config_file | docker run --rm -i -a stdout -a stdin $image ruby -e \
+       ports=`cat $config_file | $docker_path run --rm -i -a stdout -a stdin $image ruby -e \
               "require 'yaml'; puts YAML.load(STDIN.readlines.join)['expose'].map{|p| '-p ' << p.to_s << ' '}.join"`
 
        set_template_info
        set_volumes
+       set_links
 
-       existing=`docker ps -a | awk '{ print $1, $(NF) }' | grep "$config$" | awk '{ print $1 }'`
+       existing=`$docker_path ps -a | awk '{ print $1, $(NF) }' | grep "$config$" | awk '{ print $1 }'`
        if [ ! -z $existing ]
        then
          echo "Found an existing container by its name, recovering cidfile, please rerun"
@@ -201,8 +356,8 @@ run_start(){
          exit 1
        fi
 
-       docker run "${env[@]}" -h "`hostname`-$config" -e DOCKER_HOST_IP=$docker_ip --name $config -t --cidfile $cidfile $ports \
-                  -d $volumes $local_discourse/$config /usr/bin/runsvdir -P /etc/service
+       $docker_path run $links $attach_on_run $restart_policy "${env[@]}" -h "`hostname`-$config" -e DOCKER_HOST_IP=$docker_ip --name $config -t --cidfile $cidfile $ports \
+                  $volumes $local_discourse/$config /sbin/boot
 
        exit 0
      else
@@ -215,7 +370,7 @@ run_start(){
          exit 1
        fi
 
-       found=`docker ps -q -a --no-trunc | grep $cid`
+       found=`$docker_path ps -q -a --no-trunc | grep $cid`
        if [ -z $found ]
        then
          echo "Invalid cid file, deleting, please re-run"
@@ -224,62 +379,102 @@ run_start(){
        fi
 
        echo "cid found, ensuring container is started"
-       docker start `cat $cidfile`
+       $docker_path start $attach_on_start `cat $cidfile`
        exit 0
   fi
 
 }
 
-case "$command" in
-  bootstrap)
+run_bootstrap(){
 
-      get_ssh_pub_key
+  host_run
 
-      # Is the image available?
-      # If not, pull it here so the user is aware what's happening.
-      docker history $image >/dev/null 2>&1 || docker pull $image
+  get_ssh_pub_key
 
-      set_template_info
+  # Is the image available?
+  # If not, pull it here so the user is aware what's happening.
+  $docker_path history $image >/dev/null 2>&1 || $docker_path pull $image
 
-      base_image=`cat $config_file | docker run --rm -i -a stdin -a stdout $image ruby -e \
-        "require 'yaml'; puts YAML.load(STDIN.readlines.join)['base_image']"`
+  set_template_info
 
-      update_pups=`cat $config_file | docker run --rm -i -a stdin -a stdout $image ruby -e \
-        "require 'yaml'; puts YAML.load(STDIN.readlines.join)['update_pups']"`
+  base_image=`cat $config_file | $docker_path run --rm -i -a stdin -a stdout $image ruby -e \
+    "require 'yaml'; puts YAML.load(STDIN.readlines.join)['base_image']"`
 
-      if [[ ! X"" = X"$base_image" ]]; then
-        image=$base_image
-      fi
+  update_pups=`cat $config_file | $docker_path run --rm -i -a stdin -a stdout $image ruby -e \
+    "require 'yaml'; puts YAML.load(STDIN.readlines.join)['update_pups']"`
+
+  if [[ ! X"" = X"$base_image" ]]; then
+    image=$base_image
+  fi
 
-      set_volumes
+  set_volumes
+  set_links
 
-      rm -f $cidbootstrap
+  rm -f $cidbootstrap
 
-      run_command="cd /pups &&"
-      if [[ ! "false" =  $update_pups ]]; then
-        run_command="$run_command git pull &&"
-      fi
-      run_command="$run_command /pups/bin/pups --stdin"
+  run_command="cd /pups &&"
+  if [[ ! "false" =  $update_pups ]]; then
+    run_command="$run_command git pull &&"
+  fi
+  run_command="$run_command /pups/bin/pups --stdin"
 
-      echo $run_command
+  echo $run_command
 
-      env=("${env[@]}" "-e" "SSH_PUB_KEY=\"$ssh_pub_key\"")
+  env=("${env[@]}" "-e" "SSH_PUB_KEY=$ssh_pub_key")
 
-      (exec echo "$input" | docker run "${env[@]}" -e DOCKER_HOST_IP=$docker_ip --cidfile $cidbootstrap -i -a stdin -a stdout -a stderr $volumes $image \
-         /bin/bash -c "$run_command") \
-         || (docker rm `cat $cidbootstrap` && rm $cidbootstrap)
+  (exec echo "$input" | $docker_path run $links "${env[@]}" -e DOCKER_HOST_IP=$docker_ip --cidfile $cidbootstrap -i -a stdin -a stdout -a stderr $volumes $image \
+     /bin/bash -c "$run_command") \
+     || ($docker_path rm `cat $cidbootstrap` && rm $cidbootstrap)
 
-      [ ! -e $cidbootstrap ] && echo "FAILED TO BOOTSTRAP" && exit 1
+  [ ! -e $cidbootstrap ] && echo "FAILED TO BOOTSTRAP" && exit 1
 
-      sleep 5
+  sleep 5
 
-      docker commit `cat $cidbootstrap` $local_discourse/$config || echo 'FAILED TO COMMIT'
-      docker rm `cat $cidbootstrap` && rm $cidbootstrap
+  $docker_path commit `cat $cidbootstrap` $local_discourse/$config || echo 'FAILED TO COMMIT'
+  $docker_path rm `cat $cidbootstrap` && rm $cidbootstrap
+}
 
+case "$command" in
+  bootstrap)
+      run_bootstrap
       echo "Successfully bootstrapped, to startup use ./launcher start $config"
       exit 0
       ;;
 
+  mailtest)
+      run_mailtest
+      exit 0
+      ;;
+
+  enter)
+
+      if [ ! -e $cidfile ]
+         then
+           echo "No cid found"
+           exit 1
+      fi
+
+      if [ ! $UID -eq 0 ] ;
+        then
+        echo "enter command must run as root, will attempt to sudo"
+        echo
+      fi
+
+      if [ ! -e bin/nsenter ]
+        then
+          echo "Downloading nsenter"
+          $docker_path pull samsaffron/nsenter
+          ($docker_path run --rm samsaffron/nsenter cat /nsenter > bin/nsenter1) || exit 1
+          cp bin/nsenter1 bin/nsenter
+          chmod +x bin/nsenter
+      fi
+
+      PID=$($docker_path inspect --format {{.State.Pid}} `cat $cidfile`)
+      SHELL=/bin/bash sudo -E bin/nsenter --target $PID --mount --uts --ipc --net --pid
+
+      exit 0;
+      ;;
+
   ssh)
       if [ ! -e $cidfile ]
          then
@@ -287,7 +482,7 @@ case "$command" in
            exit 1
          else
            cid="`cat $cidfile`"
-           address="`docker port $cid 22`"
+           address="`$docker_path port $cid 22`"
            split=(${address//:/ })
            exec ssh -o StrictHostKeyChecking=no root@${split[0]} -p ${split[1]}
       fi
@@ -305,7 +500,7 @@ case "$command" in
            echo "No cid found"
            exit 1
          else
-           docker logs `cat $cidfile`
+           $docker_path logs `cat $cidfile`
            exit 0
       fi
       ;;
@@ -321,13 +516,35 @@ case "$command" in
       exit 0
       ;;
 
+  rebuild)
+      if [ "$(git symbolic-ref --short HEAD)" == "master" ]; then
+        echo "Updating discourse docker"
+        git pull || (echo 'failed to update' && exit 1)
+      fi
+      if [ -e $cidfile ]
+        then
+          echo "Stopping old container"
+          $docker_path stop -t 10 `cat $cidfile`
+      fi
+
+      run_bootstrap
+
+      if [ -e $cidfile ]
+        then
+          $docker_path rm `cat $cidfile` && rm $cidfile
+      fi
+
+      run_start
+      exit 0
+      ;;
+
 
   destroy)
       if [ -e $cidfile ]
         then
           echo "destroying container $cidfile"
-          docker stop -t 10 `cat $cidfile`
-          docker rm `cat $cidfile` && rm $cidfile
+          $docker_path stop -t 10 `cat $cidfile`
+          $docker_path rm `cat $cidfile` && rm $cidfile
           exit 0
         else
           echo "nothing to destroy cidfile does not exist"