# Published metadata selection and snapshots
# vim:set filetype=sh:
# shellcheck shell=sh

# Explicit overrides never replace the startup selection.
metadata_dir() {
	local record provider
	if { [ "$CLOUD" = openstack ] || [ "$CLOUD" = digitalocean ]; } &&
		{ [ "${IMDS_SOURCE:-auto}" != auto ] ||
		  { [ "$CLOUD" = openstack ] && [ "${IMDS_FORMAT:-auto}" != auto ]; } ||
		  [ -n "$IMDS_API_VERSION" ]; }; then
		printf '%s\n' "$IMDS_TMP_DIR/source"
		return
	fi
	if [ -f "$TINY_CLOUD_RUN/selection" ]; then
		_perms_safe "$TINY_CLOUD_RUN/selection" || return
		read -r provider record < "$TINY_CLOUD_RUN/selection" || return
		if [ "$provider" != "$CLOUD" ]; then
			printf '%s\n' "$IMDS_TMP_DIR/source"
			return
		fi
	fi
	printf '%s\n' "$TINY_CLOUD_RUN/metadata"
}

# Records are data, never shell code: provider source format version.
metadata_record() {
	local directory="$1" record provider source format version extra
	record="$directory/.selection"
	if [ "$directory" = "$TINY_CLOUD_RUN/metadata" ] && [ "$2" != internal ] &&
		[ -f "$TINY_CLOUD_RUN/selection" ]; then
		record="$TINY_CLOUD_RUN/selection"
	fi
	if [ "$directory" = "$TINY_CLOUD_RUN/metadata" ]; then
		_perms_safe "$record" || return
	fi
	read -r provider source format version extra < "$record" || return
	case "$provider:$source:$format" in
		openstack:configdrive:native|openstack:configdrive:ec2|openstack:http:auto|openstack:http:native|openstack:http:ec2|digitalocean:configdrive:native|digitalocean:http:native|nocloud:nocloud:yaml) ;;
		*) return 1;;
	esac
	[ "$provider" = "$CLOUD" ] && [ -z "$extra" ] || return 1
	case "$source" in
		configdrive|nocloud)
			if [ "$directory" = "$TINY_CLOUD_RUN/metadata" ]; then
				_perms_safe "$directory" || return
			fi
			[ -d "$directory" ] && [ -x "$directory" ] || {
				echo "imds: metadata snapshot is not accessible: $directory" >&2
				return 1
			}
			;;
	esac
	case "$version" in ''|.*|*[!A-Za-z0-9_.-]*) return 1;; esac
	printf '%s %s %s %s\n' "$provider" "$source" "$format" "$version"
}

metadata_source() {
	local record provider source format version
	record=$(metadata_select "$@") || return
	read -r provider source format version <<-EOT
		$record
	EOT
	printf '%s\n' "$source"
}

metadata_format() {
	local record provider source format version
	record=$(metadata_record "$(metadata_dir)") || return
	read -r provider source format version <<-EOT
		$record
	EOT
	printf '%s\n' "$format"
}

metadata_marker() (
	local directory="$1" record tmpfile
	record=$(metadata_record "$directory" internal) || return
	tmpfile=$(mktemp "$TINY_CLOUD_RUN/selection.XXXXXX") || return
	trap 'rm -f "$tmpfile"' 0
	trap 'exit 1' HUP INT TERM
	case "$record" in
		nocloud\ nocloud\ *)
			if [ -e "$TINY_CLOUD_VAR/.nocloud_loaded" ]; then
				_perms_safe "$TINY_CLOUD_VAR/.nocloud_loaded" &&
					rm -f "$TINY_CLOUD_VAR/.nocloud_loaded" || return
			fi
			;;
	esac
	printf '%s\n' "$record" > "$tmpfile" &&
		perms set "$tmpfile" "$TINY_CLOUD_RUN/selection" &&
		mv "$tmpfile" "$TINY_CLOUD_RUN/selection" || return
)

metadata_select() (
	local provider="$1" format="$2" version="$3" source="${IMDS_SOURCE:-auto}"
	local directory stage file rv own_tmp=
	[ "$provider" != nocloud ] || source=auto
	case "$provider:$format" in
		openstack:auto|openstack:native|openstack:ec2|digitalocean:native|nocloud:yaml) ;;
		*) echo "imds: invalid metadata format '$format'" >&2; return 1;;
	esac
	case "$version" in
		''|.*|*[!A-Za-z0-9_.-]*) echo "imds: invalid metadata version '$version'" >&2; return 1;;
	esac
	case "$source" in auto|http|configdrive) ;; *) echo "imds: invalid metadata source '$source'" >&2; return 1;; esac
	# Reusing a complete publication requires neither a lock nor scratch space.
	directory=$(metadata_dir) || return
	if [ "$directory" = "$TINY_CLOUD_RUN/metadata" ] && [ -f "$TINY_CLOUD_RUN/selection" ]; then
		metadata_record "$directory"
		return
	fi
	# Network discovery also calls this outside the imds executable.
	if [ -z "$IMDS_TMP_DIR" ]; then
		IMDS_TMP_DIR=$(private_tmpdir imds) || return
		own_tmp=1
	fi
	trap '
		rv=$?
		[ -z "$stage" ] || rm -rf "$stage"
		if [ -n "$own_tmp" ]; then
			for file in "$IMDS_TMP_DIR"/metadata-volume.*; do
				[ ! -d "$file" ] || exit "$rv"
			done
			rm -rf "$IMDS_TMP_DIR"
		fi
		exit "$rv"
	' 0
	trap 'exit 1' HUP INT TERM
	directory=$(metadata_dir) || return
	if { [ "$directory" != "$TINY_CLOUD_RUN/metadata" ] && [ -f "$directory/.selection" ]; } ||
		{ [ "$directory" = "$TINY_CLOUD_RUN/metadata" ] && [ -f "$TINY_CLOUD_RUN/selection" ]; }; then
		metadata_record "$directory"
		return
	fi
	if [ "$directory" = "$TINY_CLOUD_RUN/metadata" ]; then
		[ "$(id -u)" = 0 ] || { echo 'imds: root must initialize metadata selection' >&2; return 1; }
		lock_metadata || return
		if [ -f "$directory/.selection" ]; then
			# Recover publication interrupted between the directory and marker.
			metadata_marker "$directory" || return
			metadata_record "$directory"
			return
		fi
		stage=$(mktemp -d "$TINY_CLOUD_RUN/.metadata.XXXXXX") || return
	else
		stage=$(mktemp -d "$IMDS_TMP_DIR/source.XXXXXX") || return
	fi
	rmdir "$stage" || return
	if [ "$provider" = nocloud ]; then
		_load_nocloud "$stage" || return
	elif [ "$source" != http ]; then
		if [ "$source" = configdrive ] && [ "$(id -u)" != 0 ]; then
			echo 'imds: ConfigDrive loading requires root' >&2
			return 1
		fi
		if configdrive_load "$provider" "$format" "$version" "$stage"; then
			source=configdrive
		else
			rv=$?
			[ "$rv" -eq 1 ] && [ "$source" = auto ] || return "$rv"
			source=http
		fi
	fi
	if [ "$source" = http ]; then
		[ "$provider" != digitalocean ] || version="${IMDS_API_VERSION:-v1}"
		private_mkdir "$stage" || return
		(umask 077; printf '%s http %s %s\n' "$provider" "$format" "$version" > "$stage/.selection") || return
	fi
	if [ "$directory" = "$TINY_CLOUD_RUN/metadata" ]; then
		for file in "$stage/"* "$stage/.selection"; do
			[ -f "$file" ] || continue
			perms set "$file" "$directory/${file##*/}" || return
		done
		perms set "$stage" "$directory" || return
	fi
	[ ! -e "$directory" ] && [ ! -L "$directory" ] || { echo 'imds: incomplete metadata state; reset required' >&2; return 1; }
	mv "$stage" "$directory" || return
	stage=
	if [ "$directory" = "$TINY_CLOUD_RUN/metadata" ]; then
		metadata_marker "$directory" || return
	fi
	metadata_record "$directory"
)

# HTTP has no shared response payload. Root may retain the first successful
# format probe without making ordinary readers take a writable lock.
metadata_set_format() (
	local format="$1" directory provider source saved version record tmpfile
	directory=$(metadata_dir) || return
	if [ "$directory" != "$TINY_CLOUD_RUN/metadata" ] || [ "$(id -u)" != 0 ]; then
		printf '%s\n' "$format"
		return
	fi
	if [ "$METADATA_LOCKED" != 1 ]; then
		lock_metadata || return
	fi
	record=$(metadata_record "$directory") || return
	read -r provider source saved version <<-EOT
		$record
	EOT
	[ "$source:$saved" = http:auto ] || { printf '%s\n' "$saved"; return; }
	tmpfile=$(mktemp "$directory/.selection.XXXXXX") || return
	trap 'rm -f "$tmpfile"' 0
	trap 'exit 1' HUP INT TERM
	printf '%s http %s %s\n' "$provider" "$format" "$version" > "$tmpfile" &&
		perms set "$tmpfile" "$directory/.selection" &&
		mv "$tmpfile" "$directory/.selection" && metadata_marker "$directory" || return
	printf '%s\n' "$format"
)

# Completed instances still need runtime metadata after reboot. The early
# service runs after networking, so remote NoCloud seeds can be loaded here
# without repeating bootstrap actions or modifying bootstrap working copies.
metadata_init() {
	case "$CLOUD" in
		nocloud|openstack|digitalocean) ;;
		*) return 0;;
	esac
	. "$LIBDIR/tiny-cloud/imds-connection"
	load_imds_connection
	if [ "$CLOUD" = nocloud ]; then
		. "$LIBDIR/tiny-cloud/cloud/nocloud/imds"
		metadata_select nocloud yaml latest >/dev/null
	else
		imds_targets >/dev/null
	fi
}

# Try the provider's filesystem types and labels in order.
metadata_volume_mount() {
	local mntdir="$1" labels="$2" fstypes="$3" fstype label

	for fstype in $fstypes; do
		for label in $labels; do
			mount -o ro -t "$fstype" "LABEL=$label" "$mntdir" 2>/dev/null && return 0
		done
	done
	return 1
}

metadata_volume_umount() {
	umount "$1" || {
		log err -s "Unable to unmount metadata volume at $1"
		return 1
	}
	rmdir "$1"
}
