Merge pull request #36 from ochenstarik-ui/fix/version-comparison

fix(bootstrap): compare release versions semantically against the installed one
This commit is contained in:
ochenstarik-ui 2026-08-11 01:12:39 +07:00 committed by GitHub
commit 61471f033d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 121 additions and 16 deletions

View file

@ -140,6 +140,54 @@ validate_platform() {
[[ "$(ps -p 1 -o comm=)" == "systemd" ]] || fail "systemd must be PID 1."
}
# Release tags carry a leading "v" while some manifest fields and recorded values may not.
normalize_version() {
printf '%s' "${1#v}"
}
# True when the first version orders strictly before the second. Lexicographic comparison is
# wrong here: "0.1.0-alpha.10" sorts before "0.1.0-alpha.9" as a string, which would reject
# every release after the ninth as a downgrade.
version_lt() {
local left right first
left="$(normalize_version "$1")"
right="$(normalize_version "$2")"
[[ "$left" != "$right" ]] || return 1
first="$(printf '%s\n%s\n' "$left" "$right" | sort -V | head -n1)"
[[ "$first" == "$left" ]]
}
installed_version_file() {
printf '%s' "$ETC_DIR/installed-version-$1"
}
read_installed_version() {
local file
file="$(installed_version_file "$1")"
[[ -r "$file" ]] || return 0
tr -d '\r\n' <"$file"
}
record_installed_version() {
local role="$1" version="$2" file
[[ -n "$version" ]] || return 0
file="$(installed_version_file "$role")"
install -d -m 0755 "$ETC_DIR"
printf '%s\n' "$version" >"$file"
chmod 0644 "$file"
}
manifest_version_field() {
local manifest="$1" field="$2"
[[ -r "$manifest" ]] || return 0
awk -F'"' -v key="$field" '$2 == key { print $4; exit }' "$manifest" || true
}
# Version recorded for an archive being installed, empty when the release predates manifests.
archive_version() {
manifest_version_field "$(dirname "$1")/server-monitor-manager-manifest.json" version
}
validate_node_id() {
[[ "$1" =~ ^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$ ]] \
|| fail "Node id must contain 1-63 lowercase letters, digits, or hyphens."
@ -799,6 +847,7 @@ EOF
systemctl status --no-pager "$CONTROL_UNIT" >&2 || true
fail "Control service failed; backup is $backup_id"
}
record_installed_version control "$(archive_version "$archive")"
log "Control installed. Backup: $backup_id"
log "CA fingerprint: $(openssl x509 -in "$ETC_DIR/control-ca.crt" -noout -fingerprint -sha256 | cut -d= -f2)"
}
@ -895,6 +944,7 @@ EOF
systemctl status --no-pager "$AGENT_UNIT" >&2 || true
fail "Agent service failed; backup is $backup_id"
}
record_installed_version agent "$(archive_version "$archive")"
log "Agent $node_id installed and enrolled. Backup: $backup_id"
}
@ -1012,25 +1062,41 @@ update_role() {
esac
[[ -x "$TEMP_DIR/$role/$binary" ]] || fail "$role binary is missing."
local manifest="$(dirname "$archive")/server-monitor-manager-manifest.json"
local manifest new_version m_control m_agent installed peer_role peer_expected
manifest="$(dirname "$archive")/server-monitor-manager-manifest.json"
if [[ -f "$manifest" ]]; then
local new_version m_control m_agent m_helper
new_version="$(awk -F'"' '/"version":/ {print $4}' "$manifest" || true)"
m_control="$(awk -F'"' '/"control":/ {print $4}' "$manifest" || true)"
m_agent="$(awk -F'"' '/"agent":/ {print $4}' "$manifest" || true)"
m_helper="$(awk -F'"' '/"helper":/ {print $4}' "$manifest" || true)"
new_version="$(manifest_version_field "$manifest" version)"
m_control="$(manifest_version_field "$manifest" control)"
m_agent="$(manifest_version_field "$manifest" agent)"
if [[ -n "$new_version" ]]; then
if [[ "$new_version" < "$PROGRAM_VERSION" && "${SMM_ALLOW_DOWNGRADE:-0}" != "1" ]]; then
fail "Downgrade from $PROGRAM_VERSION to $new_version is not allowed. Set SMM_ALLOW_DOWNGRADE=1 to bypass."
# Compared against the version recorded when this role was installed, not against
# PROGRAM_VERSION: that constant describes the bootstrap source tree and never the
# deployed component, so it can neither detect a downgrade nor match a release tag.
installed="$(read_installed_version "$role")"
if [[ -n "$installed" ]] \
&& version_lt "$new_version" "$installed" \
&& [[ "${SMM_ALLOW_DOWNGRADE:-0}" != "1" ]]; then
fail "Downgrade of $role from $installed to $new_version is not allowed. Set SMM_ALLOW_DOWNGRADE=1 to bypass."
fi
if [[ "$role" == "control" ]] && systemctl list-unit-files | grep -q "^${AGENT_UNIT}"; then
if [[ "$PROGRAM_VERSION" != "$m_agent" ]]; then
fail "Incompatible versions: installed agent is $PROGRAM_VERSION, but archive requires agent $m_agent. Update rejected."
fi
elif [[ "$role" == "agent" ]] && systemctl list-unit-files | grep -q "^${CONTROL_UNIT}"; then
if [[ "$PROGRAM_VERSION" != "$m_control" ]]; then
fail "Incompatible versions: installed control is $PROGRAM_VERSION, but archive requires control $m_control. Update rejected."
# Cross-role compatibility: the peer component already on this host must run the
# version this archive expects of it. An unknown peer version is reported but not
# treated as failure, because installations predating version recording have
# nothing to compare against.
case "$role" in
control) peer_role="agent"; peer_expected="$m_agent" ;;
agent) peer_role="control"; peer_expected="$m_control" ;;
*) peer_role="" ;;
esac
if [[ -n "$peer_role" ]] \
&& systemctl list-unit-files 2>/dev/null | grep -q "^ochenstarik-smm-${peer_role}.service"; then
local peer_actual
peer_actual="$(read_installed_version "$peer_role")"
if [[ -z "$peer_actual" ]]; then
log "Warning: installed $peer_role version is unknown; compatibility check skipped."
elif [[ -n "$peer_expected" ]] \
&& [[ "$(normalize_version "$peer_actual")" != "$(normalize_version "$peer_expected")" ]]; then
fail "Incompatible versions: installed $peer_role is $peer_actual, but this archive expects $peer_role $peer_expected. Update rejected."
fi
fi
@ -1083,6 +1149,7 @@ update_role() {
CONTROL_UPDATE_BACKUP_ID=""
CONTROL_UPDATE_LEGACY_ITEMS=()
fi
record_installed_version "$role" "$(archive_version "$archive")"
log "$role updated. Backup: $backup_id"
}

View file

@ -830,5 +830,43 @@ else
fi
fi
# Version ordering. Lexicographic comparison would rank "0.1.0-alpha.10" below
# "0.1.0-alpha.9" and reject every release after the ninth as a downgrade.
version_helpers="$(sed -n '/^normalize_version()/,/^}/p;/^version_lt()/,/^}/p' "$bootstrap")"
version_case() {
local left="$1" right="$2" expected="$3" actual
if bash -c "$version_helpers
version_lt \"\$1\" \"\$2\"" _ "$left" "$right"; then actual="lt"; else actual="ge"; fi
if [[ "$actual" != "$expected" ]]; then
printf 'version_lt %s %s returned %s, expected %s\n' "$left" "$right" "$actual" "$expected" >&2
exit 1
fi
}
version_case "v0.1.0-alpha.9" "v0.1.0-alpha.10" lt
version_case "v0.1.0-alpha.10" "v0.1.0-alpha.9" ge
version_case "v0.1.0-alpha.9" "v0.1.0-alpha.9" ge
version_case "0.1.0-alpha.9" "v0.1.0-alpha.9" ge
version_case "v0.1.0-alpha.9" "v0.2.0" lt
version_case "v0.2.0" "v0.1.0-alpha.9" ge
# The downgrade guard must compare against the recorded installed version, never against
# PROGRAM_VERSION: that constant tracks the source tree, not the deployed component.
grep -Fq 'installed="$(read_installed_version "$role")"' "$bootstrap" || {
printf '%s\n' "update_role must compare against the recorded installed version" >&2
exit 1
}
if grep -qE '"\$new_version" *< *"\$PROGRAM_VERSION"' "$bootstrap"; then
printf '%s\n' "lexicographic version comparison against PROGRAM_VERSION reintroduced" >&2
exit 1
fi
grep -Fq 'record_installed_version control' "$bootstrap" || {
printf '%s\n' "install-control must record the installed version" >&2
exit 1
}
grep -Fq 'record_installed_version agent' "$bootstrap" || {
printf '%s\n' "install-agent must record the installed version" >&2
exit 1
}
printf '%s\n' "BOOTSTRAP_CONTRACT=PASS"