#!/bin/bash

# Wrapper script for gen_livepatch. Runs gen_livepatch inside
# of a docker container. Makes it much easier to install dependencies,
# control build environment, etc.
#
set -o pipefail
# shellcheck source=SPECS/kpatch/scripts/livepatch.sh
source ./livepatch.sh 2>/dev/null || source /usr/lib/livepatch.sh 2>/dev/null || {
    echo "Error: livepatch.sh not found in current directory or /usr/lib"
    exit 1
}

AUTO_LIVEPATCH_DIR=$(pwd)/auto_livepatch_workspace
GEN_LIVEPATCH_DIR=/var/opt/gen_livepatch
GEN_LIVEPATCH_WORKSPACE=${GEN_LIVEPATCH_DIR}/gen_livepatch_workspace
OUTPUT_DIR=$(pwd)/output
LOGS_DIR=$OUTPUT_DIR/logs/
PATCHES_DIR=$AUTO_LIVEPATCH_DIR/patches
FAILED=0
EXPORT_DEBUGINFO=0
DESC_GIVEN=0
RPM_DESC_GIVEN=0
SRC_RPM_LOCAL_PATH=""
DEBUGINFO_LOCAL_PATH=""
SIGNC=""
SIGNC_UID=0
SIGNC_UNAME=""
TMP_DIR=/tmp
BLD_DIR=/build
BLDLIB_DIR=/bldmnt
AUTH_DIR=/etc/signing

DOCKERFILE_DIR=${DOCKERFILE_DIR:-/usr/share/livepatch/dockerfiles/}
DOCKER_KPATCH_BUILDLOG=/root/.kpatch/build.log
DOCKER=/usr/bin/docker
DOCKER_LIVEPATCH_OUTPUT_DIR=${GEN_LIVEPATCH_DIR}/livepatches
DOCKER_IMAGE_NAME=""
DOCKER_CONTAINER_NAME=""
DOCKERFILE_NAME=""

check_dockerfile() {
    local dockerfile=$DOCKERFILE_DIR/$DOCKERFILE_NAME
    [[ -f "$dockerfile" ]] || error "Dockerfile '$dockerfile' is either missing or unreadable due to insufficient permissions."
}

# just get what we need for this script from the arguments.
# do some error checking here so that it will error out before creating
# docker container, which could take some time if the image is not
# already created.
parse_args() {
    if [ $# -eq 0 ]; then
        print_help 1
    fi

    mkdir -p "$PATCHES_DIR"
    local opt=""
    local options=( "-s" "-v" "-p" "-o" "-h" "--help" "-k" "-n" "-R" "--export-debuginfo" "-d" "--rpm" "--rpm-version" "--rpm-release" "--rpm-desc" "--verbose" "--sign" )
    local no_arg_options=( "-R" "--export-debuginfo" "-h" "--help" "--rpm" "--verbose" "--sign" )

    while (( "$#" )); do
        arg=$1
        if [[ $1 == -* ]]; then
            opt=$1
            if ! is_in_array "$opt" ${options[*]}; then
                error "Unknown option: $opt" >&2
            elif [[ $1 == -h || $1 == --help ]]; then
                print_help 0
            elif [[ $1 == --export-debuginfo ]]; then
                EXPORT_DEBUGINFO=1
            elif [[ $1 == --sign ]]; then
                SIGNC=sign_livepatch
                [[ -f "$SIGNC" ]] || error "Signc does not exist in the system: $SIGNC"
                SIGNC_UID=$(id -u)
                SIGNC_UNAME="$(id -u -n)"
            elif ! is_in_array "$opt" ${no_arg_options[*]} && [[ ($2 == -* || -z $2) ]]; then
                error "$1 needs at least one argument"
            elif  [[ $3 != -* && $opt != -p && -n $3 ]] && ! is_in_array "$opt" ${no_arg_options[*]} ; then
                error "$1 only takes one argument"
            fi
        else
            case "$opt" in
                -p)
                    patch_given=1
                    cp "$1" $PATCHES_DIR > /dev/null || error "Could not find patch file $1"
                    arg="$GEN_LIVEPATCH_WORKSPACE/patches/$(basename "$arg")"
                    ;;
                -k)
                    KERNEL_VERSION_RELEASE=$1
                    ;;
                -o)
                    OUTPUT_DIR=$1
                    LOGS_DIR=$OUTPUT_DIR/logs/
                    ;;
                -d)
                    DESC_GIVEN=1
                    cp -f  "$1" "$AUTO_LIVEPATCH_DIR/description.txt" > /dev/null || error "Description file $1 not found"
                    ;;
                --rpm-desc)
                    RPM_DESC_GIVEN=1
                    cp -f "$1" "$AUTO_LIVEPATCH_DIR/rpm-description.txt" > /dev/null || error "RPM description file $1 not found"
                    ;;
                -s)
                    [[ -f "$1" ]] || error "Local source rpm $1 not present"
                    SRC_RPM_LOCAL_PATH=$1
                    ;;
                -v)
                    [[ -f "$1" ]] || error "Local debuginfo rpm $1 not present"
                    DEBUGINFO_LOCAL_PATH=$1
                    ;;
                esac
        fi

        # pass all arguments except for output directory into docker container
        if [[ $opt != "-o" ]] && [[ $opt != "-d" ]] && [[ $opt != "--rpm-desc" ]] && [[ $opt != "-s" ]] && [[ $opt != "-v" ]]; then
            if [ -z "$ARGS" ]; then
                ARGS="$arg"
            else
                ARGS="$ARGS $arg"
            fi
        fi

        # shift to the next argument
        shift
    done

    [[ -z "$patch_given" ]] && error "Please input at least one patch file"
    [[ -n "$SRC_RPM_LOCAL_PATH" ]] && ARGS="$ARGS -s $GEN_LIVEPATCH_WORKSPACE/$(basename "$SRC_RPM_LOCAL_PATH")"
    [[ -n "$DEBUGINFO_LOCAL_PATH" ]] && ARGS="$ARGS -v $GEN_LIVEPATCH_WORKSPACE/$(basename "$DEBUGINFO_LOCAL_PATH")"
    [[ $DESC_GIVEN -eq 1 ]] && ARGS="$ARGS -d $GEN_LIVEPATCH_WORKSPACE/description.txt"
    [[ $RPM_DESC_GIVEN -eq 1 ]] && ARGS="$ARGS --rpm-desc $GEN_LIVEPATCH_WORKSPACE/rpm-description.txt"
    [[ -n "$SIGNC" ]] && ARGS="$ARGS --signer-id $SIGNC_UID --signer-name $SIGNC_UNAME"

    ARGS="$ARGS -o ${DOCKER_LIVEPATCH_OUTPUT_DIR}"

    [[ -z "$KERNEL_VERSION_RELEASE" ]] && KERNEL_VERSION_RELEASE=$(uname -r)

    [[ $KERNEL_VERSION_RELEASE =~ \.ph[0-9]+ ]] && PH_TAG="${BASH_REMATCH:1}"

    if [[ "$PH_TAG" != *ph* ]]; then
        echo "Wrong kernel version detected: $KERNEL_VERSION_RELEASE"
        echo "Check for typos. Make sure it is in the same format as uname -r"
        exit 1
    fi
}

config_container() {
    echo "Configuring docker container"
    DOCKER_IMAGE_NAME=livepatch-$PH_TAG
    DOCKER_CONTAINER_NAME=$PH_TAG-livepatch-container
    DOCKERFILE_NAME=Dockerfile.$PH_TAG
    local signc_mnts=()
    check_dockerfile

    if [[ -n "$(docker ps -a -f "name=$DOCKER_CONTAINER_NAME" -q 2> /dev/null)" ]]; then
        # clean up old docker container if it is still lurking around
        $DOCKER rm -f "$DOCKER_CONTAINER_NAME" || error "Deletion of old docker container failed"
        echo "Deleted Docker container from the previous run."
    fi
    $DOCKER rmi -f "$DOCKER_IMAGE_NAME" > /dev/null
    [[ -n "$SIGNC" ]] && signc_mnts+=(
        -v "$TMP_DIR":"$TMP_DIR"
        -v "$AUTH_DIR":"$AUTH_DIR"
        -v "$BLD_DIR":"$BLD_DIR"
        -v "$BLDLIB_DIR":"$BLDLIB_DIR"
    )

    echo -e "Building Docker image. This may take a few minutes.\n"
    $DOCKER buildx build --network=host -f $DOCKERFILE_DIR/"$DOCKERFILE_NAME" -t "$DOCKER_IMAGE_NAME" . > /dev/null || error "Failed to create Livepatch docker image."
    $DOCKER run "${signc_mnts[@]}" -t -d --name "$DOCKER_CONTAINER_NAME" "$DOCKER_IMAGE_NAME" /bin/bash > /dev/null || error "Creation of docker container failed"

    # copy all necessary files into docker container, put patches into patches folder
    $DOCKER cp $PATCHES_DIR "$DOCKER_CONTAINER_NAME":$GEN_LIVEPATCH_WORKSPACE/ || error "Failed to copy patch files to docker container."
    if [[ $DESC_GIVEN -eq 1 ]]; then
       $DOCKER cp "$AUTO_LIVEPATCH_DIR/description.txt" "$DOCKER_CONTAINER_NAME":$GEN_LIVEPATCH_WORKSPACE/ || error "Failed to copy livepatch module description file to docker container."
    fi
    if [[ $RPM_DESC_GIVEN -eq 1 ]]; then
       $DOCKER cp "$AUTO_LIVEPATCH_DIR/rpm-description.txt" "$DOCKER_CONTAINER_NAME":$GEN_LIVEPATCH_WORKSPACE/ || error "Failed to copy rpm description file to docker container."
    fi
    if [[ -n "$SRC_RPM_LOCAL_PATH" ]]; then
       $DOCKER cp "$SRC_RPM_LOCAL_PATH" "$DOCKER_CONTAINER_NAME":$GEN_LIVEPATCH_WORKSPACE/ || error "Failed to copy kernel source rpm to docker container."
    fi
    if [[ -n "$DEBUGINFO_LOCAL_PATH" ]]; then
       $DOCKER cp "$DEBUGINFO_LOCAL_PATH" "$DOCKER_CONTAINER_NAME":$GEN_LIVEPATCH_WORKSPACE/ || error "Failed to copy kernel debuginfo rpm to docker container."
    fi
    if [[ -n "$SIGNC" ]]; then
       $DOCKER cp "$SIGNC" "$DOCKER_CONTAINER_NAME":/usr/bin || error "Failed to copy sign_livepatch script."
    fi
}

save_kpatch_buildlog() {
    echo "Copying kpatch build log from docker container to ${LOGS_DIR}"
    mkdir -p ${LOGS_DIR}
    if docker exec "$DOCKER_CONTAINER_NAME" test -f "$DOCKER_KPATCH_BUILDLOG"; then
        $DOCKER cp "$DOCKER_CONTAINER_NAME":"$DOCKER_KPATCH_BUILDLOG" ${LOGS_DIR} > /dev/null || echo "Couldn't copy kpatch build log"
    fi
}

cleanup() {
    if [[ -n "$DOCKER_CONTAINER_NAME" ]]  && [[ -n "$(docker ps -a -f "name=$DOCKER_CONTAINER_NAME" -q 2> /dev/null)" ]]; then
        mkdir -p "$OUTPUT_DIR"
        $DOCKER cp "$DOCKER_CONTAINER_NAME":$DOCKER_LIVEPATCH_OUTPUT_DIR/. "$OUTPUT_DIR" || echo "Failed to copy gen_livepatch output dir from docker container."
        [[ $FAILED -eq 1 ]] && save_kpatch_buildlog
        docker rm -f "$DOCKER_CONTAINER_NAME" || echo "Failed to delete existing docker container: $DOCKER_CONTAINER_NAME"
    fi

    # delete workspace dir
    rm -rf $AUTO_LIVEPATCH_DIR
}

# make sure things are cleaned up if interrupted, especially because this is a long process
trap cleanup SIGINT SIGTERM EXIT
check_permission
parse_args "$@"

config_container

# run gen_livepatch script
$DOCKER exec -w "$GEN_LIVEPATCH_DIR" "$DOCKER_CONTAINER_NAME" /usr/bin/gen_livepatch $ARGS || FAILED=1
