fix(upgrade): use authenticated status polling after restart
Remove the version header from the health endpoint and use upgrade status polling to determine completion.
This commit is contained in:
parent
1932673c22
commit
6492d08136
4 changed files with 38 additions and 80 deletions
|
|
@ -292,20 +292,13 @@ public function feedback(Request $request)
|
||||||
|
|
||||||
#[OA\Get(
|
#[OA\Get(
|
||||||
summary: 'Healthcheck',
|
summary: 'Healthcheck',
|
||||||
description: 'Healthcheck endpoint. Includes the running Coolify version in the X-Coolify-Version header.',
|
description: 'Healthcheck endpoint.',
|
||||||
path: '/health',
|
path: '/health',
|
||||||
operationId: 'healthcheck',
|
operationId: 'healthcheck',
|
||||||
responses: [
|
responses: [
|
||||||
new OA\Response(
|
new OA\Response(
|
||||||
response: 200,
|
response: 200,
|
||||||
description: 'Healthcheck endpoint.',
|
description: 'Healthcheck endpoint.',
|
||||||
headers: [
|
|
||||||
new OA\Header(
|
|
||||||
header: 'X-Coolify-Version',
|
|
||||||
description: 'Currently running Coolify version.',
|
|
||||||
schema: new OA\Schema(type: 'string', example: '4.3.1'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
content: new OA\MediaType(
|
content: new OA\MediaType(
|
||||||
mediaType: 'text/html',
|
mediaType: 'text/html',
|
||||||
schema: new OA\Schema(type: 'string'),
|
schema: new OA\Schema(type: 'string'),
|
||||||
|
|
@ -323,6 +316,6 @@ public function feedback(Request $request)
|
||||||
)]
|
)]
|
||||||
public function healthcheck(Request $request)
|
public function healthcheck(Request $request)
|
||||||
{
|
{
|
||||||
return response('OK')->header('X-Coolify-Version', (string) config('constants.coolify.version'));
|
return response('OK');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -256,34 +256,6 @@ class="flex flex-wrap items-center justify-end gap-2 border-t border-neutral-200
|
||||||
return 4;
|
return 4;
|
||||||
},
|
},
|
||||||
|
|
||||||
hasReachedTargetVersion(running, target) {
|
|
||||||
if (!running || !target) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const normalize = (version) => String(version).replace(/^v/i, '');
|
|
||||||
running = normalize(running);
|
|
||||||
target = normalize(target);
|
|
||||||
if (running === target) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return running.localeCompare(target, undefined, {
|
|
||||||
numeric: true,
|
|
||||||
sensitivity: 'base',
|
|
||||||
}) >= 0;
|
|
||||||
},
|
|
||||||
|
|
||||||
isReadyToReload(runningVersion) {
|
|
||||||
if (this.hasReachedTargetVersion(runningVersion, this.latestVersion)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Releases before this header existed (e.g. 4.3.1) still
|
|
||||||
// return a healthy /api/health with no X-Coolify-Version.
|
|
||||||
// Only treat that as done after the instance actually went down.
|
|
||||||
return !runningVersion && this.instanceWentDown;
|
|
||||||
},
|
|
||||||
|
|
||||||
startHealthWatch() {
|
startHealthWatch() {
|
||||||
if (this.checkHealthInterval) {
|
if (this.checkHealthInterval) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -293,38 +265,36 @@ class="flex flex-wrap items-center justify-end gap-2 border-t border-neutral-200
|
||||||
}, 2000);
|
}, 2000);
|
||||||
},
|
},
|
||||||
|
|
||||||
probeHealth() {
|
async probeHealth() {
|
||||||
this.healthCheckAttempts++;
|
this.healthCheckAttempts++;
|
||||||
const elapsedMinutes = Math.floor((Date.now() - this.startTime) / 60000);
|
const elapsedMinutes = Math.floor((Date.now() - this.startTime) / 60000);
|
||||||
|
|
||||||
return fetch('/api/health')
|
try {
|
||||||
.then(response => {
|
const response = await fetch('/api/health');
|
||||||
const runningVersion = response.headers.get('X-Coolify-Version');
|
if (!response.ok) {
|
||||||
if (!response.ok) {
|
|
||||||
this.instanceWentDown = true;
|
|
||||||
this.currentStep = 4;
|
|
||||||
this.currentStatus = this.getReviveStatusMessage(elapsedMinutes, this.healthCheckAttempts);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (this.isReadyToReload(runningVersion)) {
|
|
||||||
this.showSuccess();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!this.instanceWentDown && this.currentStep < 4) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (runningVersion) {
|
|
||||||
this.currentStatus = `Coolify is still on ${runningVersion}. Waiting for ${this.latestVersion}...`;
|
|
||||||
} else {
|
|
||||||
this.currentStatus = this.getReviveStatusMessage(elapsedMinutes, this.healthCheckAttempts);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error('Health check failed:', error);
|
|
||||||
this.instanceWentDown = true;
|
this.instanceWentDown = true;
|
||||||
this.currentStep = 4;
|
this.currentStep = 4;
|
||||||
this.currentStatus = this.getReviveStatusMessage(elapsedMinutes, this.healthCheckAttempts);
|
this.currentStatus = this.getReviveStatusMessage(elapsedMinutes, this.healthCheckAttempts);
|
||||||
});
|
return;
|
||||||
|
}
|
||||||
|
if (!this.instanceWentDown && this.currentStep < 4) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await this.$wire.getUpgradeStatus();
|
||||||
|
if (data.status === 'complete') {
|
||||||
|
this.showSuccess();
|
||||||
|
} else if (data.status === 'error') {
|
||||||
|
this.showError(data.message);
|
||||||
|
} else {
|
||||||
|
this.currentStatus = data.message ?? this.getReviveStatusMessage(elapsedMinutes, this.healthCheckAttempts);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Health check failed:', error);
|
||||||
|
this.instanceWentDown = true;
|
||||||
|
this.currentStep = 4;
|
||||||
|
this.currentStatus = this.getReviveStatusMessage(elapsedMinutes, this.healthCheckAttempts);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
getReviveStatusMessage(elapsedMinutes, attempts) {
|
getReviveStatusMessage(elapsedMinutes, attempts) {
|
||||||
|
|
@ -434,13 +404,7 @@ class="flex flex-wrap items-center justify-end gap-2 border-t border-neutral-200
|
||||||
this.currentStep = this.mapStepToUI(data.step);
|
this.currentStep = this.mapStepToUI(data.step);
|
||||||
this.currentStatus = data.message;
|
this.currentStatus = data.message;
|
||||||
} else if (data.status === 'complete') {
|
} else if (data.status === 'complete') {
|
||||||
if (this.isReadyToReload(data.running_version)) {
|
this.showSuccess();
|
||||||
this.showSuccess();
|
|
||||||
} else {
|
|
||||||
this.currentStep = 4;
|
|
||||||
this.currentStatus = `Waiting for Coolify ${this.latestVersion} to come online...`;
|
|
||||||
this.revive();
|
|
||||||
}
|
|
||||||
} else if (data.status === 'error') {
|
} else if (data.status === 'error') {
|
||||||
this.showError(data.message);
|
this.showError(data.message);
|
||||||
} else if (data.status === 'none' && this.instanceWentDown) {
|
} else if (data.status === 'none' && this.instanceWentDown) {
|
||||||
|
|
|
||||||
|
|
@ -3,20 +3,20 @@
|
||||||
use App\Http\Controllers\Api\OtherController;
|
use App\Http\Controllers\Api\OtherController;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
it('adds the running version header on the healthcheck response', function () {
|
it('does not expose the running version on the healthcheck response', function () {
|
||||||
config(['constants.coolify.version' => '4.3.1']);
|
config(['constants.coolify.version' => '4.3.1']);
|
||||||
|
|
||||||
$response = (new OtherController)->healthcheck(Request::create('/api/health', 'GET'));
|
$response = (new OtherController)->healthcheck(Request::create('/api/health', 'GET'));
|
||||||
|
|
||||||
expect($response->getContent())->toBe('OK')
|
expect($response->getContent())->toBe('OK')
|
||||||
->and($response->headers->get('X-Coolify-Version'))->toBe('4.3.1');
|
->and($response->headers->has('X-Coolify-Version'))->toBeFalse();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('exposes the running Coolify version on the public health endpoint', function () {
|
it('does not expose the running Coolify version on the public health endpoint', function () {
|
||||||
config(['constants.coolify.version' => '4.3.1']);
|
config(['constants.coolify.version' => '4.3.1']);
|
||||||
|
|
||||||
$this->get('/api/health')
|
$this->get('/api/health')
|
||||||
->assertSuccessful()
|
->assertSuccessful()
|
||||||
->assertSee('OK')
|
->assertSee('OK')
|
||||||
->assertHeader('X-Coolify-Version', '4.3.1');
|
->assertHeaderMissing('X-Coolify-Version');
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -119,23 +119,24 @@
|
||||||
expect($result['status'])->toBe('none');
|
expect($result['status'])->toBe('none');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('waits for the running version to match the target before showing reload', function () {
|
it('checks the running version through authenticated upgrade status before showing reload', function () {
|
||||||
$upgradeView = file_get_contents(__DIR__.'/../../resources/views/livewire/upgrade.blade.php');
|
$upgradeView = file_get_contents(__DIR__.'/../../resources/views/livewire/upgrade.blade.php');
|
||||||
|
|
||||||
expect($upgradeView)
|
expect($upgradeView)
|
||||||
->toContain('X-Coolify-Version')
|
->not->toContain('X-Coolify-Version')
|
||||||
->toContain('hasReachedTargetVersion')
|
->toContain('this.$wire.getUpgradeStatus()')
|
||||||
|
->toContain("data.status === 'complete'")
|
||||||
->toContain('livewireFailures');
|
->toContain('livewireFailures');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('treats a healthy instance without a version header as ready only after downtime', function () {
|
it('uses the public health endpoint only for liveness during an upgrade', function () {
|
||||||
$upgradeView = file_get_contents(__DIR__.'/../../resources/views/livewire/upgrade.blade.php');
|
$upgradeView = file_get_contents(__DIR__.'/../../resources/views/livewire/upgrade.blade.php');
|
||||||
|
|
||||||
expect($upgradeView)
|
expect($upgradeView)
|
||||||
->toContain('instanceWentDown')
|
->toContain('instanceWentDown')
|
||||||
->toContain('isReadyToReload')
|
|
||||||
->toContain('startHealthWatch')
|
->toContain('startHealthWatch')
|
||||||
->toContain('data.status === \'none\'');
|
->toContain("fetch('/api/health')")
|
||||||
|
->not->toContain('response.headers.get');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('starts the upgrade after the Livewire response so status polling is not blocked', function () {
|
it('starts the upgrade after the Livewire response so status polling is not blocked', function () {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue