Der Agent klemmt Ziel und Zaehler an der Decke
parent
cfae6e990a
commit
63328d44d4
|
|
@ -43,6 +43,13 @@ RESTARTLAST="$STATE_DIR/restart-last-run.json"
|
|||
# initial admin password an instance holds until somebody notes it down.
|
||||
ARCHIVE_KEY="$STATE_DIR/archive-key.json"
|
||||
LOCK="$STATE_DIR/.agent.lock"
|
||||
# Wie weit dieser Server gehen darf. Von der Konsole geschrieben, hier gelesen.
|
||||
#
|
||||
# Sie faellt ZU, nicht auf: ist die Datei unlesbar, formwidrig oder zeigt sie
|
||||
# auf einen Tag, den es nicht (mehr) gibt, wird NICHTS angeboten. Ein Rueckfall
|
||||
# auf "neueste Version" installierte genau das, wovon weggenagelt wurde — eine
|
||||
# Sicherung, die im Zweifel oeffnet, ist keine.
|
||||
CEILING_FILE="$STATE_DIR/release-ceiling"
|
||||
# Ein Lebenszeichen, geschrieben als ERSTES bei jedem Lauf — vor dem Abruf der
|
||||
# Gegenstelle, vor allem, was blockieren kann.
|
||||
#
|
||||
|
|
@ -329,6 +336,13 @@ sync_console_allowlist() {
|
|||
sync_vpn_certificate() {
|
||||
local stamp seen="$STATE_DIR/.vpn-cert-stamp" path
|
||||
|
||||
# Ohne diese Wache stirbt der ganze Agent an dieser Stelle, wenn `.env`
|
||||
# fehlt: `sed` auf einer nicht vorhandenen Datei liefert Status 2, und eine
|
||||
# einfache (nicht-`local`) Zuweisung reicht diesen Status unter `pipefail`
|
||||
# an `set -e` weiter — bevor je eine Statusdatei geschrieben wurde. Auf
|
||||
# einem echten Wirt existiert `.env` immer; das aendert hier nichts.
|
||||
[[ -f "$ROOT/.env" ]] || return 0
|
||||
|
||||
path="$(sed -n 's/^VPN_CERT_PATH=//p' "$ROOT/.env" 2>/dev/null | tail -1)"
|
||||
[[ -n "$path" ]] || return 0
|
||||
|
||||
|
|
@ -368,6 +382,17 @@ json_escape() {
|
|||
# needed once, to record which step a failed run died at.
|
||||
read_phase() { cut -f1 "$PHASE_FILE" 2>/dev/null | tr -d '\r\n' || true; }
|
||||
|
||||
# Die wählbaren Versionen als JSON-Liste. Eigene Funktion, weil eine Schleife
|
||||
# nicht in eine Here-Dokument-Ersetzung passt.
|
||||
releases_json() {
|
||||
local tag out=''
|
||||
while read -r tag; do
|
||||
[[ -n "$tag" ]] || continue
|
||||
out+="\"$(json_escape "$tag")\","
|
||||
done <<< "${RELEASES:-}"
|
||||
printf '%s' "${out%,}"
|
||||
}
|
||||
|
||||
write_status() {
|
||||
local state="$1" error="${2-}"
|
||||
cat > "$STATUS.tmp" <<EOF
|
||||
|
|
@ -383,6 +408,9 @@ write_status() {
|
|||
"remote_commit": "$(json_escape "${REMOTE_COMMIT:-}")",
|
||||
"target_release": "$(json_escape "${TARGET_RELEASE:-}")",
|
||||
"behind": ${BEHIND:-null},
|
||||
"ceiling": "$(json_escape "${CEILING:-}")",
|
||||
"ceiling_error": "$(json_escape "${CEILING_ERROR:-}")",
|
||||
"releases": [$(releases_json)],
|
||||
"branch": "$(json_escape "$BRANCH")",
|
||||
"error": "$(json_escape "$error")"
|
||||
}
|
||||
|
|
@ -442,6 +470,20 @@ FETCH_ERROR=''
|
|||
# Compared by VERSION, not by ancestry or by tag date: a tag can be cut from
|
||||
# anywhere, and what an operator is being offered is a version number. `sort -V`
|
||||
# does the comparing, so v1.10.0 correctly beats v1.9.0.
|
||||
# Die Decke, in zwei Schritten geprueft: Form hier, Existenz nach dem Abruf —
|
||||
# vorher sind die Tags noch nicht da.
|
||||
CEILING=''
|
||||
CEILING_ERROR=''
|
||||
RELEASES=''
|
||||
|
||||
if [[ -f "$CEILING_FILE" ]]; then
|
||||
CEILING="$(tr -d ' \t\r\n' < "$CEILING_FILE" 2>/dev/null || true)"
|
||||
|
||||
if [[ ! "$CEILING" =~ ^v[0-9]+(\.[0-9]+)*$ ]]; then
|
||||
CEILING_ERROR='ceiling_invalid'
|
||||
fi
|
||||
fi
|
||||
|
||||
DEPLOYED_VERSION="$(release_manifest_version)"
|
||||
[[ -n "$DEPLOYED_VERSION" ]] || DEPLOYED_VERSION="$(release_version)"
|
||||
|
||||
|
|
@ -451,25 +493,36 @@ DEPLOYED_VERSION="$(release_manifest_version)"
|
|||
# grosszuegig fuer einen fetch gegen EINE Gegenstelle; laenger ist kein
|
||||
# langsames Netz mehr, sondern eines, das nicht antwortet.
|
||||
if timeout -k 10 120 git fetch --quiet --tags --force origin 2>/dev/null; then
|
||||
# Newest by version order, not by tag date. Through the helper rather than
|
||||
# `| head -1`: head exits after one line, git takes SIGPIPE, and pipefail
|
||||
# ends the agent — see release_newest_tag.
|
||||
NEWEST_TAG="$(release_newest_tag)"
|
||||
NEWEST_VERSION="${NEWEST_TAG#v}"
|
||||
# Erst jetzt sind die Tags da, also erst jetzt laesst sich sagen, ob die
|
||||
# Decke auf etwas Wirkliches zeigt.
|
||||
if [[ -n "$CEILING" && -z "$CEILING_ERROR" ]] && ! release_tag_exists "$CEILING"; then
|
||||
CEILING_ERROR='ceiling_missing'
|
||||
fi
|
||||
|
||||
if [[ -n "$NEWEST_TAG" ]] && release_version_gt "$NEWEST_VERSION" "$DEPLOYED_VERSION"; then
|
||||
# How many releases ahead, so the console can say "2 Aktualisierungen"
|
||||
# rather than only that something exists. Both helpers live in
|
||||
# deploy/lib/release.sh so a test can run them — the version that lived
|
||||
# here ended the agent on every tick under `set -e`, and no test in a
|
||||
# PHP suite can reach a bash pipeline.
|
||||
BEHIND="$(release_tags_ahead "$DEPLOYED_VERSION")"
|
||||
TARGET_RELEASE="$NEWEST_TAG"
|
||||
REMOTE_COMMIT="$(git rev-parse "refs/tags/${NEWEST_TAG}^{commit}" 2>/dev/null || echo '')"
|
||||
else
|
||||
# Includes "no tags at all yet": nothing has been released, so there is
|
||||
# nothing to offer, and saying "up to date" is the truth.
|
||||
# Was ueberhaupt als Decke in Frage kommt — unabhaengig davon, ob gerade
|
||||
# eine gesetzt ist. Ohne diese Liste hat die Konsole nichts anzubieten.
|
||||
RELEASES="$(release_tags_from "$DEPLOYED_VERSION" 20)"
|
||||
|
||||
if [[ -n "$CEILING_ERROR" ]]; then
|
||||
# Eine kaputte Decke bietet nichts an. Bewusst kein `else`-Zweig, der
|
||||
# auf die neueste Version zurueckfaellt.
|
||||
BEHIND=0
|
||||
else
|
||||
# Newest by version order, not by tag date — und nicht ueber die Decke
|
||||
# hinaus. Durch den Helfer statt `| head -1`: head exits after one
|
||||
# line, git takes SIGPIPE, and pipefail ends the agent.
|
||||
NEWEST_TAG="$(release_newest_tag "$CEILING")"
|
||||
NEWEST_VERSION="${NEWEST_TAG#v}"
|
||||
|
||||
if [[ -n "$NEWEST_TAG" ]] && release_version_gt "$NEWEST_VERSION" "$DEPLOYED_VERSION"; then
|
||||
BEHIND="$(release_tags_ahead "$DEPLOYED_VERSION" "$CEILING")"
|
||||
TARGET_RELEASE="$NEWEST_TAG"
|
||||
REMOTE_COMMIT="$(git rev-parse "refs/tags/${NEWEST_TAG}^{commit}" 2>/dev/null || echo '')"
|
||||
else
|
||||
# Schliesst „gar keine Tags" und „Decke gleich dem Ausgelieferten"
|
||||
# ein: beides heisst, es gibt nichts anzubieten.
|
||||
BEHIND=0
|
||||
fi
|
||||
fi
|
||||
else
|
||||
# Reported, not swallowed: "cannot reach the repository" and "already up to
|
||||
|
|
|
|||
|
|
@ -0,0 +1,123 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\Process;
|
||||
|
||||
/**
|
||||
* Die Decke, gefahren vom echten Agenten.
|
||||
*
|
||||
* Nicht nachgerechnet: die Klemmung sitzt zwischen `git fetch` und dem
|
||||
* Schreiben der Statusdatei, und genau dieses Zusammenspiel ist die Frage.
|
||||
* `git` liegt als Attrappe im PATH, deren `fetch` sofort gelingt, ohne eine
|
||||
* Gegenstelle zu brauchen — alle uebrigen git-Aufrufe gehen ans echte git im
|
||||
* Wegwerf-Repo.
|
||||
*/
|
||||
function runAgentWithCeiling(?string $ceiling, array $tags, string $deployedVersion = '1.7.3'): array
|
||||
{
|
||||
// Ein WEGWERF-Checkout mit EIGENEM .git — niemals das Repository dieses
|
||||
// Arbeitsbaums.
|
||||
//
|
||||
// Tags liegen im gemeinsamen .git und sind damit auch fuer den Hauptbaum
|
||||
// und jede Parallelsitzung sichtbar. Ein hier angelegtes v9.9.9 wuerde
|
||||
// `git tag -l 'v*' --sort=-v:refname | head -1` falsch beantworten — und
|
||||
// genau diese Frage entscheidet, wohin ein Server aktualisiert. Stirbt der
|
||||
// Test vor seinem Aufraeumen, bliebe es liegen.
|
||||
//
|
||||
// Der Agent bestimmt seine Wurzel aus dem eigenen Pfad
|
||||
// (`ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"`). Es genuegt
|
||||
// also, deploy/ zu kopieren und ihn dort zu starten.
|
||||
$root = sys_get_temp_dir().'/clupilot-ceiling-'.bin2hex(random_bytes(6));
|
||||
File::ensureDirectoryExists($root.'/deploy/lib');
|
||||
File::ensureDirectoryExists($root.'/storage/app/deploy');
|
||||
|
||||
File::copy(base_path('deploy/update-agent.sh'), $root.'/deploy/update-agent.sh');
|
||||
File::copy(base_path('deploy/lib/release.sh'), $root.'/deploy/lib/release.sh');
|
||||
|
||||
// MANIFEST_FILE ist `storage/app/deployment.json` — eine Ebene UEBER
|
||||
// storage/app/deploy/. Siehe deploy/lib/release.sh:19.
|
||||
File::put($root.'/storage/app/deployment.json', json_encode([
|
||||
'version' => $deployedVersion,
|
||||
'commit' => 'deadbeef',
|
||||
'source' => 'refs/tags/v'.$deployedVersion,
|
||||
]));
|
||||
|
||||
if ($ceiling !== null) {
|
||||
File::put($root.'/storage/app/deploy/release-ceiling', $ceiling);
|
||||
}
|
||||
|
||||
$tagCommands = '';
|
||||
foreach ($tags as $tag) {
|
||||
$tagCommands .= "git tag {$tag}\n";
|
||||
}
|
||||
|
||||
$result = Process::path($root)->timeout(90)->run(<<<BASH
|
||||
set -e
|
||||
git init -q .
|
||||
git -c user.email=t@example.com -c user.name=t commit -q --allow-empty -m init
|
||||
{$tagCommands}
|
||||
stub="\$(mktemp -d)"
|
||||
# Nur `fetch` wird abgefangen — es gibt keine Gegenstelle. Alles andere
|
||||
# geht ans echte git und wirkt im Wegwerf-Checkout.
|
||||
cat > "\$stub/git" <<'GIT'
|
||||
#!/bin/sh
|
||||
if [ "\$1" = "fetch" ]; then exit 0; fi
|
||||
exec /usr/bin/git "\$@"
|
||||
GIT
|
||||
chmod +x "\$stub/git"
|
||||
printf '#!/bin/sh\nexit 1\n' > "\$stub/docker"
|
||||
chmod +x "\$stub/docker"
|
||||
PATH="\$stub:\$PATH" bash deploy/update-agent.sh >/dev/null 2>&1 || true
|
||||
rm -rf "\$stub"
|
||||
BASH);
|
||||
|
||||
expect($result->successful())->toBeTrue($result->errorOutput());
|
||||
|
||||
$status = json_decode(File::get($root.'/storage/app/deploy/update-status.json'), true);
|
||||
|
||||
File::deleteDirectory($root);
|
||||
|
||||
return $status;
|
||||
}
|
||||
|
||||
it('offers only up to the ceiling', function () {
|
||||
// Ausgeliefert 1.7.3, vorhanden bis v9.9.9, Decke auf v9.9.8: genau eine
|
||||
// Aktualisierung, und das Ziel ist die Decke.
|
||||
$status = runAgentWithCeiling('v9.9.8', ['v9.9.8', 'v9.9.9']);
|
||||
|
||||
expect($status['target_release'])->toBe('v9.9.8')
|
||||
->and($status['behind'])->toBe(1)
|
||||
->and($status['ceiling'])->toBe('v9.9.8')
|
||||
->and($status['ceiling_error'])->toBe('');
|
||||
});
|
||||
|
||||
it('offers nothing when the ceiling points at a tag that does not exist', function () {
|
||||
// Die Decke faellt ZU. Ein Rueckfall auf "neueste" installierte genau das,
|
||||
// wovon der Besitzer weggenagelt hat.
|
||||
$status = runAgentWithCeiling('v9.9.7', ['v9.9.8', 'v9.9.9']);
|
||||
|
||||
expect($status['behind'])->toBe(0)
|
||||
->and($status['ceiling_error'])->toBe('ceiling_missing')
|
||||
->and($status['target_release'])->toBe('');
|
||||
});
|
||||
|
||||
it('offers nothing when the ceiling is malformed', function () {
|
||||
$status = runAgentWithCeiling('neueste bitte', ['v9.9.8', 'v9.9.9']);
|
||||
|
||||
expect($status['behind'])->toBe(0)
|
||||
->and($status['ceiling_error'])->toBe('ceiling_invalid');
|
||||
});
|
||||
|
||||
it('behaves exactly as before without a ceiling', function () {
|
||||
$status = runAgentWithCeiling(null, ['v9.9.8', 'v9.9.9']);
|
||||
|
||||
expect($status['target_release'])->toBe('v9.9.9')
|
||||
->and($status['ceiling'])->toBe('')
|
||||
->and($status['ceiling_error'])->toBe('');
|
||||
});
|
||||
|
||||
it('reports the versions that may be pinned to', function () {
|
||||
$status = runAgentWithCeiling(null, ['v9.9.8', 'v9.9.9']);
|
||||
|
||||
expect($status['releases'])->toContain('v9.9.9')
|
||||
->and($status['releases'])->toContain('v9.9.8');
|
||||
});
|
||||
Loading…
Reference in New Issue