Die gebrauchte Vertragsversion steht an einer Stelle und wird gemeldet
parent
daeea1db0e
commit
512fad11fb
|
|
@ -325,6 +325,20 @@ final class UpdateChannel
|
||||||
'remote_commit' => isset($status['remote_commit']) ? (string) $status['remote_commit'] : null,
|
'remote_commit' => isset($status['remote_commit']) ? (string) $status['remote_commit'] : null,
|
||||||
'checked_at' => $checkedAt,
|
'checked_at' => $checkedAt,
|
||||||
|
|
||||||
|
// Der root-eigene Helfer auf dem Wirt. Fehlt die Meldung ganz
|
||||||
|
// (alte Agentenfassung, allererster Lauf), gilt er als in
|
||||||
|
// Ordnung: „ich weiß es nicht" ist nicht „zu alt", und ein
|
||||||
|
// Warnkasten, der auf jedem frisch aufgesetzten Wirt steht, wird
|
||||||
|
// nach zwei Tagen nicht mehr gelesen.
|
||||||
|
'host_step_have' => isset($status['host_step_contract'])
|
||||||
|
? (int) $status['host_step_contract']
|
||||||
|
: null,
|
||||||
|
'host_step_needs' => isset($status['host_step_needs'])
|
||||||
|
? (int) $status['host_step_needs']
|
||||||
|
: null,
|
||||||
|
'host_step_ok' => ! isset($status['host_step_contract'], $status['host_step_needs'])
|
||||||
|
|| (int) $status['host_step_contract'] >= (int) $status['host_step_needs'],
|
||||||
|
|
||||||
'agent_seen' => $agentAlive,
|
'agent_seen' => $agentAlive,
|
||||||
// Seit wann der Agent nur noch überspringt, und wer die Sperre
|
// Seit wann der Agent nur noch überspringt, und wer die Sperre
|
||||||
// hält. Beides null, solange er arbeitet.
|
// hält. Beides null, solange er arbeitet.
|
||||||
|
|
|
||||||
|
|
@ -216,3 +216,17 @@ json_escape() {
|
||||||
# raw; dropping them beats emitting a broken document.
|
# raw; dropping them beats emitting a broken document.
|
||||||
printf '%s' "$s" | tr -d '\000-\037'
|
printf '%s' "$s" | tr -d '\000-\037'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# release_host_step_needs — welche Vertragsversion des root-eigenen Helfers
|
||||||
|
# diese Fassung braucht.
|
||||||
|
#
|
||||||
|
# Sie stand zweimal im Repo: als `HOST_STEP_NEEDS=3` in update.sh und als
|
||||||
|
# hartkodierte 3 im Agenten. Zwei Zahlen, die zusammenpassen müssen, laufen
|
||||||
|
# irgendwann auseinander — und das Auseinanderlaufen zeigt sich erst auf einem
|
||||||
|
# Wirt, dessen Helfer zu alt ist.
|
||||||
|
#
|
||||||
|
# Angehoben wird sie, wenn install-agent.sh dem Helfer einen Schritt beibringt,
|
||||||
|
# auf den sich etwas anderes verlässt. Dann braucht JEDER Wirt einmal
|
||||||
|
# `sudo bash deploy/install-agent.sh` — das ist Absicht und die Grenze, hinter
|
||||||
|
# der root sitzt.
|
||||||
|
release_host_step_needs() { printf '%s' 3; }
|
||||||
|
|
|
||||||
|
|
@ -176,7 +176,7 @@ release_stuck_lock() {
|
||||||
have="$("$step" contract 2>/dev/null || true)"
|
have="$("$step" contract 2>/dev/null || true)"
|
||||||
[[ "$have" =~ ^[0-9]+$ ]] || have=0
|
[[ "$have" =~ ^[0-9]+$ ]] || have=0
|
||||||
|
|
||||||
if (( have < 3 )); then
|
if (( have < $(release_host_step_needs) )); then
|
||||||
write_unblock failed unblock_helper_old
|
write_unblock failed unblock_helper_old
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
@ -393,6 +393,22 @@ releases_json() {
|
||||||
printf '%s' "${out%,}"
|
printf '%s' "${out%,}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Welchen Vertrag der Wirt-Helfer erfüllt — und welchen diese Fassung braucht.
|
||||||
|
#
|
||||||
|
# Gemeldet statt automatisiert: `sudoers` gewährt dem Dienstbenutzer genau
|
||||||
|
# drei benannte Befehle, und etwas Root-Eigenes, das ungeprüft aus dem
|
||||||
|
# beschreibbaren Checkout ausführt, gäbe jedem, der je an diesen Benutzer
|
||||||
|
# kommt, Root auf dem Wirt. Die Grenze bleibt; die Konsole soll nur aufhören,
|
||||||
|
# den Betreiber raten zu lassen.
|
||||||
|
#
|
||||||
|
# `|| true` und der Zahlentest: ein fehlender Helfer, ein Helfer ohne diesen
|
||||||
|
# Schritt und ein Helfer, der etwas Unerwartetes druckt, sind alle „0" — und
|
||||||
|
# keiner davon darf den Agenten unter `set -e` beenden, bevor er eine
|
||||||
|
# Statusdatei schreibt.
|
||||||
|
HOST_STEP_HAVE="$( { /usr/local/sbin/clupilot-host-step contract 2>/dev/null || true; } | head -1 )"
|
||||||
|
[[ "$HOST_STEP_HAVE" =~ ^[0-9]+$ ]] || HOST_STEP_HAVE=0
|
||||||
|
HOST_STEP_NEEDS="$(release_host_step_needs)"
|
||||||
|
|
||||||
write_status() {
|
write_status() {
|
||||||
local state="$1" error="${2-}"
|
local state="$1" error="${2-}"
|
||||||
cat > "$STATUS.tmp" <<EOF
|
cat > "$STATUS.tmp" <<EOF
|
||||||
|
|
@ -408,6 +424,8 @@ write_status() {
|
||||||
"remote_commit": "$(json_escape "${REMOTE_COMMIT:-}")",
|
"remote_commit": "$(json_escape "${REMOTE_COMMIT:-}")",
|
||||||
"target_release": "$(json_escape "${TARGET_RELEASE:-}")",
|
"target_release": "$(json_escape "${TARGET_RELEASE:-}")",
|
||||||
"behind": ${BEHIND:-null},
|
"behind": ${BEHIND:-null},
|
||||||
|
"host_step_contract": ${HOST_STEP_HAVE:-0},
|
||||||
|
"host_step_needs": ${HOST_STEP_NEEDS:-0},
|
||||||
"ceiling": "$(json_escape "${CEILING:-}")",
|
"ceiling": "$(json_escape "${CEILING:-}")",
|
||||||
"ceiling_error": "$(json_escape "${CEILING_ERROR:-}")",
|
"ceiling_error": "$(json_escape "${CEILING_ERROR:-}")",
|
||||||
"releases": [$(releases_json)],
|
"releases": [$(releases_json)],
|
||||||
|
|
|
||||||
|
|
@ -70,9 +70,11 @@ STATE_FILE="storage/app/deployed-commit"
|
||||||
# fixed by deploy/install-agent.sh; this script only asks. Raising HOST_STEP_NEEDS
|
# fixed by deploy/install-agent.sh; this script only asks. Raising HOST_STEP_NEEDS
|
||||||
# here is what makes an update tell the operator to run the installer again.
|
# here is what makes an update tell the operator to run the installer again.
|
||||||
HOST_STEP=/usr/local/sbin/clupilot-host-step
|
HOST_STEP=/usr/local/sbin/clupilot-host-step
|
||||||
# 3 seit `release-update-lock`: ohne den Schritt bleibt der Knopf „Sperre lösen"
|
# Seit `release-update-lock`: ohne den Schritt bleibt der Knopf „Sperre lösen"
|
||||||
# in der Konsole ein Knopf, der nichts tun kann.
|
# in der Konsole ein Knopf, der nichts tun kann. Die Zahl selbst steht nur noch
|
||||||
HOST_STEP_NEEDS=3
|
# in deploy/lib/release.sh (release_host_step_needs) — hier gäbe eine zweite
|
||||||
|
# Kopie die Möglichkeit, dass sie mit der im Agenten auseinanderläuft.
|
||||||
|
HOST_STEP_NEEDS="$(release_host_step_needs)"
|
||||||
# Which step is running, for the console to show. Written as a KEY, not as the
|
# Which step is running, for the console to show. Written as a KEY, not as the
|
||||||
# sentence below it: the console is translated and this script is not, so an
|
# sentence below it: the console is translated and this script is not, so an
|
||||||
# English line here would surface untranslated in the interface. A run that dies
|
# English line here would surface untranslated in the interface. A run that dies
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,85 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Services\Deployment\UpdateChannel;
|
||||||
|
use Illuminate\Support\Facades\File;
|
||||||
|
use Illuminate\Support\Facades\Process;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Der Wirt-Helfer ist root-eigen und wird von `install-agent.sh` gesetzt.
|
||||||
|
* Hebt ein Release seine Vertragsversion an, braucht JEDER Wirt einmal
|
||||||
|
* Handarbeit als root — bei zehn Servern zehnmal.
|
||||||
|
*
|
||||||
|
* Automatisieren lässt sich das nicht: `sudoers` gewährt dem Dienstbenutzer
|
||||||
|
* genau drei benannte Befehle, und etwas Root-Eigenes, das ungeprüft aus dem
|
||||||
|
* (vom Dienstbenutzer beschreibbaren) Checkout ausführt, gäbe jedem, der je
|
||||||
|
* an diesen Benutzer kommt, Root auf dem Wirt.
|
||||||
|
*
|
||||||
|
* Also wird gemeldet statt automatisiert — aber VOLLSTÄNDIG und BEVOR jemand
|
||||||
|
* einen Knopf drückt, der daran scheitert.
|
||||||
|
*/
|
||||||
|
|
||||||
|
afterEach(function () {
|
||||||
|
File::deleteDirectory(storage_path('app/deploy'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps the needed contract version in exactly one place', function () {
|
||||||
|
// Sie stand zweimal im Repo: `HOST_STEP_NEEDS=3` in update.sh und eine
|
||||||
|
// hartkodierte 3 im Agenten. Zwei Zahlen, die zusammenpassen müssen,
|
||||||
|
// laufen irgendwann auseinander.
|
||||||
|
$lib = File::get(base_path('deploy/lib/release.sh'));
|
||||||
|
$update = File::get(base_path('deploy/update.sh'));
|
||||||
|
$agent = File::get(base_path('deploy/update-agent.sh'));
|
||||||
|
|
||||||
|
expect($lib)->toContain('release_host_step_needs')
|
||||||
|
->and($update)->toContain('release_host_step_needs')
|
||||||
|
->and($agent)->toContain('release_host_step_needs');
|
||||||
|
|
||||||
|
// Und keine nackte Zahl mehr an den beiden alten Stellen.
|
||||||
|
expect($update)->not->toContain('HOST_STEP_NEEDS=3')
|
||||||
|
->and($agent)->not->toContain('(( have < 3 ))');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('answers the needed contract version from the shell', function () {
|
||||||
|
$result = Process::path(base_path())->timeout(30)->run(
|
||||||
|
'bash -c '.escapeshellarg('set -Eeuo pipefail; . deploy/lib/release.sh; release_host_step_needs')
|
||||||
|
);
|
||||||
|
|
||||||
|
expect($result->exitCode())->toBe(0)
|
||||||
|
->and(trim($result->output()))->toMatch('/^[0-9]+$/');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reports the helper as not ok when the host has an older one', function () {
|
||||||
|
File::ensureDirectoryExists(storage_path('app/deploy'));
|
||||||
|
File::put(storage_path('app/deploy/update-status.json'), json_encode([
|
||||||
|
'state' => 'idle',
|
||||||
|
'host_step_contract' => 2,
|
||||||
|
'host_step_needs' => 3,
|
||||||
|
]));
|
||||||
|
|
||||||
|
$state = app(UpdateChannel::class)->state();
|
||||||
|
|
||||||
|
expect($state['host_step_ok'])->toBeFalse()
|
||||||
|
->and($state['host_step_have'])->toBe(2)
|
||||||
|
->and($state['host_step_needs'])->toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reports the helper as ok when it is current', function () {
|
||||||
|
File::ensureDirectoryExists(storage_path('app/deploy'));
|
||||||
|
File::put(storage_path('app/deploy/update-status.json'), json_encode([
|
||||||
|
'state' => 'idle',
|
||||||
|
'host_step_contract' => 3,
|
||||||
|
'host_step_needs' => 3,
|
||||||
|
]));
|
||||||
|
|
||||||
|
expect(app(UpdateChannel::class)->state()['host_step_ok'])->toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not cry wolf when the agent has not reported yet', function () {
|
||||||
|
// Ein Wirt, dessen Agent die Zahlen noch nie gemeldet hat (alte Fassung,
|
||||||
|
// erster Lauf), darf nicht als kaputt dastehen. „Ich weiß es nicht" ist
|
||||||
|
// nicht dasselbe wie „zu alt".
|
||||||
|
File::ensureDirectoryExists(storage_path('app/deploy'));
|
||||||
|
File::put(storage_path('app/deploy/update-status.json'), json_encode(['state' => 'idle']));
|
||||||
|
|
||||||
|
expect(app(UpdateChannel::class)->state()['host_step_ok'])->toBeTrue();
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue