mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-12 15:53:46 -04:00
fix(cloud): refresh local versions.json hourly on every node
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
666ae60c2c
commit
b1f2abf868
@@ -51,6 +51,11 @@ class Kernel extends ConsoleKernel
|
||||
->name('cleanup:ssh-mux')
|
||||
->hourly()
|
||||
->when(fn () => config('constants.ssh.mux_enabled') && ! config('constants.coolify.is_windows_docker_desktop'));
|
||||
// Every cloud node reads its own versions.json, so each node refreshes it (no onOneServer).
|
||||
$this->scheduleInstance->call(fn () => refresh_versions_file())
|
||||
->name('versions:refresh-local')
|
||||
->hourly()
|
||||
->when(fn () => isCloud());
|
||||
$this->scheduleInstance->command('cleanup:redis --clear-locks')->daily();
|
||||
$this->scheduleInstance->call(fn () => app(ScheduledJobDeliveryService::class)->deleteOldOccurrences())
|
||||
->name('cleanup:scheduled-job-occurrences')
|
||||
|
||||
@@ -8,9 +8,6 @@ use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class CheckForUpdatesJob implements ShouldBeEncrypted, ShouldQueue
|
||||
{
|
||||
@@ -22,67 +19,16 @@ class CheckForUpdatesJob implements ShouldBeEncrypted, ShouldQueue
|
||||
if (isDev() || isCloud()) {
|
||||
return;
|
||||
}
|
||||
$settings = instanceSettings();
|
||||
$response = Http::retry(3, 1000)->get(config('constants.coolify.versions_url'));
|
||||
if ($response->successful()) {
|
||||
$versions = $response->json();
|
||||
|
||||
$latest_version = data_get($versions, 'coolify.v4.version');
|
||||
$current_version = config('constants.coolify.version');
|
||||
|
||||
// Read existing cached version
|
||||
$existingVersions = null;
|
||||
$existingCoolifyVersion = null;
|
||||
if (File::exists(base_path('versions.json'))) {
|
||||
$existingVersions = json_decode(File::get(base_path('versions.json')), true);
|
||||
$existingCoolifyVersion = data_get($existingVersions, 'coolify.v4.version');
|
||||
}
|
||||
|
||||
// Determine the BEST version to use (CDN, cache, or current)
|
||||
$bestVersion = $latest_version;
|
||||
|
||||
// Check if cache has newer version than CDN
|
||||
if ($existingCoolifyVersion && version_compare($existingCoolifyVersion, $bestVersion, '>')) {
|
||||
Log::warning('CDN served older Coolify version than cache', [
|
||||
'cdn_version' => $latest_version,
|
||||
'cached_version' => $existingCoolifyVersion,
|
||||
'current_version' => $current_version,
|
||||
]);
|
||||
$bestVersion = $existingCoolifyVersion;
|
||||
}
|
||||
|
||||
// CRITICAL: Never allow bestVersion to be older than currently running version
|
||||
if (version_compare($bestVersion, $current_version, '<')) {
|
||||
Log::warning('Version downgrade prevented in CheckForUpdatesJob', [
|
||||
'cdn_version' => $latest_version,
|
||||
'cached_version' => $existingCoolifyVersion,
|
||||
'current_version' => $current_version,
|
||||
'attempted_best' => $bestVersion,
|
||||
'using' => $current_version,
|
||||
]);
|
||||
$bestVersion = $current_version;
|
||||
}
|
||||
|
||||
// Use data_set() for safe mutation (fixes #3)
|
||||
data_set($versions, 'coolify.v4.version', $bestVersion);
|
||||
$latest_version = $bestVersion;
|
||||
|
||||
// ALWAYS write versions.json (for Sentinel, Helper, Traefik updates)
|
||||
File::put(base_path('versions.json'), json_encode($versions, JSON_PRETTY_PRINT));
|
||||
|
||||
// Invalidate cache to ensure fresh data is loaded
|
||||
invalidate_versions_cache();
|
||||
|
||||
CheckTraefikVersionJob::dispatch();
|
||||
|
||||
// Only mark new version available if Coolify version actually increased
|
||||
if (version_compare($latest_version, $current_version, '>')) {
|
||||
// New version available
|
||||
$settings->update(['new_version_available' => true]);
|
||||
} else {
|
||||
$settings->update(['new_version_available' => false]);
|
||||
}
|
||||
$versions = refresh_versions_file();
|
||||
if ($versions === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
CheckTraefikVersionJob::dispatch();
|
||||
|
||||
$latestVersion = data_get($versions, 'coolify.v4.version');
|
||||
$newVersionAvailable = version_compare($latestVersion, config('constants.coolify.version'), '>');
|
||||
instanceSettings()->update(['new_version_available' => $newVersionAvailable]);
|
||||
} catch (\Throwable $e) {
|
||||
// Consider implementing a notification to administrators
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* Get cached versions data from versions.json.
|
||||
@@ -51,3 +53,59 @@ function invalidate_versions_cache(): void
|
||||
{
|
||||
Cache::forget('coolify:versions:all');
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch versions.json from the CDN and write it to the local file of this node.
|
||||
* The Coolify version never goes below the cached or the running version.
|
||||
* When the CDN fails, the local file stays as it is.
|
||||
*
|
||||
* @return array|null The written versions data, or null if the CDN fetch failed
|
||||
*/
|
||||
function refresh_versions_file(): ?array
|
||||
{
|
||||
$response = Http::timeout(10)->retry(3, 1000, throw: false)->get(config('constants.coolify.versions_url'));
|
||||
$versions = $response->json();
|
||||
if ($response->failed() || ! is_array($versions)) {
|
||||
Log::warning('Could not fetch versions.json from the CDN, using the local file', ['status' => $response->status()]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$latestVersion = data_get($versions, 'coolify.v4.version');
|
||||
$currentVersion = config('constants.coolify.version');
|
||||
|
||||
$existingCoolifyVersion = null;
|
||||
if (File::exists(base_path('versions.json'))) {
|
||||
$existingVersions = json_decode(File::get(base_path('versions.json')), true);
|
||||
$existingCoolifyVersion = data_get($existingVersions, 'coolify.v4.version');
|
||||
}
|
||||
|
||||
$bestVersion = $latestVersion;
|
||||
|
||||
if ($existingCoolifyVersion && version_compare($existingCoolifyVersion, $bestVersion, '>')) {
|
||||
Log::warning('CDN served older Coolify version than cache', [
|
||||
'cdn_version' => $latestVersion,
|
||||
'cached_version' => $existingCoolifyVersion,
|
||||
'current_version' => $currentVersion,
|
||||
]);
|
||||
$bestVersion = $existingCoolifyVersion;
|
||||
}
|
||||
|
||||
if (version_compare($bestVersion, $currentVersion, '<')) {
|
||||
Log::warning('Version downgrade prevented in CheckForUpdatesJob', [
|
||||
'cdn_version' => $latestVersion,
|
||||
'cached_version' => $existingCoolifyVersion,
|
||||
'current_version' => $currentVersion,
|
||||
'attempted_best' => $bestVersion,
|
||||
'using' => $currentVersion,
|
||||
]);
|
||||
$bestVersion = $currentVersion;
|
||||
}
|
||||
|
||||
data_set($versions, 'coolify.v4.version', $bestVersion);
|
||||
|
||||
File::put(base_path('versions.json'), json_encode($versions, JSON_PRETTY_PRINT));
|
||||
invalidate_versions_cache();
|
||||
|
||||
return $versions;
|
||||
}
|
||||
|
||||
@@ -41,6 +41,20 @@ it('schedules ssh mux cleanup locally on every scheduler host', function () {
|
||||
expect($event->getSummaryForDisplay())->toBe('cleanup:ssh-mux');
|
||||
});
|
||||
|
||||
it('refreshes the local versions.json hourly on every cloud node', function (bool $cloud) {
|
||||
config(['constants.coolify.self_hosted' => ! $cloud]);
|
||||
$schedule = app(Schedule::class);
|
||||
|
||||
$event = collect($schedule->events())->first(
|
||||
fn ($e) => (string) $e->description === 'versions:refresh-local'
|
||||
);
|
||||
|
||||
expect($event)->not->toBeNull();
|
||||
expect($event->onOneServer)->toBeFalse();
|
||||
expect($event->expression)->toBe('0 * * * *');
|
||||
expect($event->filtersPass(app()))->toBe($cloud);
|
||||
})->with(['cloud' => true, 'self-hosted' => false]);
|
||||
|
||||
it('schedules every production job with onOneServer', function () {
|
||||
$schedule = app(Schedule::class);
|
||||
|
||||
|
||||
@@ -163,3 +163,11 @@ it('preserves other component versions when preventing Coolify downgrade', funct
|
||||
$job = new CheckForUpdatesJob;
|
||||
$job->handle();
|
||||
});
|
||||
|
||||
it('keeps the local versions.json when the CDN fetch fails', function () {
|
||||
Http::fake(['*' => Http::response('', 500)]);
|
||||
File::shouldReceive('put')->never();
|
||||
Cache::shouldReceive('forget')->never();
|
||||
|
||||
expect(refresh_versions_file())->toBeNull();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user