#!/bin/bash
#
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License.  You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied.  See the License for the
# specific language governing permissions and limitations
# under the License.
#

# A script that does rolling upgrade of Pinot components
# from one version to the other given 2 commit hashes. It first builds
# Pinot in the 2 given directories and then upgrades in the following order:
# Controller -> Broker -> Server
#
# TODO Some ideas to explore:
#  It will be nice to have the script take arguments about what is to be done.
#  For example, we may want to verify the upgrade path in a different order.
#  Better yet, test all orders to decide that the upgrade can be done in any order.
#  Or, we may want to test upgrade of a specific component only.
#
#  For now, this script runs specific yaml files as a part of testing between
#  component upgrades/rollbacks. Perhaps we can change it to take a directory name
#  and run all the scripts in the directory in alpha order, one script at each
#  "stage" of upgrade.
#
#  We may modify to choose a minimal run in which the same set of operations are run
#  between any two component upgrades/rollbacks -- this may consist of adding
#  one more segment to table, adding some more rows to the stream topic, and
#  running some queries with the new data.

RM="/bin/rm"
logCount=1
#Declare the number of mandatory args
cmdName=`basename $0`
source `dirname $0`/utils.inc

function cleanupControllerDirs() {
  local dirName=$(grep -F controller.data.dir "${CONTROLLER_CONF}" | awk '{print $3}')
  if [ -n "$dirName" ]; then
    ${RM} -rf "${dirName}"
  fi
}

function cleanupServerDirs() {
  local dirName=$(grep -F pinot.server.instance.dataDir "${SERVER_CONF}" | awk '{print $3}')
  if [ -n "$dirName" ]; then
    ${RM} -rf "${dirName}"
  fi
  dirName=$(grep -F pinot.server.instance.segmentTarDir "${SERVER_CONF}" | awk '{print $3}')
  if [ -n "$dirName" ]; then
    ${RM} -rf "${dirName}"
  fi

  # Cleanup directories for server 2 if a second server is configured
  if [ -f "${SERVER_CONF_2}" ]; then
    local dirName=$(grep -F pinot.server.instance.dataDir "${SERVER_CONF_2}" | awk '{print $3}')
      if [ -n "$dirName" ]; then
        ${RM} -rf "${dirName}"
      fi
      dirName=$(grep -F pinot.server.instance.segmentTarDir "${SERVER_CONF_2}" | awk '{print $3}')
      if [ -n "$dirName" ]; then
        ${RM} -rf "${dirName}"
      fi
  fi
}

# get usage of the script
function usage() {
  echo "Usage: $cmdName -w <workingDir> -t <testSuiteDir> [-k]"
  echo -e "MANDATORY:"
  echo -e "  -w, --working-dir                      Working directory where olderCommit and newCommit target files reside."
  echo -e "  -t, --test-suite-dir                   Test suite directory\n"
  echo -e "OPTIONAL:"
  echo -e "  -k, --keep-cluster-on-failure          Keep cluster on test failure"
  echo -e "  -h, --help                             Prints this help\n"
}

function waitForZkReady() {
  status=1
  while [ $status -ne 0 ]; do
    sleep 1
    echo "Checking port ${ZK_PORT} for zk ready"
    echo x | nc localhost ${ZK_PORT} 1>/dev/null 2>&1
    status=$?
  done
}

function waitForControllerReady() {
  status=1
  while [ $status -ne 0 ]; do
    sleep 1
    echo "Checking port ${CONTROLLER_PORT} for controller ready"
    curl localhost:${CONTROLLER_PORT}/health 1>/dev/null 2>&1
    status=$?
  done
}

function waitForKafkaReady() {
  status=1
  while [ $status -ne 0 ]; do
    sleep 1
    echo "Checking port 19092 for kafka ready"
    echo x | nc localhost 19092 1>/dev/null 2>&1
    status=$?
  done
}

function waitForBrokerReady() {
  local status=1
  while [ $status -ne 0 ]; do
    sleep 1
    echo "Checking port ${BROKER_QUERY_PORT} for broker ready"
    curl localhost:${BROKER_QUERY_PORT}/debug/routingTable 1>/dev/null 2>&1
    status=$?
  done
}

function waitForServerReady() {
  local status=1
  while [ $status -ne 0 ]; do
    sleep 1
    echo "Checking port ${SERVER_ADMIN_PORT} for server ready"
    curl localhost:${SERVER_ADMIN_PORT}/health 1>/dev/null 2>&1
    status=$?
  done
}

function waitForServer2Ready() {
  local status=1
  while [ $status -ne 0 ]; do
    sleep 1
    echo "Checking port ${SERVER_2_ADMIN_PORT} for server ready"
    curl localhost:${SERVER_2_ADMIN_PORT}/health 1>/dev/null 2>&1
    status=$?
  done
}

function waitForClusterReady() {
  waitForBrokerReady
  waitForServerReady

  # Check second server if configured
  if [ -f "${SERVER_CONF_2}" ]; then
      waitForServer2Ready
  fi

  waitForKafkaReady
}

#set config file is present or not
function setConfigFileArg() {
  if [[ -f $1 ]]; then
    echo "-configFileName ${1}"
  fi
}

# Given a component and directory, start that version of the specific component
# Start the service in background.
# Record the pid file in $2/$1.pid
# $1 is service name
# $2 is directory name
# TODO get rid of exit from this function. Exit only returns from a function.
function startService() {
  serviceName=$1
  dirName=$2
  echo Starting $serviceName in $dirName
  local configFileArg=$(setConfigFileArg "$3")
  # Upon start, save the pid of the process for a component into a file in /working_dir/{component}.pid, which is then used to stop it
  pushd "$dirName"/pinot-tools/target/pinot-tools-pkg/bin 1>/dev/null || exit 1
  if [ "$serviceName" = "zookeeper" ]; then
    # Remove all previous zk data
    ${RM} -rf ${dirName}/zkdir
    ./pinot-admin.sh StartZookeeper -dataDir ${LOG_DIR}/zkdir 1>${LOG_DIR}/zookeeper.${logCount}.log 2>&1 &
    echo $! >${PID_DIR}/zookeeper.pid
  elif [ "$serviceName" = "controller" ]; then
    ./pinot-admin.sh StartController ${configFileArg} 1>${LOG_DIR}/controller.${logCount}.log 2>&1 &
    echo $! >${PID_DIR}/controller.pid
  elif [ "$serviceName" = "broker" ]; then
    ./pinot-admin.sh StartBroker ${configFileArg} 1>${LOG_DIR}/broker.${logCount}.log 2>&1 &
    echo $! >${PID_DIR}/broker.pid
  elif [ "$serviceName" = "server" ]; then
    ./pinot-admin.sh StartServer ${configFileArg} 1>${LOG_DIR}/server.${logCount}.log 2>&1 &
    echo $! >${PID_DIR}/server.pid
  elif [ "$serviceName" = "server2" ]; then
    ./pinot-admin.sh StartServer ${configFileArg} 1>${LOG_DIR}/server2.${logCount}.log 2>&1 &
        echo $! >${PID_DIR}/server2.pid
  elif [ "$serviceName" = "kafka" ]; then
    ./pinot-admin.sh StartKafka -zkAddress localhost:${ZK_PORT}/kafka 1>${LOG_DIR}/kafka.${logCount}.log 2>&1 &
    echo $! >${PID_DIR}/kafka.pid
  fi
  # Keep log files distinct so we can debug
  logCount=$((logCount + 1))

  echo "${serviceName} started"
  popd 1>/dev/null || exit 1
}

# Given a component, check if it known to be running and stop that specific component
function stopService() {
  serviceName=$1
  if [ -f "${PID_DIR}/${serviceName}".pid ]; then
    pid=$(cat "${PID_DIR}/${serviceName}".pid)
    kill -9 $pid 1>/dev/null 2>&1
    # TODO Kill without -9 and add a while loop waiting for process to die
    status=0
    while [ $status -ne 1 ]; do
      echo "Waiting for $serviceName (pid $pid) to die"
      sleep 1
      ps -p $pid
      status=$(echo $?)
    done
    ${RM} -f "${PID_DIR}/${serviceName}".pid
    echo "${serviceName} stopped"
  else
    echo "Pid file ${PID_DIR}/${serviceName}.pid  not found. Failed to stop component ${serviceName}"
  fi
}

# Starts a Pinot cluster given a specific target directory
function startServices() {
  dirName=$1
  startService zookeeper "$dirName" "unused"
  # Controller depends on zookeeper, if not wait zookeeper to be ready, controller will crash.
  waitForZkReady
  startService controller "$dirName" "$CONTROLLER_CONF"
  # Broker depends on controller, if not wait controller to be ready, broker will crash.
  waitForControllerReady
  startService broker "$dirName" "$BROKER_CONF"
  startService server "$dirName" "$SERVER_CONF"

  # Start second server if configured
  if [ -f "${SERVER_CONF_2}" ]; then
    startService server2 "$dirName" "$SERVER_CONF_2"
  fi

  startService kafka "$dirName" "unused"
  echo "Cluster started."
  waitForClusterReady
}

# Stops the currently running Pinot cluster
function stopServices() {
  stopService controller
  stopService broker
  stopService server

  # Stop second server if configured
  if [ -f "${SERVER_CONF_2}" ]; then
    stopService server2
  fi

  stopService zookeeper
  stopService kafka
  echo "Controller logs:"
  cat ${LOG_DIR}/controller.*.log
  echo "Broker logs:"
  cat ${LOG_DIR}/broker.*.log
  echo "Server logs:"
  cat ${LOG_DIR}/server.*.log
  echo "Cluster stopped."
}

# Setup the path and classpath prefix for compatibility tester executable
function setupCompatTester() {
  COMPAT_TESTER="$(dirname $0)/../${COMPAT_TESTER_PATH}"
  local pinotCompatibilityVerifierRelDir="$(dirname $0)/../pinot-compatibility-verifier/target"
  local pinotCompatibilityVerifierAbsDir=$( (
    cd ${pinotCompatibilityVerifierRelDir}
    pwd
  ))
  JAR_LIST="$(ls ${pinotCompatibilityVerifierAbsDir}/pinot-compatibility-verifier-*.jar)"
  CLASSPATH_PREFIX="$(echo $JAR_LIST | tr ' ' :)"
  echo "CLASSPATH_PREFIX is set as: $CLASSPATH_PREFIX"
  export CLASSPATH_PREFIX
}

function setupControllerVariables() {
  if [ -f ${CONTROLLER_CONF} ]; then
    local port=$(grep -F controller.port ${CONTROLLER_CONF} | awk '{print $3}')
    if [ ! -z "$port" ]; then
      CONTROLLER_PORT=$port
    fi
  fi
}

function setupBrokerVariables() {
  if [ -f ${BROKER_CONF} ]; then
    local port=$(grep -F pinot.broker.client.queryPort ${BROKER_CONF} | awk '{print $3}')
    if [ -n "$port" ]; then
      BROKER_QUERY_PORT=$port
    fi
  fi
}

function setupServerVariables() {
  if [ -f "${SERVER_CONF}" ]; then
    local port
    port=$(grep -F pinot.server.adminapi.port "${SERVER_CONF}" | awk '{print $3}')
    if [ -n "$port" ]; then
      SERVER_ADMIN_PORT=$port
    fi
    port=$(grep -F pinot.server.netty.port "${SERVER_CONF}" | awk '{print $3}')
    if [ -n "$port" ]; then
      SERVER_NETTY_PORT=$port
    fi
    port=$(grep -F pinot.server.grpc.port "${SERVER_CONF}" | awk '{print $3}')
    if [ -n "$port" ]; then
      SERVER_GRPC_PORT=$port
    fi
  fi

  if [ -f "${SERVER_CONF_2}" ]; then
    local port
    port=$(grep -F pinot.server.adminapi.port "${SERVER_CONF_2}" | awk '{print $3}')
    if [ -n "$port" ]; then
      SERVER_2_ADMIN_PORT=$port
    fi
    port=$(grep -F pinot.server.netty.port "${SERVER_CONF_2}" | awk '{print $3}')
    if [ -n "$port" ]; then
      SERVER_2_NETTY_PORT=$port
    fi
    port=$(grep -F pinot.server.grpc.port "${SERVER_CONF_2}" | awk '{print $3}')
    if [ -n "$port" ]; then
      SERVER_2_GRPC_PORT=$port
    fi
  fi
}

function checkPortAvailable() {
  if lsof -t -i:"$1" -s TCP:LISTEN; then
    echo "Port number $1 not available. Check any existing process that may be using this port."
    return 1
  fi
}

#
# Main
#

# create subdirectories for given commits
workingDir=
testSuiteDir=
keepClusterOnFailure="false"

# Args while-loop
while [ "$1" != "" ]; do
  case $1 in
  -w | --working-dir)
    shift
    workingDir=$(absPath $1)
    ;;
  -t | --test-suite-dir)
    shift
    testSuiteDir=$(absPath $1)
    ;;
  -k | keep-cluster-on-failure)
    keepClusterOnFailure="true"
    ;;
  -h | --help)
    usage
    exit 0
    ;;
  *)
    echo "illegal option $1"
    usage
    exit 1 # error
    ;;
  esac
  shift
done

if [ -z "$workingDir" -o -z "$testSuiteDir" ]; then
  usage
  exit 1
fi

COMPAT_TESTER_PATH="pinot-compatibility-verifier/target/pinot-compatibility-verifier-pkg/bin/pinot-compat-test-runner.sh"

BROKER_CONF=${testSuiteDir}/config/BrokerConfig.properties
CONTROLLER_CONF=${testSuiteDir}/config/ControllerConfig.properties
SERVER_CONF=${testSuiteDir}/config/ServerConfig.properties
SERVER_CONF_2=${testSuiteDir}/config/ServerConfig2.properties

cleanupControllerDirs
cleanupServerDirs

BROKER_QUERY_PORT=8099
ZK_PORT=2181
CONTROLLER_PORT=9000
SERVER_ADMIN_PORT=8097
SERVER_2_ADMIN_PORT=9097
SERVER_NETTY_PORT=8098
SERVER_2_NETTY_PORT=9098
SERVER_GRPC_PORT=8090
SERVER_2_GRPC_PORT=9090

PID_DIR=${workingDir}/pids
LOG_DIR=${workingDir}/logs
${RM} -rf ${PID_DIR}
${RM} -rf ${LOG_DIR}

setupControllerVariables
setupBrokerVariables
setupServerVariables

export JAVA_OPTS="-DControllerPort=${CONTROLLER_PORT} -DBrokerQueryPort=${BROKER_QUERY_PORT} -DServerAdminPort=${SERVER_ADMIN_PORT}"

mkdir ${PID_DIR}
mkdir ${LOG_DIR}

oldTargetDir="$workingDir"/oldTargetDir
newTargetDir="$workingDir"/newTargetDir

setupCompatTester

# check that the default ports are open
if ! checkPortAvailable ${SERVER_ADMIN_PORT} || ! checkPortAvailable ${SERVER_NETTY_PORT} || ! checkPortAvailable ${SERVER_GRPC_PORT} ||
  ! checkPortAvailable ${BROKER_QUERY_PORT} || ! checkPortAvailable ${CONTROLLER_PORT} || ! checkPortAvailable ${ZK_PORT} ||
  { [ -f "${SERVER_CONF_2}" ] && { ! checkPortAvailable ${SERVER_2_ADMIN_PORT} || ! checkPortAvailable ${SERVER_2_NETTY_PORT} || ! checkPortAvailable ${SERVER_2_GRPC_PORT}; } ; }; then
  exit 1
fi

# Setup initial cluster with olderCommit and do rolling upgrade
# Provide abspath of filepath to $COMPAT_TESTER
echo "Setting up cluster before upgrade"
startServices "$oldTargetDir"

genNum=0
if [ -f $testSuiteDir/pre-controller-upgrade.yaml ]; then
  genNum=$((genNum+1))
  $COMPAT_TESTER $testSuiteDir/pre-controller-upgrade.yaml $genNum
  if [ $? -ne 0 ]; then
    if [ $keepClusterOnFailure == "false" ]; then
      stopServices
    fi
    echo "Failed before controller upgrade"
    exit 1
  fi
fi

echo "Upgrading controller"
stopService controller
startService controller "$newTargetDir" "$CONTROLLER_CONF"
waitForControllerReady

if [ -f $testSuiteDir/pre-broker-upgrade.yaml ]; then
  genNum=$((genNum+1))
  echo "Running tests after controller upgrade"
  $COMPAT_TESTER $testSuiteDir/pre-broker-upgrade.yaml $genNum
  if [ $? -ne 0 ]; then
    if [ $keepClusterOnFailure == "false" ]; then
      stopServices
    fi
    echo "Failed before broker upgrade"
    exit 1
  fi
fi

echo "Upgrading broker"
stopService broker
startService broker "$newTargetDir" "$BROKER_CONF"
waitForBrokerReady

if [ -f $testSuiteDir/pre-server-upgrade.yaml ]; then
  echo "Running tests after broker upgrade"
  genNum=$((genNum+1))
  $COMPAT_TESTER $testSuiteDir/pre-server-upgrade.yaml $genNum
  if [ $? -ne 0 ]; then
    if [ $keepClusterOnFailure == "false" ]; then
      stopServices
    fi
    echo "Failed before server upgrade"
    exit 1
  fi
fi

echo "Upgrading server"
stopService server
startService server "$newTargetDir" "$SERVER_CONF"
waitForServerReady

if [ -f $testSuiteDir/post-server-upgrade.yaml ]; then
  echo "Running tests after server upgrade"
  genNum=$((genNum+1))
  $COMPAT_TESTER $testSuiteDir/post-server-upgrade.yaml $genNum
  if [ $? -ne 0 ]; then
    if [ $keepClusterOnFailure == "false" ]; then
      stopServices
    fi
    echo "Failed after server upgrade"
    exit 1
  fi
fi

if [ -f "${SERVER_CONF_2}" ]; then
  echo "Upgrading server 2"
  stopService server2
  startService server2 "$newTargetDir" "$SERVER_CONF_2"
  waitForServer2Ready

  if [ -f $testSuiteDir/post-server-2-upgrade.yaml ]; then
    echo "Running tests after server 2 upgrade"
    genNum=$((genNum+1))
    $COMPAT_TESTER $testSuiteDir/post-server-2-upgrade.yaml $genNum
    if [ $? -ne 0 ]; then
      if [ $keepClusterOnFailure == "false" ]; then
        stopServices
      fi
      echo "Failed after server 2 upgrade"
      exit 1
    fi
  fi

  echo "Downgrading server 2"
  # Upgrade completed, now do a rollback
  stopService server2
  startService server2 "$oldTargetDir" "$SERVER_CONF_2"
  waitForServer2Ready

  if [ -f $testSuiteDir/post-server-2-rollback.yaml ]; then
    echo "Running tests after server 2 downgrade"
    genNum=$((genNum+1))
    $COMPAT_TESTER $testSuiteDir/post-server-2-rollback.yaml $genNum
    if [ $? -ne 0 ]; then
      if [ $keepClusterOnFailure == "false" ]; then
        stopServices
      fi
      echo "Failed after server 2 downgrade"
      exit 1
    fi
  fi
fi

echo "Downgrading server"
# Upgrade completed, now do a rollback
stopService server
startService server "$oldTargetDir" "$SERVER_CONF"
waitForServerReady

if [ -f $testSuiteDir/post-server-rollback.yaml ]; then
  echo "Running tests after server downgrade"
  genNum=$((genNum+1))
  $COMPAT_TESTER $testSuiteDir/post-server-rollback.yaml $genNum
  if [ $? -ne 0 ]; then
    if [ $keepClusterOnFailure == "false" ]; then
      stopServices
    fi
    echo "Failed after server downgrade"
    exit 1
  fi
fi

echo "Downgrading broker"
stopService broker
startService broker "$oldTargetDir" "$BROKER_CONF"
waitForBrokerReady

if [ -f $testSuiteDir/post-broker-rollback.yaml ]; then
  echo "Running tests after broker downgrade"
  genNum=$((genNum+1))
  $COMPAT_TESTER $testSuiteDir/post-broker-rollback.yaml $genNum
  if [ $? -ne 0 ]; then
    if [ $keepClusterOnFailure == "false" ]; then
      stopServices
    fi
    echo "Failed after broker downgrade"
    exit 1
  fi
fi

echo "Downgrading controller"
stopService controller
startService controller "$oldTargetDir" "$CONTROLLER_CONF"
waitForControllerReady
waitForControllerReady

if [ -f $testSuiteDir/post-controller-rollback.yaml ]; then
  echo "Running tests after controller downgrade"
  genNum=$((genNum+1))
  $COMPAT_TESTER $testSuiteDir/post-controller-rollback.yaml $genNum
  if [ $? -ne 0 ]; then
    if [ $keepClusterOnFailure == "false" ]; then
      stopServices
    fi
    echo "Failed after controller downgrade"
    exit 1
  fi
fi

stopServices

echo "All tests passed"
exit 0
