#!/usr/bin/env bash
#
#  Copyright 2020 WoozyMasta <woozymasta@gmail.com>
#
#  This program is free software; you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation; either version 2 of the License, or
#  (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program; if not, write to the Free Software
#  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
#  MA 02110-1301, USA.
#
# shellcheck disable=SC2015

set -e

# Messages
log () {
  printf '%s [%s] %s\n' "$(date '+%Y/%m/%d %H:%M:%S')" "$1" "${@:2}"
}
msg-start () {
  [ "$silent" == 'true' ] && return; if [ -t 1 ]; then
    printf '\e[1;33m%-15s\e[m%-30s%s\n' 'Processing' "$1" "${@:2}"
  else log INFO "Processing dump $*"; fi
}
msg-end () {
  [ "$silent" == 'true' ] && return; if [ -t 1 ]; then
    printf '\e[1A\e[1;32m%-15s\e[m%-30s%s\n' 'Success' "$1" "${@:2}"
  else log INFO "Successfully dumped $*"; fi
}
msg-fail () {
  [ "$silent" == 'true' ] && return; if [ -t 1 ]; then
    printf '\e[1A\e[1;31m%-15s\e[m%-30s%s\n' 'Fail' "$1" "${@:2}"
  else log WARNING "Failed dump $*"; fi
}
success () {
  [ "$silent" == 'true' ] && return; if [ -t 1 ]; then
    printf '%s \e[1;36m%s\e[m %s\n' "$1" "$2" "${@:3}"
  else log INFO "$*"; fi
  score=$((score+1))
}
heading () {
  [ "$silent" == 'true' ] && return; if [ -t 1 ]; then
  printf '%s \e[1;34m%s\e[m %s\n%-15s%-30s%s\n' \
         "$1" "$2" 'started' 'STATE' 'RESOURCE' 'NAME'
  else log INFO "$*"; fi
}
warn () {
  if [ -t 1 ]; then
    >&2 printf '\e[1;31m%-10s\e[m%s\n' 'Warning:' "$*"
  else log WARNING "$*"; fi
}
fail () {
  if [ -t 1 ]; then
    >&2 printf '\n\e[1;31m%-10s\e[m%s\n' 'Error:' "$*"; exit 1
  else log ERROR "$*"; exit 1; fi
}

# Check command is exist
require () {
  for command in "$@"; do
    if ! [ -x "$(command -v "$command")" ]; then
      fail "'$command' util not found, please install it first"
    fi
  done
}

# Usage message
usage () {
  cat <<-EOF
Dump kubernetes cluster resources

Usage:
  ${0##*/} [command] [[flags]]

Available Commands:
  all, dump                     Dump all kubernetes resources
  ns,  dump-namespaces          Dump namespaced kubernetes resources
  cls, dump-cluster             Dump cluster wide kubernetes resources

The command can also be passed through the environment variable MODE.
All flags presented below have a similar variable in uppercase, with underscores
For example:
  --destination-dir == DESTINATION_DIR

Flags:
  -h, --help                    This help
  -s, --silent                  Execute silently, suppress all stdout messages
  -d, --destination-dir         Path to dir for store dumps, default ./data
  -f, --force-remove            Delete resources in data directory before launch

Kubernetes flags:
  -n, --namespaces              List of kubernetes namespaces
  -r, --namespaced-resources    List of namespaced resources
  -k, --cluster-resources       List of cluster resources
      --kube-config             Path to kubeconfig file
      --kube-context            The name of the kubeconfig context to use
      --kube-cluster            The name of the kubeconfig cluster to use
      --kube-insecure-tls       Skip check server's certificate for validity

Git commit flags:
  -c, --git-commit              Commit changes
  -p, --git-push                Commit changes and push to origin
  -b, --git-branch              Branch name
      --git-commit-user         Commit author username
      --git-commit-email        Commit author email
      --git-remote-name         Remote repo name, defualt is origin
      --git-remote-url          Remote repo URL

Archivate flags:
  -a, --archivate               Create archive of data dir
      --archive-rotate-days     Rotate archives older than N days
      --archive-type            Archive type xz, gz or bz2, default is tar

Example of use:
  ${0##*/} dump-namespaces -n default,dev -d /mnt/dump -spa --archive-type gz

Report bugs at:
  https://github.com/WoozyMasta/kube-dump/issues
  <woozymasta@gmail.com>

EOF
  exit 0
}

# Set common vars
working_dir="$(pwd)"
timestamp="$(date '+%Y.%m.%d_%H-%M')"

# Read vars from env
# shellcheck disable=SC1090,SC1091
[ -f "$working_dir/.env" ] && . "$working_dir/.env"

# Parse args commands
if [[ "${1:-$MODE}" =~ ^(dump|all|dump-namespaces|ns|dump-cluster|cls)$ ]]; then
  mode="${1:-$MODE}"; else usage; fi

# Parse args flags
args=$(
  getopt \
    -l "namespaces:,namespaced-resources:,cluster-resources:" \
    -l "kube-config:,kube-context:,kube-cluster:,kube-insecure-tls" \
    -l "help,silent,destination:,force-remove," \
    -l "git-commit,git-push,git-branch:,git-commit-user:,git-commit-email:" \
    -l "git-remote-name:,git-remote-url:" \
    -l "archivate,archive-rotate-days:,archive-type:" \
    -o "n:,r:,k:,h,s,d:,f,c,p,b:,a" -- "${@:2}"
)
eval set -- "$args"
while [ $# -ge 1 ]; do
  case "$1" in
# Resources
    -n|--namespaces)            namespaces+="$2,";                shift; shift;;
    -r|--namespaced-resources)  namespaced_resources+="$2,";      shift; shift;;
    -k|--cluster-resources)     cluster_resources+="$2,";         shift; shift;;
# Kubectl opts
       --kube-config)           kube_config="$2";                 shift; shift;;
       --kube-context)          kube_context="$2";                shift; shift;;
       --kube-cluster)          kube_cluster="$2";                shift; shift;;
       --kube-insecure-tls)     kube_insecure_tls='true';                shift;;
# Common opts
    -h|--help)                  usage;;
    -s|--silent)                silent='true';                           shift;;
    -d|--destination-dir)       destination_dir="$2";             shift; shift;;
# Dump opts
    -f|--force-remove)          force_remove='true';                     shift;;
# Commit opts
    -c|--git-commit)            git_commit='true';                       shift;;
    -p|--git-push)              git_push='true';                         shift;;
    -b|--git-branch)            git_branch="$2";                  shift; shift;;
       --git-commit-user)       git_commit_user="$2";             shift; shift;;
       --git-commit-email)      git_commit_email="$2";            shift; shift;;
       --git-remote-name)       git_remote_name="$2";             shift; shift;;
       --git-remote-url)        git_remote_url="$2";              shift; shift;;
# Archivate opts
    -a|--archivate)             archivate='true';                        shift;;
       --archive-rotate-days)   archive_rotate="$2";              shift; shift;;
       --archive-type)          archive_type="$2";                shift; shift;;
# Final
       --)                                                        shift; break;;
       -*)                      fail "invalid option $1";;
  esac
done

if [[ -n "$*" && "$OSTYPE" != "darwin"* ]]; then
  fail "extra arguments $*"
fi

# Set vars
silent="${silent:-$SILENT}"
kube_config="${kube_config:-$KUBE_CONFIG}"
kube_context="${kube_context:-$KUBE_CONTEXT}"
kube_cluster="${kube_cluster:-$KUBE_CLUSTER}"
kube_insecure_tls="${kube_insecure_tls:-$KUBE_INSECURE_TLS}"
git_commit="${git_commit:-$GIT_COMMIT}"
git_branch="${git_branch:-$GIT_BRANCH}"
git_commit_user="${git_commit_user:-$GIT_COMMIT_USER}"
git_commit_email="${git_commit_email:-$GIT_COMMIT_EMAIL}"
git_remote_name="${git_remote_name:-$GIT_REMOTE_NAME}"
git_remote_url="${git_remote_url:-$GIT_REMOTE_URL}"
git_push="${git_push:-$GIT_PUSH}"
archivate="${archivate:-$ARCHIVATE}"
archive_rotate="${archive_rotate:-$ARCHIVE_ROTATE}"
archive_type="${archive_type:-$ARCHIVE_TYPE}"

# Check dependency
require kubectl jq yq
[ "$git_commit" == 'true' ] && \
require git
[ "$archivate" == 'true' ] && [ "$archive_type" == 'xz' ] && \
require tar xz
[ "$archivate" == 'true' ] && [ "$archive_type" == 'gzip' ] && \
require tar gzip
[ "$archivate" == 'true' ] && [ "$archive_type" == 'bzip2' ] && \
require tar bzip2

# Kubectl args
[ -n "$kube_config" ] && k_args+=("--kubeconfig=$kube_config")
[ -n "$kube_context" ] && k_args+=("--context=$kube_context")
[ -n "$kube_cluster" ] && k_args+=("--cluster=$kube_cluster")
[ "$kube_insecure_tls" == 'true' ] && \
  k_args+=("--insecure-skip-tls-verify=true")

# Use serviceaccount
if [ -n "$KUBERNETES_SERVICE_HOST" ] && \
   [ -n "$KUBERNETES_SERVICE_PORT" ] && \
   [ -z "$kube_config" ]
then

  require curl
  kube_api="$KUBERNETES_SERVICE_HOST:$KUBERNETES_SERVICE_PORT"
  kube_api_token=$(</var/run/secrets/kubernetes.io/serviceaccount/token)
  kube_api_ca=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt

  _api_code=$(
    curl --fail --location --output /dev/null --write-out '%{http_code}\n' --cacert $kube_api_ca \
    -H "Authorization: Bearer $kube_api_token" --silent "https://$kube_api/livez"
  )
  if [ "$_api_code" == "200" ]
  then
    kubectl config set-cluster "${kube_context:-k8s}" \
      --server="https://$kube_api" \
      --certificate-authority="$kube_api_ca" >/dev/null
    kubectl config set-credentials "${kube_user:-kube-dump}" \
      --token="$kube_api_token"  >/dev/null
    kubectl config set-context "${kube_context:-k8s}" \
      --cluster "${kube_cluster:-k8s}" \
      --user "${kube_user:-kube-dump}" >/dev/null
    kubectl config use-context "${kube_context:-k8s}" >/dev/null

  else
    fail 'Found running on kubernetes cluster but attempting connect' \
      "to API $kube_api failed with code $_api_code"

  fi
fi

# Check kube config context
if [ -n "$kube_context" ]; then
  kubectl config view \
    --kubeconfig="${kube_config:-$HOME/.kube/config}" --output='json' | \
  jq --exit-status --monochrome-output --raw-output '.contexts[].name' | \
  grep --quiet "^$kube_context$" && \
  context="$kube_context" || \
  fail "Context $kube_context not exist in kubeconfig"
elif kubectl config current-context "${k_args[@]}" >/dev/null 2>&1; then
  context=$(kubectl config current-context "${k_args[@]}")
else
  fail "Current context not configured in kubeconfig"
fi

# Check kube config cluster
if [ -n "$kube_cluster" ]; then
  kubectl config view \
    --kubeconfig="${kube_config:-$HOME/.kube/config}" --output='json' | \
  jq --exit-status --monochrome-output --raw-output '.clusters[].name' | \
  grep --quiet "^$kube_cluster$" || \
  fail "Cluster $kube_cluster not exist in kubeconfig"
fi

# Try get cluster info
kubectl cluster-info "${k_args[@]}" >/dev/null || \
  fail "Cluster $kube_api not accessible"

# Set namespaces list
if [ -z "${namespaces:-$NAMESPACES}" ]; then
  if ! namespaces=$(kubectl get namespaces \
      --output=jsonpath=\{.items[*].metadata.name\} "${k_args[@]}")
  then
    fail 'Cant get namespaces from cluster'
  fi
else
  namespaces=${namespaces:-$NAMESPACES}
fi

# Set namespaced resources
# https://kubernetes.io/docs/reference/kubectl/overview/#resource-types
if [ -z "${namespaced_resources:-$NAMESPACED_RESOURCES}" ]; then
  namespaced_resources="$(
    kubectl api-resources --namespaced=true --output=name "${k_args[@]}" | \
    tr '\n' ' '
  )"
else
  namespaced_resources=${namespaced_resources:-$NAMESPACED_RESOURCES}
fi

# Set cluster resources
if [ -z "${cluster_resources:-$CLUSTER_RESOURCES}" ]; then
  cluster_resources="$(
    kubectl api-resources --namespaced=false --output=name "${k_args[@]}" | \
    tr '\n' ' '
  )"
else
  cluster_resources=${cluster_resources:-$CLUSTER_RESOURCES}
fi

# Dump dir
destination_dir="${destination_dir:-${DESTINATION_DIR:-$working_dir/data}}"
destination_dir="$(realpath "$destination_dir" --canonicalize-missing)"
if [ ! -d "$destination_dir" ]; then
  mkdir -p "$destination_dir"
  success 'Dump data directory' "$destination_dir" 'created'
elif [ "${force_remove:-$FORCE_REMOVE}" == 'true' ]; then
  warn "Destination $destination_dir directory will be removed"
  sleep 3
  find "$destination_dir" -mindepth 1 -maxdepth 1 -type d ! -name '.git' \
    -exec rm -fr "{}" \;
fi

# Git try to clone
if [ "$git_push" == 'true' ] && [ ! -d "$destination_dir/.git/" ]; then

  if ! grep --quiet --extended-regexp 'https?:\/\/' <<< "$git_remote_url"; then

    _remote_url=$(sed -r 's|.*@([A-Za-z0-9\-\.]+):.*|\1|' <<< "$git_remote_url")
    ! grep --quiet "^$_remote_url " ~/.ssh/known_hosts >/dev/null 2>&1 && \
      ssh-keyscan "$_remote_url" >> ~/.ssh/known_hosts 2>/dev/null

    git clone --branch "$git_branch" --single-branch --depth 1 \
      --quiet "$_remote_url" "$destination_dir" >/dev/null 2>&1 && \
      success 'The remote repository is cloned at' "$destination_dir" || \
      warn 'Unable to clone remote repository at' "$destination_dir"

  else
    git clone --branch "$git_branch" --single-branch --depth 1 \
      --quiet "$git_remote_url" "$destination_dir" >/dev/null 2>&1 && \
      success 'The remote repository is cloned at' "$destination_dir" || \
      warn 'Unable to clone remote repository at' "$destination_dir"
  fi
fi

success 'Dump data in' "$destination_dir" 'directory' ''
score=0

# Work with namespaced reosurces
if [[ "$mode" =~ ^(dump|all|dump-namespaces|ns)$ ]]; then

  for ns in ${namespaces//,/ }; do
    # Check namespace exist
    if ! kubectl get ns "$ns" "${k_args[@]}" >/dev/null 2>&1; then
      warn "Namespace \"$ns\" not found"
      continue
    fi
    # Create namespace dir
    [ -d "$destination_dir/$ns" ] || mkdir -p "$destination_dir/$ns"
    heading 'Dump namespace' "$ns"

    # Iterate over resources
    for resource in ${namespaced_resources//,/ }; do

      # Iterate over only accessible resources
      while read -r name; do
        [ -z "$name" ] && continue

        # Skip service-account-token secrets
        if [ "$resource" == 'secret' ]; then
          type=$(
            kubectl get --namespace="${ns}" --output=jsonpath="{.type}" \
              secret "$name" "${k_args[@]}"
            )
          [ "$type" == 'kubernetes.io/service-account-token' ] && continue
          unset type
        fi

        msg-start "$resource" "$name"

        # Save resource to file
        kubectl --namespace="${ns}" get \
          --output='json' "$resource" "$name" "${k_args[@]}" 2>/dev/null | \
        jq --exit-status --compact-output --monochrome-output \
          --raw-output --sort-keys 2>/dev/null \
          'del(
            .metadata.annotations."control-plane.alpha.kubernetes.io/leader",
            .metadata.annotations."deployment.kubernetes.io/revision",
            .metadata.annotations."kubectl.kubernetes.io/last-applied-configuration",
            .metadata.annotations."kubernetes.io/service-account.uid",
            .metadata.annotations."pv.kubernetes.io/bind-completed",
            .metadata.annotations."pv.kubernetes.io/bound-by-controller",
            .metadata.finalizers,
            .metadata.managedFields,
            .metadata.creationTimestamp,
            .metadata.generation,
            .metadata.resourceVersion,
            .metadata.selfLink,
            .metadata.uid,
            .spec.clusterIP,
            .spec.progressDeadlineSeconds,
            .spec.revisionHistoryLimit,
            .spec.template.metadata.annotations."kubectl.kubernetes.io/restartedAt",
            .spec.template.metadata.creationTimestamp,
            .spec.volumeName,
            .spec.volumeMode,
            .status
          )' | \
      yq eval --prettyPrint --no-colors --exit-status - \
        >"$destination_dir/$ns/${name//:/-}_$resource".yaml 2>/dev/null && \
        msg-end "$resource" "$name" || msg-fail "$resource" "$name"

      done < <(
        kubectl --namespace="${ns}" get "$resource" \
          --output='custom-columns=NAME:.metadata.name' \
          --no-headers "${k_args[@]}" 2>/dev/null
        )
    done
    success 'Namespace' "$ns" 'resources dump completed' ''
  done
fi

# Work with cluster reosurces
if [[ "$mode" =~ ^(dump|all|dump-cluster|cls)$ ]]; then

  heading 'Dump cluster data' "$context"
  # Iterate over resources
  for resource in ${cluster_resources//,/ }; do

    # Iterate over only accessible resources
    while read -r name; do
      [ -d "$destination_dir/cluster" ] || mkdir -p "$destination_dir/cluster"
      msg-start "$resource" "$name"

      # Save resource to file
      kubectl get --output='json' "$resource" "${k_args[@]}" | \
        jq --exit-status --compact-output --monochrome-output \
          --raw-output --sort-keys 2>/dev/null \
        'del(
          .items[].metadata.annotations."kubectl.kubernetes.io/last-applied-configuration",
          .items[].metadata.annotations."control-plane.alpha.kubernetes.io/leader",
          .items[].metadata.uid,
          .items[].metadata.selfLink,
          .items[].metadata.resourceVersion,
          .items[].metadata.creationTimestamp,
          .items[].metadata.generation
        )' | \
      yq eval --prettyPrint --no-colors --exit-status - \
        >"$destination_dir/cluster/${name//:/-}_$resource".yaml 2>/dev/null && \
      msg-end "$resource" "$name" || msg-fail "$resource" "$name"

    done < <(
      kubectl get "$resource" \
        --output='custom-columns=NAME:.metadata.name' \
        --no-headers "${k_args[@]}" 2>/dev/null
      )
  done
  success 'Cluster' "$context" 'resources dump completed' ''
fi

# Git
if [ "$git_commit" == 'true' ] || [ "$git_push" == 'true' ]; then

  cd "$destination_dir"
  # Init repo if not exist
  if [ ! -d "$destination_dir/.git/" ]; then
    git init . --quiet
    success 'The repository is created in the' "$destination_dir/.git/" \
      'directory'
  fi
  # Set branch
  if [ -z "$git_branch" ]; then
    git_branch="$(git symbolic-ref --short HEAD)"
  else
    git checkout -b "$git_branch" >/dev/null 2>&1 || true
  fi

  # Set git username if need
  if [ -z "$git_commit_user" ] && ! git config user.name >/dev/null 2>&1; then
    git config --local user.name 'Kube-dump'
  elif [ -n "$git_commit_user" ]; then
    git config --local user.name "$git_commit_user"
  fi
  # Set git email if need
  if [ -z "$git_commit_email" ] && ! git config user.email >/dev/null 2>&1; then
    git config --local user.email "$context"
  elif [ -n "$git_commit_email" ]; then
    git config --local user.email "$git_commit_email"
  fi

  # Ignore archives
  [ ! -f "$destination_dir/.gitignore" ] && printf '%s\n' \
    '*.tar.xz' '*.tar.gz' '*.tar.bz2' '*.tar' > "$destination_dir/.gitignore"

  # Commit all
  if [ -n "$(git status --porcelain)" ]; then
    _commit_message="Kubernetes $context cluster data dumped at $timestamp"
    git add .
    git add . --all
    git commit --all --quiet --message="$_commit_message"
    success 'Changes commited with message:' "$_commit_message"
  else
    success 'No changes,' 'nothing to commit' 'in git repository'
  fi

  if [ "$git_push" == 'true' ]; then

    # Set git repository url if need
    if ! git ls-remote >/dev/null 2>&1 && [ -z "$git_remote_url" ]; then
      warn "Remote git repository url not set, cannot push"
    elif [ -n "$git_remote_url" ]; then
      git remote add "${git_remote_name:-origin}" \
        "$git_remote_url" > /dev/null 2>&1 || true
    fi
    git pull --ff-only "$git_remote_url" "$git_branch" || true
    git push --quiet --set-upstream "${git_remote_name:-origin}" "$git_branch"
  fi
fi

# Archivate
if [ "$archivate" == 'true' ]; then

  # Set compression flag and archive name
  [ "$archive_type" == 'xz' ] && _compress='--xz'
  [ "$archive_type" == 'gz' ] && _compress='--gzip'
  [ "$archive_type" == 'bz2' ] && _compress='--bzip2'
  if [ -n "$_compress" ]; then
    _archive="${destination_dir}/dump_$timestamp.tar.$archive_type"
  else
    _archive="${destination_dir}/dump_$timestamp.tar"
  fi

  # Create archive
  tar --create --file="$_archive" --absolute-names $_compress \
    --exclude-vcs --exclude='*.tar' --exclude='*.tar.xz' \
    --exclude='*.tar.gz' --exclude='*.tar.bz2' \
    --directory="${destination_dir%/*}" "${destination_dir##*/}"
  success 'Archive' "$_archive" 'created'

  # Rotate archives
  if [ -n "$archive_rotate" ]; then
    find "${destination_dir}" -mindepth 1 -maxdepth 1 -type f -name "*.tar" \
      -name "*.tar.xz" -name "*.tar.gz" -name "*.tar.bz2" \
      -mtime +"$archive_rotate" -delete
    success 'Rotatinon for older than' "$archive_rotate days" \
      "*.tar.${archive_type:-xz} archives removed"
  fi
fi

# Done
if [ "$score" -ge 0 ]; then
  success 'Done!' "$score" 'task completed'
  exit 0
else
  fail 'No task has been completed'
fi