#!/bin/bash

# Set error code to $? if any part of a piped command fails
set -o pipefail

echo "Configured ENV vars"
env

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

  shared_dir="."
  container_id=$( cat /etc/hostname )
  tmp_dir_root="."
  if [ -n "${container_id}" ]; then
    tmp_dir_root=$( docker inspect "${container_id}" | grep '"Source": ".*/ce-client' | sed 's/\s*\"Source\"\: \"\(.*\)\"[,]*/\1/' )
  fi
  # 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
    echo 'removing old CA certificate'
    chmod u+w certs/ca.cert.pem private/ca.key.pem
    rm -f certs/*.pem
  fi
  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
  chmod 777 /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"
  chmod 777 "${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 ../ --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 -p 172.17.0.1:3333:3333 -p 172.17.0.1:16286:16286 -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 -v /etc/localtime:/etc/localtime:ro -v /etc/timezone:/etc/timezone:ro --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"

# The config file that contains runtime for the VM
config_file="${shared_dir}/state.default.json"

# Load the environment variable option from config
# This looks for the "env":{} object, which can be multiple lines, and parses
# out each comma delimited pair, converts key:value to key=value, trims spaces
# and quotes, and prepends a "-e" for use with docker
env_vars=$(awk '/"env":/,/}/' ${config_file} | tr -d '\r\n' | sed -nE 's/.*"env":\s?\{\s?([^}]*)\}.*/\1/p' | tr ':' '=' | tr -d '[[:space:]]' | tr -d '"' | sed 's/,/ -e /g')
docker_args="${docker_args} -e ${env_vars}"

# Limit RAM and CPU
if [ -n "${CE_MEMORY_LIMIT}" ]; then
  docker_args="${docker_args} --memory=${CE_MEMORY_LIMIT}m"
fi
if [ -n "${CE_CPU_LIMIT}" ]; then
  docker_args="${docker_args} --cpus=${CE_CPU_LIMIT}"
fi
if [ -n "${USE_NVIDIA}" ]; then
  # Settings for using NVIDIA GPUs
  is_old_docker=$( docker run --gpus 2>&1 | grep "unknown flag" )
  gpu_list="all"

  NVIDIA_VISIBLE_DEVICES=$(sed -n 's/.*"NVIDIA_VISIBLE_DEVICES":"\([^"]*\).*/\1/p' < "${config_file}")
  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

# 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

# The passcode to use for encrypting / decrypting containers
passcode="c3N4t10nb8"

# Add additional SSL-related docker args, to ensure python scripts use the
# correct certificate authority
docker_args="${docker_args} -e REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt -e SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt"

# Save a loaded docker image to cache
save_image_to_cache() {
  if [ "$1" != "" ]; then
    curl --silent -k -L "$1" | openssl enc -aes-256-cbc -md sha256 -k "${passcode}" > "scratch/${arch}/${imagefile}"
  else
    echo "saving ${container} to cache (${arch}/${imagefile})"
    docker save "${container}" | openssl enc -aes-256-cbc -md sha256 -k "${passcode}" > "scratch/${arch}/${imagefile}"
  fi
}

# Load a cached docker image from cache
load_image_from_cache() {
  openssl enc -d -aes-256-cbc -md sha256 -k "${passcode}" -in "scratch/${arch}/${imagefile}" | docker load
  # Reset timestamp so that ce11 cleaner does not remove it
  touch "scratch/${arch}/${imagefile}"
}

# Check for an updated version of the image on docker
update_image_in_cache() {
  echo "checking for updates to cached ${container} image"
  result=$( docker pull "${container}" )
  image_updated=$( echo "${result}" | grep '^Status: Downloaded newer image' && echo "true" || echo "" )
  image_same=$( echo "${result}" | grep 'Image is up to date' && echo "true" || echo "" )
  if [ "${image_updated}" ]; then
    echo "updated image found"
    save_image_to_cache
  elif [ "${image_same}" ]; then
    echo "image is up-to-date"
  else
    echo "error in container update check for ${container}; cleaning up"
    docker image prune -af
    image_exists=""
  fi
}

# 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))
failure_count=0
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
  if [ -z "${NO_VM}" ]; then
    # If in a VM, check that the manager container has not exited
    if [ ! "$(docker ps -aq -f status=running -f name=primary)" ]; then
      echo "manager container has exited"
      break
    fi
  fi
  if [ "${failure_count}" -gt 5 ]; then
    echo "too many failed download attempts"
    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 "" )
        image_exists_enc=$( test -f "scratch/${arch}/${imagefile}.enc" && echo "true" || echo "" )
        if [ "${image_exists_enc}" ]; then
          # Check file size - may have been expired by setting the contents to "deleted"
          filesize=$(wc -c < "scratch/${arch}/${imagefile}.enc")
          if [ "$filesize" -ge 8 ]; then
            image_exists="true"
            imagefile="${imagefile}.enc"
          else
            image_exists=""
            image_exists_enc=""
          fi
        fi
        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..."
            imagefile="${imagefile}.enc"
            save_image_to_cache "${url}"
            image_exists_enc=$( 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..."
              if [ "${image_exists_enc}" = "" ]; then
                # Existing image was not encrypted, but from now on it will be
                rm "scratch/${arch}/${imagefile}"
                imagefile="${imagefile}.enc"
              fi
              save_image_to_cache "${url}"
              image_exists_enc=$( test -f scratch/${arch}/${imagefile} && echo "true" || echo "" )
            else
              echo "${container} is up-to-date; loading cached image into docker..."
            fi
          fi
          if [ "${image_exists_enc}" ]; then
            container=$( load_image_from_cache )
            if [ "${container}" = "" ]; then
              image_load_error "scratch/${arch}/${imagefile}"
            fi
          else
            container=$( docker load < "scratch/${arch}/${imagefile}" )
          fi
          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
              imagefile="${imagefile}.enc"
              save_image_to_cache
              if [ $? -ne 0 ]; then
                rm scratch/${arch}/${imagefile}
                echo "failed to save ${container}"
                exists_now=""
              else
                image_exists_enc=$( test -f scratch/${arch}/${imagefile} && echo "true" || echo "" )
              fi
            else
              echo "failed to load ${container}!"
            fi
          else
            image_loaded=$( docker images -q "${container}" 2> /dev/null )
            if [ "${image_loaded}" = "" ]; then
              if [ "${image_exists_enc}" ]; then
                echo "loading ${container} from cache (${arch}/${imagefile})"
                load_image_from_cache
                update_image_in_cache
              else
                docker load < "scratch/${arch}/${imagefile}"
              fi
            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
        echo $? > "${shared_dir}/${logname}.exit"
        rm "${file}"
      else
        failure_count=$(( ${failure_count} + 1 ))
        echo "no container found to run; retrying ${file} (failures: ${failure_count})"
      fi
    else
      echo "could not parse container name; aborting ${file}"
      rm "${file}"
    fi
  done
  timeout=$((timeout-1))
done
