#!/bin/bash
set -Eeo pipefail

# Copyright (C) 2018 - 2022 Gunter Miegel coinboot.io
#
# This file is part of Coinboot.
# This software may be modified and distributed under the terms
# of the MIT license.  See the LICENSE file for details.

# Helper script to convert initramfs archive into a Docker container.

display_help() {
  echo
  echo 'Coinbootmaker creates an environment for building Coinboot plugins from a'
  echo 'given Coinboot Initramfs.'
  echo
  echo 'Packaged Coinboot pluings are written to the ./builds directory'
  echo
  echo 'Usage: coinbootmaker [-i] [-h] [-l] [-p <plugin build script path>]'
  echo
  echo '-i                             Interactive mode - opens a shell in the build environment'
  echo '-p <plugin build script path>  Plugin to build'
  echo '-l                             List plugins available to build'
  echo '-h                             Display this help'
  echo
}

list_plugins() {
  echo
  echo 'Available plugin build scripts'
  echo
  pushd . > /dev/null
  cd src
  find . -type f  ! -wholename '*\/upstream*' -name "*.yaml" -printf '%P\n'
  popd  > /dev/null
  echo
  echo 'Usage: ./coinbootmaker -p <plugin build script path>'
  echo

}

while getopts "ip:lh" opt; do
  case $opt in
    i)
      interactive=true
      ;;
    p)
      plugin=$OPTARG
      ;;
    l)
      list_plugins
      exit 1
      ;;
    h)
      display_help
      exit 1
      ;;
    \?)
      echo "Invalid option: -$OPTARG" >&2
      ;;
  esac
done

shift $((OPTIND -1))

WGET='wget --retry-connrefused --waitretry=5 --read-timeout=20 --timeout=15 -t 0'
CURL='curl --max-time 5 --retry-max-time 20 --retry 999'
CACHE_DIR=$(readlink -f ./cache)
GITHUB_REPO=frzb/coinboot
RELEASE=${RELEASE:-latest}
## initramfs and kernel vmlinuz ##
# RELEASE is set via an environment variable under ./conf/environment
# If the value is 'latest' we determine the latest release, else we use the set value.

if [ $RELEASE = latest ]; then
  RESPONSE=$($CURL --silent "https://api.github.com/repos/${GITHUB_REPO}/tags")
  sleep 5
  while ! TAG=$(echo $RESPONSE | jq -r '[ .[].name | select(test("^pre.*") | not) ] | sort | last'  ); do
    echo "Calling the Github API has failed, repeat ..."
    RESPONSE=$($CURL --silent "https://api.github.com/repos/${GITHUB_REPO}/tags")
    sleep 5
  done
  echo "Coinbootmaker is using the latest (default) Coinboot release: $TAG"
else
  TAG=$RELEASE
  echo "Coinbootmaker is using Coinboot release: $TAG"
fi

DOWNLOAD_URL=https://github.com/${GITHUB_REPO}/releases/download/${TAG}

if [ -z $KERNEL ]; then
  KERNEL=5.11.0-46-generic
fi
INITRAMFS=coinboot-initramfs-$KERNEL

if ! [ -f $CACHE_DIR/$INITRAMFS ]; then
$WGET $DOWNLOAD_URL/$INITRAMFS -P $CACHE_DIR
fi

BASEDIR=$PWD
LOWER=/tmp/$(basename $INITRAMFS)_extracted_by_coinbootmaker/lower
UPPER=/tmp/$(basename $INITRAMFS)_extracted_by_coinbootmaker/upper
WORKING_DIRECTORY=/tmp/$(basename $INITRAMFS)_extracted_by_coinbootmaker/working_dir
MERGED=/tmp/$(basename $INITRAMFS)_extracted_by_coinbootmaker/merged

# Initial Cleanup

while sudo runc list | grep coinbootmaker | grep -q running; do
  echo 'Waiting for Coinbootmaker container to be stopped ...'
  sudo runc kill coinbootmaker KILL
  sleep 1
done

while sudo runc list | grep coinbootmaker | grep -q stopped; do
  echo 'Waiting for Coinbootmaker container to be cleaned up ...'
  sudo runc delete coinbootmaker
  sleep 1
done

while sudo ip link | grep -q cbm-host; do
  echo 'Waiting for Coinbootmaker network interface to be cleaned up ...'
  sudo ip link delete cbm-host
  sleep 1
done

while sudo ip netns | grep -q coinbootmaker; do
  echo 'Waiting for Coinbootmaker network namespace to be cleaned up ...'
  sudo ip netns delete coinbootmaker
  sleep 1
done

if mountpoint -q $MERGED; then
  sudo umount $MERGED
fi

sudo rm -rf $UPPER $LOWER $WORKING_DIRECTORY $MERGED

# End of initial Cleanup

sudo mkdir -p $UPPER $LOWER $WORKING_DIRECTORY $MERGED
# We create our own TMPFS.
# Beforehand we tried to use '/dev/shm' for our purposes but it is mounted with
# a 'nosuid' flag so that all commands inside the container that leverage SUID
# like e.g. 'sudo' are failing.
# TODO: Check/Define size of TMPFS
sudo mount -t overlay overlay -o lowerdir=$LOWER,upperdir=$UPPER,workdir=$WORKING_DIRECTORY $MERGED
sudo mkdir -p $LOWER/rootfs

# Do some file work in the base/lower directory
cd $LOWER/rootfs

# We have to use 'sudo' for 'cpio' else the ownership of the files in the
# archive is messed up.
# We just extract the nested initramfs archive
zstd -d $CACHE_DIR/$INITRAMFS -c | sudo cpio -idm --quiet "rootfs.czst"
zstd -d rootfs.czst -c | sudo cpio -idm --quiet

# The nested initramfs archive can be removed now
sudo rm rootfs.czst

# Adapt nameserver settings.
# resolv.conf is a symling to the systemd stub resolver which we have to delete beforehand.
sudo rm etc/resolv.conf
sudo tee etc/resolv.conf << EOF 1> /dev/null
nameserver 1.1.1.1
EOF

sudo tee etc/hosts << EOF 1> /dev/null
127.0.1.1 coinbootmaker
EOF

[ -d $BASEDIR/cache/apt ] || mkdir $BASEDIR/cache/apt

cd $LOWER
# Generate container spec file config.json
# We have to manipulate the config.json created by 'runc spec'.
# At first I tried to handle this with jq - this was a nice exercise.
# But we have a further dependency to jq in this case so we
# now go with just putting the JSON in this file as here-document.
# So we omit the jq limbo and the dependency to jq.
# We use the same set of capabilities as Docker by default does.
#https://github.com/moby/moby/blob/master/oci/defaults.go#L14-L30
sudo tee ./config.json << EOF 1> /dev/null
{
        "ociVersion": "1.0.0",
        "process": {
                "terminal": false,
                "user": {
                        "uid": 0,
                        "gid": 0
                },
                "args": ["/usr/bin/sleep", "infinity"
                ],
                "env": [
                        "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
                        "TERM=xterm",
                        "KERNEL_RELEASE=$(echo $INITRAMFS | grep -oP '(?<=coinboot-initramfs-)[^\s]+')",
                        "LC_ALL=en_US.UTF-8"
                ],
                "cwd": "/",
                "capabilities": {
                        "bounding": [
                                "CAP_CHOWN",
                                "CAP_DAC_OVERRIDE",
                                "CAP_FSETID",
                                "CAP_FOWNER",
                                "CAP_MKNOD",
                                "CAP_NET_RAW",
                                "CAP_SETGID",
                                "CAP_SETUID",
                                "CAP_SETFCAP",
                                "CAP_SETPCAP",
                                "CAP_NET_BIND_SERVICE",
                                "CAP_SYS_CHROOT",
                                "CAP_KILL",
                                "CAP_AUDIT_WRITE"
                        ],
                        "effective": [
                                "CAP_CHOWN",
                                "CAP_DAC_OVERRIDE",
                                "CAP_FSETID",
                                "CAP_FOWNER",
                                "CAP_MKNOD",
                                "CAP_NET_RAW",
                                "CAP_SETGID",
                                "CAP_SETUID",
                                "CAP_SETFCAP",
                                "CAP_SETPCAP",
                                "CAP_NET_BIND_SERVICE",
                                "CAP_SYS_CHROOT",
                                "CAP_KILL",
                                "CAP_AUDIT_WRITE"
                        ],
                        "inheritable": [
                                "CAP_CHOWN",
                                "CAP_DAC_OVERRIDE",
                                "CAP_FSETID",
                                "CAP_FOWNER",
                                "CAP_MKNOD",
                                "CAP_NET_RAW",
                                "CAP_SETGID",
                                "CAP_SETUID",
                                "CAP_SETFCAP",
                                "CAP_SETPCAP",
                                "CAP_NET_BIND_SERVICE",
                                "CAP_SYS_CHROOT",
                                "CAP_KILL",
                                "CAP_AUDIT_WRITE"
                        ],
                        "permitted": [
                                "CAP_CHOWN",
                                "CAP_DAC_OVERRIDE",
                                "CAP_FSETID",
                                "CAP_FOWNER",
                                "CAP_MKNOD",
                                "CAP_NET_RAW",
                                "CAP_SETGID",
                                "CAP_SETUID",
                                "CAP_SETFCAP",
                                "CAP_SETPCAP",
                                "CAP_NET_BIND_SERVICE",
                                "CAP_SYS_CHROOT",
                                "CAP_KILL",
                                "CAP_AUDIT_WRITE"
                        ],
                        "ambient": [
                                "CAP_CHOWN",
                                "CAP_DAC_OVERRIDE",
                                "CAP_FSETID",
                                "CAP_FOWNER",
                                "CAP_MKNOD",
                                "CAP_NET_RAW",
                                "CAP_SETGID",
                                "CAP_SETUID",
                                "CAP_SETFCAP",
                                "CAP_SETPCAP",
                                "CAP_NET_BIND_SERVICE",
                                "CAP_SYS_CHROOT",
                                "CAP_KILL",
                                "CAP_AUDIT_WRITE"
                        ]
                },
                "rlimits": [
                        {
                                "type": "RLIMIT_NOFILE",
                                "hard": 1024,
                                "soft": 1024
                        }
                ],
                "noNewPrivileges": true
        },
        "root": {
                "path": "rootfs",
                "readonly": false
        },
        "hostname": "coinbootmaker",
        "mounts": [
                {
                        "destination": "/proc",
                        "type": "proc",
                        "source": "proc"
                },
                {
                        "destination": "/dev",
                        "type": "tmpfs",
                        "source": "tmpfs",
                        "options": [
                                "nosuid",
                                "strictatime",
                                "mode=755",
                                "size=65536k"
                        ]
                },
                {
                        "destination": "/dev/pts",
                        "type": "devpts",
                        "source": "devpts",
                        "options": [
                                "nosuid",
                                "noexec",
                                "newinstance",
                                "ptmxmode=0666",
                                "mode=0620",
                                "gid=5"
                        ]
                },
                {
                        "destination": "/dev/shm",
                        "type": "tmpfs",
                        "source": "shm",
                        "options": [
                                "nosuid",
                                "noexec",
                                "nodev",
                                "mode=1777",
                                "size=65536k"
                        ]
                },
                {
                        "destination": "/dev/mqueue",
                        "type": "mqueue",
                        "source": "mqueue",
                        "options": [
                                "nosuid",
                                "noexec",
                                "nodev"
                        ]
                },
                {
                        "destination": "/sys",
                        "type": "sysfs",
                        "source": "sysfs",
                        "options": [
                                "nosuid",
                                "noexec",
                                "nodev",
                                "ro"
                        ]
                },
                {
                        "destination": "/sys/fs/cgroup",
                        "type": "cgroup",
                        "source": "cgroup",
                        "options": [
                                "nosuid",
                                "noexec",
                                "nodev",
                                "relatime",
                                "ro"
                        ]
                },
                {
                        "destination": "/mnt",
                        "type": "none",
                        "source": "$BASEDIR",
                        "options": [
                                "bind",
                                "rw"
                        ]
                },
                {
                        "destination": "/mnt/plugin",
                        "type": "none",
                        "source": "$UPPER",
                        "options": [
                                "bind",
                                "ro"
                        ]
                },
                {
                        "destination": "/var/cache/apt",
                        "type": "none",
                        "source": "$BASEDIR/cache/apt",
                        "options": [
                                "bind",
                                "rw"
                        ]
                }
        ],
        "linux": {
                "resources": {
                        "devices": [
                                {
                                        "allow": false,
                                        "access": "rwm"
                                }
                        ]
                },
                "namespaces": [
                        {
                                "type": "pid"
                        },
                        {
                                "type": "network",
                                "path": "/var/run/netns/coinbootmaker"
                        },
                        {
                                "type": "ipc"
                        },
                        {
                                "type": "uts"
                        },
                        {
                                "type": "mount"
                        }
                ],
                "maskedPaths": [
                        "/proc/kcore",
                        "/proc/latency_stats",
                        "/proc/timer_list",
                        "/proc/timer_stats",
                        "/proc/sched_debug",
                        "/sys/firmware",
                        "/proc/scsi"
                ],
                "readonlyPaths": [
                        "/proc/asound",
                        "/proc/bus",
                        "/proc/fs",
                        "/proc/irq",
                        "/proc/sys",
                        "/proc/sysrq-trigger"
                ]
        }
}
EOF

# We hijack the default Docker bridge docker0 for network access of our container.
# Least effort approch to determine a free und usable adress:
# Pick the highest available address of the 172.17.0.0. network which is 172.17.255.254/16.
# There are good chances that this address is unused.
# TODO: We assume that the default Docker network 172.17.0.0/16 exists.
# This is pretty naive and we need to handle this better - at least to add
# exeption handling.

sudo ip link add name cbm-host type veth peer name cbm-guest
sudo ip link set cbm-host up
sudo brctl addif docker0 cbm-host
sudo ip netns add coinbootmaker
sudo ip link set cbm-guest netns coinbootmaker

cd $MERGED

# TODO: We should go rootless: https://github.com/opencontainers/runc/pull/774
sudo runc run -d coinbootmaker

# This commands can only be executed if the container is already running.
# So let's wait until it is ready.
while ! sudo runc list | grep -q coinbootmaker; do
  echo 'Waiting for Coinbootmaker container...'
  sleep 1
done

sudo ip netns exec coinbootmaker ip link set cbm-guest name eth0
sudo ip netns exec coinbootmaker ip addr add 172.17.255.254/16 dev eth0
sudo ip netns exec coinbootmaker ip link set eth0 up
sudo ip netns exec coinbootmaker ip route add default via 172.17.0.1

if [ ! -z $interactive ] && [ $interactive = 'true' ]; then
  sudo runc exec -t coinbootmaker /usr/bin/bash
elif [[ ./src/$plugin  == *.yaml ]]; then
 sudo runc exec --cwd /mnt/build coinbootmaker create_plugin.py start
 sudo runc exec --cwd /mnt/build coinbootmaker /usr/bin/bash -e < <(yq eval '.run' $BASEDIR/src/$plugin)
 NAME=$(yq eval '.archive_name' $BASEDIR/src/$plugin)
 VERSION=$(yq eval '.version' $BASEDIR/src/$plugin)
 BUILD_DATE=$(date +%Y%m%d.%H%M)
 sudo runc exec --cwd /mnt/build coinbootmaker create_plugin.py finish coinboot_${NAME}_${VERSION}_$BUILD_DATE
else
  sudo runc exec coinbootmaker /usr/bin/bash -c "cd /mnt/build/ && /mnt/src/$plugin"
fi

# Cleanup
sudo runc kill coinbootmaker KILL
while ! sudo runc list | grep coinbootmaker | grep -q stopped; do
  echo 'Waiting for Coinbootmaker container to be stopped ...'
  sleep 1
done
sudo runc delete coinbootmaker

# Dismantle the networking limbo we have done before.
sudo ip link delete cbm-host
sudo ip netns delete coinbootmaker

echo "Cleaning up temporary working directories ..."
cd $BASEDIR
sudo umount --quiet $MERGED
sudo rm -rf $BASEDIR/plugin $UPPER $LOWER $WORKING_DIRECTORY $MERGED
