#!/usr/bin/env bash
# author: joe.zheng
# version: 24.04.02

set -e

# @self {
# AUTO-GENERATED, DO NOT EDIT!

SELF="$(basename $0)"
SELF_VERSION="$(sed -n '1,4s/# version: \(.*\)/\1/p' $0)"

# @self }

# @sudo {
# AUTO-GENERATED, DO NOT EDIT!

SUDO="sudo"
if [[ $(id -u) == "0" ]]; then
  SUDO=""
fi

function validate_sudo() {
  if [[ -n $SUDO ]]; then
    msg "validate sudo"
    sudo -v
  fi
}

# @sudo }

DOCKER="$SUDO docker"
if id -nG | grep -qw docker; then
  DOCKER="docker"
fi

PRIVATE_CIDR="127.0.0.0/8,169.254.0.0/16,172.16.0.0/12,192.168.0.0/16,10.0.0.0/8"
HOSTNAME="$(cat /proc/sys/kernel/hostname)"
DIST="$(source /etc/os-release && echo $ID | tr '[:upper:]' '[:lower:]')"

PORT_HTTP="912"
PORT_HTTPS="912"
PROXY_HOST=""
PROXY_PORT=""
PROXY_SKIP="localhost,$HOSTNAME,istio-system.svc,$PRIVATE_CIDR"
REGISTRY_MIRROR="${REGISTRY_MIRROR:-}"
UNINSTALL=n             # uninstall completely
VERSION="latest"        # version to install

BASEURL="https://download.docker.com"
MIRROR_URL="${MIRROR_URL:-https://mirrors.aliyun.com/docker-ce}"
URL_TO_CHECK="$BASEURL"

PKGS_ESSENTIAL="docker-ce docker-ce-cli containerd.io"     # DO NOT change the last one
PKGS_OPTIONAL="docker-buildx-plugin docker-compose-plugin"

SERVICE_CONFIG="/etc/systemd/system/docker.service.d/http-proxy.conf"
DOCKERD_CONFIG="/etc/docker/daemon.json"
DOCKERC_CONFIG=~/.docker/config.json    # not quoted to expand tilde

# @dry-run {
# AUTO-GENERATED, DO NOT EDIT!

DRY_RUN="${DRY_RUN:-n}"        # "y" to enable
RUN=''                         # command prefix for dry run
if [[ $DRY_RUN == 'y' ]]; then
  RUN='echo'
fi

# @dry-run }

function usage() {
  cat <<EOF
Usage: $SELF [-M <mirror>] [-N <noproxy>] [-H <port>] [-S <port>] [-m <type>]
  [-v <ver>] [-U] [-n] [-h] [<host>] [<port>]

  Install Docker and configure it the easy way.

  -M <mirror>:  Docker registry mirror, default: $REGISTRY_MIRROR
  -N <noproxy>: no_proxy settings, default: $PROXY_SKIP
  -H <port>:    proxy port for http, default: $PORT_HTTP
  -S <port>:    proxy port for https, default: $PORT_HTTPS
  -m <type>:    install via mirror [$MIRRORS], default: $MIRROR
  -v <ver>:     target version, default: $VERSION
  -U:           uninstall completely, default: $UNINSTALL
  -n:           dry run, print out information only, default: $DRY_RUN
  -h:           print the usage message

  <host>:       proxy host, "" to disable proxy, default: $PROXY_HOST
  <port>:       proxy port, override -HS options, default: $PROXY_PORT

Environment variables:

  MIRROR_URL:   mirror to install docker, default: $MIRROR_URL
  OFFLINE:      force into offline mode [y n], default: $OFFLINE

Examples:

  1. Deploy with default configuration
     $SELF

  2. Deploy without proxy
     $SELF ""

  3. Deploy with specified proxy host and port (913 for http and https)
     $SELF http://proxy.example.com 913

  4. Deploy with specified proxy host and port (911 for http, 912 for https)
     $SELF -H 911 -S 912 http://proxy.example.com

  5. Install specific version
     $SELF -v $VERSION

  6. Uninstall
     $SELF -U

Version: $SELF_VERSION
EOF

}

function main() {
  # adjust default version if already deployed
  local v="$(current_version)"
  VERSION="${v:-$VERSION}"

  while getopts ":N:M:H:S:m:v:Unh" opt
  do
    case $opt in
      M ) REGISTRY_MIRROR=$OPTARG;;
      N ) PROXY_SKIP=$OPTARG;;
      H ) PORT_HTTP=$OPTARG;;
      S ) PORT_HTTPS=$OPTARG;;
      m ) MIRROR=$OPTARG
          if echo $MIRRORS | grep -v -w $MIRROR >/dev/null 2>&1; then
            err "invalid mirror option $MIRROR"
            usage && exit 1
          fi
          ;;
      v ) VERSION=${OPTARG#v};;
      U ) UNINSTALL=y;;
      n ) DRY_RUN=y && RUN='echo';;
      h ) usage && exit;;
      * ) usage && err "invalid option: $OPTARG" && exit 1;;
    esac
  done
  shift $((OPTIND-1))

  if (( $# >= 1 )); then
    PROXY_HOST="$1"
  fi
  if (( $# >= 2 )); then
    PROXY_PORT="$2"
  fi

  # export proxy settings
  if [[ -n $PROXY_HOST ]]; then
    export http_proxy=$PROXY_HOST:${PROXY_PORT:-$PORT_HTTP}
    export https_proxy=$PROXY_HOST:${PROXY_PORT:-$PORT_HTTPS}
    export no_proxy=$PROXY_SKIP
  else
    export http_proxy=
    export https_proxy=
    export no_proxy=
  fi

  for v in CACHE DIST DOCKERC_CONFIG DOCKERD_CONFIG MIRROR_URL DRY_RUN MIRROR OFFLINE PORT_HTTP \
	   PORT_HTTPS PROXY_HOST PROXY_PORT PROXY_SKIP REGISTRY_MIRROR SERVICE_CONFIG VERSION; do
    eval echo "$v: \${$v}"
  done

  [[ $DRY_RUN == "y" ]] && exit

  warn_as_root
  validate_sudo
  check_prerequisites
  check_network
  check_mirror
  ensure_workdir
  ensure_cleared
  if [[ $UNINSTALL == "y" ]]; then
    ensure_uninstalled
  else
    ensure_installed
    ensure_configured
  fi

  msg "done"
}

function warn_as_root() {
  if [[ $(id -u) == "0" ]]; then
    msg "you are running as root user, docker will be configured to root only!"
    sleep 3
  fi
}

function current_version() {
  local version=
  if [[ -n $(which docker) ]]; then
    version="$($DOCKER version -f '{{.Server.Version}}')"
    version=${version#v}
  fi
  echo $version
}

function check_prerequisites() {
  msg "check prerequisites"
  if [[ -z $(which curl) ]]; then
    err "curl is not available"
    exit 1
  fi
  if [[ -z $(which python3) ]]; then
    err "python3 is not available"
    exit 1
  fi
}

function ensure_cleared() {
  msg "ensure old docker is cleared"

  if [[ $DIST == "centos" || $DIST == "openeuler" ]]; then
    $SUDO yum remove docker docker-client docker-client-latest docker-common \
      docker-latest docker-latest-logrotate docker-logrotate docker-engine || true
  elif [[ $DIST == "ubuntu" || $DIST == "debian" ]]; then
    $SUDO apt remove docker docker-engine docker.io containerd runc || true
  else
    err "\"$DIST\" is not supported!"
    exit 1
  fi
}

function save_package_list() {
  local path="${1:?argument missing}"
  msg "save current package list to $path"

  mkdir -p $(dirname $path)

  if [[ $DIST == "ubuntu" || $DIST == "debian" ]]; then
    ls -1 /var/cache/apt/archives/*.deb | sort > $path
  elif [[ $DIST == "centos" || $DIST == "openeuler" ]]; then
    if [[ -d /var/cache/dnf ]]; then
      ls -1 /var/cache/dnf/*/packages/*.rpm | sort > $path
    else
      ls -1 /var/cache/yum/*/*/*/packages/*.rpm | sort > $path
    fi
  else
    err "\"$DIST\" is not supported!"
    exit 1
  fi
}

function install_from_cache() {
  local cachedir="${1:?argument missing}"
  msg "install docker from cache: $cachedir"

  if [[ $DIST == "ubuntu" || $DIST == "debian" ]]; then
    msg "install $cachedir/*.deb"
    $SUDO dpkg -i $cachedir/*.deb
  elif [[ $DIST == "centos" || $DIST == "openeuler" ]]; then
    msg "install $cachedir/*.rpm"
    $SUDO yum install -y -C --disablerepo=* $cachedir/*.rpm
  else
    err "\"$DIST\" is not supported!"
    exit 1
  fi
}

function install_docker() {
  msg "install docker, version:$VERSION"

  local base_url="$BASEURL/linux"
  if [[ $USE_MIRROR == "y" ]]; then
    base_url="$MIRROR_URL/linux"
  fi

  if [[ $DIST == "openeuler" ]]; then
    # TODO: install latest version once openEuler has fuse-overlayfs
    msg "WARNING: only verified on openEuler 22.04 LTS"
    local repo_url="$base_url/centos/docker-ce.repo"
    $SUDO yum-config-manager --add-repo $repo_url
    echo "8" | $SUDO tee /etc/yum/vars/centosver
    $SUDO sed -i 's/\$releasever/\$centosver/g' /etc/yum.repos.d/docker-ce.repo
    $SUDO yum install -y docker-ce-19.03.15-3.el8
  elif [[ $DIST == "centos" ]]; then
    msg "setup yum repository"
    $SUDO yum install -y yum-utils
    $SUDO yum-config-manager --add-repo $base_url/$DIST/docker-ce.repo

    local pkgs_needed="$PKGS_ESSENTIAL"
    if [[ $VERSION != "latest" ]]; then
      msg "search packages for version $VERSION"
      local allver="$(yum list --showduplicates docker-ce | sed '1,/Available Packages/d' | awk '{ print $2 }')"
      local pkgver="$(echo "$allver" | grep $VERSION | tail -n1)"
      if [[ -z $pkgver ]]; then
        msg "available versions:"
        echo "$(echo $allver | fold -s)"
        err "$VERSION not found"
        exit 1
      else
        msg "found $pkgver"
        pkgver="$(echo $pkgver | cut -d':' -f 2)" # remove epoch
        # replace space " " with "-$pkgver", the last one will not be replaced
        pkgs_needed="${pkgs_needed// /-$pkgver }"
      fi
    fi
    msg "install core components"
    $SUDO yum install -y $pkgs_needed
    msg "install optional components"
    $SUDO yum install -y $PKGS_OPTIONAL || true
  elif [[ $DIST == "ubuntu" || $DIST == "debian" ]]; then
    # use apt-get instead of apt to keep the downloaded packages
    msg "ensure gpupg is installed"
    $SUDO apt-get update
    $SUDO apt-get -y install ca-certificates curl gnupg

    msg "add gpg key"
    local keydir="/etc/apt/keyrings"
    local gpgkey="$keydir/docker.gpg"
    $SUDO install -m 0755 -d $keydir
    curl -fsSL $base_url/$DIST/gpg | $SUDO gpg --dearmor --yes -o $gpgkey
    $SUDO chmod a+r $gpgkey

    msg "add source list"
    local codename="$(. /etc/os-release && echo $VERSION_CODENAME)"
    local archname="$(dpkg --print-architecture)"
    cat <<EOF | $SUDO tee /etc/apt/sources.list.d/docker.list
deb [arch=$archname signed-by=$gpgkey] $base_url/$DIST $codename stable
EOF

    $SUDO apt-get update
    local pkgs_needed="$PKGS_ESSENTIAL"
    if [[ $VERSION != "latest" ]]; then
      msg "search packages for version $VERSION"
      local allver="$(apt-cache madison docker-ce | awk '{ print $3 }')"
      local pkgver="$(echo "$allver" | grep $VERSION | head -n1)"
      if [[ -z $pkgver ]]; then
        msg "available versions:"
        echo "$(echo $allver | fold -s)"
        err "$VERSION not found"
        exit 1
      else
        msg "foud $pkgver"
        # replace space " " with "=$pkgver", the last one will not be replaced
        pkgs_needed="${pkgs_needed// /=$pkgver }"
      fi
    fi
    msg "install core components"
    $SUDO apt-get install -y $pkgs_needed
    msg "install optional components"
    $SUDO apt-get install -y $PKGS_OPTIONAL || true
  else
    err "\"$DIST\" is not supported!"
    exit 1
  fi
}

function ensure_installed() {
  msg "ensure docker is installed"
  local installed="$(current_version)"

  if [[ -z $installed ]]; then
    msg "docker is not available, install it"

    local cachedir="$CACHE_PACKAGES/$DIST/docker-$VERSION"
    if [[ $OFFLINE == "y" ]]; then
      msg "WARNING: offline mode, cache must be ready"
      install_from_cache $cachedir
      return
    fi

    local packages_old="$CACHE_RUN/packages-old.txt"
    local packages_new="$CACHE_RUN/packages-new.txt"

    save_package_list "$packages_old"
    install_docker
    save_package_list "$packages_new"

    msg "save the installed packages to $cachedir"
    local packages_delta="$CACHE_RUN/packages-delta.txt"
    comm -13 $packages_old $packages_new > $packages_delta

    mkdir -p $cachedir
    while read f; do
      msg "saving $f"
      cp -t $cachedir $f
    done <$packages_delta

    msg "docker installation is done"
  else
    msg "version $installed has already installed"
    if [[ $VERSION != "latest" && $installed != $VERSION ]]; then
      msg "WARNING: the target version $VERSION is different from the installed one"
      msg "uninstall current version with following command and install again:"
      msg "  $SELF -U"
    fi
  fi
}

function ensure_uninstalled() {
  msg "ensure docker is uninstalled"
  local installed="$(current_version)"

  if [[ -n $installed ]]; then
    if [[ $DIST == "centos" || $DIST == "openeuler" ]]; then
      $SUDO yum remove $PKGS_ESSENTIAL
      $SUDO yum remove $PKGS_OPTIONAL || true
    elif [[ $DIST == "ubuntu" || $DIST == "debian" ]]; then
      $SUDO apt purge --auto-remove $PKGS_ESSENTIAL
      $SUDO apt purge --auto-remove $PKGS_OPTIONAL || true
    else
      err "\"$DIST\" is not supported!"
      exit 1
    fi
    # try to stop docker.socket in bogus state
    $SUDO systemctl stop docker.socket || true
  fi

  msg "try to remove user configurtion"
  for f in $SERVICE_CONFIG $DOCKERD_CONFIG $DOCKERC_CONFIG; do
    if [[ -e $f ]]; then
      read -p "delete $f? y/N: " result
      if [[ $result == "y" ]]; then
        $SUDO rm $f
      fi
    fi
  done

  msg "images, containers and volumes aren't removed, do it by yourself:"
  cat <<EOF
$SUDO rm -rf /var/lib/docker
$SUDO rm -rf /var/lib/containerd
EOF
}

function ensure_configured() {
  msg "ensure docker is configured"

  if [[ -e $SERVICE_CONFIG || -e $DOCKERD_CONFIG || -e $DOCKERC_CONFIG ]]; then
    cat <<EOF
any of the following configuration files already exist
* $SERVICE_CONFIG
* $DOCKERD_CONFIG
* $DOCKERC_CONFIG
EOF
    read -p "overwrite them and continue? y/N: " result
    if [[ $result != "y" ]]; then
      exit
    fi
  fi

  msg "configure docker client: $DOCKERC_CONFIG"
  DOCKERC_CONFIG="$DOCKERC_CONFIG" python3 <<'EOF'
import os, json, pathlib, collections

nest_dict = lambda: collections.defaultdict(nest_dict)

f = pathlib.Path(os.environ['DOCKERC_CONFIG']).expanduser()
d = nest_dict()

try:
  with f.open() as fh:
    d.update(json.load(fh))
except FileNotFoundError:
  pass

m = dict(httpProxy='http_proxy', httpsProxy='https_proxy', noProxy='no_proxy')
d['proxies']['default'].update({ k : os.environ[v] for k, v in m.items() })

f.parent.mkdir(parents=True, exist_ok=True)
with f.open('w') as fh:
  json.dump(d, fh, indent=4)
EOF

  msg "configure docker daemon"
  msg "configure $SERVICE_CONFIG"
  $SUDO mkdir -p $(dirname $SERVICE_CONFIG)
  cat <<EOF | $SUDO tee $SERVICE_CONFIG
[Service]
Environment="HTTP_PROXY=$http_proxy"
Environment="HTTPS_PROXY=$https_proxy"
Environment="NO_PROXY=$no_proxy"
EOF

  msg "configure $DOCKERD_CONFIG"
  $SUDO mkdir -p $(dirname $DOCKERD_CONFIG)
  local mirrors=""
  if [[ -n $REGISTRY_MIRROR ]]; then
    mirrors="\"registry-mirrors\": [\"$REGISTRY_MIRROR\"],"
  fi
  cat <<EOF | $SUDO tee $DOCKERD_CONFIG
{
  "log-opts": {
    "max-size": "500m"
  },
  $mirrors
  "exec-opts": ["native.cgroupdriver=systemd"],
  "insecure-registries": ["10.0.0.0/8", "127.0.0.0/8"]
}
EOF

  msg "enable docker service"
  $SUDO systemctl enable docker

  msg "restart docker service"
  $SUDO systemctl daemon-reload
  $SUDO systemctl restart docker

  if groups | grep -qwv docker; then
    msg "add docker group to $USER"
    $SUDO usermod -aG docker $USER
    msg "logout and login again or reboot to take effect"
  fi
}

# @base {
# AUTO-GENERATED, DO NOT EDIT!

function msg {
  echo "> $@"
}

function err {
  echo "> $@" >&2
}

function has() {
  [[ -z "${1##*$2*}" ]] && [[ -z "$2" || -n "$1" ]]
}

# @base }

# @network {
# AUTO-GENERATED, DO NOT EDIT!

OFFLINE="${OFFLINE:-n}"                           # no external network
MIRROR="${MIRROR:-auto}"                          # mirror option to use
MIRRORS="yes no auto"                             # valid mirror options
USE_MIRROR="${USE_MIRROR:-n}"                     # need to use mirror
MIRROR_URL="${MIRROR_URL:-https://iffi.me/proxy}" # mirror or http proxy to use
URL_TO_CHECK="${URL_TO_CHECK:-https://raw.githubusercontent.com}"

# the target URL is reachable but the response may be 4xx or 5xx
function can_access() {
   curl -IL -s -m 5 $1 >/dev/null 2>&1
}

function check_network() {
  msg "check network"
  if [[ $OFFLINE != 'y' ]]; then
    if ! can_access example.com; then
      msg "can't access external network"
      OFFLINE="y"
    fi
  fi
  msg "offline mode: $OFFLINE"
}

function check_mirror() {
  msg "use mirror or not"

  local mirror;
  if [[ $MIRROR == "auto" ]]; then
    mirror="yes" # default as needed
    if [[ $OFFLINE == "y" ]]; then
      msg "offline mode, assume mirror is needed"
    else
      local target="$URL_TO_CHECK"
      msg "check whether we can access $target"
      if can_access $target; then
        msg "curl $target is OK"
        mirror="no"
      else
        msg "curl $target is FAILED"
      fi
    fi
  fi
  if [[ $MIRROR == "yes" || $mirror == "yes" ]]; then
    msg "mirror is needed"
    if can_access $MIRROR_URL; then
      USE_MIRROR="y"
    else
      err "failed to access mirror ($MIRROR_URL)"
    fi
  fi
  msg "use mirror: $USE_MIRROR"
}

function mirror_url() {
  local origin="${1:?argument missing}"

  if [[ $USE_MIRROR == "y" ]]; then
    echo -n "$MIRROR_URL/$origin"
  else
    echo -n "$origin"
  fi
}

# @network }

# @cache {
# AUTO-GENERATED, DO NOT EDIT!

CACHE="${CACHE:-cache}"                                    # the cache directory
CACHE_RUN="${CACHE_RUN:-$CACHE/run/$SELF}"                 # runtime generated files
CACHE_IMAGES="${CACHE_IMAGES:-$CACHE/images}"              # exported container images
CACHE_PACKAGES="${CACHE_PACKAGES:-$CACHE/packages}"        # software packages, e.g. .deb, .tar.gz
CACHE_MANIFESTS="${CACHE_MANIFESTS:-$CACHE/manifests}"     # manifest files in YAML format
CACHE_DOWNLOADING="${CACHE_DOWNLOADING:-$CACHE/.download}" # temporary directory for downloading

function ensure_workdir() {
  msg "ensure workdir"

  for d in $CACHE_RUN $CACHE_IMAGES $CACHE_PACKAGES $CACHE_MANIFESTS $CACHE_DOWNLOADING; do
    if [[ ! -d $d ]]; then
      msg "create dir: $d"
      mkdir -p $d
    fi
  done
}

function download_file() {
  local src="${1:?missing source url}"
  local dst="${2:?missing destination}"

  if [[ -f "$dst" ]]; then
    msg "already exist: $dst"
  else
    msg "download: $src"
    local tmp="$CACHE_DOWNLOADING/$(basename $dst)"
    $RUN curl -fsSL $src > $tmp
    $RUN mkdir -p $(dirname $dst) && mv $tmp $dst
    msg "saved at: $dst"
  fi
}

# @cache }

main "$@"
