#!/bin/sh
# Shardkeep quick install: https://shardkeep.gg/install.sh
#
#   curl -fsSL https://shardkeep.gg/install.sh | sh
#
# Installs the Shardkeep Controller, its PostgreSQL database and a bundled
# Agent (so this machine is also your first Node) with Docker Compose, then
# tells you how to finish setup in your browser. It is meant to be short
# enough to read before you run it.
#
# What it does:
#   1. Checks for Docker Engine and the Docker Compose plugin. It never
#      installs them: see https://docs.docker.com/engine/install/
#   2. Downloads compose.yml from the latest Shardkeep release on GitLab (or
#      SHARDKEEP_VERSION) and checks it against the release's checksums.
#   3. In the install directory, writes .env (mode 0600) with the version,
#      the name people use to reach this machine and a generated database
#      password. An existing .env keeps its settings: only the version
#      changes, which is how running it again upgrades.
#   4. Runs docker compose pull and docker compose up -d.
#
# Settings, all optional, as environment variables:
#   SHARDKEEP_DIR          install directory (default /opt/shardkeep)
#   SHARDKEEP_VERSION      a release such as 1.20.0 (default: the latest)
#   SHARDKEEP_PUBLIC_NAME  the name or IP address people and other Nodes use
#                          to reach this machine (default: asked, or the
#                          host name when there is no terminal to ask on)
#
# Source: https://gitlab.com/shardkeep/website (static/install.sh)
# Documentation: https://docs.shardkeep.gg

set -eu

RELEASES="https://gitlab.com/shardkeep/shardkeep/-/releases"

say() {
	printf '%s\n' "$*"
}

fail() {
	printf 'shardkeep install: %s\n' "$*" >&2
	exit 1
}

sha256() {
	if command -v sha256sum >/dev/null 2>&1; then
		sha256sum "$1" | cut -d ' ' -f 1
	else
		shasum -a 256 "$1" | cut -d ' ' -f 1
	fi
}

# The name or IP address for the Controller's certificates and links.
public_name() {
	if [ -n "${SHARDKEEP_PUBLIC_NAME:-}" ]; then
		printf '%s' "$SHARDKEEP_PUBLIC_NAME"
		return
	fi
	guess=$(hostname -f 2>/dev/null || hostname)
	answer=""
	# Piped into sh, standard input is the script: ask on the terminal.
	if (exec </dev/tty) 2>/dev/null; then
		printf 'Name or IP address people will use to reach this machine [%s]: ' "$guess" >/dev/tty
		read -r answer </dev/tty || answer=""
	fi
	printf '%s' "${answer:-$guess}"
}

main() {
	dir=${SHARDKEEP_DIR:-/opt/shardkeep}

	command -v curl >/dev/null 2>&1 || fail "curl is required."
	command -v sha256sum >/dev/null 2>&1 || command -v shasum >/dev/null 2>&1 ||
		fail "sha256sum (or shasum) is required."
	command -v docker >/dev/null 2>&1 ||
		fail "Docker is not installed. Install Docker Engine first: https://docs.docker.com/engine/install/"
	docker compose version >/dev/null 2>&1 ||
		fail "the Docker Compose plugin is missing: https://docs.docker.com/compose/install/linux/"
	docker info >/dev/null 2>&1 ||
		fail "cannot reach Docker. Run this as root, or as a user in the docker group."
	if ! mkdir -p "$dir" 2>/dev/null || [ ! -w "$dir" ]; then
		fail "cannot write to $dir. Run it as root (curl -fsSL https://shardkeep.gg/install.sh | sudo sh) or set SHARDKEEP_DIR."
	fi

	if [ -n "${SHARDKEEP_VERSION:-}" ]; then
		downloads="$RELEASES/v${SHARDKEEP_VERSION#v}/downloads"
	else
		downloads="$RELEASES/permalink/latest/downloads"
	fi
	tmp=$(mktemp -d)
	trap 'rm -rf "$tmp"' EXIT INT TERM
	say "Downloading Shardkeep's Compose file..."
	curl -fsSL -o "$tmp/compose.yml" "$downloads/compose.yml" ||
		fail "could not download $downloads/compose.yml"
	curl -fsSL -o "$tmp/checksums.txt" "$downloads/checksums.txt" ||
		fail "could not download $downloads/checksums.txt"
	want=$(awk '$2 == "compose.yml" { print $1 }' "$tmp/checksums.txt")
	[ -n "$want" ] && [ "$want" = "$(sha256 "$tmp/compose.yml")" ] ||
		fail "compose.yml does not match the release's checksums.txt; nothing was changed."
	version=$(sed -n 's/.*SHARDKEEP_VERSION:-\([0-9][0-9.]*\)}.*/\1/p' "$tmp/compose.yml" | head -n 1)
	[ -n "$version" ] || fail "could not find the release version in compose.yml."

	cd "$dir"
	if [ -f .env ]; then
		current=$(sed -n 's/^SHARDKEEP_VERSION=//p' .env | head -n 1)
		if [ "$current" = "$version" ]; then
			say "Shardkeep $version is already installed in $dir; making sure it is running."
		else
			say "Upgrading Shardkeep in $dir from ${current:-an unknown version} to $version."
			# Only the version changes; .env keeps its other lines, owner and mode.
			if grep -q '^SHARDKEEP_VERSION=' .env; then
				sed "s/^SHARDKEEP_VERSION=.*/SHARDKEEP_VERSION=$version/" .env >"$tmp/env"
			else
				cat .env >"$tmp/env"
				printf 'SHARDKEEP_VERSION=%s\n' "$version" >>"$tmp/env"
			fi
			cat "$tmp/env" >.env
		fi
	else
		name=$(public_name)
		case $name in
		"" | *[!A-Za-z0-9.:-]*) fail "\"$name\" is not a host name or IP address." ;;
		esac
		password=$(od -An -N24 -tx1 /dev/urandom | tr -d ' \n')
		[ ${#password} -eq 48 ] || fail "could not generate a database password."
		say "Installing Shardkeep $version in $dir for $name."
		(
			umask 077
			cat >.env <<EOF
# Shardkeep settings (see https://docs.shardkeep.gg). Keep this file private.
# To upgrade, run the install script again, or change SHARDKEEP_VERSION and
# run: docker compose pull && docker compose up -d
SHARDKEEP_VERSION=$version
SHARDKEEP_PUBLIC_NAME=$name
POSTGRES_PASSWORD=$password
EOF
		)
	fi
	cp "$tmp/compose.yml" compose.yml

	say "Pulling images..."
	docker compose pull --quiet
	docker compose up -d --remove-orphans

	say "Waiting for the Controller to start..."
	i=0
	until [ "$(docker compose ps --format '{{.State}}' controller 2>/dev/null)" = running ] &&
		docker compose logs controller 2>/dev/null | grep -q 'serving agent sessions'; do
		i=$((i + 1))
		[ "$i" -le 90 ] || fail "the Controller did not start within 3 minutes. See: cd $dir && docker compose logs controller"
		sleep 2
	done

	name=$(sed -n 's/^SHARDKEEP_PUBLIC_NAME=//p' .env | head -n 1)
	say ""
	say "Shardkeep $version is running."
	say ""
	if docker compose exec -T controller shardkeep admin setup-code >/dev/null 2>&1; then
		say "Finish setup in your browser: https://$name:8080"
		say "(It uses a self-signed certificate until you configure your own, so your browser will warn once.)"
		say ""
		say "It asks for a setup code. Print it with:"
		say "  cd $dir && docker compose exec controller shardkeep admin setup-code"
	else
		say "Open https://$name:8080 and sign in."
	fi
	say ""
	say "Next steps: https://docs.shardkeep.gg"
}

# Everything runs from here, so a partly downloaded script does nothing.
main "$@"
