mirror of
https://github.com/coollabsio/coolify.git
synced 2026-08-24 10:05:47 -05:00
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:
@@ -292,20 +292,13 @@ class OtherController extends Controller
|
||||
|
||||
#[OA\Get(
|
||||
summary: 'Healthcheck',
|
||||
description: 'Healthcheck endpoint. Includes the running Coolify version in the X-Coolify-Version header.',
|
||||
description: 'Healthcheck endpoint.',
|
||||
path: '/health',
|
||||
operationId: 'healthcheck',
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
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(
|
||||
mediaType: 'text/html',
|
||||
schema: new OA\Schema(type: 'string'),
|
||||
@@ -323,6 +316,6 @@ class OtherController extends Controller
|
||||
)]
|
||||
public function healthcheck(Request $request)
|
||||
{
|
||||
return response('OK')->header('X-Coolify-Version', (string) config('constants.coolify.version'));
|
||||
return response('OK');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -256,34 +256,6 @@
|
||||
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() {
|
||||
if (this.checkHealthInterval) {
|
||||
return;
|
||||
@@ -293,38 +265,36 @@
|
||||
}, 2000);
|
||||
},
|
||||
|
||||
probeHealth() {
|
||||
async probeHealth() {
|
||||
this.healthCheckAttempts++;
|
||||
const elapsedMinutes = Math.floor((Date.now() - this.startTime) / 60000);
|
||||
|
||||
return fetch('/api/health')
|
||||
.then(response => {
|
||||
const runningVersion = response.headers.get('X-Coolify-Version');
|
||||
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);
|
||||
try {
|
||||
const response = await fetch('/api/health');
|
||||
if (!response.ok) {
|
||||
this.instanceWentDown = true;
|
||||
this.currentStep = 4;
|
||||
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) {
|
||||
@@ -434,13 +404,7 @@
|
||||
this.currentStep = this.mapStepToUI(data.step);
|
||||
this.currentStatus = data.message;
|
||||
} else if (data.status === 'complete') {
|
||||
if (this.isReadyToReload(data.running_version)) {
|
||||
this.showSuccess();
|
||||
} else {
|
||||
this.currentStep = 4;
|
||||
this.currentStatus = `Waiting for Coolify ${this.latestVersion} to come online...`;
|
||||
this.revive();
|
||||
}
|
||||
this.showSuccess();
|
||||
} else if (data.status === 'error') {
|
||||
this.showError(data.message);
|
||||
} else if (data.status === 'none' && this.instanceWentDown) {
|
||||
|
||||
@@ -3,20 +3,20 @@
|
||||
use App\Http\Controllers\Api\OtherController;
|
||||
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']);
|
||||
|
||||
$response = (new OtherController)->healthcheck(Request::create('/api/health', 'GET'));
|
||||
|
||||
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']);
|
||||
|
||||
$this->get('/api/health')
|
||||
->assertSuccessful()
|
||||
->assertSee('OK')
|
||||
->assertHeader('X-Coolify-Version', '4.3.1');
|
||||
->assertHeaderMissing('X-Coolify-Version');
|
||||
});
|
||||
|
||||
@@ -119,23 +119,24 @@ it('ignores stale status files older than ten minutes', function () {
|
||||
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');
|
||||
|
||||
expect($upgradeView)
|
||||
->toContain('X-Coolify-Version')
|
||||
->toContain('hasReachedTargetVersion')
|
||||
->not->toContain('X-Coolify-Version')
|
||||
->toContain('this.$wire.getUpgradeStatus()')
|
||||
->toContain("data.status === 'complete'")
|
||||
->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');
|
||||
|
||||
expect($upgradeView)
|
||||
->toContain('instanceWentDown')
|
||||
->toContain('isReadyToReload')
|
||||
->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 () {
|
||||
|
||||
Reference in New Issue
Block a user