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,
|
||||
'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,
|
||||
// Seit wann der Agent nur noch überspringt, und wer die Sperre
|
||||
// hält. Beides null, solange er arbeitet.
|
||||
|
|
|
|||
|
|
@ -216,3 +216,17 @@ json_escape() {
|
|||
# raw; dropping them beats emitting a broken document.
|
||||
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" =~ ^[0-9]+$ ]] || have=0
|
||||
|
||||
if (( have < 3 )); then
|
||||
if (( have < $(release_host_step_needs) )); then
|
||||
write_unblock failed unblock_helper_old
|
||||
return 0
|
||||
fi
|
||||
|
|
@ -393,6 +393,22 @@ releases_json() {
|
|||
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() {
|
||||
local state="$1" error="${2-}"
|
||||
cat > "$STATUS.tmp" <<EOF
|
||||
|
|
@ -408,6 +424,8 @@ write_status() {
|
|||
"remote_commit": "$(json_escape "${REMOTE_COMMIT:-}")",
|
||||
"target_release": "$(json_escape "${TARGET_RELEASE:-}")",
|
||||
"behind": ${BEHIND:-null},
|
||||
"host_step_contract": ${HOST_STEP_HAVE:-0},
|
||||
"host_step_needs": ${HOST_STEP_NEEDS:-0},
|
||||
"ceiling": "$(json_escape "${CEILING:-}")",
|
||||
"ceiling_error": "$(json_escape "${CEILING_ERROR:-}")",
|
||||
"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
|
||||
# here is what makes an update tell the operator to run the installer again.
|
||||
HOST_STEP=/usr/local/sbin/clupilot-host-step
|
||||
# 3 seit `release-update-lock`: ohne den Schritt bleibt der Knopf „Sperre lösen"
|
||||
# in der Konsole ein Knopf, der nichts tun kann.
|
||||
HOST_STEP_NEEDS=3
|
||||
# Seit `release-update-lock`: ohne den Schritt bleibt der Knopf „Sperre lösen"
|
||||
# in der Konsole ein Knopf, der nichts tun kann. Die Zahl selbst steht nur noch
|
||||
# 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
|
||||
# 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
|
||||
|
|
|
|||
|
|
@ -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