Fix: Version downgrade prevention with cache validation (#7396)

This commit is contained in:
Andras Bacsai
2025-11-28 16:31:46 +01:00
committed by GitHub
7 changed files with 438 additions and 9 deletions
+71 -2
View File
@@ -3,6 +3,8 @@
namespace App\Actions\Server;
use App\Models\Server;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Sleep;
use Lorisleiva\Actions\Concerns\AsAction;
@@ -29,7 +31,59 @@ class UpdateCoolify
return;
}
CleanupDocker::dispatch($this->server, false, false);
$this->latestVersion = get_latest_version_of_coolify();
// Fetch fresh version from CDN instead of using cache
try {
$response = Http::retry(3, 1000)->timeout(10)
->get(config('constants.coolify.versions_url'));
if ($response->successful()) {
$versions = $response->json();
$this->latestVersion = data_get($versions, 'coolify.v4.version');
} else {
// Fallback to cache if CDN unavailable
$cacheVersion = get_latest_version_of_coolify();
// Validate cache version against current running version
if ($cacheVersion && version_compare($cacheVersion, config('constants.coolify.version'), '<')) {
Log::error('Failed to fetch fresh version from CDN and cache is corrupted/outdated', [
'cached_version' => $cacheVersion,
'current_version' => config('constants.coolify.version'),
]);
throw new \Exception(
'Cannot determine latest version: CDN unavailable and cache version '.
"({$cacheVersion}) is older than running version (".config('constants.coolify.version').')'
);
}
$this->latestVersion = $cacheVersion;
Log::warning('Failed to fetch fresh version from CDN (unsuccessful response), using validated cache', [
'version' => $cacheVersion,
]);
}
} catch (\Throwable $e) {
$cacheVersion = get_latest_version_of_coolify();
// Validate cache version against current running version
if ($cacheVersion && version_compare($cacheVersion, config('constants.coolify.version'), '<')) {
Log::error('Failed to fetch fresh version from CDN and cache is corrupted/outdated', [
'error' => $e->getMessage(),
'cached_version' => $cacheVersion,
'current_version' => config('constants.coolify.version'),
]);
throw new \Exception(
'Cannot determine latest version: CDN unavailable and cache version '.
"({$cacheVersion}) is older than running version (".config('constants.coolify.version').')'
);
}
$this->latestVersion = $cacheVersion;
Log::warning('Failed to fetch fresh version from CDN, using validated cache', [
'error' => $e->getMessage(),
'version' => $cacheVersion,
]);
}
$this->currentVersion = config('constants.coolify.version');
if (! $manual_update) {
if (! $settings->is_auto_update_enabled) {
@@ -42,6 +96,20 @@ class UpdateCoolify
return;
}
}
// ALWAYS check for downgrades (even for manual updates)
if (version_compare($this->latestVersion, $this->currentVersion, '<')) {
Log::error('Downgrade prevented', [
'target_version' => $this->latestVersion,
'current_version' => $this->currentVersion,
'manual_update' => $manual_update,
]);
throw new \Exception(
"Cannot downgrade from {$this->currentVersion} to {$this->latestVersion}. ".
'If you need to downgrade, please do so manually via Docker commands.'
);
}
$this->update();
$settings->new_version_available = false;
$settings->save();
@@ -56,8 +124,9 @@ class UpdateCoolify
$image = config('constants.coolify.registry_url').'/coollabsio/coolify:'.$this->latestVersion;
instant_remote_process(["docker pull -q $image"], $this->server, false);
$upgradeScriptUrl = config('constants.coolify.upgrade_script_url');
remote_process([
'curl -fsSL https://cdn.coollabs.io/coolify/upgrade.sh -o /data/coolify/source/upgrade.sh',
"curl -fsSL {$upgradeScriptUrl} -o /data/coolify/source/upgrade.sh",
"bash /data/coolify/source/upgrade.sh $this->latestVersion",
], $this->server);
}
+46 -5
View File
@@ -10,6 +10,7 @@ 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,20 +23,60 @@ class CheckForUpdatesJob implements ShouldBeEncrypted, ShouldQueue
return;
}
$settings = instanceSettings();
$response = Http::retry(3, 1000)->get('https://cdn.coollabs.io/coolify/versions.json');
$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();
// 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]);
File::put(base_path('versions.json'), json_encode($versions, JSON_PRETTY_PRINT));
// Invalidate cache to ensure fresh data is loaded
invalidate_versions_cache();
} else {
$settings->update(['new_version_available' => false]);
}
+1 -1
View File
@@ -21,7 +21,7 @@ class CheckHelperImageJob implements ShouldBeEncrypted, ShouldQueue
public function handle(): void
{
try {
$response = Http::retry(3, 1000)->get('https://cdn.coollabs.io/coolify/versions.json');
$response = Http::retry(3, 1000)->get(config('constants.coolify.versions_url'));
if ($response->successful()) {
$versions = $response->json();
$settings = instanceSettings();