#!/usr/bin/env bash set -euo pipefail SERVICE_NAME="${SERVICE_NAME:-skd}" UPDATE_URL="${SKD_UPDATE_URL:-https://update.wlkns.org}" UPDATE_TOKEN="${SKD_UPDATE_TOKEN:-}" STATUS_URL="${SKD_UPDATE_STATUS_URL:-https://update.wlkns.org/status}" STATUS_FILE="${SKD_UPDATE_STATUS_FILE:-/var/lib/skd/update_status.json}" LOG_FILE="${SKD_UPDATE_LOG_FILE:-/var/lib/skd/update_logs.jsonl}" INSTALL_DIR="${INSTALL_DIR:-/opt/sk}" STAGING_DIR="" MANIFEST_FILE="" ARTIFACT_FILE="" BACKUP_DIR="" log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" } report_status() { local status="$1" local error="${2:-}" local timestamp timestamp="$(date -u '+%Y-%m-%dT%H:%M:%SZ')" python3 - <<'PY' import json import os from datetime import datetime, timezone from pathlib import Path device_id = os.uname().nodename version = os.environ.get("SKD_VERSION", "unknown") status = os.environ.get("SKD_STATUS", "unknown") error = os.environ.get("SKD_ERROR", "") timestamp = os.environ.get("SKD_TIMESTAMP") or datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") status_file = Path(os.environ["SKD_STATUS_FILE"]) log_file = Path(os.environ["SKD_LOG_FILE"]) status_file.parent.mkdir(parents=True, exist_ok=True) log_file.parent.mkdir(parents=True, exist_ok=True) payload = { "device_id": device_id, "version": version, "status": status, "error": error, "timestamp": timestamp, } status_file.write_text(json.dumps(payload), encoding="utf-8") with log_file.open("a", encoding="utf-8") as handle: handle.write(json.dumps(payload) + "\n") PY local payload payload=$(cat </dev/null || true else curl -sS -X POST -H "Content-Type: application/json" -d "${payload}" "${STATUS_URL}" >/dev/null || true fi } cleanup() { if [[ -n "${STAGING_DIR}" && -d "${STAGING_DIR}" ]]; then rm -rf "${STAGING_DIR}" fi if [[ -n "${MANIFEST_FILE}" && -f "${MANIFEST_FILE}" ]]; then rm -f "${MANIFEST_FILE}" fi if [[ -n "${ARTIFACT_FILE}" && -f "${ARTIFACT_FILE}" ]]; then rm -f "${ARTIFACT_FILE}" fi } trap cleanup EXIT require_cmd() { if ! command -v "$1" >/dev/null 2>&1; then echo "Required command not found: $1" >&2 exit 1 fi } require_cmd curl require_cmd tar require_cmd sha256sum require_cmd python3 log "Fetching update manifest from ${UPDATE_URL}..." MANIFEST_FILE="$(mktemp)" if [[ -n "${UPDATE_TOKEN}" ]]; then curl -fsS -H "Authorization: Bearer ${UPDATE_TOKEN}" "${UPDATE_URL}" -o "${MANIFEST_FILE}" else curl -fsS "${UPDATE_URL}" -o "${MANIFEST_FILE}" fi read_manifest() { python3 - <<'PY' "${MANIFEST_FILE}" import json import sys path = sys.argv[1] with open(path, "r", encoding="utf-8") as fh: data = json.load(fh) for key in ("version", "artifact_url", "sha256"): if key not in data: sys.exit(f"Manifest missing '{key}'") print(data["version"]) print(data["artifact_url"]) print(data["sha256"]) PY } readarray -t manifest_values < <(read_manifest) VERSION="${manifest_values[0]}" ARTIFACT_URL="${manifest_values[1]}" ARTIFACT_SHA256="${manifest_values[2]}" log "Downloading artifact ${ARTIFACT_URL} (version ${VERSION})..." ARTIFACT_FILE="$(mktemp --suffix=.tar.gz)" if [[ -n "${UPDATE_TOKEN}" ]]; then curl -fsS -H "Authorization: Bearer ${UPDATE_TOKEN}" "${ARTIFACT_URL}" -o "${ARTIFACT_FILE}" else curl -fsS "${ARTIFACT_URL}" -o "${ARTIFACT_FILE}" fi log "Verifying checksum..." CALC_SHA="$(sha256sum "${ARTIFACT_FILE}" | awk '{print $1}')" if [[ "${CALC_SHA}" != "${ARTIFACT_SHA256}" ]]; then echo "Checksum mismatch: expected ${ARTIFACT_SHA256} got ${CALC_SHA}" >&2 SKD_VERSION="${VERSION}" SKD_STATUS="failed" SKD_ERROR="checksum mismatch" \ SKD_TIMESTAMP="$(date -u '+%Y-%m-%dT%H:%M:%SZ')" SKD_STATUS_FILE="${STATUS_FILE}" \ SKD_LOG_FILE="${LOG_FILE}" report_status "failed" "checksum mismatch" exit 1 fi STAGING_DIR="$(mktemp -d /opt/sk_update.XXXXXX)" log "Extracting to staging ${STAGING_DIR}..." tar -xzf "${ARTIFACT_FILE}" -C "${STAGING_DIR}" BACKUP_DIR="/opt/sk_backup_${VERSION}_$(date +%s)" log "Stopping service ${SERVICE_NAME}..." sudo systemctl stop "${SERVICE_NAME}.service" log "Swapping ${INSTALL_DIR} -> ${BACKUP_DIR}..." if [[ -d "${INSTALL_DIR}" ]]; then sudo mv "${INSTALL_DIR}" "${BACKUP_DIR}" fi sudo mv "${STAGING_DIR}" "${INSTALL_DIR}" STAGING_DIR="" log "Starting service ${SERVICE_NAME}..." if sudo systemctl start "${SERVICE_NAME}.service"; then log "Update applied successfully." SKD_VERSION="${VERSION}" SKD_STATUS="success" SKD_ERROR="" \ SKD_TIMESTAMP="$(date -u '+%Y-%m-%dT%H:%M:%SZ')" SKD_STATUS_FILE="${STATUS_FILE}" \ SKD_LOG_FILE="${LOG_FILE}" report_status "success" "" else log "Service failed to start, rolling back..." sudo rm -rf "${INSTALL_DIR}" if [[ -d "${BACKUP_DIR}" ]]; then sudo mv "${BACKUP_DIR}" "${INSTALL_DIR}" fi sudo systemctl start "${SERVICE_NAME}.service" || true SKD_VERSION="${VERSION}" SKD_STATUS="failed" SKD_ERROR="service start failed" \ SKD_TIMESTAMP="$(date -u '+%Y-%m-%dT%H:%M:%SZ')" SKD_STATUS_FILE="${STATUS_FILE}" \ SKD_LOG_FILE="${LOG_FILE}" report_status "failed" "service start failed" exit 1 fi