#!/bin/sh
# Обновление агента с платы, с проверкой и откатом.
#
# Запускается отдельным транзиентным юнитом, а не из самого агента: apt в конце
# перезапускает dashdash-agent, то есть убивает процесс, который его вызвал.
# Команда, оборвавшаяся на середине, оставила бы в админке вечное «выполняется»
# и не сказала бы, чем всё кончилось. Поэтому результат пишется в файл, а
# отчитывается о нём уже новый агент, когда поднимется.
#
# Откат — не перестраховка. Экран стоит в чужом офисе, консоли у него нет, и
# версия, после которой агент не поднимается, означает поездку. Поэтому после
# установки мы ждём, пока агент ответит на /health, и если он не ответил —
# возвращаем предыдущую версию из кэша apt.
set -eu

CACHE="${DASHDASH_CACHE_DIR:-/var/lib/dashdash}"
RESULT="$CACHE/update-result.json"
LOG=/var/log/dashdash-update.log
PACKAGE=dashdash-agent
PORT="${SCREEN_PORT:-3000}"
HEALTH_TIMEOUT=90

version_now() {
	dpkg-query -W -f='${Version}' "$PACKAGE" 2>/dev/null || echo ""
}

write_result() {
	mkdir -p "$CACHE"
	cat > "$RESULT" <<JSON
{"ok": $1, "before": "$2", "after": "$3", "message": "$4", "at": "$(date -Is)"}
JSON
}

# Живой агент — это не запущенный юнит, а отвечающий порт: процесс, который
# упал на импорте, systemd будет бодро перезапускать по кругу.
agent_healthy() {
	i=0
	while [ "$i" -lt "$HEALTH_TIMEOUT" ]; do
		if curl -fsS -o /dev/null "http://127.0.0.1:${PORT}/health"; then
			return 0
		fi
		i=$((i + 1))
		sleep 1
	done
	return 1
}

rollback() {
	previous="$1"
	cached="/var/cache/apt/archives/${PACKAGE}_$(echo "$previous" | tr ':' '%3a')_all.deb"
	echo "откатываюсь на $previous"
	if [ -f "$cached" ] && dpkg -i "$cached"; then
		return 0
	fi
	# Пакета в кэше нет — пробуем репозиторий. Сработает, если там оставлены
	# прошлые версии (publish-repo.sh их не выбрасывает).
	DEBIAN_FRONTEND=noninteractive apt-get -y --allow-downgrades \
		install "${PACKAGE}=${previous}"
}

exec >>"$LOG" 2>&1
echo "--- $(date -Is) обновление"

BEFORE="$(version_now)"
[ -n "$BEFORE" ] || {
	# Пакета в системе нет вовсе — значит плата собрана не тем образом, что мы
	# думаем. Доустанавливать агента поверх непонятно чего хуже, чем сказать
	# об этом вслух.
	write_result false "нет" "нет" "пакет dashdash-agent не установлен, обновлять нечего"
	exit 1
}

# Источник обновлений должен быть прописан. Без него apt на
# «--only-upgrade install» отвечает «пакет не обновлён» и выходит с нулём —
# то есть обновление честно рапортует об успехе, ничего не сделав. Ровно тот
# сорт молчаливой лжи, который дороже любой ошибки.
if [ ! -f /etc/apt/sources.list.d/dashdash.sources ]; then
	write_result false "$BEFORE" "$BEFORE" \
		"репозиторий обновлений не подключён: образ собран без --apt-url/--apt-key"
	exit 1
fi

if ! apt-get -o Acquire::Retries=3 update; then
	write_result false "$BEFORE" "$BEFORE" "не удалось обновить список пакетов"
	exit 1
fi

# Что apt вообще готов поставить. Сравнение версий — его работа, поэтому
# спрашиваем его, а не гадаем по строкам.
CANDIDATE="$(apt-cache policy "$PACKAGE" | awk '/Candidate:/{print $2}')"
if [ -z "$CANDIDATE" ] || [ "$CANDIDATE" = "(none)" ]; then
	write_result false "$BEFORE" "$BEFORE" \
		"в репозитории нет пакета $PACKAGE — проверьте адрес репозитория и ключ"
	exit 1
fi

if ! DEBIAN_FRONTEND=noninteractive apt-get -y --only-upgrade install "$PACKAGE"; then
	write_result false "$BEFORE" "$(version_now)" "apt не смог поставить пакет"
	exit 1
fi

AFTER="$(version_now)"
if [ "$BEFORE" = "$AFTER" ]; then
	# Называем версию, которую предлагает репозиторий: если она совпала с
	# установленной — обновлять правда нечего; если нет — что-то не так с
	# источником, и это видно сразу, а не после часа раскопок.
	write_result true "$BEFORE" "$AFTER" "уже последняя версия (в репозитории $CANDIDATE)"
	echo "обновлять нечего, кандидат $CANDIDATE"
	exit 0
fi

if agent_healthy; then
	write_result true "$BEFORE" "$AFTER" "обновлено"
	echo "было $BEFORE, стало $AFTER"
	exit 0
fi

echo "после $AFTER агент не ответил за ${HEALTH_TIMEOUT}с"
if rollback "$BEFORE" && agent_healthy; then
	write_result false "$BEFORE" "$BEFORE" "версия $AFTER не заработала, откатился"
	exit 1
fi

write_result false "$BEFORE" "$(version_now)" \
	"версия $AFTER не заработала, откат тоже не удался — нужна перепрошивка"
exit 1
