#!/bin/bash

# Copyright 2021 The KubeEdge Authors.
#
# Licensed 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.

# Developers can run `hack/local-up.sh` to setup up a local environment:
# 1. a local k8s cluster with a master node.
# 2. a kubeedge node.
# 3. our gm/lc.

# Based on the kubeedge-local-up script which builds a local k8s cluster and kubeedge,
# our local-up script installs our package locally for
# simply developing and preparing for e3e tests.

# It does:
# 1. build the gm/lc/worker images.
# 2. download kubeedge source code and run its localup script.
# 3. prepare our k8s env.
# 4. config gm config and start gm.
# 5. start lc.
# 6. add cleanup.

# For cleanup, it needs to do our cleanups before kubeedge cleanup.
# Otherwise lc cleanup (via kubectl delete) is stuck and lc is kept running.

set -o errexit
set -o nounset
set -o pipefail

SEDNA_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)"

cd "$SEDNA_ROOT"

NO_CLEANUP=${NO_CLEANUP:-false}

IMAGE_REPO=localhost/sedna
IMAGE_TAG=localup

# local k8s cluster name for local-up-kubeedge.sh
CLUSTER_NAME=sedna
MASTER_NODENAME=${CLUSTER_NAME}-control-plane
EDGE_NODENAME=edge-node
NAMESPACE=sedna

TMP_DIR="$(realpath local-up-tmp)"

GM_BIND_PORT=9000
LC_BIND_PORT=9100

arch() {
  local arch=$(uname -m)
  case "$arch" in
    x86_64) arch=amd64;;
    *);;
  esac
  echo "$arch"
}

get_latest_version() {
  # get the latest version of specified gh repo
  local repo=${1}
  # output of this latest page:
  # ...
  # "tag_name": "v1.0.0",
  # ...
  curl -s https://api.github.com/repos/$repo/releases/latest | awk '/"tag_name":/&&$0=$2' | sed 's/[",]//g'
}

download_and_extract_kubeedge() {

  [ -d kubeedge ] && return
  local version=${KUBEEDGE_VERSION:-$(get_latest_version kubeedge/kubeedge)}

  git clone -b $version --depth 1 https://github.com/kubeedge/kubeedge
  return

  # the archive file can't works since local-up-kubeedge.sh depends git tag
  # https://github.com/kubeedge/kubeedge/archive/${version}.tar.gz
}

get_kubeedge_pid() {
  ps -e -o pid,comm,args |
   grep -F "$TMP_DIR" |
   # match executable name and print the pid
   awk -v bin="${1:-edgecore}" 'NF=$2==bin'
}

localup_kubeedge() {
  pushd $TMP_DIR >/dev/null
  download_and_extract_kubeedge
  # without setsid when hits ctrl-c, edgecore/cloudclore will be terminated
  # before cleanup called.
  # but we need cloudcore/edgecore alive to clean our container(mainly lc),
  # so here new a session to run local-up-kubeedge.sh
  setsid bash -c "
    cd kubeedge

    # no use ENABLE_DAEMON=true since it has not-fully-cleanup problem.
    TIMEOUT=90 CLUSTER_NAME=$CLUSTER_NAME ENABLE_DAEMON=false
    # 
    # here unset OUT_DIR
    # since local-up-kubeedge.sh needs default coded OUT_DIR
    unset OUT_DIR
    source hack/local-up-kubeedge.sh
   " &
  KUBEEDGE_ROOT_PID=$!
  add_cleanup '
    # for the case sometimes kube-proxy container in local machine
    # not cleanup.
    kubectl delete ds -n kube-system kube-proxy

    echo "found kubeedge pid, kill it: $KUBEEDGE_ROOT_PID"
    for((i=0;i<60;i++)); do
      ((i%15==0)) && kill "$KUBEEDGE_ROOT_PID"
      kill -0 "$KUBEEDGE_ROOT_PID" || break
      echo "waiting for $KUBEEDGE_ROOT_PID exists"
      sleep 1
    done
    # sometimes cloudcore/edgecore cant be stopped(one kill command
    # local-up-kubeedge.sh is not enough),
    # so to ensure this cleanup we clean it manully.
    for bin in cloudcore edgecore; do
      pid=$(get_kubeedge_pid $bin)
      if [ -n "$pid" ]; then
        echo "found $bin: $pid, try to kill it"
        # cloudcore/edgecore is started by sudo
        sudo kill $pid
        sudo kill $pid
      fi
    done
  '

  # wait ${MASTER_NODENAME} container to be running
  while ! docker ps --filter=name=${MASTER_NODENAME} | grep -q ${MASTER_NODENAME}; do
    # errexit when kubeedge-local pid exited
    kill -0 "$KUBEEDGE_ROOT_PID"
    sleep 3
  done

  # wait edgecore
  while [ -z "$(get_kubeedge_pid edgecore)" ]; do
    # errexit when kubeedge-local pid exited
    kill -0 "$KUBEEDGE_ROOT_PID"
    sleep 3
  done

  local parent=$$
  {
    # healthcheck for kubeedge-local pid
    # if it died, we died.
    while true; do
      if ! kill -0 "$KUBEEDGE_ROOT_PID"; then
        kill -INT $parent
        break
      fi
      sleep 1
    done
  }&
  popd

}

build_component_image() {
  local bin
  for bin; do
    echo "building $bin image"
    make -C "${SEDNA_ROOT}" ${bin}image IMAGE_REPO=$IMAGE_REPO IMAGE_TAG=$IMAGE_TAG
    eval ${bin^^}_IMAGE="'${IMAGE_REPO}/sedna-${bin}:${IMAGE_TAG}'"
  done
  # no clean up for images
}

load_images_to_master() {
  local image

  for image in $GM_IMAGE; do
    # just use the docker-image command of kind instead of ctr
    # docker save $image | docker exec -i $MASTER_NODENAME ctr --namespace k8s.io image import -
    kind load --name $CLUSTER_NAME docker-image $image
  done
}

prepare_k8s_env() {
  kind get kubeconfig --name $CLUSTER_NAME > $TMP_DIR/kubeconfig
  export KUBECONFIG=$(realpath $TMP_DIR/kubeconfig)
  # prepare our k8s environment

  # create these crds including dataset, model, joint-inference etc.
  kubectl create -f build/crds/

  kubectl create namespace $NAMESPACE 

  # create the cluster role for gm
  kubectl create -f build/gm/rbac/

  add_cleanup "
    kubectl delete -f build/crds/
    kubectl delete namespace $NAMESPACE --timeout=5s
  "
  load_images_to_master
}

start_gm() {
  # config gm and start as pod

  pushd $TMP_DIR >/dev/null

  local gm_node_name=${MASTER_NODENAME}
  local gm_pod_name=gm-pod

  # prepare gm config
  cat > gmconfig <<EOF
kubeConfig: ""
namespace: ""
websocket:
  port: $GM_BIND_PORT
localController:
  server: http://localhost:$LC_BIND_PORT
EOF

  add_cleanup "kubectl delete cm config -n $NAMESPACE"

  # create configmap for gm config
  kubectl create -n $NAMESPACE configmap config --from-file=gmconfig

  add_cleanup "
    kubectl delete deployment gm -n $NAMESPACE
    kubectl delete service gm -n $NAMESPACE
  "

  # start gm as pod with specified node name
  # TODO: create a k8s service, but kubeedge can't support this.
  kubectl create -f - <<EOF
apiVersion: v1
kind: Service
metadata:
  name: gm
  namespace: sedna
spec:
  selector:
    app: gm
  type: NodePort
  ports:
    - protocol: TCP
      port: $GM_BIND_PORT
      targetPort: $GM_BIND_PORT
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: gm
  labels:
    app: gm
  namespace: sedna
spec:
  replicas: 1
  selector:
    matchLabels:
      app: gm
  template:
    metadata:
      labels:
        app: gm
    spec:
      nodeName: $gm_node_name
      serviceAccountName: sedna
      containers:
      - name: gm
        image: $GM_IMAGE
        command: ["sedna-gm", "--config", "/config/gmconfig", "-v2"]
        resources:
          requests:
            memory: 32Mi
            cpu: 100m
          limits:
            memory: 128Mi
        volumeMounts:
        - name: config
          mountPath: /config
      volumes:
        - name: config
          configMap:
            name: config
EOF

  local gm_ip=$(kubectl get node $gm_node_name -o jsonpath='{ .status.addresses[?(@.type=="InternalIP")].address }')
  local gm_port=$(kubectl -n $NAMESPACE get svc gm -ojsonpath='{.spec.ports[0].nodePort}')
  
  GM_ADDRESS=$gm_ip:$gm_port

  add_debug_info "See GM status: kubectl get deploy -n $NAMESPACE gm"
  popd
}

start_lc() {
  local lc_ds_name=lc

  add_cleanup "
  # so here give a timeout in case edgecore is exited unexpectedly
  kubectl delete --timeout=5s ds lc -n sedna

  # if edgecore exited unexpectedly, we need to clean lc manually
  [ -z \"\$(get_kubeedge_pid edgecore)\" ] && {
    # TODO: find a better way to do this
    echo 'try to stop lc and its pause in edgenode manually'
    docker stop \$(
      docker ps |
      # find lc and its pause container id
      # kubeedge/k8s container name rule:
      #   pod: k8s_${lc_ds_name}_{pod_name}_${NAMESPACE}_{pod_uid}_
      #   pause: k8s_POD_{pod_name}_${NAMESPACE}_{pause_uid}_
      #   where pod_name is ${lc_ds_name}-[a-z0-9]{5}
      grep 'k8s_.*_${lc_ds_name}-[a-z0-9]*_${NAMESPACE}_' |
      awk NF=1
    ) 2>/dev/null
  }

  "

  # start lc as daemonset
  kubectl create -f- <<EOF
apiVersion: apps/v1
kind: DaemonSet
metadata:
  labels:
    k8s-app: sedna-lc
  name: $lc_ds_name
  namespace: $NAMESPACE
spec:
  selector:
    matchLabels:
      k8s-app: $lc_ds_name
  template:
    metadata:
      labels:
        k8s-app: $lc_ds_name
    spec:
      nodeSelector:
        # only schedule to edge node
        node-role.kubernetes.io/edge:  ""
      containers:
        - name: $lc_ds_name
          image: $LC_IMAGE
          env:
            - name: GM_ADDRESS
              value: $GM_ADDRESS
            - name: BIND_PORT
              value: "$LC_BIND_PORT"
            - name: NODENAME
              valueFrom:
                fieldRef:
                  fieldPath: spec.nodeName
            - name: ROOTFS_MOUNT_DIR
              # the value of ROOTFS_MOUNT_DIR is same with the mount path of volume
              value: /rootfs
          resources:
            requests:
              memory: 32Mi
              cpu: 100m
            limits:
              memory: 128Mi
          volumeMounts:
            - name: localcontroller
              mountPath: /rootfs
      volumes:
        - name: localcontroller
          hostPath:
            path: /
      hostNetwork: true
EOF
  add_debug_info "See LC status: kubectl get ds -n $NAMESPACE $lc_ds_name"

}

declare -a CLEANUP_CMDS=()
add_cleanup() {
  CLEANUP_CMDS+=("$@")
}

cleanup() {
  if [[ "${NO_CLEANUP}" = true ]]; then
    echo "No clean up..."
    return
  fi

  set +o errexit

  echo "Cleaning up sedna..."

  local idx=${#CLEANUP_CMDS[@]} cmd
  # reverse call cleanup
  for((;--idx>=0;)); do
    cmd=${CLEANUP_CMDS[idx]}
    echo "calling $cmd:"
    eval "$cmd"
  done

  set -o errexit
}

check_healthy() {
  # TODO
  true
}

debug_infos=""
add_debug_info() {
  debug_infos+="$@
"
}

check_prerequisites() {
  # TODO
  true
}

NO_COLOR='\033[0m'
RED='\033[0;31m'
GREEN='\033[0;32m'
green_text() {
  echo -ne "$GREEN$@$NO_COLOR"
}

red_text() {
  echo -ne "$RED$@$NO_COLOR"
}

fix_path() {
  # since we depends some tools in $GOPATH/bin,
  # fix the case the user don't add $GOPATH/bin to PATH.
  export PATH="$PATH:${GOPATH:-$(go env GOPATH)}/bin"
}

do_up() {
  cleanup

  mkdir -p "$TMP_DIR"
  add_cleanup 'rm -rf "$TMP_DIR"'

  fix_path

  # here local up kubeedge before building our images, this could avoid our
  # images be removed since edgecore image gc would be triggered when high
  # image usage(>=80%), see https://github.com/kubeedge/sedna/issues/26 for
  # more details.
  localup_kubeedge

  build_component_image gm lc

  check_prerequisites

  prepare_k8s_env

  start_gm
  start_lc

}

do_up_fg() {
  trap cleanup EXIT

  do_up

  echo "Local Sedna cluster is $(green_text running).
  Currently local-up script only support foreground running.
  Press $(red_text Ctrl-C) to shut it down!

  You can use it with: kind export kubeconfig --name ${CLUSTER_NAME}

  $debug_infos
  "
  while check_healthy; do sleep 5; done
}

main() {

if [ -z "${__WITH_SOURCE__:-}" ]; then
  do_up_fg
else  # __WITH_SOURCE__ mode, for run-e2e.sh
  do_up
fi

}

main
