#!/bin/sh
# SPDX-License-Identifier: MIT

[ -z "${CONTAINERS_CONF+x}" ] && \
	[ -e /etc/atmark/containers.conf ] && \
	export CONTAINERS_CONF=/etc/atmark/containers.conf
[ -z "${CONTAINERS_STORAGE_CONF+x}" ] && \
	[ -e /etc/atmark/containers_storage.conf ] && \
	export CONTAINERS_STORAGE_CONF=/etc/atmark/containers_storage.conf
[ -z "${CONTAINERS_REGISTRIES_CONF+x}" ] && \
	[ -e /etc/atmark/containers_registries.conf ] && \
	export CONTAINERS_REGISTRIES_CONF=/etc/atmark/containers_registries.conf

error() {
	local line rc=$?
	[ $rc -gt 0 ] || rc=1
	for line in "$@"; do
		printf "error: %s%s\n" "${name:+$name: }" "$line" >&2
	done
	exit $rc
}

error_logger() {
	local line rc=$?
	[ $rc -gt 0 ] || rc=1
	for line in "$@"; do
		logger -t podman_atmark "${name:+$name: }$line"
	done
	exit $rc
}

warning() {
	printf "warning: %s\n" "$@" >&2
	return 1
}

info() {
	printf "info: %s\n" "$@" >&2
}

trace() {
	[ -z "$VERBOSE" ] && return
	printf "trace: %s\n" "$@" >&2
}

ADD_HOOK_USAGE="add_hook --stage <stage> [--stage <stage>...] [--] <command> [args...]"
SET_HEALTHCHECK_USAGE="set_healthcheck [--action <action>] [--retries <retries>]
                 [--interval <time>] [--start-period <initial delay>]
                 [--timeout <timeout>] [--] <command> [args...]"
usage() {
	cat <<EOF
Usage: $0 [-a|container [container...]]

Helper to start configured container, pod or network

  -a, --all           Start all configured containers/pods/networks
                      Note only autostarting containers are started
  -i, --no-replace    Do not replace any existing container/pod/network
                      note this means some options will not be reapplied
  -c, --config <dir>  Alternate configuration directory
  -v, --verbose       Verbose
  -n, --dry-run       Dry run
  --remote            Use podman --remote
  --foreground        Use podman run without --detach option
  -h, --help          This help
  --long-help         Also describe available config commands
EOF

	if [ "$1" = "long" ]; then
		cat <<EOF


Available commands for containers config:
 set_image [<image>/--rootfs <rootfs path>]
   set container image; for rootfs, path is relative to /var/app/volumes
 set_command [command] [arg...]
   command to run in container. Note args are properly escaped, so
   no quoting is required. Default runs command configured in image.
 add_volumes hostpath[:contpath[:opts]] [more volumes...]
    Add volumes per podman run --volume syntax
 add_volume hostpath [contpath [opts]]
    Add a single volume. This can be used when hostpath contains columns.
 add_devices hostpath[:contpath[:opts]] [more devices...]
    Add devices as per podman run --device syntax
 add_device hostpath [contpath [opts]]
    Add a single device. This can be used when hostpath contains columns.
 add_hotplugs <arg> [more args...]
    Add hotplugged devices in container. See /proc/devices for valid args.
 add_ports [hostip:]<containerport>:<hostport> [more ports...]
    Add ports as per podman run --publish syntax.
 $ADD_HOOK_USAGE
   Run command as hook to container for given stages
     Stage must be one of the following:
       precreate, prestart, createRuntime, createContainer,
       startContainer, poststart, poststop
 add_armadillo_env
    Add environment variables set by Atmark Techno.
     The following environment variables are added:
      AT_ABOS_VERSION: ABOS version of this board.
      AT_LAN_MACn: MAC address of Ethernet on this board. (n=1,2,...)
      AT_PRODUCT_NAME: Product name of this board.
      AT_SERIAL_NUMBER: Serial number of this board.
 add_args [arg] [more args]
    Arguments compatible with 'podman run', e.g. --privileged, --add-cap=...
 set_network <network>
    Assign podman network, or 'host' for host network. Defaults to podman.
 set_ip <ip>
    Configure IP in container. Defaults is automatically chosen.
 set_readonly <yes/no>
    Ensure the container is not writable. Defaults to no.
 set_pull <always/missing/never>
    Allow pulling image from registry. Defaults is never.
 set_autostart <yes/create/no>
    Do not start container on boot. Defaults to yes.
    Setting create will pre-create container without starting it.
 set_restart <always/no/on-failure/unless-stopped>
    Restart container when it exits. Default is on-failure.
 set_pod <pod>
    Assign pod to container
 set_log_max_size <size>
    Sets maximum size for 'podman logs' buffer. Defaults to 1mb.
 set_init <yes/no/auto>
    Add tiny init process to forward signals. Defaults to auto,
    which adds init unless no command or command is itself init.
 $SET_HEALTHCHECK_USAGE
    Setup a health check for this container:
      The command runs inside the container through /bin/sh -c.
      Action is acted upon after retries consecuting failures,
        and must be one of none, restart, kill, stop, reboot or rollback.
        Defaults to 'restart'.
      Retries defaults to 3.
      Interval must be a number of seconds or something 'schedule_ts'
        understands after 'now +' (e.g. '2 min 3 second').
        Defaults to '1 min'.
      Start period is the initial time to wait after container start, also
        in schedule_ts format.
        Defaults to interval value.
      Timeout is the maximum allowed time for the command to run.
        The timeout is passed to the 'timeout' command, e.g. '10s'.
        Defaults to unset.
  set_healthcheck_start_command [command]
    Command to run after the first time healthcheck succeeds.
  set_healthcheck_fail_command [command]
    Command to run after retries failure. Note that 'set_healthcheck --action'
    is run as well, so set that to none if this is meant to replace it.
  set_healthcheck_recovery_command [command]
    Command to run when container becomes healthy after a previous failure.
    If the start command was run (because the container failed before becoming
    healthy at start) then this is not run.
  add_pre_command [command [args]]
    Command to run before starting the container.
    It is possible to define a function in config file to run e.g.
       somefunc() { ... do something...; }
       add_pre_command somefunc
    This is useful because some environments parse the config file
    and risk executing any command set directly there, when interacting
    with hardware please use this mechanism.

Using 'set_type pod' will create a podman pod.
Available commands for pod:
 add_ports, set_network, set_ip
   as above
 add_args
   Arguments compatible with 'podman pod create'
 set_infra_image <image>
   Empty image to use for pod. Defaults to podman default.

Using 'set_type network' will create a podman network.
Available commands for network:
 set_subnet <CIDR>
   set subnet e.g. 192.168.1.1/24
 add_args [arg] [args...]
   Arguments compatible with 'podman network create', e.g. --ipv6
EOF
	fi
}

quote() {
	case "$1" in
	# do not escape strings that only contain the following sh-safe chars
	*[!a-zA-Z0-9=_.,:/-]*)
		# sh-compliant quote function from http://www.etalabs.net/sh_tricks.html
		printf %s\\n "$1" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/'/"
		;;
	"")
		printf %s\\n "''"
		;;
	*)
		printf %s\\n "$1"
		;;
	esac
}

json_quote() {
	# same for json:
	# - add external double quotes
	# - escape existing double-quotes and backslashes
	printf %s\\n "$1" | sed 's/\\/\\\\/g;s/"/\\"/g;s/^/"/;s/$/"/'
}

# compat
append_args() { add_args "$@"; }

# helpers
assert_type() {
	local check cmd="$1"
	shift

	for check in "$@"; do
		case "$check" in
		container)
			case "$type" in
			""|cont|container) return 0;;
			esac
			;;
		network)
			case "$type" in
			net|network) return 0;
			esac
			;;
		pod)
			case "$type" in
			pod) return 0
			esac
			;;
		*)
			error "Bad usage of assert_type, unknown type $check"
			;;
		esac
	done
	error "$cmd can only be used with $*"
}

assert_empty() {
	local var
	for var in "$@"; do
		[ -z "$(eval printf %s "\"\$$var\"")" ] \
			|| error "$var cannot be set in $type config"
	done
}

add_args() {
	local arg

	for arg in "$@"; do
		args="${args:+$args }$(quote "$arg")"
	done
}

add_args_pre() {
	local arg tmp=""

	for arg in "$@"; do
		tmp="${tmp:+$tmp }$(quote "$arg")"
	done
	[ -z "$tmp" ] && return
	args="$tmp${args:+ $args}"
}

add_pre_command() {
	local arg

	[ -n "$pre_commands" ] && pre_commands="$pre_commands &&"
	for arg in "$@"; do
		pre_commands="${pre_commands:+$pre_commands }$(quote "$arg")"
	done
}

is_mountpoint() {
	local dir="$1"

	# busybox 'mountpoint' stats target and checks for device change, so
	# bind mounts like /var/lib/containers/overlay are not properly detected
	# as mountpoint by it.
	# util-linux mountpoint parses /proc/self/mountinfo correctly though so we
	# could use it if installed, but it is simpler to always reuse our
	# implementation instead
	! awk '$5 == "'"$dir"'" { exit 1 }' < /proc/self/mountinfo
}

mklink_hostpath() {
	local linkpath="$STATEDIR/${name}_${devnum}"

	[ -d "$STATEDIR" ] || run mkdir "$STATEDIR" || error "Could not mkdir $STATEDIR"

	if is_mountpoint "$linkpath"; then
		run umount "$linkpath" || error "Could not umount previous linkpath"
	fi
	if [ -d "$linkpath" ]; then
		run rmdir "$linkpath" || error "Could not remove previous linkpath"
	else
		run rm -f "$linkpath"
	fi

	if [ -d "$hostpath" ]; then
		run mkdir "$linkpath" || error "Could not create mount target for $hostpath"
		run mount --bind "$hostpath" "$linkpath" \
			|| error "Could not bind mount $hostpath -> $linkpath"
	else
		run ln -s "$hostpath" "$linkpath" \
			|| error "Could not symlink $hostpath -> $linkpath"
	fi
	hostpath="$linkpath"
	devnum=$((devnum + 1))
}

add_device() {
	assert_type add_device container
	local hostpath="$1"
	local contpath="${2:-$hostpath}"
	local opts="${3:+:$3}"

	[ $# -le 3 ] || error "Usage: add_device hostpath [contpath [opts]]"
	[ -n "$REMOTE" ] || [ -e "$hostpath" ] || error "device $hostpath does not exist"
	[ "${contpath#*:}" = "$contpath" ] || error "device path '$contpath' cannot contain columns"

	# contpath cannot be specified twice in podman rule command (hard
	# error), filter duplicates out...
	# Note this isn't perfect (because of possible quoting of contpath
	# contains single quotes), but this should be good enough
	# shellcheck disable=SC2140 ## pattern must be quoted separately
	case "$args" in
	*"--device=$hostpath:$contpath$opts"|*"--device=$hostpath:$contpath$opts"["' "]*)
		trace "skipping duplicate device $hostpath:$contpath$opts"
		return
		;;
	*":$contpath"|*":$contpath"["' :"]*)
		warning "$contpath already mounted by something else, skipping device $hostpath:$contpath$opts"
		return 1
		;;
	esac

	# if hostpath contains a colon, make a symlink to workaround silly syntax
	if [ "${hostpath#*:}" != "$hostpath" ]; then
		[ -n "$REMOTE" ] && error "Cannot handle path with : in remote mode ($hostpath)"
		mklink_hostpath "$hostpath"
	fi

	add_args "--device=$hostpath:$contpath$opts"
}

add_devices() {
	assert_type add_devices container
	local device tmp
	for device in "$@"; do
		case "$device" in
		*:*:*:*) error "Too many colons for a device path!";;
		*:*:*)
			tmp="${device#*:}"
			add_device "${device%%:*}" "${tmp%:*}" "${tmp#*:}"
			;;
		*:*)
			add_device "${device%:*}" "${device#*:}"
			;;
		*)
			add_device "$device"
			;;
		esac
	done
}

add_volume() {
	assert_type add_volume container
	local hostpath="$1"
	local contpath="${2:-$hostpath}"
	local opts="${3:+:$3}"

	[ $# -le 3 ] || error "Usage: add_volume hostpath [contpath [opts]]"
	[ "${contpath#*:}" = "$contpath" ] || error "volume path '$contpath' cannot contain columns"

	# prefix /var/app/rollback/volumes if relative path
	if [ "${hostpath#/}" = "$hostpath" ]; then
		hostpath="/var/app/rollback/volumes/$hostpath"
	fi

	# contpath cannot be specified twice in podman rule command (hard
	# error), filter duplicates out...
	# Note this isn't perfect (because of possible quoting of contpath
	# contains single quotes), but this should be good enough
	# shellcheck disable=SC2140 ## pattern must be quoted separately
	case "$args" in
	*"--volume=$hostpath:$contpath$opts"|*"--volume=$hostpath:$contpath$opts"["' "]*)
		trace "skipping duplicate volume $hostpath:$contpath$opts"
		return
		;;
	*":$contpath"|*":$contpath"["' :"]*)
		warning "$contpath already mounted by something else, skipping volume $hostpath:$contpath$opts"
		return 1
		;;
	esac

	if [ -z "$REMOTE" ]; then
		# create hostpath, but only if inside managed volume directories or /tmp, /run
		case "$hostpath" in
		/var/app/volumes/*|/var/app/rollback/volumes/*|/tmp/*|/run/*|/dev/shm/*)
			[ -e "$hostpath" ] || run mkdir -p "$hostpath" \
				|| error "could not create volume root $hostpath";;
		*)
			[ -e "$hostpath" ] || error "volume root $hostpath must exist";;
		esac
	fi

	# if hostpath contains a colon, make a symlink to workaround silly syntax
	if [ "${hostpath#*:}" != "$hostpath" ]; then
		[ -n "$REMOTE" ] && error "Cannot handle path with : in remote mode ($hostpath)"
		mklink_hostpath "$hostpath"
	fi

	add_args "--volume=$hostpath:$contpath$opts"
}

add_volumes() {
	assert_type add_volumes container
	local volume tmp
	for volume in "$@"; do
		case "$volume" in
		*:*:*:*) error "Too many colons for a volume path!";;
		*:*:*)
			tmp="${volume#*:}"
			add_volume "${volume%%:*}" "${tmp%:*}" "${tmp#*:}"
			;;
		*:*)
			add_volume "${volume%:*}" "${volume#*:}"
			;;
		*)
			add_volume "$volume"
			;;
		esac
	done
}

setup_hotplug() {
	# only run once
	[ -n "$hotplug_added" ] && return
	hotplug_added=1

	local hooks_dir="$STATEDIR/$name-hooks.d"
	assert_type setup_hotplug container

	add_volume "/dev" "/dev"
	add_volume "/run/udev" "/run/udev" "ro"

	command -v udevfw > /dev/null || [ -n "$TEST_HOTPLUG" ] \
		|| error "udevfw is required for hotplug"
	[ "$(id -u)" = 0 ] || [ -n "$TEST_HOTPLUG" ] \
		|| error "hotplug only works as root"

	if [ -z "$DRYRUN" ]; then
		[ -e "$hooks_dir" ] || mkdir -p "$hooks_dir" \
			|| error "Could not create hook dir $hooks_dir"
		{
			cat <<EOF
#!/bin/sh

PID_FILE="$hooks_dir/udevfw.pid"
NAME="$name"

EOF
			cat <<'EOF'

info() {
	local line
	for line in "$@"; do
		logger -t podman_atmark "$line"
	done
}

get_config() {
	local bundle

	bundle=$(sed -e 's/.*bundle"\s*:\s*"\([^"]*\).*/\1/') || return
	config="$bundle/config.json"
}

get_netns() {
	netns=$(sed -ne 's@.*namespaces"\s*:*\[[^]]*"type"\s*:\s*"network"\s*,\s*"path"\s*:\s*"\([^"]*\).*@\1@p' \
		"$config") || return
	[ -n "$netns" ]
}

get_pid() {
	[ -e "$PID_FILE" ] || return

	pid="$(cat $PID_FILE)"
	if ! grep -q udevfw "/proc/$pid/cmdline"; then
		info "warning: PID=$pid is not udevfw"
		rm "$PID_FILE"
		unset pid
		return 1
	fi
}

start() {
	local config netns pid
	if ! get_config; then
		info "Could not get config from state (stdin)"
		exit 1
	fi
	if ! get_netns; then
		info "Could not find netns from config -- $NAME running as --net=host ?"
		exit 0
	fi
	if get_pid; then
		if grep "$netns" "/proc/$pid/cmdline"; then
			info "udevfw $pid already running for $NAME"
			exit 0
		fi
		info "killing previous udevfw $pid for $NAME"
		kill -9 $pid
	fi
	udevfw "$netns" &
	pid=$!
	info "Started udevfw $pid for $NAME"
	echo "$pid" > "$PID_FILE"
}

stop() {
	local pid

	# flush stdin to avoid SIGPIPE for caller
	cat > /dev/null

	if ! get_pid; then
		return
	fi
	info "killing udevfw $pid for $NAME"
	kill -9 $pid
}

"$1"
EOF
		} > "$hooks_dir/hotplug_start.sh" \
			|| error "Could not write $hooks_dir/hotplug_start.sh"
		chmod +x "$hooks_dir/hotplug_start.sh" \
			|| error "Could not make $hooks_dir/hotplug_start.sh executable"
	fi

	add_hook --stage prestart "$hooks_dir/hotplug_start.sh" start
	add_hook --stage poststop "$hooks_dir/hotplug_start.sh" stop
}

add_hotplugs() {
	assert_type add_hotplugs container
	local hotplug type_majors type_major

	for hotplug in "$@"; do
		# hardcode video as it can be missing from /proc/devices
		# if hot loaded
		case "$hotplug" in
		video|video4linux) type_majors="c 81";;
		*) type_majors=$(awk -v category="$hotplug" '
			$1 == "Character" { type="c" }
			$1 == "Block" { type="b" }
			$1 ~ /^[0-9]+$/ && $2 == category { print type, $1 }' < /proc/devices)
			;;
		esac
		[ -n "$type_majors" ] || error "$hotplug is not supported for hotplug device"
		# shellcheck disable=SC3003 ## busybox ash/mksh have $'..'
		local IFS=$'\n'
		for type_major in $type_majors; do
			add_args "--device-cgroup-rule=${type_major}:* rw"
		done
		# shellcheck disable=SC3003 ## busybox ash/mksh have $'..'
		IFS=$' \t\n'
	done
	setup_hotplug
}

add_ports() {
	assert_type add_ports container pod
	local port
	for port in "$@"; do
		add_args "--publish=$port"
	done
}

add_hook() {
	assert_type add_hook container
	local arg stages="" hook_cmd="" hook_args=""
	local hooks_dir="$STATEDIR/$name-hooks.d"

	[ -z "$REMOTE" ] || error "Cannot setup hook in --remote mode"

	while [ "$#" -ge 1 ]; do
		case "$1" in
		--*=*)
			# split --foo=bar into --foo bar and try again
			arg="$1"
			shift
			set -- "${arg%%=*}" "${arg#*=}" "$@"
			continue
			;;
		"--stage")
			case "$2" in
			precreate|prestart|createRuntime|createContainer|startContainer|poststart|poststop)
				stages="${stages:+$stages, }\"$2\""
				;;
			*) error "stage must be one of precreate, prestart, createRuntime, createContainer, startContainer, poststart, poststop";;
			esac
			shift 2
			;;
		"--")
			shift
			break
			;;
		"-"*)
			error "Usage: $ADD_HOOK_USAGE"
			;;
		*)
			break;;
		esac
	done

	[ -n "$stages" ] || error "add_hook requires at least one --stage argument"
	[ "$#" -ge 1 ] || error "Usage: $ADD_HOOK_USAGE"

	# don't fail if command was not found in dry-run
	# (because command might not have been created...)
	hook_cmd=$(command -v "$1") \
		|| [ -n "$DRYRUN" ] \
		|| error "Hook command $1 does not appear to be a valid command, failing"
	hook_cmd="$(json_quote "$hook_cmd")"
	shift
	for arg in "$@"; do
		hook_args="${hook_args:+$hook_args, }$(json_quote "$arg")"
	done

	if [ -z "$DRYRUN" ]; then
		[ -e "$hooks_dir" ] || mkdir -p "$hooks_dir" \
			|| error "Could not create hook dir $hooks_dir"
		cat > "$hooks_dir/hook_$podman_start_hooknum.json" <<EOF
{
  "version": "1.0.0",
  "when": {"always": true},
  "hook": {
    "path": $hook_cmd${hook_args:+",
    \"args\": [$hook_cmd, $hook_args]"}
  },
  "stages": [$stages]
}
EOF
	fi

	# only add hooks-dir once
	[ "$podman_start_hooknum" = 0 ] && add_args_pre "--hooks-dir=$hooks_dir"
	podman_start_hooknum=$(( podman_start_hooknum + 1))
}

add_armadillo_env() {
	assert_type add_armadillo_env container
	local dev_info=""
	dev_info=$(device-info --env -a -m -p -s --se-param) \
		|| error "add_armadillo_env: Could not add armadillo env"
	eval "$dev_info"
	add_args --env=AT_ABOS_VERSION --env=AT_PRODUCT_NAME --env=AT_SERIAL_NUMBER
	add_args --env=AT_LAN_MAC1 --env=AT_LAN_MAC2 --env=AT_SE_PARAM
}

set_network() {
	assert_type set_network container pod
	[ "$#" = 1 ] || error "Usage: set_network <networkname>"
	network=$(quote "$1")
}

set_ip() {
	assert_type set_ip container pod
	[ "$#" = 1 ] || error "Usage: set_ip <ip>"
	ip=$(quote "$1")
}

set_image() {
	assert_type set_image container
	if [ "$#" = 2 ] && [ "$1" = "--rootfs" ]; then
		rootfs=1
		image="$2"
		# if not an absolute path prepend /var/app/volumes
		[ "${image#/}" = "$image" ] \
			&& image="/var/app/volumes/$image"
		return;
	fi
	[ "$#" = 1 ] || error "Usage: set_image [--rootfs] <image>"
	image="$1"
}

set_readonly() {
	assert_type set_readonly container
	[ "$#" = 1 ] || error "Usage: set_readonly <yes/no, default no>"
	readonly=$1
}

set_pull() {
	assert_type set_pull container
	[ "$#" = 1 ] || error "Usage: set_pull <always/missing/never, default never>"
	pull=$(quote "$1")
}

set_autostart() {
	[ "$#" = 1 ] || error "Usage: set_autostart <yes/create/no, default yes>"
	# if the value itself is invalid it will fail in the $autostart check
	# when actually running, there is no need to duplicate it
	autostart=$1
}

set_restart() {
	assert_type set_restart container
	[ "$#" = 1 ] || error "Usage: set_restart <always/no/on-failure/unless-stopped, default on-failure>"
	restart=$(quote "$1")
}

set_pod() {
	assert_type set_pod container
	[ "$#" = 1 ] || error "Usage: set_pod <pod name>"
	pod=$(quote "$1")
}

set_log_max_size() {
	assert_type set_log_max_size container
	[ "$#" = 1 ] || error "Usage: set_log_max_size <size, default 1mb>"
	log_max_size=$(quote "$1")
}

set_init() {
	assert_type set_init container
	[ "$#" = 1 ] || error "Usage: set_init <yes/no/auto>, default auto"

	init="$1"
}

set_healthcheck() {
	local action=restart retries=3 interval='1 min'
	local start_period="" timeout="" arg cmd=""

	assert_type set_healthcheck container
	while [ "$#" -ge 1 ]; do
		case "$1" in
		--*=*)
			# split --foo=bar into --foo bar and try again
			arg="$1"
			shift
			set -- "${arg%%=*}" "${arg#*=}" "$@"
			continue
			;;
		"--action")
			[ "$#" -ge 2 ] || error "set_healthcheck --action requires an argument"
			action="$2"
			case "$action" in
			none|restart|kill|stop|reboot|rollback) ;;
			*) error "set_healthcheck action must be one of none, restart, kill, stop, reboot, rollback."
			esac
			shift 2
			;;
		"--retries")
			[ "$#" -ge 2 ] || error "set_healthcheck --retries requires an argument"
			retries="$2"
			[ "$retries" -ge 0 ] || error "healthcheck retries must be a positive integer"
			shift 2
			;;
		"--interval")
			[ "$#" -ge 2 ] || error "set_healthcheck --interval requires an argument"
			case "$2" in
			*[!0-9]*) interval="now + $2";;
			*) interval="now + $2 second";;
			esac
			schedule_ts "$interval" >/dev/null \
				|| error "interval was not accepted by schedule_ts: $2"
			shift 2
			;;
		"--start-period")
			[ "$#" -ge 2 ] || error "set_healthcheck --start-period requires an argument"
			case "$2" in
			*[!0-9]*) start_period="now + $2";;
			*) start_period="now + $2 second";;
			esac
			schedule_ts "$start_period" >/dev/null \
				|| error "start-period was not accepted by schedule_ts: $2"
			shift 2
			;;
		"--timeout")
			[ "$#" -ge 2 ] || error "set_healthcheck --timeout requires an argument"
			timeout="$(quote "$2")"
			eval timeout "$timeout" true \
				|| error "healthcheck timeout must be accepted by 'timeout' command"
			shift 2
			;;
		"--")
			shift
			break
			;;
		"-"*)
			error "Usage: $SET_HEALTHCHECK_USAGE"
			;;
		*)
			break;;
		esac
	done

	[ "$#" -ge 1 ] || error "Usage: $SET_HEALTHCHECK_USAGE"
	for arg in "$@"; do
		cmd="${cmd:+$cmd }$(quote "$arg")"
	done

	healthcheck_command="$cmd"
	healthcheck_timeout="$timeout"
	healthcheck_interval="$interval"
	healthcheck_action="$action"
	healthcheck_start_period="$start_period"
	healthcheck_retries="$retries"
}

set_healthcheck_start_command() {
	local cmd=""

	assert_type set_healthcheck_start_command container

	for arg in "$@"; do
		cmd="${cmd:+$cmd }$(quote "$arg")"
	done

	healthcheck_start_command="$cmd"
}

set_healthcheck_fail_command() {
	local cmd=""

	assert_type set_healthcheck_fail_command container

	for arg in "$@"; do
		cmd="${cmd:+$cmd }$(quote "$arg")"
	done

	healthcheck_fail_command="$cmd"
}

set_healthcheck_recovery_command() {
	local cmd=""

	assert_type set_healthcheck_recovery_command container

	for arg in "$@"; do
		cmd="${cmd:+$cmd }$(quote "$arg")"
	done

	healthcheck_recovery_command="$cmd"
}

healthcheck_handle_success() {
	fail_count=0

	# no handler for repeat halthy
	if [ "$state" = healthy ]; then
		return
	fi
	logger -t podman_atmark "$name healthcheck is now healthy (was $state)"


	case "$state" in
	starting*)
		if [ -n "$healthcheck_start_command" ]; then
			logger -t podman_atmark "$name first healthy check: running $healthcheck_start_command"
			eval "$healthcheck_start_command"
		fi
		;;
	failed|restart)
		if [ -n "$healthcheck_recovery_command" ]; then
			logger -t podman_atmark "$name recovered after $state: running $healthcheck_recovery_command"
			eval "$healthcheck_recovery_command"
		fi
		;;
	esac
	state=healthy
}

healthcheck_handle_failure() {
	fail_count=$((fail_count + 1))
	logger -t podman_atmark "$name healthcheck failed (from $state, $fail_count / $retries)"

	# only consider failure after retries consecutive failures
	# (retries = 3 -> runs on 3rd pass)
	if [ "$fail_count" -lt "$retries" ]; then
		return
	fi

	# we repeat action every retries failures if required
	fail_count=0

	case "$state" in
	starting*)
		state=starting_failed
		;;
	*)
		state=failed
		;;
	esac

	if [ -n "$healthcheck_fail_command" ]; then
		logger -t podman_atmark "$name is unhealthy, running $healthcheck_fail_command"
		eval "$healthcheck_fail_command"
	fi

	case "$action" in
	none)
		# no action, was already logged so just skip.
		;;
	restart)
		logger -t podman_atmark "$name is unhealthy, restarting container"
		# restart with podman_start to get clean state
		# Using `podman restart` keeps old overlay and has been reported to
		# be a problem with --restart=on-failure...
		if ! podman ${REMOTE:+--remote} stop "$name" \
		    || ! eval podman ${REMOTE:+--remote} run "$args"; then
			error_logger "Could not restart $name"
		fi
		;;
	stop|kill)
		logger -t podman_atmark "$name is unhealthy, ${action}ing container"
		if ! podman "$action" "$name"; then
			error_logger "Could not $action $name"
		fi
		# stay in background until container is removed or restarted
		healthcheck_wait_start failure
		;;
	reboot)
		logger -t podman_atmark "$name is unhealthy, rebooting"
		if ! abos-ctrl reboot; then
			logger -t podman_atmark "Could not reboot!"
		fi
		exit
		;;
	rollback)
		logger -t podman_atmark "$name is unhealthy, rolling back"
		if ! abos-ctrl rollback --reboot --allow-downgrade; then
			logger -t podman_atmark "rollback failed (not available?), rebooting anyway"
		fi
		if ! abos-ctrl reboot; then
			logger -t podman_atmark "Could not reboot!"
		fi
		exit
		;;
	*)
		# should never happen as action is checked in set_healthcheck...
		error_logger "$name had unrecognized action set: $action; aborting healthcheck loop"
		;;
	esac
}

healthcheck_wait_start() {
	case "$1" in
	start|failure)
		# don't log here.
		;;
	*)
		logger -t podman_atmark "$name healthcheck: container stopped, waiting for restart..."
		# XXX should that be a failure? but it is redundant with the
		# --restart=on-failure option...
		case "$state" in
		starting*)
			state=starting_restart
			;;
		*)
			state=restart
			;;
		esac
	esac
	# wait for container to (re)start
	# We split this from main loop to ensure at least start_period
	# has elapsed
	while true; do
		case "$(podman inspect --format="{{.State.Status}}" "$name" 2>/dev/null \
				|| echo removed)" in
		running)
			# done, make main loop wait a start period
			next=$(schedule_ts "$start_period") \
				|| error_logger "schedule_ts failed $start_period @ $(date)"
			return
			;;
		removed)
			error_logger "$name removed, stopping loop."
			;;
		esac
		next=$(schedule_ts "$start_period") \
				|| error_logger "schedule_ts failed $start_period @ $(date)"
		now=$(date +%s)
		delay=$((next - now)) \
			|| error_logger "Could not compute delay ($next - $now)"
		[ "$delay" -gt 0 ] && sleep "$delay"
	done
}

healthcheck_loop() {
	local now next delay
	local interval="$healthcheck_interval"
	local action="$healthcheck_action"
	local retries="$healthcheck_retries"
	local start_period="$healthcheck_start_period"
	local fail_count=0
	# valid states & transitions
	# starting -> healthy -> failed/restart -> healthy/failed/restart
	# starting -> starting_failed -> healthy/starting_restart
	# starting -> starting_restart -> healthy/starting_failed
	local state=starting

	[ -n "$start_period" ] || start_period="$interval"

	healthcheck_wait_start start
	while true; do
		now=$(date +%s)
		delay=$((next - now)) \
			|| error_logger "Could not compute delay ($next - $now)"
		[ "$delay" -gt 0 ] && sleep "$delay"
		next=$(schedule_ts "$interval") \
			|| error_logger "schedule_ts failed $interval @ $(date)"

		# command has been quoted and requires eval
		eval ${healthcheck_timeout:+timeout "$healthcheck_timeout"} \
			podman ${REMOTE:+--remote} exec "$name" \
				"$healthcheck_command"
		case "$?" in
		0)
			healthcheck_handle_success
			;;
		255)
			# podman returns 255 if container is dead, check it's still up
			if [ "$(podman inspect --format="{{.State.Status}}" "$name" 2>/dev/null)" = "running" ]; then
				healthcheck_handle_failure
			else
				healthcheck_wait_start
			fi
			;;
		*)
			healthcheck_handle_failure
			;;
		esac
	done
}

run_healthcheck_loop() {
	local pidfile="$STATEDIR/healtcheck_$name.pid" pid
	if [ -n "$DRYRUN" ]; then
		echo "Skipping healthcheck process to dry run"
		return
	fi
	# kill any old process
	if pid=$(cat "$pidfile" 2>/dev/null) && [ -n "$pid" ] \
	    && [ "$(cat "/proc/$pid/comm" 2>/dev/null)" = "podman_health" ]; then
		kill -9 "$pid"
	fi
	[ -d "$STATEDIR" ] || mkdir "$STATEDIR" || error "Could not mkdir $STATEDIR"
	(
		printf "podman_health" > /proc/self/comm 2>/dev/null
		# get PID of the subshell...
		sh -c 'echo $PPID' > "$pidfile" || error "Could not record pidfile of healthcheck process"
		# shellcheck disable=SC2064 # expand now...
		trap "rm -f $pidfile" EXIT
		# kill stdio unless set -x or verbose
		case "$-${VERBOSE:+x}" in
		*x*) ;;
		*) exec </dev/null >/dev/null 2>&1;;
		esac
		healthcheck_loop
	) &
}

set_infra_image() {
	assert_type set_infra_image pod
	[ "$#" = 1 ] || error "Usage: set_infra_image <image, default unset = podman default>"
	infra_image=$(quote "$1")
}

set_subnet() {
	assert_type set_subnet network
	[ "$#" = 1 ] || error "Usage: set_subnet <subnet>"
	subnet=$(quote "$1")
}


set_type() {
	[ "$#" = 1 ] || error "Usage: set_type <container/network/pod, default container>"
	[ "$type" = "" ] || [ "$type" = "$1" ] \
		|| error "Type was already set to $type, cannot change"
	case "$1" in
	cont|container)
		type=container
		assert_empty infra_image subnet
		;;
	net|network)
		type=network
		assert_empty image ports devices volumes pod network ip
		assert_empty infra_image
		;;
	pod)
		type=pod
		assert_empty subnet image devices volumes pod
		;;
	*)
		error "Invalid type $1"
		;;
	esac
}

set_command() {
	assert_type set_command container
	local arg
	if [ -n "$command" ]; then
		warning "previously set command is overwritten. The following command will NOT be executed:" \
			"$command"
	fi
	command=""

	for arg in "$@"; do
		command="${command:+$command }$(quote "$arg")"
	done
}

run() {
	[ -n "$VERBOSE" ] && echo "$@"
	[ -n "$DRYRUN" ] || "$@"
}

start_pod() {
	# already created?
	[ -z "$DRYRUN" ] && [ -z "$REPLACE" ] \
		&& eval podman ${REMOTE:+--remote} pod exists "$name" \
		&& return

	# build options and start
	# shellcheck disable=SC2086 # expand ports on purpose
	add_ports $ports
	[ -n "$infra_image" ] && add_args_pre "--infra-image=$infra_image"
	[ -n "$network" ] && add_args_pre "--network=$network"
	[ -n "$ip" ] && add_args_pre "--ip=$ip"
	[ -n "$REPLACE" ] && add_args_pre "--replace"
	add_args_pre --name "$name"

	[ -z "$pre_commands" ] \
	       || run eval "$pre_commands" \
	       || error "pre commands failed"

	echo "Creating pod '$name'${DRYRUN:+ (dry run)}"
	run eval podman ${REMOTE:+--remote} pod create \
			"$args" \
		|| error "Could not start $name: $?"
}

start_network() {
	# already created?
	if [ -z "$DRYRUN" ] && eval podman ${REMOTE:+--remote} network exists "$name"; then
		if [ -n "$ALL" ] || [ -z "$REPLACE" ]; then
			echo "Skipping existing network '$name'"
			return
		fi
		echo "Removing old network $name"
		run eval podman ${REMOTE:+--remote} network remove -f "$name"
	fi

	# build options and start
	[ -n "$subnet" ] && add_args_pre "--subnet=$subnet"

	# If options contain macvlan and no subnet,
	# start dhcp-proxy for the user
	case "$args" in
	*macvlan*)
		case "$args" in
		*"--subnet"*) ;;
		*)
			echo "Starting netavark dhcp-proxy"
			run rc-service netavark-dhcp-proxy start -q
			;;
		esac
	esac

	add_args "$name"

	[ -z "$pre_commands" ] \
	       || run eval "$pre_commands" \
	       || error "pre commands failed"

	echo "Creating network '$name'${DRYRUN:+ (dry run)}"
	run eval podman ${REMOTE:+--remote} network create "$args"
}

start_container() {
	[ -z "$image" ] && image="$name"

	# let shell split ports/devices/volumes for us
	# backwards compatibility for original ports/devices/volumes variables
	# shellcheck disable=SC2086
	add_ports $ports
	# shellcheck disable=SC2086
	add_devices $devices
	# shellcheck disable=SC2086
	add_volumes $volumes
	[ -n "$pull" ] && add_args_pre "--pull=$pull"
	[ -n "$pod" ] && add_args_pre "--pod=$pod"
	if [ -n "$network" ]; then
		[ -z "$pod" ] || warning "Cannot set network with a pod, ignoring option" \
			&& add_args_pre "--network=$network"
	fi
	if [ -n "$ip" ]; then
		[ -z "$pod" ] || warning "Cannot set ip with a pod, ignoring option" \
			&& add_args_pre "--ip=$ip"
	fi
	[ -n "$restart" ] && add_args_pre "--restart=$restart"
	[ -n "$log_max_size" ] && add_args_pre --log-opt "max-size=$log_max_size"
	case "$readonly" in
	[Yy]|[Yy][Ee][Ss]|[Tt][Rr][Uu][Ee]|1)
		add_args_pre --read-only --read-only-tmpfs=false --mount type=tmpfs,target=/dev/shm,ro=true;;
	[Nn]|[Nn][Oo]|[Ff][Aa][Ll][Ss][Ee]|0|"") ;;
	*) warning "Invalid value for readonly: $readonly, assuming no";;
	esac
	# auto mode: add init unless no command or it ends with init
	case "$init" in
	[Aa]|[Aa][Uu][Tt][Oo]|"")
		case "$command@@$args" in
		# empty command, or command ends in 'init' or 'systemd'
		@@*|*init@@*|*init"'"@@*|*systemd@@*|*systemd"'"@@*) ;;
		# args contains --pid=host
		*@@*--pid=host*|*@@*"--pid host"*) ;;
		*) add_args_pre "--init";;
		esac;;
	[Yy]|[Yy][Ee][Ss]|[Tt][Rr][Uu][Ee]|1)
		add_args_pre "--init";;
	[Nn]|[Nn][Oo]|[Ff][Aa][Ll][Ss][Ee]|0)
		;;
	*)
		warning "Invalid value for $init, assuming no";;
	esac
	[ -n "$REPLACE" ] && add_args_pre "--replace"
	if [ "$run_mode" = run ] && [ -z "$FOREGROUND" ]; then
		add_args_pre "-d"
	fi

	add_args_pre --name "$name"
	# --rootfs must be last before image (image is path to rootfs)
	[ -n "$rootfs" ] && add_args "--rootfs"
	# image/command are already escaped
	args="$args $image $command"

	[ -z "$pre_commands" ] \
	       || run eval "$pre_commands" \
	       || error "pre commands failed"

	echo "Starting '$name'${DRYRUN:+ (dry run)}"
	run eval podman ${REMOTE:+--remote} "$run_mode" "$args" \
		|| error "Could not start $name: $?"

	[ -z "$healthcheck_interval" ] || run_healthcheck_loop
}

start_conf_subshell() {
	# common parameters (network, pod, container)
	local name="$name" args="" autostart=yes type=""
	local pre_commands=""
	# network parameters
	local subnet=""
	# pod parameters
	local infra_image=""
	# pod & container parameters
	local network="" ip=""
	# (backwards compatibility)
	local ports=""
	# container parameters
	local command=""
	local image="" rootfs="" init=auto log_max_size=1mb
	local pod="" pull=never restart=on-failure readonly="no"
	local run_mode="$RUN_MODE" hotplug_added=""
	local healthcheck_action="" healthcheck_command="" healthcheck_interval=""
	local healthcheck_fail_command="" healthcheck_recovery_command=""
	local healthcheck_retries="" healthcheck_timeout=""
	local healthcheck_start_command="" healthcheck_start_period=""
	# (backwards compatibility)
	local devices="" volumes=""
	# real local variables
	local dropin

	. "$config"
	for dropin in "$config.d/"*.conf; do
		[ -e "$dropin" ] || break
		. "$dropin"
	done

	[ -n "$name" ] || error "Container name not set ($config)"

	case "$autostart" in
	[Yy]|[Yy][Ee][Ss]|[Tt][Rr][Uu][Ee]|1) ;;
	[Cc][Rr][Ee][Aa][Tt][Ee]) [ -n "$ALL" ] && run_mode=create;;
	[Nn]|[Nn][Oo]|[Ff][Aa][Ll][Ss][Ee]|0|"") [ -n "$ALL" ] && return;;
	*)	warning "Invalid value for autostart: $autostart, assuming no"
		[ -n "$ALL" ] && return;;
	esac

	case "$type" in
	network|net) start_network;;
	pod) start_pod;;
	""|container|cont) start_container;;
	*) error "Invalid type $type for $name";;
	esac
}

start_conf() {
	local name="$1" filter="${2:-any}"
	local config="$CONF_DIR/$name.conf"
	local devnum=0 podman_start_hooknum=0

	[ -e "$config" ] || warning "No config found for $name, skipping" || return

	case "$filter" in
	net) grep -qE "^(set_)?type[= \t]*net(work)?" "$config" || return 0;;
	pod) grep -qE "^(set_)?type[= \t]*pod$" "$config" || return 0;;
	cont)
		# type either unset or cont/container: we invert the check
		# to say if a type is set and it's not container return
		grep -E "^(set_)?type[= \t]" "$config" | grep -qvE "^(set_)?type[= \t]echocont(ainer)?" \
			&& return 0;;
	any) ;;
	*) error "Invalid filter $filter";;
	esac

	# run in subshell to avoid leaking config env / allow using error/exit
	(
		start_conf_subshell
	# record we had a failure
	) || FAIL=$?
}

start_all() {
	local filter="$1" ALL=1 conf

	for conf in "$CONF_DIR/"*.conf; do
		[ -e "$conf" ] || continue
		conf="${conf%.conf}"
		conf="${conf##*/}"
		start_conf "$conf" "$filter"
	done
}

init_network() {
	# skip dryrun
	[ -n "$DRYRUN" ] && return

	# podman default network has dns and ipv6 off,
	# ipv6 might break things but we want dns
	# so create it manually unless present...
	# for ipv6, create a net=
	[ -e "$NETWORKSDIR/podman.json" ] && return

	[ -e "$NETWORKSDIR" ] || mkdir -p "$NETWORKSDIR"

	# We'd normally run and modify the result of 'podman network inspect podman',
	# but that command takes 2s on A6E so use a cached file here.
	# busybox date does not have %N nor %:z -- skipping nanoseconds is apparently
	# fine, but we need the : for timezone so add it manually...
	local ts
	ts=$(date +%Y-%m-%dT%H:%M:%S%z)
	# Tests should catch if podman upgrades in an incompatible way.
	if ! cat > "$NETWORKSDIR/podman.json.tmp" << EOF \
	    || ! mv "$NETWORKSDIR/podman.json.tmp" "$NETWORKSDIR/podman.json"; then
{
     "name": "podman",
     "id": "$(tr -dc '0-9a-f' < /dev/urandom | head -c 64)",
     "driver": "bridge",
     "network_interface": "podman0",
     "created": "${ts%??}:${ts#"${ts%??}"}",
     "subnets": [
          {
               "subnet": "10.88.0.0/16",
               "gateway": "10.88.0.1"
          }
     ],
     "ipv6_enabled": false,
     "internal": false,
     "dns_enabled": true,
     "ipam_options": {
          "driver": "host-local"
     },
     "containers": {}
}
EOF
		rm -f "$NETWORKSDIR/podman.json.tmp"
		error "Could not initialize podman network, refusing to start containers"
	fi
}

[ $# -lt 1 ] && usage && exit 1

VERBOSE=""
DRYRUN=""
ALL=""
REPLACE=1
REMOTE=
RUN_MODE="run"
FOREGROUND=""
CONF_DIR="/etc/atmark/containers"
STATEDIR="/run/podman_start"
# /home/atmark/.local/share/containers/storage/networks/ for rootless
NETWORKSDIR="/etc/containers/networks"

while [ $# -ge 1 ]; do
	case "$1" in
	--*=*)
		# split --foo=bar into --foo bar and try again
		arg="$1"
		shift
		set -- "${arg%%=*}" "${arg#*=}" "$@"
		unset arg
		continue
		;;
	"-v"|"--verbose")
		VERBOSE=1
		;;
	"-a"|"--all")
		ALL=1
		;;
	"-i"|"--no-replace")
		REPLACE=""
		;;
	"-c"|"--config")
		[ "$#" -ge 2 ] || error "$1 needs an argument"
		CONF_DIR=$(realpath "$2") && [ -e "$CONF_DIR" ] \
			|| error "$2 must exist"
		[ -d "$CONF_DIR" ] || error "$CONF_DIR is not a directory"
		shift
		;;
	"-n"|"--dry-run")
		DRYRUN=1
		VERBOSE=1
		;;
	"--create")
		RUN_MODE="create"
		;;
	"--foreground")
		FOREGROUND=1
		;;
	"--remote")
		REMOTE="1"
		;;
	"--source")
		# for tests
		return 0
		;;
	"--statedir")
		# for tests
		[ "$#" -ge 2 ] || error "$1 needs an argument"
		STATEDIR="$2"
		shift
		;;
	"--networksdir")
		[ "$#" -ge 2 ] || error "$1 needs an argument"
		NETWORKSDIR="$2"
		shift
		;;
	"--")
		break
		;;
	"--long-help")
		usage long
		exit 0
		;;
	"-h"|"--help"|"-"*)
		usage
		# set exit code
		[ "$1" = "-h" ] || [ "$1" = "--help" ]
		exit
		;;
	*)
		break
		;;
	esac
	shift
done

FAIL=0
# we want podman to use the tmpfs /tmp, not btrfs /var/tmp
export TMPDIR=/tmp

init_network

if [ -n "$FOREGROUND" ]; then
	[ -z "$ALL" ] && [ "$#" = 1 ] || error "--foreground only makes sense with exactly one container"
fi

if [ -n "$ALL" ]; then
	[ $# -ne 0 ] && error "Trailing arguments after -a"
	start_all net
	start_all pod
	start_all cont
else
	for container in "$@"; do
		start_conf "$container"
	done
fi

exit $FAIL
