#!/bin/sh
echo "Configured ENV vars"
env

if [ -n "${NO_VM}" ]; then

  shared_dir="."
  container_id=$( cat /proc/self/cgroup | grep "/docker/" | sed 's/^.*\///' | tail -n1 )
  if [ -z "${container_id}" ]; then
    container_id=$( cat /proc/self/cgroup | grep "/docker.service/" | sed 's/^.*\///' | tail -n1 )
  fi
  tmp_dir_root=$( docker inspect "${container_id}" | grep '"Source": ".*/ce-client' | sed 's/\s*\"Source\"\: \"\(.*\)\"[,]*/\1/' )
  # On first boot, copy scripts to a read-only dir
  if [ ! -d vm-manager ]; then
    tar xzf vm-manager.tar.gz
  fi

else

  unset DOCKER_TLS

  shared_dir="shared"
  tmp_dir_root="/root/ce-client"
  mkdir /root/ce-client

  cd /root

  # On first boot, copy scripts to a read-only dir
  if [ ! -d vm-manager ]; then
    tar xzf shared/vm-manager.tar.gz
  fi


  # Set system DNS server for docker pull support
  echo 'nameserver 172.17.0.1' > /etc/resolv.conf

  mkdir -p scratch/vm/ca/certs
  mkdir -p scratch/vm/ca/newcerts
  mkdir -p scratch/vm/ca/private

  cp shared/openssl.cnf scratch/vm/ca

  # Set up the local CA for certificate management
  cd scratch/vm/ca
  if [ ! -f certs/ca.cert.pem ]; then
    openssl genrsa -out private/ca.key.pem 4096
    chmod 444 private/ca.key.pem
    openssl req -config openssl.cnf -key private/ca.key.pem -new -x509 -days 365 -sha256 -extensions v3_ca -out certs/ca.cert.pem -subj "/CN=*.charityengine.com/O=Proxy/C=US/ST=Massachusetts"
    chmod 444 certs/ca.cert.pem
  fi
  cp certs/ca.cert.pem /usr/local/share/ca-certificates/
  ln -s /usr/local/share/ca-certificates/ca.cert.pem /etc/ssl/certs/
  cd /etc/ssl/certs
  ln -s ca.cert.pem `openssl x509 -noout -hash -in /usr/local/share/ca-certificates/ca.cert.pem`.0
  cp ca-certificates.crt ca-certificates.crt.backup
  cat /usr/local/share/ca-certificates/ca.cert.pem >> ca-certificates.crt

  cd /root

  # Restart the docker daemon... no containers should be running yet!
  echo "restarting docker service"
  /etc/init.d/docker stop

  # Slow down docker pull commands to prevent timeouts
  cat > /var/lib/boot2docker/profile << EOL
  EXTRA_ARGS="--max-concurrent-downloads 1"
EOL

  /etc/init.d/docker start

  sleep 5

fi

if [ ! -d start ]; then
  mkdir start
fi

if [ -n "${NO_VM}" ]; then
  # Generate a unique dir as multiple containers can run in parallel
  tmp_dir=$(mktemp -d -p /ce-client/ CE-XXXXXXXX | perl -pe 's/^\/ce-client//g')

  echo "Creating /ce-client${tmp_dir}/input"
  mkdir -p /ce-client${tmp_dir}/input
  echo "Creating /ce-client${tmp_dir}/output"
  mkdir -p /ce-client${tmp_dir}/output
else
  # No parallel containers inside a VM
  tmp_dir=""

  echo "Creating ${tmp_dir_root}${tmp_dir}/input"
  mkdir -p "${tmp_dir_root}${tmp_dir}/input"
  echo "Creating ${tmp_dir_root}${tmp_dir}/output"
  mkdir -p "${tmp_dir_root}${tmp_dir}/output"
fi

if [ -z "${NO_VM}" ]; then
  # VM runs this as root, so make tmp_dir writeable
  chmod -R +rwx ${tmp_dir_root}${tmp_dir}
fi

# Set up the docker image cache, if needed
arch=$(uname -m)
mkdir -p scratch/${arch}

echo "launching vm-manager"
if [ -n "${NO_VM}" ]; then
  echo "node main.js --data-folder /ce-client${tmp_dir}/ --job-folder ../start/ --shared-folder ../ --scratch-folder ../scratch/ --proxy false > ../vm-manager.log 2>&1 &"

  cd vm-manager
  node main.js --data-folder /ce-client${tmp_dir}/ --job-folder ../start/ --shared-folder ../ --scratch-folder ../scratch/ --proxy false > ../vm-manager.log 2>&1 &
  cd ..
else
  if [ "$(docker ps -aq -f status=exited -f name=primary)" ]; then
    # Remove the primary container if it already exists
    docker rm primary
  fi
  # Select the newest node image
  node_image=$(ls scratch/${arch}/node-alpine-*.docker 2> /dev/null | sort -r | head -n 1)
  echo "using most recent node image: ${node_image}"
  docker load < ${node_image}
  docker run -td -p 172.17.0.1:53:53/udp -p 172.17.0.1:80:3646 -p 172.17.0.1:443:3647 -v /root/scratch:/root/scratch -v /root/shared:/root/shared -v /root/vm-manager:/root/scripts:ro -v ${tmp_dir_root}${tmp_dir}:/local -v /root/start:/root/start --name='primary' node:alpine
  docker exec -d primary sh -c 'cd /root/scripts; node main.js > /root/shared/main.js.log 2>&1'
fi

#
# Monitor the "start" directory for new containers to run
#

# Set args that will be passed to every container that is run
docker_args="-v ${tmp_dir_root}${tmp_dir}:/local"
if [ -n "${USE_NVIDIA}" ]; then
  # Settings for using NVIDIA GPUs
  is_old_docker=$( docker run --gpus 2>&1 | grep "unknown flag" )
  gpu_list="all"
  if [ -n "${NVIDIA_VISIBLE_DEVICES}" ]; then
    gpu_list="\"device=${NVIDIA_VISIBLE_DEVICES}\""
  fi
  docker_args="${docker_args} --gpus ${gpu_list}"
elif [ -n "${USE_AMD}" ]; then
  # Placeholder for AMD setup
  echo "AMD not yet supported"
else
  if [ -z "${NO_VM}" ]; then
    # If we are in a VM, set network details
  docker_args="${docker_args} --dns=172.17.0.1 -v /etc/ssl/certs:/etc/ssl/certs:ro -v /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro"
  fi
fi
# Check the start directory every "sleeptime" seconds
sleeptime=5
# The config file that contains runtime for the VM
config_file="${shared_dir}/state.default.json"

# Attempt to read from config the total time (seconds) to continue looping
runtime=$(sed -nE 's/.*"timeLimit":([0-9]*).*/\1/p' ${config_file})
if [ "${runtime}" = "" ]; then
  runtime=7200
fi

# Load the CE_JOB_COPY option from config
ce_job_copy=$(sed -nE 's/.*"copy":\s?"?([0-9]*)"?.*/\1/p' ${config_file})
docker_args="${docker_args} -e CE_JOB_COPY=${ce_job_copy}"

# Load the CE_JOB_TAG data from config
ce_job_tag=$(sed -nE 's/.*"tag":\s?"([^"]*)".*/\1/p' ${config_file})
docker_args="${docker_args} -e CE_JOB_TAG=${ce_job_tag}"

# If an image fails to load, stop execution of the script
image_load_error() {
  set -e
  bad_image=$1
  echo "Failed to load image, removing ${bad_image}"
  rm $bad_image
  exit 1
}

timeout=$((runtime/sleeptime))
echo "ready - checking start directory every ${sleeptime} seconds for new jobs"
while [ ${timeout} -gt 0 ]
do
  if [ -f completion_trigger_file ]; then
    echo "vm-manager exited"
    rm completion_trigger_file
    break
  fi

  sleep "${sleeptime}"
  for file in start/*; do
    container=""
    url=""
    imagefile=""
    command=""
    name=""
    image_exists=""
    exists_now=""
    [ -e ${file} ] || continue
    echo "new start file found: ${file}"
    while read -r line || [ -n "${line}" ]; do
      if [ "${container}" = "" ]; then
        container=$( echo "${line}" | cut -d " " -f 1 )
        url=$( echo "${line}" | cut -s -d " " -f 2 )
        imagefile=$( echo "${container}" | sed -r 's/\:/\-/g' | sed -r 's/\//__/g' ).docker
        image_exists=$( test -f "scratch/${arch}/${imagefile}" && echo "true" || echo "" )
        if [ "${url}" != "" ]; then
          # Use curl to determine if a file needs to be downloaded
          if [ "${image_exists}" = "" ]; then
            echo "downloading ${container} from ${url} and loading into docker..."
            curl --silent -k -L -o "scratch/${arch}/${imagefile}" "${url}"
            exists_now=$( test -f scratch/${arch}/${imagefile} && echo "true" || echo "" )
          else
            local_mod_time=$(date -r "scratch/${arch}/${imagefile}" +%s)
            last_modified_header=$( curl --silent -k -L -I ${url} | grep -i Last-Modified )
            remote_mod_time=$(date -D "%a, %d %b %Y %H:%M:%S %Z" -d "$( echo ${last_modified_header} | sed -r 's/Last\-Modified: //gi')" +%s)
            time_diff=$((${remote_mod_time} - ${local_mod_time}))
            if [ "${last_modified_header}" = "" ]; then
              echo "no Last-Modified header data could be parsed from ${url}; falling back to local cached image"
            elif [ "${time_diff}" -gt 0 ]; then
              # Even a 404 response can lead here if Last-Modified is returned
              echo "updating ${container} from ${url} and loading into docker..."
              curl --silent -k -L -o "scratch/${arch}/${imagefile}" "${url}"
            else
              echo "${container} is up-to-date; loading cached image into docker..."
            fi
          fi
          container=$( docker load < "scratch/${arch}/${imagefile}" )
           if [ $? -ne 0 ]; then
             image_load_error "scratch/${arch}/${imagefile}"
           fi
           container=$( echo "${container}" | cut -d " " -f 3 )
           exists_now=$( docker images --quiet "${container}" )
        else
          # Retrieve from dockerhub, if missing
          if [ "${image_exists}" = "" ]; then
            echo "image not found locally"
            echo "pulling ${container} from dockerhub..."
            docker pull "${container}"
            exists_now=$( docker images --quiet "${container}" )
            if [ "${exists_now}" != "" ]; then
              echo "adding ${container} to persistent local docker image cache"
              docker save -o "scratch/${arch}/${imagefile}" "${container}"
            else
              echo "failed to load ${container}!"
            fi
          else
            image_loaded=$( docker images -q "${container}" 2> /dev/null )
            if [ "${image_loaded}" = "" ]; then
              docker load < "scratch/${arch}/${imagefile}"
            fi
          fi
        fi
      elif [ "${command}" = "" ]; then
        command="${line}"
      fi
    done < "${file}"
    if [ "${container}" != "" ]; then
      if [ "${image_exists}" != "" ] || [ "${exists_now}" != "" ]; then
        name=${file##start\/}
        logname=${name}
        random=$( cat /dev/urandom | tr -cd 'a-f0-9' | head -c 8 )
        name="${name}_${random}"
        # Determine how to launch the container based on Entrypoint and
        # whether a command has been specified in the start file
        if [ "${command}" != "" ]; then
          # Get the Entrypoint from the Config section, not ContainerConfig
          image_detail_config=$( docker image inspect ${container} | grep -A 1000 '"Config"' | grep -B 1000 '"ContainerConfig"' )
          if [ "$image_detail_config" = "" ]; then
            image_detail_config=$( docker image inspect ${container} | grep -A 1000 '"Config"' )
          fi
          # Parse out the Entrypoint, which may be in simple string or array format
          image_entrypoint=$( echo ${image_detail_config} | sed -nE 's/.*\"Entrypoint\": (\[ \"[^]]*\" ]|\"[^"]*\"|[^,]*).*/\1/p'  | sed -E 's/\", \"/ /g' | sed -E 's/(\[ | \])//g' | tr -d '"' )
          if [ "$image_entrypoint" != "" ] && ! echo "$image_entrypoint" | grep -q "null"; then
            if [ "${image_entrypoint}" = "/bin/bash" ] || [ "${image_entrypoint}" = "/bin/sh" ]; then
              # Entrypoint is set, but just to sh or bash, so wrap the command
              echo "launching image: docker run --rm ${docker_args} --name='${name}' ${container} ${image_entrypoint} -c \"${command}\""
              docker run --rm ${docker_args} --name=${name} ${container} ${image_entrypoint} -c "${command}" >> ${shared_dir}/${logname}.log 2>&1
            else
              # Entrypoint is set, so pass the given command directly
              echo "launching image: docker run --rm ${docker_args} --name='${name}' ${container} \"${command}\""
              docker run --rm ${docker_args} --name=${name} ${container} "${command}" >> ${shared_dir}/${logname}.log 2>&1
            fi
          else
            # Launch the container to run the given command and wrap the
            # command in a /bin/sh call to prevent character escaping issues
            echo "launching image: docker run --rm ${docker_args} --name='${name}' ${container} /bin/sh -c \"${command}\""
            docker run --rm ${docker_args} --name=${name} ${container} /bin/sh -c "${command}" >> ${shared_dir}/${logname}.log 2>&1
          fi
        else
          # Launch the container without a specified command; this will use
          # the default ENTRYPOINT to run the default CMD, if configured
          echo "launching image: docker run --rm ${docker_args} --name='${name}' ${container}"
          docker run --rm ${docker_args} --name=${name} ${container} >> ${shared_dir}/${logname}.log 2>&1
        fi
        rm "${file}"
      else
        echo "no container found to run; retrying ${file}"
      fi
    else
      echo "could not parse container name; aborting ${file}"
      rm "${file}"
    fi
  done
  timeout=$((timeout-1))
done
