|
| 1 | +#!/usr/bin/env bash |
| 2 | +set -euo pipefail |
| 3 | + |
| 4 | +# Apply one already downloaded, SHA-verified, built, and retained 1Helm Linux |
| 5 | +# release as a single root transaction outside an older updater unit's mount |
| 6 | +# namespace. No URL, command, or arbitrary destination is accepted here. |
| 7 | + |
| 8 | +INSTALL_ROOT="/opt/1helm" |
| 9 | +RELEASES_ROOT="$INSTALL_ROOT/releases" |
| 10 | +APP_ROOT="$INSTALL_ROOT/current" |
| 11 | +NODE_LINK="$INSTALL_ROOT/node-current" |
| 12 | +STATE_ROOT="/var/lib/1helm" |
| 13 | +SERVICE_USER="1helm" |
| 14 | +SERVICE_NAME="1helm.service" |
| 15 | +PORT="8123" |
| 16 | +RELEASE_ROOT="$(readlink -f "${1:-}" 2>/dev/null || true)" |
| 17 | +TARGET_VERSION="${2:-}" |
| 18 | +STATUS_FILE="$STATE_ROOT/host-update-status.json" |
| 19 | +HOST_CONTRACT_PATHS=( |
| 20 | + /usr/libexec/1helm-lxc-runtime |
| 21 | + /usr/libexec/1helm-lxc-net |
| 22 | + /etc/1helm/lxc-unprivileged.conf |
| 23 | + /etc/1helm/lxc-idmap |
| 24 | + /etc/sudoers.d/1helm-lxc-runtime |
| 25 | + /etc/default/lxc-net |
| 26 | + /etc/subuid |
| 27 | + /etc/subgid |
| 28 | + /etc/systemd/system/1helm-lxc-net.service |
| 29 | + /etc/systemd/system/1helm.service |
| 30 | + /etc/systemd/system/1helm-update.service |
| 31 | + /etc/systemd/system/1helm-update.path |
| 32 | + /opt/1helm/update-host.sh |
| 33 | + /opt/1helm/uninstall-host.sh |
| 34 | +) |
| 35 | +HOST_UNITS=(1helm-lxc-net.service 1helm.service 1helm-update.path) |
| 36 | +TRANSACTION_ACTIVE=0 |
| 37 | +ROLLING_BACK=0 |
| 38 | +TEMP_ROOT="" |
| 39 | +PREVIOUS_RELEASE="" |
| 40 | + |
| 41 | +[[ "${EUID}" -eq 0 ]] || { echo "The Linux release transaction must run as root." >&2; exit 1; } |
| 42 | +[[ "$RELEASE_ROOT" == "$RELEASES_ROOT/"* && -d "$RELEASE_ROOT" ]] \ |
| 43 | + || { echo "The Linux release transaction requires a verified retained release." >&2; exit 1; } |
| 44 | +[[ "$TARGET_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] \ |
| 45 | + || { echo "The Linux release transaction requires an exact version." >&2; exit 1; } |
| 46 | +PACKAGE_VERSION="$("$NODE_LINK/bin/node" -p 'require(process.argv[1]).version' "$RELEASE_ROOT/package.json" 2>/dev/null || true)" |
| 47 | +[[ "$PACKAGE_VERSION" == "$TARGET_VERSION" ]] \ |
| 48 | + || { echo "The retained release does not match the requested version." >&2; exit 1; } |
| 49 | +[[ -x "$RELEASE_ROOT/site/public/install-lxc-runtime.sh" \ |
| 50 | + && -x "$RELEASE_ROOT/site/public/install-linux-units.sh" \ |
| 51 | + && -x "$RELEASE_ROOT/site/public/update-host.sh" \ |
| 52 | + && -x "$RELEASE_ROOT/site/public/uninstall-host.sh" \ |
| 53 | + && -x "$RELEASE_ROOT/scripts/1helm-lxc-runtime" \ |
| 54 | + && -x "$RELEASE_ROOT/scripts/1helm-lxc-net" ]] \ |
| 55 | + || { echo "The verified release is missing its Linux host contract." >&2; exit 1; } |
| 56 | + |
| 57 | +TEMP_ROOT="$(mktemp -d)" |
| 58 | +chmod 0700 "$TEMP_ROOT" |
| 59 | + |
| 60 | +json_string() { |
| 61 | + "$NODE_LINK/bin/node" -e 'process.stdout.write(JSON.stringify(process.argv[1] || ""))' "$1" |
| 62 | +} |
| 63 | + |
| 64 | +write_status() { |
| 65 | + local state="$1" message="$2" error="${3:-}" candidate="$TEMP_ROOT/status.json" |
| 66 | + printf '{"mode":"linux-systemd","status":%s,"version":%s,"checked_at":%s,"message":%s,"error":%s}\n' \ |
| 67 | + "$(json_string "$state")" "$(json_string "$TARGET_VERSION")" "$(( $(date +%s) * 1000 ))" \ |
| 68 | + "$(json_string "$message")" "$([[ -n "$error" ]] && json_string "$error" || printf null)" >"$candidate" |
| 69 | + chown "$SERVICE_USER:$SERVICE_USER" "$candidate" |
| 70 | + chmod 0600 "$candidate" |
| 71 | + mv -f -- "$candidate" "$STATUS_FILE" |
| 72 | +} |
| 73 | + |
| 74 | +snapshot_host_contract() { |
| 75 | + install -d -m 0700 "$TEMP_ROOT/files" "$TEMP_ROOT/units" |
| 76 | + local path encoded unit |
| 77 | + for path in "${HOST_CONTRACT_PATHS[@]}"; do |
| 78 | + if [[ -e "$path" || -L "$path" ]]; then |
| 79 | + encoded="${path#/}" |
| 80 | + install -d -m 0700 "$TEMP_ROOT/files/$(dirname "$encoded")" |
| 81 | + cp -a -- "$path" "$TEMP_ROOT/files/$encoded" |
| 82 | + fi |
| 83 | + done |
| 84 | + for unit in "${HOST_UNITS[@]}"; do |
| 85 | + systemctl is-enabled "$unit" >"$TEMP_ROOT/units/$unit.enabled" 2>/dev/null || true |
| 86 | + systemctl is-active "$unit" >"$TEMP_ROOT/units/$unit.active" 2>/dev/null || true |
| 87 | + done |
| 88 | +} |
| 89 | + |
| 90 | +rollback_host_contract() { |
| 91 | + local path encoded unit enabled active restored_healthy=1 |
| 92 | + ROLLING_BACK=1 |
| 93 | + systemctl disable --now 1helm-update.path 1helm-lxc-net.service >/dev/null 2>&1 || true |
| 94 | + systemctl stop "$SERVICE_NAME" >/dev/null 2>&1 || true |
| 95 | + for path in "${HOST_CONTRACT_PATHS[@]}"; do |
| 96 | + encoded="${path#/}" |
| 97 | + rm -f -- "$path" |
| 98 | + if [[ -e "$TEMP_ROOT/files/$encoded" || -L "$TEMP_ROOT/files/$encoded" ]]; then |
| 99 | + install -d -m 0755 "$(dirname "$path")" |
| 100 | + cp -a -- "$TEMP_ROOT/files/$encoded" "$path" |
| 101 | + fi |
| 102 | + done |
| 103 | + if [[ -n "$PREVIOUS_RELEASE" && -d "$PREVIOUS_RELEASE" ]]; then |
| 104 | + ln -s "$PREVIOUS_RELEASE" "$TEMP_ROOT/rollback-current" |
| 105 | + mv -Tf "$TEMP_ROOT/rollback-current" "$APP_ROOT" |
| 106 | + fi |
| 107 | + systemctl daemon-reload |
| 108 | + for unit in "${HOST_UNITS[@]}"; do |
| 109 | + enabled="$(cat "$TEMP_ROOT/units/$unit.enabled" 2>/dev/null || true)" |
| 110 | + active="$(cat "$TEMP_ROOT/units/$unit.active" 2>/dev/null || true)" |
| 111 | + [[ "$enabled" == "enabled" ]] && systemctl enable "$unit" >/dev/null 2>&1 || true |
| 112 | + [[ "$active" == "active" ]] && systemctl start "$unit" >/dev/null 2>&1 || true |
| 113 | + done |
| 114 | + if [[ "$(cat "$TEMP_ROOT/units/$SERVICE_NAME.active" 2>/dev/null || true)" == "active" ]]; then |
| 115 | + restored_healthy=0 |
| 116 | + for _ in {1..300}; do |
| 117 | + if curl -fsS "http://127.0.0.1:$PORT/api/setup/status" >/dev/null; then restored_healthy=1; break; fi |
| 118 | + sleep 0.2 |
| 119 | + done |
| 120 | + fi |
| 121 | + TRANSACTION_ACTIVE=0 |
| 122 | + ROLLING_BACK=0 |
| 123 | + [[ "$restored_healthy" -eq 1 ]] |
| 124 | +} |
| 125 | + |
| 126 | +cleanup_transaction() { |
| 127 | + local command_status=$? |
| 128 | + trap - EXIT |
| 129 | + if [[ "$TRANSACTION_ACTIVE" -eq 1 && "$ROLLING_BACK" -eq 0 ]]; then |
| 130 | + if rollback_host_contract; then |
| 131 | + write_status "error" "1Helm v$TARGET_VERSION failed its host health transaction; the prior healthy release was restored." "Host update failed and was rolled back." || true |
| 132 | + else |
| 133 | + write_status "error" "1Helm v$TARGET_VERSION failed and the prior host could not be proven healthy after rollback." "Host update and rollback health check failed." || true |
| 134 | + fi |
| 135 | + fi |
| 136 | + [[ -z "$TEMP_ROOT" ]] || rm -rf -- "$TEMP_ROOT" |
| 137 | + exit "$command_status" |
| 138 | +} |
| 139 | +trap cleanup_transaction EXIT |
| 140 | + |
| 141 | +PREVIOUS_RELEASE="$(readlink -f "$APP_ROOT" 2>/dev/null || true)" |
| 142 | +[[ "$PREVIOUS_RELEASE" == "$RELEASES_ROOT/"* && -d "$PREVIOUS_RELEASE" ]] \ |
| 143 | + || { echo "The currently installed 1Helm release is not inside the verified release store." >&2; exit 1; } |
| 144 | +snapshot_host_contract |
| 145 | +TRANSACTION_ACTIVE=1 |
| 146 | +write_status "installing" "The host verified v$TARGET_VERSION and is applying one atomic runtime and application transaction." |
| 147 | +HELM_HOST_APPLY_DELEGATED=1 "$RELEASE_ROOT/site/public/install-lxc-runtime.sh" "$RELEASE_ROOT" |
| 148 | +ln -s "$RELEASE_ROOT" "$TEMP_ROOT/current" |
| 149 | +mv -Tf "$TEMP_ROOT/current" "$APP_ROOT" |
| 150 | +HELM_HOST_APPLY_DELEGATED=1 "$RELEASE_ROOT/site/public/install-linux-units.sh" "$RELEASE_ROOT" |
| 151 | +write_status "restarting" "The host installed v$TARGET_VERSION and is restarting 1Helm." |
| 152 | +systemctl restart "$SERVICE_NAME" |
| 153 | +healthy=0 |
| 154 | +for _ in {1..300}; do |
| 155 | + if curl -fsS "http://127.0.0.1:$PORT/api/setup/status" >/dev/null; then healthy=1; break; fi |
| 156 | + sleep 0.2 |
| 157 | +done |
| 158 | +[[ "$healthy" -eq 1 ]] || { echo "1Helm v$TARGET_VERSION failed its host health check." >&2; exit 1; } |
| 159 | +TRANSACTION_ACTIVE=0 |
| 160 | +write_status "current" "This 1Helm host is running v$TARGET_VERSION." |
0 commit comments