#!/bin/sh

. /lib/functions.sh

BUSYBOX_EXE=$(command -v busybox)
BUSYBOX_DEV_LIST=""
BUSYBOX_LIB_MOUNT_LIST=""
BUNDLE_PATH=""

ENVNAME=""
DU_URL=""
RELOAD=1

# Make sure busybox is present
if [ ! -x "${BUSYBOX_EXE}" ]; then
    echo "ERROR: Failed to find busybox binary (${BUSYBOX_EXE})" 1>&2
    exit 1
fi

set_bundle_path() {
	local root

	config_load swmodd
	root=$(uci_get swmodd.globals.root)
	if [ ! -d "${root}" ]; then
		echo "Base path [$root] not configured/present"
		exit 1
	fi

	if [ -z "${ENVNAME}" ]; then
		ENVNAME=$(uci_get swmodd.@execenv[0].name)

		echo "Use default env [$ENVNAME]"
	fi

	if [ -n "${root}" ] && [ -n "${ENVNAME}" ]; then
		BUNDLE_PATH="${root}"
		if [ "${root: -1}" != '/' ]; then
			BUNDLE_PATH="${root}/"
		fi

		BUNDLE_PATH="${BUNDLE_PATH}${ENVNAME}"
		mkdir -p "${BUNDLE_PATH}"
	else
		echo "ERROR: Execution environment [$ENVNAME] not defined"
		exit 1
	fi
}

copy_configuration()
{
    path="${1}"
    name="${2}"
    cat <<EOF >> "${path}/config.json"
  {
	"ociVersion": "1.0.0",
	"process": {
		"terminal": true,
		"user": {
			"uid": 0,
			"gid": 0
		},
		"args": [
			"init"
		],
		"env": [
			"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
		],
		"cwd": "/",
		"capabilities": {
			"bounding": [
				"CAP_AUDIT_WRITE",
				"CAP_KILL",
				"CAP_NET_BIND_SERVICE",
				"CAP_AUDIT_CONTROL",
				"CAP_AUDIT_READ",
				"CAP_CHOWN",
				"CAP_FOWNER",
				"CAP_FSETID",
				"CAP_IPC_LOCK",
				"CAP_IPC_OWNER",
				"CAP_LEASE",
				"CAP_LINUX_IMMUTABLE",
				"CAP_MKNOD",
				"CAP_NET_ADMIN",
				"CAP_NET_BROADCAST",
				"CAP_NET_RAW",
				"CAP_SETGID",
				"CAP_SETFCAP",
				"CAP_SETUID",
				"CAP_SYS_CHROOT"
			],
			"effective": [
				"CAP_AUDIT_WRITE",
				"CAP_KILL",
				"CAP_NET_BIND_SERVICE",
				"CAP_AUDIT_CONTROL",
				"CAP_AUDIT_READ",
				"CAP_CHOWN",
				"CAP_FOWNER",
				"CAP_FSETID",
				"CAP_IPC_LOCK",
				"CAP_IPC_OWNER",
				"CAP_LEASE",
				"CAP_LINUX_IMMUTABLE",
				"CAP_MKNOD",
				"CAP_NET_ADMIN",
				"CAP_NET_BROADCAST",
				"CAP_NET_RAW",
				"CAP_SETGID",
				"CAP_SETFCAP",
				"CAP_SETUID",
				"CAP_SYS_CHROOT"
			],
			"inheritable": [
				"CAP_AUDIT_WRITE",
				"CAP_KILL",
				"CAP_NET_BIND_SERVICE",
				"CAP_AUDIT_CONTROL",
				"CAP_AUDIT_READ",
				"CAP_CHOWN",
				"CAP_FOWNER",
				"CAP_FSETID",
				"CAP_IPC_LOCK",
				"CAP_IPC_OWNER",
				"CAP_LEASE",
				"CAP_LINUX_IMMUTABLE",
				"CAP_MKNOD",
				"CAP_NET_ADMIN",
				"CAP_NET_BROADCAST",
				"CAP_NET_RAW",
				"CAP_SETGID",
				"CAP_SETFCAP",
				"CAP_SETUID",
				"CAP_SYS_CHROOT"
			],
			"permitted": [
				"CAP_AUDIT_WRITE",
				"CAP_KILL",
				"CAP_NET_BIND_SERVICE",
				"CAP_AUDIT_CONTROL",
				"CAP_AUDIT_READ",
				"CAP_CHOWN",
				"CAP_FOWNER",
				"CAP_FSETID",
				"CAP_IPC_LOCK",
				"CAP_IPC_OWNER",
				"CAP_LEASE",
				"CAP_LINUX_IMMUTABLE",
				"CAP_MKNOD",
				"CAP_NET_ADMIN",
				"CAP_NET_BROADCAST",
				"CAP_NET_RAW",
				"CAP_SETGID",
				"CAP_SETFCAP",
				"CAP_SETUID",
				"CAP_SYS_CHROOT"
			],
			"ambient": [
				"CAP_AUDIT_WRITE",
				"CAP_KILL",
				"CAP_NET_BIND_SERVICE",
				"CAP_AUDIT_CONTROL",
				"CAP_AUDIT_READ",
				"CAP_CHOWN",
				"CAP_FOWNER",
				"CAP_FSETID",
				"CAP_IPC_LOCK",
				"CAP_IPC_OWNER",
				"CAP_LEASE",
				"CAP_LINUX_IMMUTABLE",
				"CAP_MKNOD",
				"CAP_NET_ADMIN",
				"CAP_NET_BROADCAST",
				"CAP_NET_RAW",
				"CAP_SETGID",
				"CAP_SETFCAP",
				"CAP_SETUID",
				"CAP_SYS_CHROOT"
			]
		},
		"rlimits": [                           
                        {                              
                                "type": "RLIMIT_NOFILE",
                                "hard": 1024,           
                                "soft": 1024            
                        }                               
                ],
		"noNewPrivileges": false
	},
	"root": {
		"path": "rootfs",
		"readonly": false
	},
	"hostname": "${name}",
	"mounts": [
		{
			"destination": "/proc",
			"type": "proc",
			"source": "proc"
		},
		{
			"destination": "/dev",
			"type": "tmpfs",
			"source": "none",
			"options": [
				"nosuid",
				"strictatime",
				"mode=755",
				"size=65536k"
			]
		},
		{
			"destination": "/dev/pts",
			"type": "devpts",
			"source": "devpts",
			"options": [
				"rw",
				"nosuid",
				"noexec",
				"newinstance",
				"ptmxmode=0666",
				"mode=0620",
				"gid=5",
				"max=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",
				"relatime",
				"ro"
			]
		},
		{
			"destination": "/sys/fs/cgroup",
			"type": "cgroup",
			"source": "cgroup",
			"options": [
				"nosuid",
				"noexec",
				"nodev",
				"relatime",
				"ro"
			]
EOF

if [ -z "${BUSYBOX_DEV_LIST}" ] && [ -z "${BUSYBOX_LIB_MOUNT_LIST}" ]; then
    cat <<EOF >> "${path}/config.json"
		}
EOF
else
    for dev in ${BUSYBOX_DEV_LIST}; do
        cat <<EOF >> "${path}/config.json"
		},
		{
			"destination": "/dev/${dev}",
			"type": "none",
			"source": "/dev/${dev}",
			"options": [
				"bind",
				"optional",
				"create=file"
			]
EOF
    done

    for dir in ${BUSYBOX_LIB_MOUNT_LIST}; do
        cat <<EOF >> "${path}/config.json"
		},
		{
			"destination": "/${dir}",
			"type": "none",
			"source": "/${dir}",
			"options": [
				"ro",
				"bind"
			]
EOF
    done

    cat <<EOF >> "${path}/config.json"
		}
EOF

fi
    cat <<EOF >> "${path}/config.json"
	],
	"linux": {
		"uidMappings": [
			{
				"containerID": 0,
				"hostID": 0,
				"size": 1
			}
		],
		"gidMappings": [
			{
				"containerID": 0,
				"hostID": 0,
				"size": 1
			}
		],
		"resources": {
			"devices": [
				{
					"allow": false,
					"access": "rwm"
				}
			]
		},
		"namespaces": [
			{
				"type": "pid"
			},
			{
				"type": "uts"
			},
			{
				"type": "network",
				"path": "/var/run/netns/${name}"
			},
			{
				"type": "mount"
			}
		]
	}
}
EOF
}

in_userns() {
    [ -e /proc/self/uid_map ] || { echo no; return; }
    while read -r line; do
        fields="$(echo "$line" | awk '{ print $1 " " $2 " " $3 }')"
        if [ "${fields}" = "0 0 4294967295" ]; then
            echo no;
            return;
        fi
        if echo "${fields}" | grep -q " 0 1$"; then
            echo userns-root;
            return;
        fi
    done < /proc/self/uid_map

    if [ -e /proc/1/uid_map ]; then
        if [ "$(cat /proc/self/uid_map)" = "$(cat /proc/1/uid_map)" ]; then
            echo userns-root
                return
        fi
    fi
    echo yes
}

USERNS="$(in_userns)"

install_busybox()
{
    rootfs="${1}"
    container_name="${2}"
    fstree="\
        ${rootfs}/dev \
        ${rootfs}/home \
        ${rootfs}/root \
        ${rootfs}/etc \
        ${rootfs}/etc/config \
        ${rootfs}/etc/init.d \
        ${rootfs}/etc/rc.d \
        ${rootfs}/etc/swmodd \
        ${rootfs}/etc/hotplug.d \
        ${rootfs}/bin \
        ${rootfs}/sbin \
        ${rootfs}/usr/bin \
        ${rootfs}/sbin \
        ${rootfs}/usr/sbin \
        ${rootfs}/proc \
        ${rootfs}/sys \
        ${rootfs}/mnt \
        ${rootfs}/tmp \
        ${rootfs}/var/log \
        ${rootfs}/var/lock \
        ${rootfs}/var/run \
        ${rootfs}/dev/pts \
        ${rootfs}/lib \
        ${rootfs}/usr/lib \
        ${rootfs}/lib64 \
        ${rootfs}/usr/lib64"

    # shellcheck disable=SC2086
    mkdir -p ${fstree} || return 1
    # shellcheck disable=SC2086
    chmod 755 ${fstree} || return 1

    # minimal devices needed for busybox
    if [ "${USERNS}" = "yes" ]; then
        for dev in tty console tty0 tty1 ram0 null urandom; do
            BUSYBOX_DEV_LIST="${BUSYBOX_DEV_LIST} ${dev}"
        done
    fi

    # mount point needed for /lib
    libdirs="lib"
    for dir in ${libdirs}; do
        if [ -d "/${dir}" ] && [ -d "${rootfs}/${dir}" ]; then
            BUSYBOX_LIB_MOUNT_LIST="${BUSYBOX_LIB_MOUNT_LIST} ${dir}"
        fi
    done

    # make /tmp accessible to any user (with sticky bit)
    chmod 1777 "${rootfs}/tmp" || return 1

    # root user defined
    cat <<EOF >> "${rootfs}/etc/passwd"
root:x:0:0:root:/root:/bin/ash
EOF

    cat <<EOF >> "${rootfs}/etc/shadow"
root::18844:0:99999:7:::
EOF

    cat <<EOF >> "${rootfs}/etc/group"
root:x:0:
EOF
    # mount everything
    cat <<EOF >> "${rootfs}/etc/rc.local"
#!/bin/sh

echo "Running rc.local"
/bin/mount -a
/bin/udhcpc -i eth0
EOF

    # executable
    chmod 744 "${rootfs}/etc/rc.local" || return 1

    # launch rcS first then make a console available
    # and propose a shell on the tty, the last one is
    # not needed
    cat <<EOF >> "${rootfs}/etc/init.d/boot"
#!/bin/sh /etc/rc.common

# run this script to get ip
START=9

start() {
        # process user commands
        [ -f /etc/rc.local ] && {
                sh /etc/rc.local
        }

        echo "All init.d scripts started."
}
EOF

    chmod +x "${rootfs}/etc/init.d/boot" || return 1

    cat <<EOF >> "${rootfs}/bin/boot_crun"
#!/bin/sh

for f in \`ls -1 /etc/init.d/\`; do
    /etc/init.d/\$f enable
    /etc/init.d/\$f start
done

/bin/login root
EOF
    chmod +x "${rootfs}/bin/boot_crun" || return 1

    cat <<EOF >> "${rootfs}/etc/inittab"
::sysinit:/etc/init.d/rcS S boot
::shutdown:/etc/init.d/rcS K shutdown
console::respawn:/bin/boot_crun
EOF
    # writable and readable for other
    chmod 644 "${rootfs}/etc/inittab" || return 1

    # Look for the pathname of "default.script" from the help of udhcpc
    DEF_SCRIPT=$(${BUSYBOX_EXE} udhcpc --help 2>&1 | grep -E -- '-s.*Run PROG' | cut -d'/' -f2- | cut -d')' -f1)
    DEF_SCRIPT_DIR=$(dirname /"${DEF_SCRIPT}")
    mkdir -p "${rootfs}/${DEF_SCRIPT_DIR}"
    chmod 744 "${rootfs}/${DEF_SCRIPT_DIR}" || return 1

    cat <<EOF >> "${rootfs}/${DEF_SCRIPT}"
#!/bin/sh
case "\$1" in
  deconfig)
    ip addr flush dev \$interface
    ;;

  renew|bound)
    # flush all the routes
    if [ -n "\$router" ]; then
      ip route del default 2> /dev/null
    fi

    # check broadcast
    if [ -n "\$broadcast" ]; then
      broadcast="broadcast \$broadcast"
    fi

    # add a new ip address
    ip addr add \$ip/\$mask \$broadcast dev \$interface

    if [ -n "\$router" ]; then
      ip route add default via \$router dev \$interface
    fi

    [ -n "\$domain" ] && echo search \$domain > /etc/resolv.conf
    for i in \$dns ; do
      grep "nameserver \$i" /etc/resolv.conf > /dev/null 2>&1
      if [ \$? -ne 0 ]; then
        echo nameserver \$i >> /etc/resolv.conf
      fi
    done
    ;;
esac
exit 0
EOF

    chmod 744 "${rootfs}/${DEF_SCRIPT}" || return 1

    return 0
}

configure_busybox()
{
    rootfs="${1}"

    # copy busybox in the rootfs
    if ! cp "${BUSYBOX_EXE}" "${rootfs}/bin"; then
        echo "ERROR: Failed to copy busybox binary" 1>&2
        return 1
    fi

    # symlink busybox for the commands it supports
    # it would be nice to just use "chroot $rootfs busybox --install -s /bin"
    # but that only works right in a chroot with busybox >= 1.19.0
    (
        cd "${rootfs}/bin" || return 1
        ./busybox --list | grep -v busybox | xargs -n1 ln -s busybox
    )

    # /etc/fstab must exist for "mount -a"
    touch "${rootfs}/etc/fstab"

    # passwd exec must be setuid
    chmod +s "${rootfs}/bin/passwd"
    touch "${rootfs}/etc/shadow"

    return 0
}

copy_prereq()
{
    local rootfs extrapkg

    rootfs="${1}"
    shift
    extrapkg="${@}"
    req_pkg="procd uci opkg ip-full curl libstdcpp6 librt"

    for p in ${req_pkg}; do
        /usr/share/swmodd/opkg_offline "${rootfs}" "${p}"
    done

    for p in ${extrapkg}; do
        /usr/share/swmodd/opkg_offline "${rootfs}" "${p}"
    done


    cp /etc/preinit "${rootfs}/etc/preinit"
    cp /etc/diag.sh "${rootfs}/etc/diag.sh"
    cp /sbin/mount_root "${rootfs}/sbin/mount_root"
    cp /sbin/ip "${rootfs}/sbin/ip"
    cp /etc/rc.common "${rootfs}/etc/rc.common"
    if [ -f "${rootfs}/bin/uclient-fetch" ] && [ ! -f "${rootfs}/usr/bin/wget" ]; then
        ln -sf /bin/uclient-fetch "${rootfs}/usr/bin/wget"
    fi

    return 0;
}

create_container()
{
	local name extrapkg

	name="${1}"
	shift
	extrapkg="${@}"

	# Make sure container already not created
	list=$(crun list 2>&1 | tail -n +2 | grep "${name}" | awk '{print $1}')
	for cont in $list; do
	    if [ "$cont" = "$name" ]; then
		echo "ERROR: Container already exists"$'\n'"$(crun list)" 1>&2
		exit 1;
	    fi
	done

	bundle="$BUNDLE_PATH/$name"
	# Make sure bundle path not already exists
	if [ -d "${bundle}" ]; then
	    echo "ERROR: Bundle ${bundle} already exists, delete it first" 1>&2
	    exit 1;
	fi

	# Create bundle directory
	mkdir "${bundle}"

	# Create rootfs
	rootfs="$bundle/rootfs"
	mkdir "${rootfs}"

	if ! install_busybox "${rootfs}" "${name}"; then
	    echo "ERROR: Failed to install rootfs" 1>&2
	    exit 1
	fi

	if ! configure_busybox "${rootfs}"; then
	    echo "ERROR: Failed to configure busybox" 1>&2
	    exit 1
	fi

	if ! copy_configuration "${bundle}" "${name}"; then
	    echo "ERROR: Failed to write config file" 1>&2
	    exit 1
	fi

	if ! copy_prereq "${rootfs}" "${extrapkg}"; then
	    echo "ERROR: Failed to copy required binaries" 1>&2
	    exit 1
	fi
}

generate_config()
{
	local name res cfg

	name="${1}"
	if [ -z "${name}" ]; then
	    echo "ERROR: Please pass the name for the container" 1>&2
	    exit 1
	fi

	oci_bundle_root=${BUNDLE_PATH}
	touch ${oci_bundle_root}/ocicontainer

	# Now lets check if already present in config file
	exist=$(uci -q -c "${oci_bundle_root}" show ocicontainer | grep ".name='$name'")
	if [ -n "${exist}" ]; then
		echo "INFO: ${name} already exist in config file ${oci_bundle_root}/ocicontainer"
		exit 1
	fi

	# Configure in crun uci for new container #
	uci -q -c "${oci_bundle_root}" set ocicontainer."${name}"=du_eu_assoc

	# Enable the container
	uci -q -c "${oci_bundle_root}" set ocicontainer."${name}".name="${name}"
	uci -q -c "${oci_bundle_root}" set ocicontainer."${name}".autostart=1
	uci -q -c "${oci_bundle_root}" set ocicontainer."${name}".requested_state='Active'
	uci -q -c "${oci_bundle_root}" set ocicontainer."${name}".du_status='Installed'
	if [ -f "${oci_bundle_root}/$name/config" ]; then
		uci -q -c "${oci_bundle_root}" set ocicontainer."${name}".type='lxc'
		uci -q -c "${oci_bundle_root}" set ocicontainer."${name}".url="${DU_URL}"
	else
		uci -q -c "${oci_bundle_root}" set ocicontainer."${name}".type='crun'
		uci -q -c "${oci_bundle_root}" set ocicontainer."${name}".url='local://crun_template'
	fi
	uci -q -c "${oci_bundle_root}" set ocicontainer."${name}".ee_name="${ENVNAME}"
	uci -q -c "${oci_bundle_root}" set ocicontainer."${name}".eu_name="${name}"
	uci -c "${oci_bundle_root}" commit ocicontainer

	# Reload crun service
	if [ "$RELOAD" -eq "1" ]; then
		ubus call uci commit '{"config":"crun"}'
	fi
}

usage()
{
    cat <<EOF
CRUN iopsys image builder

Special arguments:

  [ -h | --help ]: Print this help message and exit.
  [ -c | --config ]: Generate config in swmodd for specified container

Arguments:

  [ -n | --name <name> ]: The container name
  [ -e | --env <name> ]: Name of the Execution Environment to create the container
  [ --no-reload ]: Do not reload crun

EOF
  return 0
}

if ! options=$(getopt -o hcn:e:u: -l help,config,name:,env:,url:,busybox-path:,no-reload -- "$@"); then
    usage
    exit 1
fi
eval set -- "$options"

config=0
while true
do
    case "$1" in
        -h|--help)    usage && exit 0;;
        -n|--name)    name=$2; shift 2;;
        -c|--config)  config=1 && shift 1;;
	-e|--env)     ENVNAME=$2; shift 2;;
	-u|--url)     DU_URL=$2; shift 2;;
        --busybox-path) BUSYBOX_EXE=$2; shift 2;;
	--no-reload)  RELOAD=0 && shift 1;;
        --)           shift 1; break ;;
        *)            break ;;
    esac
done

# Check that we have all variables we need
if [ -z "${name}" ]; then
    echo "ERROR: Please pass the name for the container" 1>&2
    exit 1
fi

set_bundle_path
if [ "$config" -eq "1" ]; then
	generate_config "$name"
else
	create_container "$name" "${@}"
	generate_config "${name}"
fi
